两个 txt 文件拖进来合并,每个各 2 行,结果只有 3 行。中间那一行是 bravocharlie:前一个文件的最后一行和后一个文件的第一行被焊在了一起,没有任何提示。行数对不上的排查是从这里开始的。
合并在这个工具里只有一次 join。整个前端 234 行、零外部依赖,分隔符下拉的默认档「无分隔」在代码里被映射成一个长度 0 的空串,于是 alpha + bravo(11 字节)拼上 charlie + delta(13 字节)得到 24 字符 3 行。split 出来的三行是 alpha、bravocharlie、delta,被吞掉的那一行不会在任何面板上报错。
根因是两个文件都没有尾换行:行尾缺的那一个字节正是下一个文件的起点,空串去补只会粘连。
勾选「插入文件名标记」防不住粘连
看起来最靠得住的补救是把文件名标记打开,但它加在内容前面。同样这两个文件,默认档下结果变成 76 字符 5 行,中间那行是 bravo===== tm_b_nonl.txt =====——粘连没消失,只是被标记字符串盖住了一段。把分隔符换到「空行」档,结果才是 78 字符 7 行、逐行干净:空行替每个文件之间垫出至少一个换行,标记和正文才各占一行。
每个文件自己的 BOM 会静默消失
另一处隐形改写发生在读取阶段。文件读成文本用的那一句没有带编码参数,浏览器按规范把每个文件开头的 U+FEFF 当作 BOM 剥掉,这是规范行为,不是 bug——但结果里就再也没有它了。拿两个 7 字节的带 BOM 文件去合,合并产物里 U+FEFF 出现 0 次,总长 9 字符 9 字节。同一对文件、同一段合并代码,换一种读法(不做 BOM 剥离)就会得到 U+FEFF 2 次、15 字节。
工具不会替你保留原文件的 BOM,下游按 BOM 识别 UTF-8 的老脚本会把产物判成无签名,而且不报错。
textarea 吃掉 CR,统计却不知道
把 CRLF 文件和 LF 文件放一起:crlf(8 字节)加 lf(6 字节)原样拼接是 15 字符,含 2 处 \r\n。这段字符串一进结果区的 textarea,浏览器就把它归一成 LF——value 只剩 13 字符,0 处 \r\n。面板上那行「总字符数」是在归一化之前算的,仍然显示 15。于是下载下来的文件比 stat 少 2 字节。
复制或下载的都是这个已经被归一的 value,两者同源。new Blob([v]) 只塞了一个元素,前面没有 U+FEFF,实测「复制 13 字符 / 下载 26 字节」这组差额全部来自 UTF-8 多字节字符本身,BOM 造成的字节差是 0。
顺序既不是自然序,也不是选中序
在文本合并的模板里搜排序相关的关键字是零命中——没有 sort,也没有 localeCompare,它压根不排。一次多选 10.txt、2.txt、1.txt,结果顺序保持 ten、two、one。
但这不等于顺序等于你选中的顺序。条目是各自读取完成后依次推进列表的,谁先读完谁先占位。把 1,048,576 字节的大文件和 11 字节的小文件同批投放,小文件排到了大文件前面。四组同批投放的用例里有一组跨了序。要固定的先后顺序,就别指望投放这一步:先过一遍文本排序去重把行序理顺,或者一次只放一个文件。
「总行数」两头都不准
行数是把结果串按 \n 切开取段数,于是两类误差同时存在:文件以换行结尾时,最后一段是空串,恒多算 1 行——单个 CRLF 文件显示 3 行,真实正文只有 2 行;发生粘连时又少 1 行——上面那组 24 字符的结果显示 3 行,实际应该是 4 行。所以它不能拿去和原文件的行数对账。
什么文件都会照单吞下
没有大小上限,也没有格式白名单:文件选择器上写的类型限制不会进入逻辑,拖什么读什么。1 MiB 级别能跑完——big(1,048,576 字节、131,072 个换行、带尾换行)加那个 11 字节的 A,默认档出 1,048,587 字符 131,074 行,粘连行是 bravoL000000;big 自己加自己换行档,出 2,097,153 字符 262,146 行。跑完了,但统计三项里没有字节数,体积要自己另外看。
二进制也一样:一张 image/png 的 12 字节 PNG 头被当文本读成 12 字符,里面含 1 个 U+FFFD 和 1 个孤立的 \r;等 \r 被归一成 \n,结果行数从 4 变成 5。GBK 的 9 字节「中文测试」按 UTF-8 读,变成 8 字符 21 字节。这里不做编码识别,只照读。
列表页的说法与代码不一致
把工具页上那些可测说法逐条对着跑,17 条里 12 条一致、5 条冲突。最硬的一条是「实时预览合并结果」:改分隔符、勾标记、删掉一个条目之后,结果区的 value 仍然是 24 字符,只有再点一次合并才更新成 37 字符——预览不跟随控件,跟随的是那次点击。另一条是分隔符只列了换行、空行、自定义三档,实际下拉里有第四档「无分隔」,而且它才是默认档,也就是把两个文件焊在一起的那一档。
| 看什么 | 实测值 | 能不能当真 |
|---|---|---|
| 中间那行 | 24 字符 3 行 | 当真,粘连直接看得见 |
| 总字符数 | 15 对产物 13 | 归一化前算的,别当体积 |
| 总行数 | 3 对实际 4 | 尾换行多 1、粘连少 1 |
| BOM 数量 | U+FEFF 出现 0 次 | 当真,读的时候就被剥了 |
| 列表顺序 | 4 组用例 1 组跨序 | 只当真到你能复现它 |
Q:合并后行数对不上,先怀疑什么?
先怀疑尾换行。两个各 2 行的文件出 3 行、中间一行是两个文件的内容拼起来的,就是缺尾换行;把分隔符切到「空行」档重跑一次,行数变 7 行、逐行干净。
Q:结果框里的字符数和下载文件不一致怎么办?
对一下有没有 CRLF。混排那组统计显示 15 字符、含 2 处 \r\n,产物是 13 字符、0 处 \r\n,差的 2 字节就是被吃掉的两行 CR。要在产物里保住 CRLF,只能自己转义之后另存。
Q:这几个统计数字能拿去对账吗?
只能对同一批会话内部的相对差:三项统计里没有字节数,行数两头不准,字符数算在归一化之前,任何一项单独看都会误导。
合并这一步不做保护,也不通知改写:尾换行自己补、行序自己定、字节自己核,粘连那行是唯一会主动露面的证据。