切口在换行上,不在字节里
手上有一份 300 行的纯文本、一共 2289 字节,按每份 7 行切下去,页面报「已切割为 43 份」。把 43 个分片按顺序首尾相接拼回去,再和原文件逐字节比,比原文少了 42 个字节。这 42 个字节没丢在打包、也没丢在编码,全部落在切口上——是夹在每两片之间的那个换行被吃掉了。
先拆掉最容易被误会的一条:这一页的按大小分支并不是真按字节硬切。它先把整段文本按换行拆成行数组,再一行一行往预算里累加,攒够就收尾成一片,切点只能落在换行符上,所以一个 3 字节的汉字在结构上就不可能被切成两半。把模板里的切割函数原样抽出来跑,240 组混合输入分别过三种模式、共 720 次切割、1717 个分片,片里出现替换字符 U+FFFD 的分片 0 片,被切成半字的分片也是 0 片。
一组决定性样本最能说明问题:输入是 100 个「测」加 1 个换行再加 100 个「测」,合起来 201 个码元、601 字节(每个「测」3 字节)。每份大小填 0.4,程序把预算取成 409 字节,切出 2 片、每片正好 100 码元、300 字节;两片的头 2 字节都是 E6 B5、尾 2 字节都是 B5 8B,两边都是完整 UTF-8,少的只是中间那 1 个换行。真要拿这一页切中文长文,文本切割 这条按行累加的路子对多字节字符是安全的。
line 与 size:切点上的换行去哪了
切点恒在换行符上,就有如下几乎每次都成立的等式:按行数、按大小这两种模式,各片字节合计等于原字节减去(片数减 1)。回到开头那份文件,2289 字节切 43 片,代入得 2247,和拼回去量到的分片合计对得上;中文样本 601 字节切 2 片,也刚好少 1。
顺着这条等式能看出预算怎么被整行击穿:每份大小取 0.1 时预算向下取整成 102 字节,而单行本身就是 300 字节,预算比一行还小。程序不切行也不报错,把每行单独封成一片,于是 300 行、每行 100 个「测」(合计 90299 字节)切成 300 片、每片 300 字节,分片合计 90000、比原文少 299。把 1000 个「测」挤在一行、每份大小填 1,也只得 1 片 3000 字节。所谓按大小,只保证切点落在整行边界,不保证每片真不超过那个数。
还有一种更隐蔽的吞字节:按大小分支在缓冲为空时把当前行直接赋值,于是每片开头的空行被跳过。输入「两个空行加 abc 加换行加 def」共 9 字节,切完只剩 1 片、内容 7 字节,缺口对不上那条等式,因为丢的是行本身而非切点上的换行。
图 1:2289 字节切 43 片、分片合计 2247 字节,差的 42 个字节全在切点上的换行里。
| 模式 | 切口落在 | 片合计与原字节之差 | 样本 |
|---|---|---|---|
| 按行数 | 换行符 | 片数减 1 | 2289 切 43 片少 42 |
| 按大小 | 换行符 | 片数减 1 | 90299 切 300 片少 299 |
| 按分隔符 | 分隔符本身 | 分隔符宽乘(片数减 1) | 53 切 4 片少 9 |
delim:分隔符本身不进任何一片
按分隔符和行、字节都无关,它拿分隔符去切,分隔符被切掉后两片都不含它,损失换成另一条等式。有一份 53 字节的样本,用三个等号串 === 当分隔符切 4 片少 9 字节;换成两个换行当分隔符切 3 片,少的是 4 字节。拿原来的分隔符把分片重新接回去,这几组都能逐字节还原,和行数、大小模式吃换行是同一类账。
拼回:18 组里只有 3 组直接相等
用 18 组固定样本(6 种输入乘 3 种模式)做逐字节比较:分片直接首尾相接、什么都不补,只有 3 组和原输入相等,其余 15 组差值恰好等于各自切点数。补回 1 个切点分隔符再比就有 17 组相等,唯一补不回来的正是上面 size 吞首部空行那组。放大到 720 次批量:空串拼接相等 360 组、补分隔符后相等 688 组,分模式看行数与分隔符各 240 组、大小只有 208 组,那 32 组即使补分隔符也对不上。要复核接缝,我习惯拿两份文本直接跑一次文本对比,一眼看出差的是换行还是内容。
页面上的字节数未必等于真实字节
算账前先确认尺子。这一页估大小是逐码元分三档:一个「测」算 3、全角逗号算 3、2 字节字符算 2、带 BOM 算 3,都和标准编码对得上;只有增补平面字符算错——它的两半被当成两个 3 字节各算一遍,一个表情符号会报成 6 字节而真值是 4,每个码点固定高报 2。一片 150 个这类字符的内容界面显示 900 B,落到文件真实 600 字节。要核对码元与字节,可以先用字数统计 对一遍再比这一页的口径。
显示单位走 1024 进制、KB 与 MB 档保留两位小数:1023 还是 1023 B,到 1024 才跳成 1.00 KB;填 100000 显示 97.66 KB,而按十进制这本来是 100 kB。拼回时要用真实字节,别拿这几个显示值去减。
文件名也不补零:前缀直接接序号再接后缀,切到 1000 片时序号位数是 1 位 9 个、2 位 90 个、3 位 900 个、4 位 1 个,按字典序最前四个依次是 split_1.txt、split_10.txt、split_100.txt、split_1000.txt;界面片号补到 3 位显示成 001,只影响徽标不进文件名。参数填错时,每份行数取 0、留空、负数或字母都报「请输入有效行数」,每份大小那几类报「请输入有效大小」,分隔符转义后为空报「请输入分隔符」,输入框空报「请输入文本」;而每份行数取 301 或 1000(都大于总行数 300)不报错、只出 1 片。
对照实验:真按字节硬切会怎样
这条只为把风险和本页实际行为分开。拿 6 个「测」共 18 字节,若硬按字节在第 4 个位置切断,左半拿去解码会得到 1 个完整汉字再加 1 个替换字符 U+FFFD,用严格模式解码直接抛类型错误。而本页按大小分支按行累加,压根不产生切在码元中间的片段——720 次切割里半字分片 0 片就是这条对照的落点。所以拼回去少的字节要算在换行和分隔符头上,而不是算成汉字被切坏。
常见问题
Q:按大小切是不是就能保证每片不超过我填的数?
不能。预算按整行累加比较,遇到单行已经超过预算(每份填 0.1、预算 102 字节,一行却有 300 字节)时不截断,只一行封一片,片合计 90000 对原文 90299 少的还是那 299 个换行。
Q:切完拼回去比原文件少字节,是不是丢内容了?
多数少的只是切点上一个换行或一个分隔符。补回对应分隔符,18 组里 17 组能逐字节还原;对不上那组是 size 吞了片首空行,得回去查输入开头有没有空行。
Q:界面显示的大小能直接拿去算字节账吗?
不建议。增补平面字符每个码点固定多算 2,单位又是 1024 进制带两位小数,核对请用真实字节口径。