资讯动态

CSV大文件分割工具:按行/字节分片与Hive落地实践

发布时间:2026/10/29 12:41:13 来源:尧图企业网站定制
简介这是一款面向数据分析、大数据处理从业者及 Hadoop 使用者的 CSV 大文件分割工具针对超百万行文件在普通编辑器或表格软件中难以打开、导入和传输的问题提供按行数或文件大小拆分 CSV 的轻量方案。压缩包为 rar 格式共 4 个文件约 533KB包含可执行主程序、txt 使用说明、htm 下载说明及 url 帮助链接解压后即可按说明设置参数并运行便于快速把大文件切成多个小文件。已有 5579 人学习下载说明其在数据预处理场景中具有一定实用性。读者可借此完成大文件拆分、分批导入、备份传输和后续分析等操作也能配合 Hadoop 生态下的数据准备流程使用减少因单文件过大造成的性能与操作障碍。1. csv 大文件分割工具从 12GB 单文件到可跑批的 part 分片12 GB 的订单明细导出成单个 csvExcel 直接拒绝加载pandas.read_csv 跑到一半机器开始 swap往 Hadoop 上传时又卡在单文件上传超时——这类场面在离线数据链路里几乎是固定节目。csv 大文件分割工具要解决的就是这一层把一个大 csv 按行数或字节切成若干 part 分片每份保留表头、命名有序、能被下游按批消费。它不负责清洗也不负责压缩只做分割数据这一件小事但做不好就会在下游放大成大问题。适合每天要处理导出日志、埋点明细、对账文件的人也适合第一次往 Hive 外表挂数据、被单文件大小卡住的人。下面按「先想清楚怎么切、再写核心循环、然后处理脏数据、最后对账落地」的顺序拆开讲中间会把我自己翻过车的几处标出来。2. 切分策略选型按行切、按字节切与表头复用的取舍动手写代码之前先确认一件事切出来的分片是给谁用的。给 Hive 外表、给 Spark 的 read.csv、给批处理脚本逐行读这三种下游对「一份多大」的诉求完全不同。选错策略的代价不是多跑几秒而是下游任务报错或者悄悄算错数。2.1 按行切与按字节切的适用边界按行切是最常见的选择逻辑是「每个文件固定 N 条记录」。它的好处是每份数据量可控、便于并行分发缺点是对文件体积没有直接控制——一行 200 字节和一行 2 KB 混在一起时同样 10 万行可能差出一个数量级。按字节切追求的是「每个文件不超过 M 字节」典型目标是 128 MB 或 256 MB对齐 HDFS 的块大小。好处是上传和读取的 I/O 更均匀缺点是如果直接在任意字节位置断开会把一条记录劈成两半下游读到半行数据必须回退到最近的记录边界。我一般按下面的场景做选择场景推荐策略原因下游是 Hive 外表或 Spark read.csv按字节切对齐 128/256 MB与块大小一致避免跨块读下游是逐行处理的批处理脚本按行切固定 5 万20 万行单份处理时间可预估行长度差异极大含长文本字段按字节切 记录边界回退按行切会导致分片体积失控需要严格等分做多进程并行按行切最后一份可能偏小行数即任务数调度简单还有一种混合做法设定目标字节数同时设一个行数下限避免出现「只有 3 行但每行都特别长」的畸形分片。这个下限一般设成 1000 行就够了。2.2 表头复用的三种处理方式表头header看起来只是第一行但它决定了后面三件事。第一种是只给第一份带表头其余分片不带。这种做法适合下游自己知道 schema、按位置解析的场景优点是省空间、不会出现「表头被当成数据」的问题缺点是单独拿某一份出来看的时候不知道列名排查很麻烦。第二种是每份都复写表头。这是通用性最好的做法单文件自包含随便挑一份都能独立分析。代价是下游如果用 Hive 建外表并且开了skip.header.line.count1那正好抵消如果没开表头会被当成一条数据行混进去行数对不上还会在数值列上产生脏数据。第三种是拆成两个文件一个header.csv放表头若干part-xxxxx.csv只放数据。这个在跨团队交付时最清晰但要求下游多读一次文件。我的默认选择是每份复写表头同时把「下游是否 skip 首行」写进交付说明。这个习惯是被坑出来的某次给一个批处理任务供数对方按无表头解析结果每个分片的第一行都变成了数值列里的字符串聚合结果直接错位。2.3 输出命名与目录约定命名不是审美问题是序问题。part-00000.csv、part-00001.csv这种零填充的编号能让ls、sort、glob 展开的顺序稳定下游做增量时也不会因为part-10排在part-2前面而出错。输出目录建议要求「不存在或为空」。很多工具默认追加写重跑一次就在旧分片旁边多出一份最后行数翻倍。我一般会在脚本开头做一次显式检查目录非空就报错退出让人自己决定是删还是换路径。另外编号位数要跟预估分片数匹配。预估 50 份却用两位编号跑到第 100 份就开始出现part-100和part-99排序错乱这种问题在小数据上永远测不出来。2.4 用现成命令还是自己写系统自带的split命令能用split -l 100000 big.csv part_一行搞定。但它有两个明确限制一是按物理行切遇到引号内含换行的字段会切坏记录二是默认不给每个分片补表头需要额外处理。awk也能做awk NR%1000001{...}之类的写法在纯数值 csv 上很好用但同样绕不开引号换行和编码问题。所以只要数据里可能出现带引号的文本字段地址、备注、评论我就写一个几十行的 Python 脚本用csv模块处理记录边界。代码量不大但省掉了后面所有的对账麻烦。3. 流式分割核心实现三个参数与一段不爆内存的代码这一章给出可直接运行的实现。核心原则只有一条全程流式读写任何时候内存里只保留一条记录不把文件整体读进内存。只要守住这条12 GB 还是 120 GB 对内存占用没有区别。3.1 按记录数切分的骨架先看按行切的版本这是最常用的import csv import os def split_by_rows(src_path, out_dir, rows_per_file, encodingutf-8-sig): 按记录数切分 CSV每个分片都复写表头。 src_path : 源文件路径 out_dir : 输出目录必须不存在或为空 rows_per_file: 每个分片的数据行数不含表头 encoding : 输入编码utf-8-sig 可自动吃掉 BOM 返回 (分片数, 数据总行数) if rows_per_file 0: raise ValueError(rows_per_file 必须为正整数) if os.path.isdir(out_dir) and os.listdir(out_dir): raise RuntimeError(输出目录非空先清理再跑: %s % out_dir) os.makedirs(out_dir, exist_okTrue) idx 0 # 分片序号 cnt 0 # 当前分片已写数据行数 total 0 # 全局数据行数 fout None writer None with open(src_path, r, encodingencoding, newline) as fin: reader csv.reader(fin) # 按记录迭代自动处理引号内换行 header next(reader, None) # 首条记录当表头 if header is None: return 0, 0 for row in reader: if writer is None or cnt rows_per_file: if fout is not None: fout.close() part os.path.join(out_dir, part-%05d.csv % idx) fout open(part, w, encodingutf-8, newline) writer csv.writer(fout) writer.writerow(header) # 每个分片复写表头 idx 1 cnt 0 writer.writerow(row) cnt 1 total 1 if fout is not None: fout.close() return idx, total这段代码有几个地方值得单独说。newline是必须的。Python 官方文档明确要求读写 csv 时传这个参数否则在 Windows 上写出时会把\n转成\r\n读入时又会把\r\n拆成两个换行最终表现为「莫名其妙多出空行」。这个坑在 Linux 上开发、Windows 上运行的场景里特别容易出现。csv.reader(fin)而不是for line in fin是为了让引号内的换行被当成同一条记录的一部分。如果你的数据里绝对没有引号换行两者结果一样一旦有前者正确、后者会把一条记录拆成两条字段数对不上。next(reader, None)取出表头后立刻写进每个新分片。注意这里没有把表头算进cnt所以rows_per_file100000的含义是「每份 10 万条数据 1 行表头」。fout用open(..., w, encodingutf-8, newline)打开注意输出统一用utf-8而不是utf-8-sig。原因是从utf-8-sig读入时 BOM 已经被吃掉如果写出再用-sig每个分片都会重新加一个 BOM下游拼回去时中间就夹了 BOM。参数上最需要调的是rows_per_file。经验值是让单份文件落在 64256 MB 之间先取前 1 万行估算平均行长用目标字节数反推行数。比如平均一行 300 字节、目标 128 MB那rows_per_file大概是128*1024*1024/300 ≈ 44 万。3.2 按字节切分与记录边界回退字节切的难点在于不能切在记录中间。下面的版本先用csv.writer把一条记录序列化到缓冲区拿到真实字节数再判断是否该换文件这样既拿到了字节精度又不会破坏记录边界import csv import io import os def split_by_bytes(src_path, out_dir, max_bytes, encodingutf-8-sig): 按字节切分保证每个分片不超过 max_bytes表头不计入。 单条记录本身超过 max_bytes 时单独成片不拆分该记录。 if os.path.isdir(out_dir) and os.listdir(out_dir): raise RuntimeError(输出目录非空: %s % out_dir) os.makedirs(out_dir, exist_okTrue) idx 0 size 0 fout None writer None header_bytes 0 buf io.StringIO() with open(src_path, r, encodingencoding, newline) as fin: reader csv.reader(fin) header next(reader, None) if header is None: return 0 hbuf io.StringIO() csv.writer(hbuf).writerow(header) header_bytes len(hbuf.getvalue().encode(utf-8)) def new_part(): nonlocal fout, writer, size if fout is not None: fout.close() path os.path.join(out_dir, part-%05d.csv % (len(os.listdir(out_dir)))) fout open(path, w, encodingutf-8, newline) writer csv.writer(fout) writer.writerow(header) size header_bytes new_part() for row in reader: buf.seek(0) buf.truncate(0) csv.writer(buf).writerow(row) line buf.getvalue() lb len(line.encode(utf-8)) # 已经写过数据行且再加一条会超限就换新分片 if size header_bytes and size lb max_bytes: new_part() fout.write(line) size lb if fout is not None: fout.close() return idxnonlocal那段是为了在闭包里改写外层变量Python 2 环境下需要换成可变容器或者直接内联这里按 Python 3 写。关键判断是size header_bytes and size lb max_bytes。前半句保证新分片里至少会写一条数据否则遇到超长单行时会陷入「刚建好文件就又要换」的死循环后半句才是真正的容量控制。max_bytes的设定直接关系到 Hadoop 侧的读取效率。默认块大小 128 MB 时分片落在 120128 MB 之间是最理想的——太小会产生大量小文件太大则单个分片跨两个块读取时仍然要跨节点。我一般设成128 * 1024 * 1024 - 4096留 4 KB 余量给表头和行尾换行。另外注意os.listdir(out_dir)拿序号这个写法只在单线程下安全并发跑的时候要换成自增计数器加锁或者干脆用进程号拼前缀。3.3 命令行入口与参数校验把上面的函数包一层argparse就是一个能直接用的工具import argparse def main(): p argparse.ArgumentParser(descriptionCSV 大文件分割) p.add_argument(src) p.add_argument(out_dir) p.add_argument(--mode, choices[rows, bytes], defaultrows) p.add_argument(--rows, typeint, default500000) p.add_argument(--bytes, typeint, default128 * 1024 * 1024) p.add_argument(--encoding, defaultutf-8-sig) a p.parse_args() if a.mode rows: n, total split_by_rows(a.src, a.out_dir, a.rows, a.encoding) print(分片 %d 个数据行 %d 条 % (n, total)) else: n split_by_bytes(a.src, a.out_dir, a.bytes, a.encoding) print(分片 %d 个 % n) if __name__ __main__: main()运行方式# 按 50 万行切 python csv_split.py /data/orders.csv /data/out_rows --mode rows --rows 500000 # 按 128MB 切供 HDFS 上传 python csv_split.py /data/orders.csv /data/out_bytes --mode bytes --bytes 134213632参数校验里最该拦的是三件事源文件是否存在、输出目录是否非空、rows_per_file和max_bytes是否为正。这三条拦住能省掉后面大部分的返工。我第一次写这玩意的时候没校验输出目录重跑了一次分片从 24 个变成 48 个下游跑出来的总额刚好翻倍查了半个下午。4. 脏数据前置处理引号换行、BOM 与非逗号分隔符真实导出的 csv 很少是干净的。下面这几类问题不解决分割工具跑得再顺也没意义因为切出来的分片本身就是错的。4.1 引号内的换行是最大的隐形杀手一个地址字段写成某市某区\n某街道 12 号在文件里占两个物理行但逻辑上是一条记录。用wc -l统计出的行数会比真实记录数多用split -l切出来的分片会从中间断掉下游解析时报「列数不匹配」。判断方法很简单跑一段小脚本对比物理行数和逻辑记录数import csv def count_lines(path, encodingutf-8-sig): with open(path, rb) as f: phys sum(1 for _ in f) # 物理行数按 \n 计 with open(path, r, encodingencoding, newline) as f: logic sum(1 for _ in csv.reader(f)) # 逻辑记录数 print(物理行 %d逻辑记录 %d差值 %d % (phys, logic, phys - logic)) count_lines(/data/orders.csv)差值大于 0 就说明存在引号内换行必须走csv.reader那条路径不能用任何按行切的原生命令。差值还可能是负数——如果文件末尾没有换行符最后一行不会被sum(1 for _ in f)计进去这个属于误报忽略即可。4.2 BOM、编码与首列列名异常带 BOM 的 UTF-8 文件第一条记录的列名会被读成\ufefforder_id而不是order_id。在 pandas 里表现为KeyError: order_id在 Hive 里表现为首列列名带乱码。这个问题的烦人之处在于肉眼看文件完全正常。处理方式是在读取时统一用utf-8-sig它会把 BOM 当编码标记吃掉输出时用utf-8不再重复写 BOM。如果源文件是 GBK 或者 GB18030直接指定对应编码即可但要注意 GBK 编码的中文在切分后如果被下游按 UTF-8 读会出现整列乱码而不是报错——这种静默错误最难查。我一般会在脚本开头加一个快速的编码探测先按utf-8-sig试读前 1000 行抛UnicodeDecodeError就退到gb18030。不要用自动探测库去猜猜错的概率比你想的高而且错得很安静。4.3 分隔符不是逗号的情况很多系统导出的「csv」其实是制表符分隔扩展名却还是.csv。直接按逗号切所有字段会被当成一列后续全错。csv模块支持自定义分隔符把delimiter参数传进去就行reader csv.reader(fin, delimiter\t) writer csv.writer(fout, delimiter\t)如果分隔符不固定可以用csv.Sniffer嗅探但要限制候选集并且只用前几 KB 采样import csv with open(/data/unknown.csv, r, encodingutf-8-sig, newline) as f: sample f.read(8192) dialect csv.Sniffer().sniff(sample, delimiters,\t;|) print(分隔符:, repr(dialect.delimiter))Sniffer在字段值里本身含逗号或分号时容易误判所以我的做法是先嗅探然后把结果打印出来让人确认一眼再正式跑。全自动在数据管道里省事但在一次性任务里反而增加排查成本。4.4 空行、尾部空列与 CRLF空行在csv.reader里会被读成[]如果直接writer.writerow([])写出来就是一个空行下游统计行数时又会多出来。加一句if not row: continue跳过即可。尾部空列来自某些系统导出时多加的分隔符表现为最后几列全是空字符串。这个不影响切分但会影响下游按列位置取值建议在交付说明里写清楚列数和列名。CRLF 的处理前面提过只要读写都带newline就不会出现双换行。如果文件是别人给的历史文件混用了\r\n和\ncsv模块也能正常解析不需要额外做换行符归一化。5. 避坑排查五条分割翻车的现象、成因与修法下面这五条都来自实际跑批时踩过的坑按「现象 → 原因 → 解决」写遇到相似症状可以直接对号。5.1 分片行数总和比源文件多一份表头现象24 个分片每个 10 万行数据对账时发现总行数是 240 万 24多出来的正好是分片数。原因每个分片都复写了表头但统计时用的工具把表头也当成数据行计进去了。用wc -l统计尤其容易中招它数的是物理行。解决统计数据行要用csv.reader逐条计或者用wc -l的结果减去分片数。更稳妥的做法是在脚本返回值里直接给出total以脚本输出为准不要事后用 shell 命令反推。5.2 下游 Hive 查询结果里出现一行列名现象Hive 外表建好之后select count(*)比预期多 24 行而且select distinct order_id里出现字符串order_id。原因建表时没有设置skip.header.line.count每个分片的表头都被当成数据行读了进去。解决建表语句里加上TBLPROPERTIES (skip.header.line.count1)或者干脆切成「数据分片不带表头 单独一个 header 文件」的模式。两种都能用选哪种取决于下游是不是你自己控制。5.3 分片体积严重不均出现几 MB 和几百 MB 并存现象按行切完之后大部分分片是 100 MB 左右少数几个只有几 MB个别超过 400 MB。原因数据里存在长文本字段备注、JSON 串、堆栈日志行长度分布极度不均。按固定行数切行数一样但字节数差很远。解决改用按字节切或者用「目标字节 行数上限」的双阈值策略——先算累计字节超过目标就换文件同时设一个行数硬上限防止单行超长导致死循环。5.4 用 split 命令切完下游报列数不匹配现象split -l 100000切出来的分片用 pandas 读取时抛出ParserError: Error tokenizing data. C error: Expected 8 fields, saw 12。原因源文件里有引号内含换行的字段split按物理行切把一条记录拆成了两条两条各缺一部分字段。解决换成基于csv.reader的切分脚本。如果一定要用命令行先做一次「引号换行检测」确认差值为 0 再用split。5.5 分片数量过多HDFS 上小文件告警现象按 5 万行切一个 200 GB 的文件切出 4000 多个分片上传后 NameNode 内存占用明显上升后续跑 Spark 时 task 数爆炸。原因rows_per_file设得偏小加上源文件本身行数多导致分片数远超预期。HDFS 对小文件不友好每个文件、每个块在 NameNode 上都占一条元数据记录。解决按字节切并把目标定在 128256 MB分片数自然落在合理区间。已经切碎的分片可以再做一次合并或者用hadoop archive打包。经验值是单目录文件数控制在几千以内超过就该重新规划分片粒度。6. 切完怎么验行数对账、HDFS 块对齐与 Hive 外表落地切分跑完不算结束得验。我的验证顺序是固定的三步缺一步都不算交付。第一步是行数对账。源文件的逻辑记录数、所有分片的数据行数之和两者必须严格相等。注意要用csv.reader计不要用wc -limport csv, glob, os def count_records(path, encodingutf-8-sig, skip_headerFalse): with open(path, r, encodingencoding, newline) as f: r csv.reader(f) if skip_header: next(r, None) return sum(1 for _ in r) src count_records(/data/orders.csv, skip_headerTrue) parts sum(count_records(p, skip_headerTrue) for p in sorted(glob.glob(/data/out_bytes/part-*.csv))) print(源 %d分片合计 %d%s % (src, parts, 一致 if src parts else 不一致))第二步是体积检查。用du -h看一眼分片分布如果最大和最小差超过 3 倍说明字节控制有问题得回头看max_bytes和长行处理逻辑。对齐 HDFS 默认 128 MB 块大小的话分片落在 100128 MB 之间是我认为比较舒服的区间设成 256 MB 块的环境相应放大到 200256 MB。第三步是抽样读回。随手挑第一个、中间一个、最后一个分片各读前 5 行和后 5 行确认表头正确、字段数一致、末尾没有截断的半行。这一步花不了两分钟但能挡住「最后一个分片写到一半进程被杀」这类问题。对接 Hive 的建表语句大致是这样CREATE EXTERNAL TABLE ods_orders ( order_id STRING, user_id STRING, amount DECIMAL(12,2), created_at STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY , STORED AS TEXTFILE LOCATION /warehouse/ods/orders TBLPROPERTIES (skip.header.line.count1);三个要点EXTERNAL保证删表不删数据skip.header.line.count必须和「每份是否复写表头」匹配LOCATION指向的是分片所在目录而不是单个文件Hive 会自动把目录下所有文件当成同一张表的数据。上传用hdfs dfs -put /data/out_bytes/* /warehouse/ods/orders/传完用hdfs dfs -du -h再确认一遍体积。这套流程我跑了两年多中途改过三次。第一次是没做行数对账交付后下游说少了几千条回头查发现是源文件末尾一条记录没有换行符按行读被漏掉了第二次是按行切导致分片体积失控被存储侧提醒小文件太多第三次是输出编码写成了utf-8-sig每个分片都带 BOMHive 首列列名带了个不可见字符select的时候怎么都对不上。从那以后我每次分割完都强制走一遍「逻辑行数对账 体积分布 首尾抽样」这三步宁可多花三分钟也不想到下游再回来翻。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑