開頭的 U+FEFF(位元組順序記號,Byte Order Mark)就像是有時候會黏在解碼後文字最前面的那個「防空洞門把手」,而 BOM Remover 做的,就是從字串位置零精確剝除這一個不可見的碼元,同時保留其他每一個字元不動。U+FEFF 這個碼點,是與位元組順序記號相關聯的 Unicode 字元;當它出現在一段解碼後文字串流的開頭時,扮演的是編碼簽章的角色,而 UTF-8 的位元組形式 EF BB BF,連同 UTF-16 與 UTF-32 的位元組形式,其實都只是它在磁碟上的表現方式。當一個剖析器、shebang 那一行、JSON 消費端,或欄位名稱比對,預期第一個字元是可見內容時,一個看不見的 U+FEFF 就足以讓整個操作壞掉。BOM Remover 之所以有效,是因為它唯一的規則就是位置:它檢查貼上字串的第一個 UTF-16 碼元,如果那個碼元是 U+FEFF,就回傳從索引 1 開始切下的複本;如果是其他任何字元,就原樣回傳輸入內容。整個轉換完全在目前的瀏覽器分頁裡執行,絕不會上傳文字,也絕不會修剪、正規化,或對後面任何字元做全域取代。

「防空洞門把手」到底是什麼
「門把手」這個比喻很貼切,因為位元組順序記號正是一個檔案入口處字面意義上的把手:這是你的工具在開啟資料時,最先抓到的單一一個字元。在 Unicode 裡,這個字元就是U+FEFF,也就是位元組順序記號,而它也是歷來唯一同時被賦予「位元組順序記號」與「零寬不換行空格」這兩種意義的碼點。U+FEFF 在磁碟上的簽章形式,有明確的定義。WHATWG Encoding Living Standard描述了 UTF-8 解碼器如何把開頭的 EF BB BF 序列,視為一種簽章,在輸出字串裡產生單一一個 U+FEFF 碼點;類似的規則也適用於 UTF-16(FE FF 或 FF FE)與 UTF-32(FF FE 00 00 或 00 00 FE FF)。
一旦檔案被解碼,原始位元組就消失了,瀏覽器儲存的字串裡,U+FEFF 碼點是以單一一個字元的形式存在,而不是三個位元組。這個區別很重要,因為 BOM Remover 是嚴格針對解碼後的字串來運作的,也就是你的 <textarea> 裡已經存在的那些字元。它能看出那個字串的第一個碼元是不是 U+FEFF,但它沒辦法還原出這個檔案原本是以哪些位元組開頭的,或是哪一種解碼器策略產生了這個結果。位置,就是全部的規則。
單一一個看不見的第一個字元,什麼時候會造成破壞
不同的解碼器,在是否幫你自動去掉開頭 BOM 這件事上做法不一,而許多編輯器與匯出工具,會刻意把它保留下來。幾種常見的破壞情況如下:
- Shebang 那一行。一個以位元組 EF BB BF 23 21 / b i n / s h 開頭的 shell 指令碼,在核心(kernel)眼中看起來會像是以一個非 # 字元開頭,shebang 因此不會被辨識出來。
- 嚴格的 JSON 消費端。有些剖析器會拒絕第一個字元不是 {、[,或數字的輸入,而開頭的 U+FEFF,會在讀到第一個真正的語彙單元之前,就先觸發剖析錯誤。
- CSV 欄位標題。當試算表的第一欄被載入時,一個看不見的 U+FEFF 可能會併進第一個標題名稱裡,導致像 row[\"id\"] 這樣的查詢在不知情的狀況下找不到那一欄。
- 差異比對與合併工具。兩份視覺上一模一樣的檔案,如果只有其中一份保留了開頭的 BOM,就會被標記為不同。
- 相等性與前綴檢查。一個字面字串「orders.csv」,不會比對成功一個實際上包含「\uFEFForders.csv」的字串,即使兩者在畫面上看起來一模一樣。
| 情境 | 開頭 U+FEFF 造成的症狀 | 為什麼位置 0 就足以造成這個問題 |
|---|---|---|
| Shell shebang | 啟動時出現「No such file or directory」 | 核心讀到的是那個看不見的字元,而不是 # |
| JSON 剖析器 | 在偏移量 0 出現「Unexpected token」或剖析錯誤 | 第一個碼元不是 {、[,或數字 |
| CSV 標題查詢 | 找不到欄位,或出現重複欄位 | 第一個標題在不知情的狀況下被加上了 U+FEFF 前綴 |
| 字串相等性 | 視覺上一模一樣的文字比對失敗 | 字面字串裡包含了那個看不見的碼元 |
| 差異比對工具 | 整個檔案被標記為已變更 | 只有一側帶有這個簽章 |
上述正是「剝除第一個字元」是正確做法、而「剝除檔案裡每一個出現」則會造成破壞的情況。第一個字元之後仍然存在的任何 U+FEFF,都是內容,不是簽章,把它移除可能會改變意義。如果你在試算表處理流程裡遇到這個問題,可以參考不遺失資料地從 CSV 檔案移除 BOM這篇逐步指南,確認你的工作流程。
用三個步驟,精確剝除一個開頭的 U+FEFF
BOM Remover 是一個刻意做得很窄的工具:它接受已經解碼過的文字,檢查 input.charCodeAt(0) 是否等於 FEFF,然後回傳一個從索引 1 開始切下的字串,或是原樣回傳輸入內容。要對一段以看不見的 U+FEFF 開頭的文字使用BOM Remover:
- 把已經解碼過的文字貼上,貼進輸入區,如果存在那個看不見的開頭 U+FEFF,也一併貼上。從編輯器複製的內容,或先前已解碼過的檔案,都可以直接使用;不接受原始位元組資料。
- 執行移除,並閱讀狀態。摘要會回報究竟是移除了剛好一個開頭的 U+FEFF,還是根本沒找到開頭的 U+FEFF,並且會說明完整輸出以 UTF-16 碼元計的長度。
- 檢視完整的輸出結果,包括換行符號,以及任何刻意保留在後面的 U+FEFF 內容,然後在本機複製它。即使輸出結果是空的,結果面板仍然會顯示出來,所以「沒有第一個字元可移除」不會和「操作失敗」混淆。
「完整輸出長度」這個數字,就是用來確認沒有其他東西被改動的檢查依據。如果輸入長度是 100 個 UTF-16 碼元,而一個開頭的 U+FEFF 被移除了,輸出長度就會剛好是 99。算式是 100 - 1 = 99,唯一的差值,就是那一個被移除的碼元。如果輸入長度和輸出長度相等,就代表根本沒有開頭的 U+FEFF,輸入內容會被原樣回傳。
這個工具刻意處理的邊界情況
有幾種看起來很棘手的輸入,是靠明確的規則來處理的,而不是碰巧處理對了:
- 空白輸入。一個空字串是一個合法的「不變」情況。結果面板仍然會顯示出來,讓這次操作可以被確認。
- 只有 U+FEFF 的輸入。輸出結果是空字串,摘要會回報移除了一個開頭的 U+FEFF,輸出長度為零。
- 兩個開頭的 U+FEFF 碼點。這個工具只會移除第一個,並把第二個留在輸出結果的開頭。再執行一次這個工具,就會把那個現在變成開頭的 U+FEFF 移除掉,所以在重複操作之前,請先檢查狀態訊息。
- 內部或結尾的 U+FEFF。出現在換行符號之後、字串中間,或結尾的 U+FEFF,都會被保留下來。這個實作不會呼叫 trim、trimStart、replaceAll,或任何全域正規表示式。
- 換行符號、Tab、NUL、表情符號、組合符號、代理對,以及非拉丁文字系統。這些都不會被正規化或過濾掉。輸出結果是輸入內容的精確切片,最多只會少掉開頭那一個碼元。
- 200,000 碼元的上限。輸入與輸出都被限制在 200,000 個 UTF-16 碼元以內。剛好達到上限的輸入會被接受;再多一個碼元就會在處理之前被拒絕。一個剛好 200,000 個碼元、且以 U+FEFF 開頭的輸入會被接受(輸出為 199,999);而一個 200,001 個碼元的輸入,則會被直接拒絕。
編輯或清除輸入內容,會立即清除先前的輸出結果、錯誤訊息、摘要、移除旗標、複製訊息,以及等待中的複製計時器,所以一個超出上限的錯誤,不會讓先前成功的舊結果繼續顯示著。剪貼簿存取是非同步的,每一次複製都會拿到一個世代識別碼,所以一個延遲回來的權限回應,不會把過期的「已複製」狀態恢復回來。
BOM Remover 不會做的事
範圍,是這個工具最重要的一件事。下表列出了它是為哪些操作而打造的,以及它刻意不去做的操作。
| 操作 | BOM Remover 是否處理 |
|---|---|
| 檢查第一個碼元是否為 U+FEFF,並把它切掉 | 會 |
| 保留內部、結尾,以及第二個開頭的 U+FEFF | 會 |
| 保留 CRLF、LF、Tab、NUL、表情符號,與組合符號 | 會 |
| 在目前的瀏覽器分頁裡執行處理與複製 | 會 |
| 把文字上傳到任何外部服務 | 不會 |
| 偵測來源檔案原本是 UTF-8、UTF-16,還是 UTF-32 | 不會 |
| 移除每一個 U+FEFF,或每一個零寬字元 | 不會 |
| 修剪、正規化,或以其他方式改寫字串的其餘部分 | 不會 |
| 修復亂碼,或轉換原始位元組序列 | 不會 |
簡單來說,當一個可信任的解碼器或編輯器,已經暴露出一個會干擾剖析器、比對、shebang,或欄位名稱的開頭 U+FEFF 時,BOM Remover 就是正確的工具。但如果目標是推斷檔案編碼、轉換 UTF-16 位元組、驗證 UTF-8、修復亂碼文字,或把串接文件裡每一個類 BOM 字元都剝除掉,這就不是適合的工具。當檔案載入的過程在你的掌控之下時,選擇一個帶有適當 BOM 處理策略的解碼器,會比事後再用切片工具處理文字,是更好的上游解法。
如果你還在猶豫該怎麼選,如何在本機合併 Excel 檔案並移除重複項一文有更詳細的說明。