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

项目介绍

在日常开发中,我们经常需要快速了解一个 CSV 数据文件的结构和内容——有多少行、每列是什么类型、数值列的分布情况如何。通常的做法是打开 Excel 或写一段临时脚本,但都不够快捷。

今天我们用 OpenCode 从零开始,仅用自然语言对话,4 分钟内写出一个命令行 CSV 分析工具 csvstat。它接受一个 CSV 文件路径,自动输出:

  • 行列数量、列名列表
  • 每列的数据类型(数值型/分类型)及缺失值数量
  • 数值列:最小值、最大值、平均值、中位数、标准差
  • 分类型:唯一值数量、出现频率 Top 5

技术栈:Python 3,仅用标准库(csvcollectionsstatistics),零外部依赖。

准备工作

  • Python 3.8+
  • 安装 OpenCode:参考 opencode.ai 安装指引
  • 准备一个测试 CSV 文件(可自己造一个 sample.csv

实践过程

第一步:提出需求,生成骨架

打开终端,进入项目目录,对 OpenCode 说:

> 我对 OpenCode 说:
> 帮我写一个 Python 命令行工具 csvstat.py,接收 CSV 文件路径作为参数,读取文件后输出行数、列数、列名,以及每列的数据类型判断(数值型还是分类型)。

> OpenCode 的响应:
> OpenCode 自动生成了完整的 Python 脚本,包含 csv.DictReader 读取文件、detect_type() 函数判断列类型、以及 sys.argv 处理命令行参数。代码约 40 行,可以直接运行。

我运行 python csvstat.py sample.csv,立刻看到了行列统计和类型判断结果。此时核心框架已就位。

要点: 需求描述越具体越好——明确输入(文件路径)、输出(具体统计项)、边界(类型判断规则)。

第二步:补充数值统计

> 我对 OpenCode 说:
> 对数值型列加上最小值、最大值、平均值、中位数和标准差的计算,用 statistics 模块。分类列显示唯一值个数和前 5 个最常见的值。

> OpenCode 的响应:
> OpenCode 识别到需要引入 statistics 模块和 collections.Counter,在 analyze() 函数中新增了分支逻辑——数值列调用 mean()median()stdev(),分类列用 Counter.most_common(5) 统计频率。修改精准,没有改动无关代码。

新增约 20 行,统计功能直接到位。用 statistics.stdev() 时 OpenCode 还自动加了 len(nums) > 1 的判空保护,避免了只有一条数据时的异常。

要点: 增量迭代是 OpenCode 的核心使用模式——先跑通骨架,再逐步加功能。每次只提一个明确需求,OpenCode 的修改会更精准。

第三步:处理边界情况

> 我对 OpenCode 说:
> 加上异常处理:文件不存在时给出友好提示。另外支持自定义分隔符参数,比如制表符分隔的 TSV 文件也能处理。

> OpenCode 的响应:
> OpenCode 在文件读取外层加了 try-except,捕获 FileNotFoundError 输出 "文件不存在" 提示。分隔符方面,在命令行参数中增加了可选的第二个参数 delimiter,并对 \t 做了转义处理。同时将 encoding='utf-8' 改为 utf-8-sig,以兼容带 BOM 的 CSV 文件(这是 Excel 导出的常见格式)。

要点: 让 OpenCode 处理边界情况时,把场景说清楚("文件不存在"、"TSV 文件"、"Excel 导出的 CSV"),它会给出比你预期更周全的方案。

完整代码

#!/usr/bin/env python3
"""csvstat - 命令行 CSV 数据分析工具"""

import csv
import sys
from collections import Counter
from statistics import mean, median, stdev


def detect_type(values):
    """检测列数据类型:numeric 或 categorical"""
    score = 0
    total = 0
    for v in values:
        if v.strip() == '':
            continue
        total += 1
        try:
            float(v)
            score += 1
        except ValueError:
            pass
    return 'numeric' if total > 0 and score / total >= 0.5 else 'categorical'


def read_csv(filepath, delimiter=','):
    with open(filepath, 'r', encoding='utf-8-sig') as f:
        reader = csv.DictReader(f, delimiter=delimiter)
        headers = reader.fieldnames
        rows = [row for row in reader]
    return headers, rows


def analyze(headers, rows):
    print(f"总行数: {len(rows)}")
    print(f"总列数: {len(headers)}")
    print(f"列名: {', '.join(headers)}\n")

    for col in headers:
        values = [row.get(col, '') for row in rows]
        non_empty = [v for v in values if v.strip() != '']
        col_type = detect_type(non_empty)
        missing = len(values) - len(non_empty)

        print(f"┌─ {col}")
        print(f"├─ 类型: {col_type}  |  缺失: {missing}  |  有效: {len(non_empty)}")

        if col_type == 'numeric':
            nums = [float(v) for v in non_empty]
            print(f"├─ 最小值: {min(nums):.2f}")
            print(f"├─ 最大值: {max(nums):.2f}")
            print(f"├─ 平均值: {mean(nums):.2f}")
            print(f"├─ 中位数: {median(nums):.2f}")
            if len(nums) > 1:
                print(f"└─ 标准差: {stdev(nums):.2f}")
            else:
                print(f"└─ 标准差: N/A")
        else:
            counter = Counter(non_empty)
            print(f"├─ 唯一值: {len(counter)}")
            top = counter.most_common(5)
            for i, (val, cnt) in enumerate(top):
                prefix = '└─' if i == len(top) - 1 else '├─'
                print(f"{prefix} [{cnt}次] {val}")
        print()


def main():
    if len(sys.argv) < 2:
        print("用法: python csvstat.py <CSV文件路径> [分隔符]")
        print("示例: python csvstat.py data.csv")
        print("示例: python csvstat.py data.tsv '\\t'")
        sys.exit(1)

    filepath = sys.argv[1]
    delimiter = sys.argv[2] if len(sys.argv) > 2 else ','

    try:
        if delimiter == '\\t':
            delimiter = '\t'
        headers, rows = read_csv(filepath, delimiter)
        analyze(headers, rows)
    except FileNotFoundError:
        print(f"错误: 文件 '{filepath}' 不存在")
        sys.exit(1)
    except Exception as e:
        print(f"错误: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()

小结

用 OpenCode 开发这个 CSV 分析工具,全程没有手写一行代码,只靠自然语言描述需求,4 分钟内从零到完整可运行。实际只做了三件事:

描述整体需求 → OpenCode 生成主流程框架

补充统计细节 → OpenCode 精准插入新功能

加固边界情况 → OpenCode 给出了比预期更周全的处理

对比传统开发方式:手动查 csv.DictReader API、写统计逻辑、处理异常分支,保守估计需要 20-30 分钟。OpenCode 将效率提升了 5 倍以上,而且代码质量不输手写——命名清晰、逻辑分层、边界处理到位。

这个工具后续还可以用 OpenCode 继续扩展:加 --chart 输出 ASCII 柱状图、加 --filter 按条件过滤行、甚至导出为 Markdown 表格——真正实现了"想法即代码"。