☰
大文件拆分全攻略:按大小、行数与时间切割的工程实践
2026/10/18 10:03:27 网站建设 项目流程

前阵子帮某业务组处理一个跟文件拆分有关的需求:他们要从生产环境导出一张月度明细,CSV 足足 12GB。拿去做 Excel 透视,Excel 直接罢工;想一次性导入数仓,跑了大半个下午还在几亿行里打转;更糟的是有人直接双击想在电脑里预览,笔记本风扇当场起飞。折腾到最后,所有人的共识其实特别朴素——先把文件按规矩拆开,分而治之。

这篇文章想把“拆文件”这件事彻底说清楚。它包含两部分:一部分是方法论,比如什么时候按大小拆、什么时候按时间拆、拆完怎么命名、怎么摆目录;另一部分是工具和命令,比如 Linux 下的split、日志场景里的logrotate,以及用 Python 快速定制的拆分脚本。无论你是做运维、做数据开发,还是写业务代码时顺手处理大文件,看完应该都能直接照着干。

另外,这篇不会只讲成功路径。拆分看着简单,真出事时往往出在三个地方:拆完的数据能不能正确合回去、拆出来的每个分片能不能独立被消费、以及三天后别人拿到这些文件还认不认识它们属于谁。这几个问题,下面都会展开。

1. 拆文件之前,先把这几个问题想明白

1.1 不同的痛点,对应不同的拆分维度

我见过太多“为了拆而拆”的方案。有人拿到大文件第一反应就是按大小咔咔切成几块,结果下游要按天消费,发现每块文件里日期全都混在一起;也有人一上来就按时间切,结果单日数据太大,某个分片照样几 GB,处理端还是吃不消。所以动手之前,请先回答一个最基础的问题:你到底在解决谁的痛点?

常见的目标大概有四类。第一类是传输限制,邮件附件、网盘上传、FTP 工具都有单文件大小上限,这时候你按字节大小拆最直接,合并时顺序拼接即可。第二类是工具算力限制,Excel 单表有行数上限,内存分析脚本处理几十 GB 会崩,这时候必须按行数拆,保证每个分片都能被现有工具独立打开。第三类是并行消费,你有四台机器、八个 worker,想把一个大任务摊开跑,这时候既要考虑行数,也要考虑数据是否均匀,甚至要按某个业务键分桶。第四类是生命周期管理,日志要保留 30 天、离线数据要按月归档,这种场景必须按时间切,因为你要保证“删除某段数据”这个动作能按目录或文件名轻松完成。

把这四类摆在一起你会发现,拆分维度不是拍脑袋选的,它取决于数据将来怎么被使用。给下游 worker 用的文件,不能拆得连一行完整记录都没有;给归档系统用的文件,不能拆得让“按天删”变成痴人说梦。

1.2 三种主流拆法:按大小、按行数、按时间

按大小、按行数、按时间,是文件拆分里最基础的三个维度。我把它们的适用场景和典型代价整理成了一张表,每次做方案前我都会先过一遍这张表:

拆分维度优点缺点典型场景
按大小(字节)分片体积均匀,和传输通道限额天然匹配文本文件可能切断记录,分片不可独立解析大文件传输、备份分发、压缩包分卷
按行数每条记录完整,分片可独立消费实际字节大小可能差异很大,行内字段不齐时更复杂CSV、日志、数据库导出后的并行处理
按时间文件边界与业务周期对齐,方便清理归档高峰时段单个分片仍然可能很大日志滚动、按日/月分区的数据同步

除了这三种,还有一个容易被忽略的思路叫“按业务键分桶”。比如你想按用户维度拆分交易明细,可以把用户 ID 做哈希,映射到 N 个桶里。这样同一个用户的所有记录一定落在同一个分片里,下游做关联、做去重都方便。这个方案在分布式数据同步里很常见,这里先不展开,但你要知道除了横着切、竖着切,还有“按归属切”这条路。

1.3 先想好“怎么合”再动手拆

我做文件拆分这么多年,最想强调的一条经验是:任何一个拆分方案,都必须同时定义合并协议。哪怕你笃定这些分片永远不会合并,也要知道分片之间是什么关系,否则后续会被反复问“这些文件能不能拼回去”。

对顺序敏感的数据,比如增量日志、交易明细,你在拆分的时候就得把全局顺序信息放进去,要么靠文件名里的序号,要么靠每行数据里的时间戳。对顺序不敏感的数据,比如视频、普通压缩包,每个分片独立,合并只是简单的顺序拼接。如果分片会分发到不同地点、最后还要汇总,更要在拆之前统一 schema、编码方式和字段顺序,不要等到分片落地了再后悔。

拆和合必须对称。用split拆的,就该用cat按相同顺序合;用脚本按行数拆的,合回去的行顺序必须和原文件一致。很多“拆完合不回去”的事故,根源都是拆的时候对排序规则想得太少。

2. 大小与行数拆分:一条 split 命令能解决的别写脚本

2.1 最常用的 split 参数组合

Linux 自带的split命令是处理“按大小拆、按行数拆”最趁手的工具。绝大多数场景一行命令就搞定,完全不需要自己造轮子。先看三个最常用的组合:

# 按字节大小拆,每个分片 100MB split -b 100M big_export.bin part_ # 按行数拆,每个分片 20 万行 split -l 200000 big_export.csv chunk_ # 数字后缀 + 固定三位,保证排序稳定 split -d -a 3 -l 200000 big_export.csv data_

这里有几个参数需要认真理解。-b表示按字节大小,-l表示按行数,这个不用多说。真正关键的是后缀规则:split默认生成的是aa、ab、ac这种字母后缀,而且在某些环境里字母后缀的排序规则和你的直觉并不一致。所以我强烈建议任何时候都用-d指定数字后缀,再用-a 3固定后缀位数,得到data_000、data_001这样的结果。位数要提前估算,如果分片数可能超过 1000,就把-a改成4或者更大,别等到生成了data_1000才意识到后缀宽度不够。

还有一个 GNU 扩展参数叫--additional-suffix,可以给每个分片自动补充扩展名,比如split -d -a 3 -l 200000 big.csv data_ --additional-suffix=.csv,生成data_000.csv。不过要注意,macOS 上自带的 BSD 版本split并不支持这个参数,如果你要写跨平台的脚本,要么放弃这个参数,要么改用 Python。

2.2 多行一拆时,表头文件怎么处理

这是新手最容易忽略的问题。split -l会把第一行也就是表头当成普通行处理,结果第一个分片有表头,后面所有分片都没有表头。下游把这些分片分别导入数据库时,字段名对不上,整批数据全乱。

我常用的处理办法是把表头单独拎出来,再进行先拆后补:

# 1. 把表头单独存下来 head -n 1 big_export.csv > header.csv # 2. 从第二行开始按行拆分,不保留表头 tail -n +2 big_export.csv | split -d -a 3 -l 200000 - data_ # 3. 给每个分片补上表头 for f in data_000 data_001 data_002; do cat header.csv "$f" > "${f}.csv" done

注意这一步生成的文件扩展名可能是残缺的,因为split默认不给后缀。实际操作时我会在循环里统一处理成.csv再删掉中间文件,避免目录里出现一堆没有扩展名的数据文件。

还有一个更隐蔽的坑:如果 CSV 字段本身包含换行符,也就是被引号包裹的多行字段,那么“按物理行拆”会把一条逻辑记录拦腰劈开。这种情况就不能再用split -l了,得用 Python 的csv模块按逻辑记录去拆,或者用一个能感知引号状态的数据处理框架。给一个小脚本骨架:

import csv import sys src = "big_export.csv" chunk_size = 200000 with open(src, "r", encoding="utf-8", newline="") as f: reader = csv.reader(f) header = next(reader) chunk = [header] idx = 0 for row in reader: chunk.append(row) if len(chunk) >= chunk_size: with open(f"part_{idx:04d}.csv", "w", encoding="utf-8", newline="") as out: writer = csv.writer(out) writer.writerows(chunk) chunk = [header] idx += 1 if len(chunk) > 1: with open(f"part_{idx:04d}.csv", "w", encoding="utf-8", newline="") as out: writer = csv.writer(out) writer.writerows(chunk)

2.3 二进制文件拆分后如何无损还原

二进制文件没有“行”的概念,所以只能按字节拆。这个时候数字后缀的排序问题就不再是锦上添花,而是必须项。举个例子,把一个 8GB 的备份包拆成 4GB 的分片:

split -d -a 3 -b 4G backup.tar.gz segment_ cat segment_000 segment_001 > restore.tar.gz md5sum backup.tar.gz restore.tar.gz

还原时要用cat严格按照segment_000、segment_001的顺序拼接,任何顺序错误都会导致文件损坏。而且注意,千万不能用cat segment_*一锅端,除非你能百分之百确定这个前缀只有这一批文件。我见过有人把两个批次的备份都命名为segment_前缀,合并时直接混到一起,最后压缩包损坏,数据差点找不回来。

还原之后一定要用md5sum或sha256sum做校验,不要只比较文件大小。大小相同不代表字节一致,这种低级错误在文件传输场景里太常见了。

3. 时间维度拆文件:日志滚动里的门道

3.1 为什么日志一定要“主动滚”而不是等它爆

如果说按行拆分主要解决“处理不动”的问题,那么按时间拆分主要解决“清理不掉”的问题。日志文件如果只写不切,会一路膨胀到磁盘爆掉。到那时候你面对的不是优雅的归档,而是一台写不进任何新数据的机器,以及一个几 GB 到几十 GB、连tail都卡半天的巨型日志。

而且日志还有一个特殊性:它天生就是按时间顺序追加的。所以对日志做时间维度的切割,几乎不会破坏任何业务逻辑,反而天然形成“按天归档、按天可查”的结构。业界普遍的做法是“边写边滚”,也就是在服务运行期间就按天或按大小自动切换文件,而不是等磁盘满了再手动处理。

自己写 cron 脚本盯文件大小、到阈值就复制加清空,这种做法我不是很推荐。它容易出三个问题:复制大文件占用大量 IO,影响业务写入;清空时机和写入进程冲突,可能丢数据;脚本里往往没有压缩策略和保留周期,时间一长磁盘照样崩。所以多数服务器场景,直接上系统自带的logrotate更省心。

3.2 可以照抄的 logrotate 配置块

logrotate是绝大多数 Linux 发行版都自带的日志滚动工具,由cron或systemd timer定期触发。下面这个配置块,是我在项目里用得最多的模板:

/data/app/logs/app.log { daily rotate 30 compress delaycompress dateext dateformat -%Y%m%d missingok notifempty copytruncate }

每个配置项背后都有原因。daily表示每天滚动一次,实际执行时机取决于系统里logrotate被调度的时刻,通常在凌晨。rotate 30表示只保留最近 30 个滚动文件,更早的自动删除,这就是“保留周期”的落地方式。compress会把滚动后的旧日志压缩成.gz,显著省磁盘。delaycompress表示延迟一个周期再压缩,这个非常有用:刚被滚出去的旧文件可能还被进程占用着,而且压缩是吃 IO 的操作,放到下一次轮转时做,对业务影响更小。

dateext和dateformat让滚动后的文件名变成app.log-20250115,而不是app.log.1、app.log.2。数字后缀一旦轮转次数超过一位,排序就会乱,而且看到app.log.27你根本不知道这是哪天的日志。missingok表示日志文件不存在时不报错,notifempty表示文件为空就不滚动,这两个都是避免产生大量无意义操作和空文件的。

3.3 copytruncate 与 create 的取舍,以及丢数据风险

logrotate的滚动方式有两种,理解它们的区别直接从根上决定了你会不会丢日志。

默认的create模式是这样工作的:先把app.log重命名为app.log-20250115,再新建一个空的app.log。看起来没问题,但前提是写日志的进程能感知到日志文件被换走了,并主动重新打开新文件。很多服务靠接收 SIGHUP 信号来重新打开日志,例如某些 Web 服务器和 Java 日志框架。如果服务不支持重载,那么重命名之后它持有的文件描述符仍然指向旧文件,后续所有日志都会继续写进那个“历史文件”,你以为在切割,实际上新日志全跑到了老文件里。

copytruncate模式则不同:它先把当前文件复制一份作为历史文件,再把原文件清空。原文件的 inode 没有变,写日志的进程持有的文件描述符仍然有效,所以不需要服务做任何配合。但它有一个绕不开的缺陷:复制和清空之间存在一个时间窗口,这个窗口里写入的日志既不在复制出去的副本里,也可能在清空操作中被丢掉。流量越大,窗口内丢的越多。

我的经验是这样:如果服务能够重新打开日志文件,优先用create;如果服务做不到,只能copytruncate,同时接受少量丢失的现实。如果你的日志属于强审计要求、一条都不能丢,那就别在文件层面纠结了,让应用把日志写到标准输出,交给容器或统一采集系统去处理。

无论选哪种,还有一个容易踩的坑:滚动策略改了之后,文件名规则就变了。下游的采集、监控、分析脚本如果还在用旧模式匹配app.log.*,新生成带日期的文件可能根本匹配不上,导致“看似有日志,其实没人接”。文件名约定和下游消费,必须一起改。

4. 拆分产物的命名与目录约定:我踩过的最贵一坑

4.1 文件名里的信息排布与零填充逻辑

聊完命令和工具,该聊聊比工具更重要的东西:命名规范。这一节是我最有感触的部分,因为我在这上面真金白银地踩过一个巨坑。

有一年做历史数据迁移,我把每个月导出的文件命名为affair_202501_0.csv、affair_202501_1.csv,一直排到affair_202501_10.csv。结果下游同事用通配符合并时,系统按字典序展开,顺序变成了_0、_1、_10、_2、_3……数据全部错位。查了一下午才发现是排序问题。从那以后我定下死规矩:序号必须零填充等宽,宁可多写几位,也绝不让排序产生歧义。

一份合格的拆分文件命名,至少包含四个要素:数据主题、时间、序号、格式。正例是user_events_20250115_0001.csv,反例是第1批用户事件(最终版).csv。空格、中文、括号在 shell 和脚本里全都要转义,而“最终版”这种词对机器没有任何可读信息。时间格式统一用20250115这种纯数字,不要用01-15-2025、2025.01.15之类的变体,目的很单纯:让字典序等于时间序。序号固定四位,比如0001,位数要比你预估的最大分片数至少多一位。

顺手再推荐一个低成本高回报的习惯:拆完之后,在文件旁边生成一个.md5校验文件。文件只要跨机器、跨网络,校验和就能告诉你它在路上有没有被改过。

4.2 按时间分区摆放目录的工程价值

单个文件名再规范,如果几百个文件全堆在一个目录里,也是灾难。所以目录结构本身就是拆分方案的一部分。我见过两种最常见也最好用的模式。

第一种是普通文件系统的层级目录,比如data/2025/01/15/user_events_0001.csv,或者logs/2025/01/15/app.log。这么做的直接好处是:删除过期数据变成“删目录”,归档压缩变成“按目录打包”,排查问题时可以直接跳到某一天的目录,而不用在几千个文件里翻。

第二种是数据仓库和大数据平台里更常见的分区列模式,比如data/dt=2025-01-15/hour=10/data_0001.csv。这种key=value结构的好处是下游引擎能自动识别分区列,做分区裁剪,查询只扫必要目录。即使你不用大数据平台,只是把 CSV 按日归档,我也建议在目录上显式标出分区字段,因为它是机器可读的,比纯数字层级更不易错。

还有一个隐藏的效率问题:单个目录下文件数量超过几千个之后,ls、通配符展开、对象存储列举都会明显变慢。按时间拆目录不只是为了整洁,是实打实的性能需求。我自己处理日均上亿行的数据时,通常按天甚至按小时拆目录,保证每个目录下文件数在一个可控量级。

4.3 多任务并发拆分时的防冲突设计

很多拆分需求不是单机一个进程跑完的,而是分布式环境里多个 worker 同时在拆。这种场景下,命名和文件写入的冲突问题会被放大。

首先是写文件隔离。每个 worker 必须独占一个前缀或一个目录,绝对不能多个 worker 往同一个文件里追加。哪怕只是日志,两个进程同时 append 同一个文件,虽然单行小于管道缓冲时不会立刻崩,但一旦交错就会出现半行垃圾数据。正确的做法是每个 worker 用自己的唯一前缀,比如worker01_、worker02_,或者各自的子目录。

其次是原子性。拆分任务里,下游通常会在目录里扫描新文件。如果 worker 把半成品文件直接写到正式目录,下游一旦扫描到,就会读到只有一半内容的文件。我习惯先写临时文件,比如worker01_data.tmp,全部写完后统一mv成正式名。mv在同一个文件系统里是原子操作,下游扫描时要么看不到,要么看到的就是完整文件,不会读到切开一半的中间状态。

再就是全局唯一性。分片文件的命名如果在整个作业空间里不唯一,合并时就可能把两个批次的内容混在一起。固定前缀加 worker 编号加零填充序号,比如job20250115_worker01_part_001.csv,基本能杜绝这种冲突。

5. 五个高频事故和一套兜底校验流程

5.1 多字节字符被拦腰截断之后

讲一个很典型的命令级事故。UTF-8 编码下,一个中文字符占 3 个字节。split -b 100M只认字节边界,完全不知道它切到的地方是不是字符边界。如果你只是要把分片合并回去,字节切法本身是可逆的,cat回去就完全一致;但如果每个分片要被单独处理,比如单独导入数据库、单独打开预览,你就会遇到最后一行乱码或者编码错误。

这里有一条铁律:需要独立消费的文本文件,绝对不要用-b去切,用-l按行切。如果业务上必须按大小切,而且每个分片都要独立可用,那就用能感知字符流的语言去处理,或者干脆先把文本压缩,再对压缩包按字节切——压缩包没有编码问题,怎么切都行。

5.2 合并顺序错误与通配符陷阱

通配符展开和排序规则,是一个很隐蔽的陷阱。cat data_*看起来人畜无害,但 Shell 通配符默认按字典序展开。如果序号不等宽,data_2就会排在data_10前面,合并出来的文件顺序全错。这个问题我在 4.1 里已经吃了大亏,这里的建议是两条:文件名序号必须零填充等宽;合并时不要赌通配符的排序,用明确范围或者先sort再拼接。

另外注意分片前缀冲突。假如两批不同的拆分作业都叫segment_,同时出现在同一个目录里,你执行cat segment_*的时候,两批数据就混在一起了。加作业编号、加时间戳、加批次号,目的就是让前缀全局唯一。文件拆分这种操作,任何歧义最终都会变成数据事故。

5.3 拆分完必跑的六项检查

拆分不是执行完命令就结束了。按我现在的习惯,任何一次批量拆分之后都会跑一套固定校验,相当于给文件拆分加一道质检流水线。可能是环境原因,也可能是人为疏忽,但校验能兜住绝大多数低级错误。

编号检查项方法通过标准
1分片数与计划一致统计分片文件数量和每个文件的行数与预算值完全相等
2逻辑内容不丢源文件行数和各分片行数之和严格比对(表头只算一次)严格相等
3合并可逆把分片按正确顺序拼接后做md5sum比对校验和一致
4每个分片可独立读取用目标工具逐个打开或解析无报错、无乱码
5文件结尾完整检查最后一个字符是换行符,无截断行检查通过
6命名可稳定排序按文件名排序后与业务顺序比对字典序即业务序

其中第 2 项和第 3 项是最重要的,一个保证“内容没少”,一个保证“拼得回去”。后面四项是给下游的消费方兜底。做数据分发的人,最怕的就是拆出来的东西“看着挺多,其实各有残缺”。

现在我自己做任何批量拆分,都会先花十分钟把三件事确认掉:拆分维度选对没有、合并协议定义清楚没有、校验清单能不能自动化跑。这三件事想明白了,后面的一堆命令和脚本都只是执行细节。至于那些“先拆了再说”的方案,我这些年看到的结局,基本都是在某个凌晨接到数据对不上的电话。

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

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

立即咨询