春江暮客

春江暮客的个人学习分享网站

用 Python 流式处理大型 CSV:逐条检查并统计标签

2026-09-28 技术
用 Python 流式处理大型 CSV:逐条检查并统计标签

CSV 文件很大,但你可能只想知道每种标签各有多少条记录。把整个文件读进列表,会为这个简单问题占用多余内存。逐条读取并更新几个计数器,就能完成统计。

这篇教程用 Python 标准库写一个命令行工具:读取样本标签表,检查每条记录,只有整个文件通过检查后才输出 JSON 汇总。它适合用作连接蛋白质序列与标签之前的一项基础检查。

1. 先约定输入格式

示例接受 UTF-8 编码的 CSV,可以带 BOM。文件必须按下面的顺序包含两列:

列名 允许的值
sample_id 非空字符串,首尾不能有空白字符
label 严格限定为 0、1 或 unknown

这里用 unknown 表示标签尚不可用,不能把它直接当作负标签。下面的数据都是为教程编写的示例。

工具统计的是记录数,不是去重后的样本数。同一个 ID 出现多次,就会重复计数;唯一性检查需要另外制定规则,并保存额外状态。ID 始终按字符串处理,001 不会变成 1。只有表头的文件合法,计数全为零;完全空白的文件缺少表头,会报错。

2. 下载并运行

使用 Python 3.11 或更新版本,不需要安装第三方库。示例已在 Python 3.14.7 上运行检查。

下载 summarize_labels.py,保存到新目录,然后创建输入并运行:

cat > labels.csv <<'CSV'
sample_id,label
001,1
002,0
"sample,003",unknown
004,1
005,0
006,1
CSV
python3 summarize_labels.py --help
python3 summarize_labels.py labels.csv

预期输出:

{
  "records": 6,
  "label_counts": {
    "0": 2,
    "1": 3,
    "unknown": 1
  }
}

"sample,003" 中的逗号属于 ID。CSV 解析器能识别这个带引号的字段,直接使用 line.split(",") 则会错误地增加字段数。带引号的字段也可以包含换行,因此一行文本未必就是一条完整记录。格式处理方式见 Python CSV 文档。

3. 完整脚本

"""Stream a UTF-8 CSV with exactly sample_id,label columns; count 0/1/unknown."""
import argparse
import csv
import json


def summarize(path):
    counts = {"0": 0, "1": 0, "unknown": 0}
    records = 0
    with open(path, encoding="utf-8-sig", newline="") as handle:
        reader = csv.reader(handle, strict=True)
        try:
            header = next(reader, None)
            if header != ["sample_id", "label"]:
                raise ValueError("Expected header: sample_id,label (in that order)")
            for row in reader:
                where = f"Record ending at physical line {reader.line_num}"
                if len(row) != 2:
                    raise ValueError(f"{where}: expected 2 fields, got {len(row)}")
                sample_id, label = row
                if not sample_id or sample_id != sample_id.strip():
                    raise ValueError(f"{where}: sample_id is empty or has outer whitespace")
                if label not in counts:
                    raise ValueError(f"{where}: label must be 0, 1, or unknown")
                counts[label] += 1
                records += 1
        except csv.Error as exc:
            raise ValueError(f"CSV parse error near physical line {reader.line_num}: {exc}") from exc
    return {"records": records, "label_counts": counts}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input", help="Path to the input CSV")
    args = parser.parse_args()
    try:
        result = summarize(args.input)
    except (OSError, UnicodeError, ValueError) as exc:
        parser.exit(1, f"error: {exc}\n")
    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()

打开文件时使用 newline="",让 CSV 解析器处理换行约定。utf-8-sig 支持开头带有可选 BOM 的 UTF-8 输入。解析器返回字符串,strict=True 会在检测到不合法的 CSV 输入时抛出错误。这些设置不能代替业务检查,因此脚本仍然明确检查表头、字段数量、ID 和标签。相关选项见 Python CSV 方言与格式参数。

汇总会在文件成功读完并关闭后才打印。如果第 500,000 条记录出错,程序以状态码 1 退出,把原因写到标准错误,标准输出中不会出现汇总。前面的记录虽然已经在内部计数,但不会把不完整的计数当成最终结果。

4. 检查失败时的表现

遇到未约定的标签,工具应明确报错:

cat > bad-labels.csv <<'CSV'
sample_id,label
001,1
002,maybe
CSV
python3 summarize_labels.py bad-labels.csv
echo "exit=$?"

预期错误和退出状态:

error: Record ending at physical line 3: label must be 0, 1, or unknown
exit=1

错误中的行号指向记录结束时的物理行。即使带引号的记录跨越多行,也能据此定位。输入文件只以读取方式打开。

如果用 shell 重定向保存结果,要注意 shell 可能在 Python 检查输入之前就创建或清空目标文件。自动化任务应先写临时路径,确认退出状态成功后,再替换上一次成功的报告。

5. 流式读取节省什么

脚本不会保留已经处理过的记录。主要工作数据是一条解析后的记录、输入缓冲区和三个计数器,不会累积成一个随记录数增长的列表。计数增加时,整数的存储也会增长;单条记录特别大时,仍然需要相应内存。这并不是严格的内存上限。

标签集合固定,是这里的关键条件。如果改成用字典统计任意样本 ID,就需要为每个不同的 ID 保存一个条目。即使输入逐条读取,字典仍然可能不断增大。同样,调用 list(reader) 会把剩余记录全部装入内存。

流式读取也无法省去读取和解析整个文件的时间。它适合总数等小规模汇总。如果任务需要全局排序、精确去重或大型连接,而中间数据已经放不进内存,就要选择其他存储方案。

本地检查覆盖了六条记录的示例、0 和 1 各占一半的 100,000 条记录、BOM 和 CRLF、带引号的逗号与换行、仅含表头的文件、重复 ID,以及格式错误的记录。这些检查验证了相应行为,不是速度基准测试。

6. 常见问题

现象 处理方法
Expected header: sample_id,label 按此顺序导出这两个列名,并移除多余列。
expected 2 fields 检查缺失字段、空记录和未加引号的逗号。
label must be 0, 1, or unknown 运行前明确映射来源标签;空格和空标签都会被拒绝。
UTF-8 解码错误 确认来源编码并转换为 UTF-8,不要静默丢弃无法解码的字节。
CSV 字段长度错误 检查字段内容与导出格式;如果确实需要这么长的字段,在审查过的修改版中设置合适的 csv.field_size_limit()。

为了复现统计结果,读取期间不要修改源文件,并用数据集校验和清单记录文件的校验值。这个工具不会锁定文件,也不会创建快照。

7. 让汇总状态保持简单

每次读取一条 CSV 记录,检查后只保留结果需要的状态。这个标签统计任务只需要三个计数器;ID 唯一性检查和数据集连接,应当另行处理。

友情链接

其它