OpenCode 实践课:4 分钟用 Python 写出一个命令行日志分析工具

项目介绍

服务器出了问题?Nginx 日志几万行不知道怎么下手?今天我们做一个命令行日志分析工具,输入一个日志文件路径,自动解析出 Top 10 访问 IP、HTTP 状态码分布、请求方法统计和热门 URL。

  • 技术栈:Python 3,标准库零依赖
  • 最终代码:约 80 行
  • 核心价值:展示如何分步骤用自然语言指挥 OpenCode 完成开发

准备工作

安装 OpenCode(opencode.ai 下载安装包)

确保 Python 3.6+ 已安装:python --version

准备一个测试用的 Nginx 日志文件(本文使用 access.log

实践过程

第一步:写出基础框架

我先用一句话描述需求,让 OpenCode 生成初始代码。

我对 OpenCode 说:

> 用 Python 写一个命令行日志分析脚本,能解析 Nginx access log 格式,接收一个文件路径参数,打印出总请求数、Top 10 访问 IP 和 HTTP 状态码分布。用标准库,不要第三方依赖。

OpenCode 的响应:

OpenCode 先扫描了项目目录,确认没有冲突文件,然后直接生成了 log_analyzer.py,包含:

  • 正则解析 Nginx 日志的正则模式
  • argparse 处理命令行参数
  • Counter 做统计分析
  • 表格格式的输出

代码一气呵成,无需修改即可运行。

> 关键要点: 提示词里明确说了"标准库、零依赖",这样 OpenCode 就不会去 pip install 第三方包。

第二步:增加 JSON 输出

运行了一遍基础版,输出不错,但我想要更灵活的导出格式。

我对 OpenCode 说:

> 给脚本加一个 --json 参数,当指定时输出 JSON 格式而不是表格。JSON 里包含 total、top_ips、status_codes、methods 四个字段。

OpenCode 的响应:

OpenCode 读了现有代码,理解了输出逻辑,然后:

argparse 里加了 --json 参数

新增一个格式化分支,用 json.dumps 输出

保持原有表格输出不变

改动精准,只动了 show_stats 函数和 main 里的参数定义。

> 关键要点: 给已有代码加功能时,OpenCode 会先读文件再改,不会推倒重来。你可以放心地"追加需求"。

第三步:完善错误处理

代码已经能用,但还没处理文件不存在的情况。

我对 OpenCode 说:

> 加上错误处理:文件不存在时给出友好提示并退出,其他异常也捕获并输出错误信息。

OpenCode 的响应:

OpenCode 在 main() 里包了一层 try/except,分别捕获 FileNotFoundError 和通用 Exception,并设置了 sys.exit(1) 退出码。改动 10 行。

> 关键要点: OpenCode 善于做"防御性编程"补充,你不用一条条列边界情况,说一句"加上错误处理"就够了。

第四步:加测试数据验证

代码完成,我需要确认效果。

我对 OpenCode 说:

> 帮我生成一个测试用的 sample.log,包含 20 条模拟 Nginx 日志,覆盖不同 IP、状态码和 URL。

OpenCode 的响应:

OpenCode 生成了 sample.log,包含 GET/POST 请求、200/301/404/500 等状态码,以及几个不同的 IP 地址。然后直接运行 python log_analyzer.py sample.log 验证了输出。

整个过程从开始到结束,实际只用了 4 分钟

完整代码

import sys
import json
import argparse
from collections import Counter
import re

LOG_PATTERN = re.compile(
    r'(?P<ip>\S+)\s+\S+\s+\S+\s+\[(?P<time>[^\]]+)\]\s+'
    r'"(?P<method>\S+)\s+(?P<url>\S+)\s+\S+"\s+'
    r'(?P<status>\d+)\s+(?P<size>\S+)\s+"(?P<referer>[^"]*)"\s+"(?P<ua>[^"]*)"'
)

def parse_log(line):
    m = LOG_PATTERN.match(line.strip())
    if m:
        return m.groupdict()
    return None

def analyze(filepath):
    entries = []
    with open(filepath, 'r', encoding='utf-8') as f:
        for line in f:
            entry = parse_log(line)
            if entry:
                entries.append(entry)
    return entries

def show_stats(entries, fmt='table'):
    ips = Counter(e['ip'] for e in entries)
    statuses = Counter(e['status'] for e in entries)
    methods = Counter(e['method'] for e in entries)
    urls = Counter(e['url'] for e in entries)

    if fmt == 'json':
        print(json.dumps({
            'total': len(entries),
            'top_ips': ips.most_common(10),
            'status_codes': dict(statuses),
            'methods': dict(methods),
            'top_urls': urls.most_common(10),
        }, indent=2, ensure_ascii=False))
    else:
        print(f"\n{'='*60}")
        print(f"  日志分析报告")
        print(f"{'='*60}")
        print(f"\n总请求数: {len(entries)}")
        print(f"\n{'─'*40}")
        print(f"  Top 10 IP")
        print(f"{'─'*40}")
        for ip, count in ips.most_common(10):
            print(f"  {ip:<20s} {count}")
        print(f"\n{'─'*40}")
        print(f"  状态码分布")
        print(f"{'─'*40}")
        for code, count in statuses.most_common():
            print(f"  {code:<20s} {count}")
        print(f"\n{'─'*40}")
        print(f"  请求方法")
        print(f"{'─'*40}")
        for method, count in methods.most_common():
            print(f"  {method:<20s} {count}")
        print(f"\n{'─'*40}")
        print(f"  Top 10 URL")
        print(f"{'─'*40}")
        for url, count in urls.most_common(10):
            print(f"  {url:<50s} {count}")
        print()

def main():
    parser = argparse.ArgumentParser(description='命令行日志分析工具')
    parser.add_argument('file', help='日志文件路径')
    parser.add_argument('--json', action='store_true', help='JSON格式输出')
    args = parser.parse_args()

    try:
        entries = analyze(args.file)
        fmt = 'json' if args.json else 'table'
        show_stats(entries, fmt)
    except FileNotFoundError:
        print(f'错误: 文件 "{args.file}" 不存在')
        sys.exit(1)
    except Exception as e:
        print(f'错误: {e}')
        sys.exit(1)

if __name__ == '__main__':
    main()

小结

这个工具如果手写,查正则、写 argparse、调格式,磕磕绊绊大概要 20 分钟。用 OpenCode 只需要 4 分钟,且每个步骤都能实时看到结果、随时追加需求。

OpenCode 的优势不在于"一次生成完美代码",而在于分步迭代——你先描述核心需求,跑起来,再逐步完善。这和你自己写代码的思路是一样的,区别在于:你只需要说,不需要敲。

感兴趣的话,试试用 OpenCode 给你的日常运维脚本加上 JSON 输出和错误处理,你会发现那些"懒得加"的功能,现在一句话就够了。