用 Python 按记录拆分大型 CSV:重复表头、保留多行字段与成功标记
大 CSV 超出导入工具的记录上限,或需要分批交给后续任务时,可以把它拆成多个小文件。但按文本行切割会截断带换行的字段,后续文件也可能缺少表头。
这篇教程面向初级到中级读者,用 Python 标准库按数据记录拆分 CSV:每片重复表头,保留记录顺序,并在完整成功后留下标记。它是大型 CSV 流式处理的后续实践,不需要安装 pandas。
1. 方法与输入约定
- 输入为逗号分隔、双引号包裹字段的 UTF-8 CSV,可以带 BOM;第一条记录是表头。
- 表头名称不能为空或完全由空白组成,也不能重复;每条数据的字段数必须与表头一致。
--rows只计算数据记录,不含每片的表头。仅含表头的输入产生零个分片,但仍创建成功标记;完全空文件报错。- 输出目录必须不存在,父目录必须已存在。失败时可能留下分片,没有
_SUCCESS就不能继续消费。
Python CSV 官方文档说明,读写文件应使用 newline=""。字段内可以包含换行,故物理行数不能当作记录数。输出保留解析后的字段值,但引号形式、BOM 和记录换行风格可能变化,不保证与原文件逐字节一致。
这个工具只检查基础结构,不判断 ID 唯一性、字段类型或标签是否合法。固定列名与顺序的业务规则,可先按 CSV 结构校验教程检查。
2. 准备环境与测试数据
使用 Python 3.9 或更新版本,无第三方依赖。本例在 Linux、Python 3.11.4 上验证。
- 在同一终端创建新演示目录,重复实验时换一个目录名:
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
- 把下一节代码保存为当前目录的
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. 运行与退出码
- 查看帮助,再按每片两条记录拆分:
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 是空文件,只在读完输入、关闭所有分片后创建。
- 按下面的退出码判断运行结果:
| 退出码 | 含义 |
|---|---|
| 0 | 所有分片关闭成功,标记已创建,标准输出包含 JSON 汇总。 |
| 1 | 输入格式、编码、读写或路径错误;原因写入标准错误。 |
| 2 | 命令行参数错误,例如 --rows 0。 |
已有目录会使本次调用返回 1;其中原有的 _SUCCESS 也不能证明本次调用成功。自动化流程要同时检查本次退出码和本次新目录里的标记。
5. 验证记录完整性和失败分支
- 对小型演示数据,重新解析并合并所有分片,确认表头、顺序和字段值:
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
这个检查为了便于观察,把小样本放入列表;拆分工具本身不这样做。不要把这个验证脚本原封不动用于占满内存的大文件。
- 构造第三条数据缺列的输入,确认前面的分片存在也不会误报成功。只在当前演示目录运行一次;重测请使用新目录:
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. 接入流程与排错
- 用条件语句处理失败,避免后续步骤读取残留分片:
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 把失败传给调用方。标记只证明本次程序走完了关闭流程,不是文件系统事务或断电持久性保证;复制输出目录后仍需校验内容。
- 根据实际错误处理:
| 现象 | 处理方法 |
|---|---|
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 解析器按记录拆分,可以保留字段内换行,并让每片都有完整表头。用新输出目录、本次退出码和成功标记区分完整结果与失败残留,再把分片交给后续任务。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-split-csv-records.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。