春江暮客

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

用 Python 按记录拆分大型 CSV:重复表头、保留多行字段与成功标记

2026-10-21 技术
用 Python 按记录拆分大型 CSV:重复表头、保留多行字段与成功标记

大 CSV 超出导入工具的记录上限,或需要分批交给后续任务时,可以把它拆成多个小文件。但按文本行切割会截断带换行的字段,后续文件也可能缺少表头。

这篇教程面向初级到中级读者,用 Python 标准库按数据记录拆分 CSV:每片重复表头,保留记录顺序,并在完整成功后留下标记。它是大型 CSV 流式处理的后续实践,不需要安装 pandas。

1. 方法与输入约定

  1. 输入为逗号分隔、双引号包裹字段的 UTF-8 CSV,可以带 BOM;第一条记录是表头。
  2. 表头名称不能为空或完全由空白组成,也不能重复;每条数据的字段数必须与表头一致。
  3. --rows 只计算数据记录,不含每片的表头。仅含表头的输入产生零个分片,但仍创建成功标记;完全空文件报错。
  4. 输出目录必须不存在,父目录必须已存在。失败时可能留下分片,没有 _SUCCESS 就不能继续消费。

Python CSV 官方文档说明,读写文件应使用 newline=""。字段内可以包含换行,故物理行数不能当作记录数。输出保留解析后的字段值,但引号形式、BOM 和记录换行风格可能变化,不保证与原文件逐字节一致。

这个工具只检查基础结构,不判断 ID 唯一性、字段类型或标签是否合法。固定列名与顺序的业务规则,可先按 CSV 结构校验教程检查。

2. 准备环境与测试数据

使用 Python 3.9 或更新版本,无第三方依赖。本例在 Linux、Python 3.11.4 上验证。

  1. 在同一终端创建新演示目录,重复实验时换一个目录名:
python3 --version
mkdir csv-split-demo
cd csv-split-demo
python3 - <<'PY_DATA'
import csv

with open("input.csv", "w", encoding="utf-8-sig", newline="") as handle:
    writer = csv.writer(handle)
    writer.writerow(["id", "note"])
    writer.writerows([
        ["001", "plain"],
        ["002", "comma, inside"],
        ["003", "first line\nsecond line"],
        ["004", 'say "hello"'],
        ["005", "最后一条"],
    ])
PY_DATA
  1. 把下一节代码保存为当前目录的 split_csv.py。测试数据有五条记录,包含逗号、字段内换行、引号、中文和开头的 BOM。

3. 完整实现

"""Split a UTF-8 CSV by data records, repeating its header. Python 3.9+."""
import argparse
import csv
import json
from pathlib import Path
import sys


def positive_int(value):
    number = int(value)
    if number < 1:
        raise argparse.ArgumentTypeError("must be greater than zero")
    return number


def split_csv(source, output, rows_per_part):
    records = chunks = 0
    handle = None
    with source.open(encoding="utf-8-sig", newline="") as src:
        reader = csv.reader(src, strict=True)
        try:
            header = next(reader, None)
            if not header or any(not name.strip() for name in header):
                raise ValueError("missing header or empty column name")
            if len(set(header)) != len(header):
                raise ValueError("duplicate column names")
            output.mkdir()  # Existing paths are rejected, including empty dirs.
            for row in reader:
                if len(row) != len(header):
                    raise ValueError(
                        f"physical line {reader.line_num}: "
                        f"expected {len(header)} fields, got {len(row)}"
                    )
                if records % rows_per_part == 0:
                    if handle is not None:
                        handle.close()
                    chunks += 1
                    handle = (output / f"part-{chunks:08d}.csv").open(
                        "x", encoding="utf-8", newline=""
                    )
                    writer = csv.writer(handle, lineterminator="\n")
                    writer.writerow(header)
                writer.writerow(row)
                records += 1
        except csv.Error as exc:
            raise ValueError(
                f"CSV error near physical line {reader.line_num}: {exc}"
            ) from exc
        finally:
            if handle is not None:
                handle.close()
    # Only create the marker after input and all part files close successfully.
    with (output / "_SUCCESS").open("x", encoding="utf-8"):
        pass
    return {"records": records, "chunks": chunks, "output": str(output)}


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("input", type=Path)
    parser.add_argument("output", type=Path, help="new directory; parent must exist")
    parser.add_argument("--rows", type=positive_int, default=100000,
                        help="data records per part (default: 100000)")
    args = parser.parse_args()
    try:
        report = split_csv(args.input, args.output, args.rows)
    except (OSError, UnicodeError, ValueError) as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(json.dumps(report, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

输入用 utf-8-sig 解码,输出用不带 BOM 的 UTF-8。有关可选 BOM 的处理,见 Python utf-8-sig 文档。strict=True 会报告解析器检测出的 CSV 格式错误;脚本另外检查字段数,避免把缺列当作正常数据。

Path.mkdir() 默认拒绝已有目录;每个分片也用 "x" 模式创建。请使用自己控制的目录,运行期间不要让别的程序修改输入或输出。

同时只打开一个分片,内存随当前记录和表头大小变化,不随总记录数增长。单条超大记录仍会占用内存,也可能触发 CSV 字段长度限制。分片上限按记录数计,不是按字节数计,不能保证每片低于上传平台的 MB 限额。

4. 运行与退出码

  1. 查看帮助,再按每片两条记录拆分:
python3 split_csv.py --help
python3 split_csv.py input.csv parts --rows 2
{"records": 5, "chunks": 3, "output": "parts"}
parts/
  part-00000001.csv
  part-00000002.csv
  part-00000003.csv
  _SUCCESS

三个分片分别包含 2、2、1 条数据,各自另带一条表头。恰好整除时不会额外创建空分片。_SUCCESS 是空文件,只在读完输入、关闭所有分片后创建。

  1. 按下面的退出码判断运行结果:
退出码 含义
0 所有分片关闭成功,标记已创建,标准输出包含 JSON 汇总。
1 输入格式、编码、读写或路径错误;原因写入标准错误。
2 命令行参数错误,例如 --rows 0。

已有目录会使本次调用返回 1;其中原有的 _SUCCESS 也不能证明本次调用成功。自动化流程要同时检查本次退出码和本次新目录里的标记。

5. 验证记录完整性和失败分支

  1. 对小型演示数据,重新解析并合并所有分片,确认表头、顺序和字段值:
python3 - <<'PY_CHECK'
import csv
from pathlib import Path

with open("input.csv", encoding="utf-8-sig", newline="") as handle:
    original = list(csv.reader(handle))
assert Path("parts/_SUCCESS").is_file()
files = sorted(Path("parts").glob("part-*.csv"))
assert len(files) == 3
combined = []
sizes = []
for file in files:
    with file.open(encoding="utf-8", newline="") as handle:
        rows = list(csv.reader(handle))
    assert rows[0] == original[0], file
    sizes.append(len(rows) - 1)
    combined.extend(rows[1:])
assert sizes == [2, 2, 1], sizes
assert combined == original[1:]
assert combined[2][1] == "first line\nsecond line"
print("OK: headers, order, 2/2/1 records, multiline field")
PY_CHECK

这个检查为了便于观察,把小样本放入列表;拆分工具本身不这样做。不要把这个验证脚本原封不动用于占满内存的大文件。

  1. 构造第三条数据缺列的输入,确认前面的分片存在也不会误报成功。只在当前演示目录运行一次;重测请使用新目录:
python3 - <<'PY_FAILURE'
from pathlib import Path
import subprocess
import sys

Path("bad.csv").write_text("id,note\n001,ok\n002,ok\n003\n", encoding="utf-8")
result = subprocess.run(
    [sys.executable, "split_csv.py", "bad.csv", "bad-parts", "--rows", "2"],
    capture_output=True, text=True, check=False,
)
assert result.returncode == 1, result
assert result.stdout == "" and "expected 2 fields, got 1" in result.stderr
assert Path("bad-parts/part-00000001.csv").is_file()
assert not Path("bad-parts/_SUCCESS").exists()
print("OK: late failure returns 1 and leaves no success marker")
PY_FAILURE

错误发生后会保留 bad-parts 供排查,重跑需要换一个新输出目录。先修正原始数据,不要手工补上 _SUCCESS。

6. 接入流程与排错

  1. 用条件语句处理失败,避免后续步骤读取残留分片:
if python3 split_csv.py input.csv parts-next --rows 100000; then
  test -f parts-next/_SUCCESS && printf 'Ready for the next step\n'
else
  printf 'Split failed; do not consume partial files\n' >&2
fi

这段交互式演示只打印提示。放入自动化脚本时,失败分支还应使用 exit 1 把失败传给调用方。标记只证明本次程序走完了关闭流程,不是文件系统事务或断电持久性保证;复制输出目录后仍需校验内容。

  1. 根据实际错误处理:
现象 处理方法
File exists 选择新的输出目录,保留旧结果用于比对。
expected ... fields 按错误中的物理行位置检查源数据的缺列、多列或空白记录。
unexpected end of data 检查导出是否截断,或引号是否闭合,重新导出后再试。
UTF-8 解码失败 按 CSV 编码转换教程确认编码后严格转换,别用 errors="ignore" 丢字节。
field larger than field limit 核实是否为真实大字段;确认需求后显式调整 csv.field_size_limit(),不要盲目设为无限。
磁盘空间不足 分片并不减少总数据量;先按磁盘空间预算检查预留表头重复和文件系统开销。

没有分片但有成功标记,可能是仅含表头的合法输入,先看 JSON 的 records。若后续要求至少一条记录,应单独拒绝零记录结果。

7. 小结

用 CSV 解析器按记录拆分,可以保留字段内换行,并让每片都有完整表头。用新输出目录、本次退出码和成功标记区分完整结果与失败残留,再把分片交给后续任务。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging deployment desktop-app devtools disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它