☰
百万行CSV大文件分割实战:按行切、按体积切与性能优化指南
2026/10/8 23:51:56 网站建设 项目流程

简介:这是一款面向数据分析师、大数据工程师及IT从业者的CSV大文件分割工具,专门解决百万行级CSV文件难以用普通编辑器或表格软件打开、处理效率低下的问题。工具支持按行数、文件大小或指定列值等条件将大文件拆分为多个小文件,便于后续导入、备份、传输与分析,并可配合Hadoop生态完成数据预处理。资源包共4个文件,以exe可执行程序为主,辅以txt使用说明、htm下载说明及url帮助链接,整体约533KB,解压后即可运行,无需复杂配置。目前已有5571人学习下载,适合需要处理大规模CSV数据、优化数据导入流程的读者参考使用,可快速掌握大文件分割的实用方法,提升日常数据处理效率。

1. 百万行 CSV 打不开也传不动:先搞清楚分割到底在解决什么

上周同事甩来一个 2.3GB 的 CSV,说 Excel 打开直接卡死,Python 用pd.read_csv跑了十分钟还没出结果,想丢进数据库又因为单文件太大被导入工具拒绝。这不是个例——从 football-data.co.uk 下载的历史赛事数据、业务系统导出的订单流水、日志平台吐出的埋点明细,动辄几百万行、几个 GB,单文件 CSV 几乎在所有环节都会撞墙。所谓「csv 大文件分割工具」,核心就一件事:把一个超大 CSV 按行数或体积切成若干小文件,让 Excel、pandas、数据库导入工具都能正常消费。它适合三类人:需要把原始数据喂给 BI 工具的分析师、要把 CSV 灌进数据库的后端工程师、以及做数据清洗前必须先「分而治之」的数据从业者。这一篇不讲虚的,从选型到代码到踩坑,把这件事讲透。

2. 分割前先想清楚:按行切、按体积切还是按字段切

2.1 三种切法的适用边界

很多人一上来就写代码,结果切完发现不能用。分割策略选错,后面全是返工。常见做法有三种:

按固定行数切:每 N 行一个文件,比如每 10 万行一个。这是最通用的方案,适合后续用 pandas 分批读取、或导入数据库时控制单批大小。缺点是如果每行字段长度差异大,切出来的文件体积不均匀。

按目标体积切:每个文件控制在 50MB 或 100MB 以内。适合有明确上传限制的场景,比如某些导入工具限制单文件不超过 100MB。实现上需要边写边累计字节数,比按行切稍复杂。

按字段值切:比如按日期、按地区、按用户 ID 哈希分片。这不是「分割」而是「分区」,适合后续要按维度分别处理的场景。football-data.co.uk 的赛季数据就常按赛季或联赛拆分。

选哪种,取决于你下游怎么用。如果只是想让 Excel 能打开,按行数切 5 万到 10 万行一个文件最省事;如果要喂给数据库批量导入,按行数切并配合批次大小更可控;如果下游要按维度并行处理,按字段切才有意义。

提示:不确定下游需求时,优先按行数切,因为行数是最容易预测和验证的维度。

2.2 为什么不能直接split命令了事

Linux 自带的split命令确实能切文件:

# 每 100000 行切一个文件,后缀为数字 split -l 100000 bigfile.csv part_

这条命令快、简单,但它有个致命问题:它按字节切,不认 CSV 的引号规则。如果某个字段值里包含换行符(CSV 规范允许字段内换行,只要用引号包裹),split会从字段中间切断,导致切出来的文件行结构错乱,后续解析直接报错。我见过一个订单备注字段里带换行的 CSV,用split切完,下游解析器直接抛unexpected end of data,排查了半天才发现是切分位置的问题。

所以,只要你的 CSV 可能包含带引号的换行字段,就必须用能正确解析 CSV 结构的工具,而不是按字节切的split。这也是为什么需要专门的 CSV 分割工具,而不是随便一个文件切割器。

2.3 用 Python 写一个能正确处理引号的分割脚本

下面这个脚本是我常用的基础版本,按行数切,正确处理引号和字段内换行:

import csv import os import sys def split_csv_by_rows(input_path, output_dir, rows_per_file=100000): """ 按行数分割 CSV,正确处理引号包裹的字段内换行。 rows_per_file: 每个输出文件的数据行数(不含表头) """ os.makedirs(output_dir, exist_ok=True) base_name = os.path.splitext(os.path.basename(input_path))[0] with open(input_path, 'r', encoding='utf-8', newline='') as f_in: reader = csv.reader(f_in) header = next(reader) # 读取表头 file_index = 1 row_count = 0 f_out = None writer = None def open_new_file(idx): path = os.path.join(output_dir, f"{base_name}_part{idx:03d}.csv") f = open(path, 'w', encoding='utf-8', newline='') w = csv.writer(f) w.writerow(header) # 每个分片都带表头 return f, w f_out, writer = open_new_file(file_index) for row in reader: writer.writerow(row) row_count += 1 if row_count >= rows_per_file: f_out.close() file_index += 1 row_count = 0 f_out, writer = open_new_file(file_index) f_out.close() print(f"完成,共生成 {file_index} 个文件,输出目录:{output_dir}") if __name__ == '__main__': split_csv_by_rows(sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 100000)

逻辑说明:用csv.reader逐行读取,它内部会正确处理引号转义和字段内换行,保证每次拿到的是一整条逻辑记录。每写满rows_per_file行就关闭当前文件、打开新文件,并重新写入表头。

参数说明:rows_per_file默认 10 万行,这是 Excel 能流畅打开的上限附近;encoding='utf-8'是通用选择,如果源文件是 GBK 需要改;newline=''是csv模块的硬性要求,否则 Windows 下会出现空行。

性能注意:这个版本是纯 Python 逐行处理,2GB 文件大约需要 1 到 2 分钟。如果追求更快,可以用pandas的chunksize参数,但 pandas 对字段内换行的处理在某些版本有 bug,我一般不用它做分割。

3. 大文件分割的性能瓶颈与内存控制

3.1 为什么不能一次性读入内存

一个 2GB 的 CSV,如果用pd.read_csv一次性读入,pandas 会把字符串列转成 object 类型,内存占用往往是文件体积的 3 到 5 倍,也就是 6 到 10GB。普通开发机 16GB 内存直接吃满,再叠加其他进程就 OOM。所以分割工具的第一原则是流式处理:读一行、写一行、丢一行,内存占用恒定在几 MB 级别。

上面那个脚本就是流式的,csv.reader是迭代器,不会把整个文件加载到内存。但要注意,如果你在循环里做了rows.append(row)这种操作,就退化成全量加载了。我见过有人为了「先统计总行数再分割」,先遍历一遍存到列表里,结果 2GB 文件直接爆内存。统计行数应该单独遍历一次,或者用wc -l快速估算。

3.2 用wc -l和du先摸清文件底细

动手切之前,先花几秒了解文件规模:

# 查看文件大小 du -h bigfile.csv # 统计行数(大文件可能需要几秒到几十秒) wc -l bigfile.csv # 查看前 5 行,确认表头和分隔符 head -5 bigfile.csv # 查看是否有引号包裹的换行(粗略判断) grep -c '"[^"]*$' bigfile.csv

wc -l对 2GB 文件大约 2 到 5 秒,可以接受。head -5确认分隔符是逗号还是分号、制表符,这直接影响csv.reader的delimiter参数。如果分隔符不是逗号,脚本里要加csv.reader(f_in, delimiter=';')。

3.3 分片大小怎么定:Excel、pandas、数据库三个视角

分片大小没有万能值,取决于下游:

下游工具建议单文件行数建议单文件体积原因
Excel5 万到 10 万行10 到 50MBExcel 打开超过 10 万行明显卡顿
pandas50 万到 100 万行50 到 200MB单次读取内存可控,减少文件切换开销
MySQL LOAD DATA10 万到 50 万行20 到 100MB单批导入过大容易超时或锁表
PostgreSQL COPY50 万到 100 万行50 到 200MBCOPY 性能好,可以适当放大

我一般默认 10 万行,这是兼容性最好的值。如果明确只喂给 pandas,会调到 50 万行减少文件数量。如果明确只给 Excel,会降到 5 万行。

注意:分片数量不是越多越好。100 个 10MB 的文件,管理成本远高于 10 个 100MB 的文件。在满足下游限制的前提下,尽量少切。

3.4 加一个进度显示,避免「黑匣子」焦虑

2GB 文件切几分钟,没有进度显示会让人怀疑是不是卡死了。加一个简单的进度输出:

import csv import os import sys import time def split_csv_with_progress(input_path, output_dir, rows_per_file=100000): os.makedirs(output_dir, exist_ok=True) base_name = os.path.splitext(os.path.basename(input_path))[0] total_bytes = os.path.getsize(input_path) processed_bytes = 0 last_report = time.time() with open(input_path, 'r', encoding='utf-8', newline='') as f_in: reader = csv.reader(f_in) header = next(reader) file_index = 1 row_count = 0 f_out = None writer = None def open_new_file(idx): path = os.path.join(output_dir, f"{base_name}_part{idx:03d}.csv") f = open(path, 'w', encoding='utf-8', newline='') w = csv.writer(f) w.writerow(header) return f, w f_out, writer = open_new_file(file_index) for row in reader: writer.writerow(row) row_count += 1 processed_bytes = f_in.tell() # 获取当前读取位置 if time.time() - last_report > 2: pct = processed_bytes / total_bytes * 100 print(f"\r进度:{pct:.1f}% 已生成 {file_index} 个文件", end='') last_report = time.time() if row_count >= rows_per_file: f_out.close() file_index += 1 row_count = 0 f_out, writer = open_new_file(file_index) f_out.close() print(f"\n完成,共 {file_index} 个文件") if __name__ == '__main__': split_csv_with_progress(sys.argv[1], sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 100000)

f_in.tell()在文本模式下返回的是不透明的数字,但用于计算百分比是够用的。每 2 秒刷新一次进度,不会拖慢主循环。

4. 分割后的验证与常见翻车现场

4.1 切完必须做的三项校验

切完不是就完了,至少要验证三件事:

行数总和一致:所有分片的数据行数加起来,应该等于原文件行数减 1(表头)。用wc -l快速核对:

# 原文件行数 wc -l bigfile.csv # 所有分片行数总和(含每个文件的表头) wc -l output/*.csv | tail -1

如果分片行数总和比原文件多出「分片数」行,那是正常的,因为每个分片都带了一行表头。

字段数一致:随便抽几个分片,检查每行的字段数是否和表头一致。字段数错乱通常意味着切分位置切在了引号字段内部。

首尾分片可正常解析:用 pandas 读第一个和最后一个分片,确认没有解析错误:

import pandas as pd df_first = pd.read_csv('output/bigfile_part001.csv', nrows=100) df_last = pd.read_csv('output/bigfile_part010.csv', nrows=100) print(df_first.shape, df_last.shape)

4.2 编码问题:GBK 和 UTF-8 的反复横跳

国内业务系统导出的 CSV 很多是 GBK 编码,用 UTF-8 读会直接抛UnicodeDecodeError。判断方法:

file -i bigfile.csv

如果输出charset=iso-8859-1或charset=unknown,大概率是 GBK。脚本里把encoding='utf-8'改成encoding='gbk'即可。但要注意,如果文件里混了 UTF-8 和 GBK 内容(比如从多个系统拼接来的),那就需要errors='replace'兜底,但会丢字符,最好先统一编码。

4.3 表头重复导致数据库导入报错

每个分片都带表头,这对 Excel 和 pandas 是友好的,但对某些数据库导入工具是灾难——它们会把第二行开始的所有行都当数据,表头行变成一条脏数据。解决办法有两个:要么分割时不写表头(加个参数控制),要么导入时跳过第一行。我一般保留表头,因为可读性更重要,导入时用IGNORE 1 LINES或skiprows=1处理。

4.4 避坑清单:五个血泪教训

现象一:切出来的文件用 Excel 打开乱码。原因:源文件是 GBK,脚本用 UTF-8 读写。 解决:确认源文件编码,脚本里统一用encoding='gbk',或者先用iconv转成 UTF-8 再切。

现象二:分片行数对不上,少了若干行。原因:CSV 字段内有换行符,wc -l统计的是物理行数,不是逻辑行数。 解决:用csv.reader统计逻辑行数,或者用 pandas 的len(df)核对。

现象三:切分后某些行字段数多了一个。原因:字段值里包含逗号但没加引号,csv.reader按逗号切分导致字段错位。 解决:这是源文件本身不合规,需要在分割前先修复,或者用delimiter指定其他分隔符。

现象四:脚本跑了一半报MemoryError。原因:在循环里累积了所有行,比如为了统计总行数先存列表。 解决:改成流式处理,统计行数单独遍历或用wc -l。

现象五:分片文件数量太多,后续处理脚本要写循环遍历。原因:分片太小,比如每 1 万行一个,2GB 文件切出几百个。 解决:根据下游调整rows_per_file,一般不低于 5 万行。

5. 进阶:按体积切、并行切与自动化流水线

5.1 按目标体积切分的实现

有些场景明确要求单文件不超过 50MB,按行数切不好控制,需要按字节累计:

import csv import os def split_csv_by_size(input_path, output_dir, max_bytes=50*1024*1024): os.makedirs(output_dir, exist_ok=True) base_name = os.path.splitext(os.path.basename(input_path))[0] with open(input_path, 'r', encoding='utf-8', newline='') as f_in: reader = csv.reader(f_in) header = next(reader) file_index = 1 current_size = 0 f_out = None writer = None def open_new_file(idx): path = os.path.join(output_dir, f"{base_name}_size{idx:03d}.csv") f = open(path, 'w', encoding='utf-8', newline='') w = csv.writer(f) w.writerow(header) return f, w f_out, writer = open_new_file(file_index) for row in reader: # 估算当前行写入后的字节数 line_bytes = len(','.join(row).encode('utf-8')) + 1 if current_size + line_bytes > max_bytes and current_size > 0: f_out.close() file_index += 1 current_size = 0 f_out, writer = open_new_file(file_index) writer.writerow(row) current_size += line_bytes f_out.close() print(f"完成,共 {file_index} 个文件") if __name__ == '__main__': split_csv_by_size('bigfile.csv', 'output', 50*1024*1024)

这里用len(','.join(row).encode('utf-8'))估算每行字节数,比实际写入后tell()略快,误差在可接受范围。如果要求精确,可以在writer.writerow后调用f_out.tell(),但频繁调用会拖慢速度。

5.2 用多进程加速:按字节偏移预切再修正

单进程流式处理 2GB 文件大约 1 到 2 分钟,10GB 文件就要 10 分钟以上。如果追求更快,可以用多进程:先按字节偏移把文件粗略切成 N 块,每个进程处理一块,然后在块边界处修正——找到最近的换行符,确保不从字段中间切断。这个方案实现复杂,容易出 bug,我一般只在 10GB 以上文件才用。对大多数场景,单进程流式已经够用。

5.3 把分割脚本挂到自动化流水线

如果每天都有大 CSV 要处理,可以写一个 shell 包装脚本,配合cron或任务调度:

#!/bin/bash # split_daily.sh INPUT_DIR="/data/incoming" OUTPUT_DIR="/data/split" ROWS=100000 for f in "$INPUT_DIR"/*.csv; do base=$(basename "$f" .csv) python3 split_csv_with_progress.py "$f" "$OUTPUT_DIR/$base" "$ROWS" done

配合find加时间过滤,只处理当天新增文件。输出目录按原文件名建子目录,避免分片混在一起。

5.4 一个验证分割质量的小技巧

切完之后,我习惯用md5sum对「原文件所有数据行排序后的哈希」和「所有分片数据行排序后的哈希」做对比。如果一致,说明没有丢行、没有多行、没有字段错位。命令如下:

# 原文件去掉表头后排序取哈希 tail -n +2 bigfile.csv | sort | md5sum # 所有分片去掉表头后合并排序取哈希 for f in output/*.csv; do tail -n +2 "$f"; done | sort | md5sum

两个哈希一致,就可以放心把分片交给下游了。这个校验对几十万行的文件几秒完成,对几百万行也就几十秒,比逐行核对省事得多。

我做了这么多年数据工程,最大的教训就是:分割工具本身不难,难的是切完之后下游能不能无缝用。所以每次切完,我都会拿第一个和最后一个分片实际跑一遍下游流程,确认没问题再批量处理。这个习惯帮我省了无数次返工。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询