兩個 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:這幾個統計數字能拿去對帳嗎?
只能對同一批會話內部的相對差:三項統計裡沒有位元組數,行數兩頭不準,字元數算在歸一化之前,任何一項單獨看都會誤導。
合併這一步不做保護,也不通知改寫:尾末換行自己補、行序自己定、位元組自己核,粘連那行是唯一會主動露面的證據。