Unicode 規範化工具

Unicode 有四種規範化形式,它們不能互相替代。NFC 與 NFD 把同一個字元在「一個碼位」與「基字元加組合記號」之間來回擺放;NFKC 與 NFKD 走得更遠,會改寫那些只是長得像的字元 —— fi 連字、全形 A、帶圈 ①、羅馬數字 Ⅻ。本頁把同一段文本跑遍四種形式,為每個字元列印一行說明它變了什麼、為什麼變,把字素簇與碼位、UTF-8 位元組一起計數,並按你選擇的形式比較兩個字串。下半部分講的是那些根本不顯示的字元:零寬空格、連線符、雙向控制符、變體選擇符、C0 與 C1 控制字元、私用碼位和殘缺的代理項半邊,每個都給出位置和名稱,還可以把它們從文本副本里剔除。
全部處理都在你本地的瀏覽器裡完成 —— 你的資料絕不會上傳到任何伺服器。

不確定什麼是規範化? 用大白話解釋 Unicode 規範化 —— 為什麼 é 可以是一個字元也可以是兩個,以及為什麼這會讓登入失敗。
要報告並用於比較的形式:
在以下處理之後比較兩個輸入框:
NFC (暫無內容)
NFD (暫無內容)
NFKC (暫無內容)
NFKD (暫無內容)
還沒有比較。貼上兩個字串,然後按比較這兩個字串。
第一個字串,已規範化 (暫無內容)
第二個字串,已規範化 (暫無內容)
還沒有掃描。按掃描不可見與危險字元,如果想要去掉這些字元的文本,再按剔除。
去掉所選類別後的文本 (暫無內容)
在第一個輸入框裡按 Ctrl+Enter 會執行當前模式。
選一個模式並點選它的執行按鈕。規範化會寫出文本的四種形式,併為每個字元寫一行;比較判斷兩個字串在規範化之後是否相同;掃描列出那些不顯示的字元,並可以把它們剔除。自檢按鈕會重跑本頁資料檔案裡的每一個記錄案例、不可見與控制字元表點名的每一個字元,並把計數報在這裡。
每個字元一行:這個字元是什麼、四種形式對它做了什麼,以及為什麼。灰底行表示至少有一種形式改動了它;NFD 列顯示該字元在會分解時分解成了什麼。
這段文本到底算多少個字元?同一段文本用四種方式計數:字素簇(讀者數出來的個數)、碼位(Unicode 分配的個數)、UTF-16 單元(JavaScript 索引用的個數)和 UTF-8 位元組(實際傳輸的個數)。規範化只會在第二列與第三列之間挪動數字,不會改動第一列。
按出現順序,找到了什麼:位置按碼位計數,類別是該字元所屬的類別,說明則告訴你這個字元會對接下來讀它的程式做什麼。
本頁視為可疑的類別:這張表是從掃描器本身打印出來的,所以本頁上的說法與按鈕的行為不會互相跑偏。嚴重程度說的是對比較結果的影響,不是說這個字元有多可能是無辜的。
私用字元被列為可疑,原因和其餘類別不同:它們確實會顯示出來,只是沒有公認的含義,所以同一個碼位在每一個定義了它的字型裡都是不同的圖案。
本頁能接受多大的輸入,以及為什麼:規範化是對整個字串的一次重建,本頁還要做好幾次重建,外加一次掃描和一次字素計數,所以開銷隨輸入長度直線增長。下面的上限之所以存在,是因為逐字元表是畫在文件裡的,而一張有一百萬行的表才是真能拖死瀏覽器的東西。
什麼是 Unicode 規範化?

Unicode 給某些字元提供了不止一種寫法。字母 é 可以是單個碼位 U+00E9,也可以是普通的 e 加一個組合尖音符,即 U+0065 U+0301。兩者在螢幕上看起來一樣,打印出來也一樣,但作為字串它們是不同的:長度不同,UTF-8 位元組不同,而且 'é' === 'e\u0301' 的結果是 false。

這不是什麼冷知識,而是一整類 bug。你會拿到哪一種形式,取決於鍵盤佈局、作業系統、輸入法,以及這段文字有沒有被拷進過文書處理軟體。按位元組比較字串的密碼校驗、資料庫唯一索引、黑名單或快取鍵,會把這兩種寫法當成兩個不同的值 —— 於是使用者在一臺裝置上設了密碼,卻從另一臺裝置登不進去;同一個詞也會在「已被佔用」的名字列表裡出現兩次。

30 秒版本: NFC 與 NFD 是同一段文本的兩種寫法 —— NFC 做組合(e 加尖音符變成一個字元),NFD 做分解(一個字元變成 e 加尖音符)。它們被設計成無損的,也是比較和儲存該用的兩種形式。NFKC 與 NFKD 會做完前兩種形式的一切,然後再替換相容字元:連字 fi 變成兩個字母,½ 變成 1⁄2,羅馬數字 Ⅻ 變成 XII,全形 A 變成普通的 A。這正是搜尋索引或關鍵詞過濾想要的效果,也正是絕對不能用在密碼上的操作。
為什麼是四種形式而不是一種

之所以分成四種,是因為要服務兩個不同的目標。組合與分解文本是一種規範等價的改動:結果含義相同,對前兩種形式來說沒有任何資訊丟失。把連字、上標、帶圈字母和全形形式摺疊掉,則是一種相容等價的改動:它是為了匹配方便而做的,並且確實會抹掉 Unicode 認為不同的兩個字串之間的區別。四種形式就是這兩個決定的全部組合。

上面的表來自本頁資料檔案裡的案例清單,並與平臺自帶的 String.prototype.normalize 核對過,自檢還會在你面前把每一行重跑一遍。請連著讀前兩行:它們是同一段文本的兩種寫法,規範化之後就成了同一個字串。

可用的示例

這些是「示例」按鈕會依次載入的字串,由本頁即時規範化得出,而不是預先寫好答案。每一個都是因為某種形式會對它做出可見的改動才被選進來的。

相容字元:用 NFKC 要付出什麼代價

NFKC 與 NFKD 是有損的,而且這種損失是故意的。它們回答的是「這是不是同一個詞」,而不是「這是不是同一個字串」。由此產生三個後果,而這三個在生產程式碼裡都很容易搞錯:

  • 絕不要用相容形式儲存密碼、令牌或簽名。兩個不同的輸入會塌縮成同一個儲存值,於是長度檢查或字元檢查就變弱了。密碼請用 NFC 規範化(或者乾脆不規範化),在兩側做完完全相同的處理之後再按位元組比較。
  • 一次相容摺疊可以把一個字元變成好幾個。連字 fi 是一個碼位,會變成兩個字母,所以在規範化之前做過長度校驗的欄位,規範化之後可能變長。先規範化,再校驗,再儲存。
  • NFKC 並不能去掉所有不可見字元。變體選擇符在 NFKC 和 NFKD 下都會存活,零寬連線符也是。這正是本頁要有掃描模式的原因:規範化回答的是關於字元形狀與身份的問題,而掃描器回答的是「到底有沒有東西在那裡」。
字素、碼位與位元組

規範化會改動這三者中的第二個,通常不會動第一個。這三個數字都是「這段文本的長度」,卻又各不相同,所以一個限制 20 個字元的表單欄位,可能對應一個 20 個碼位、30 個 UTF-16 單元、47 個位元組的字串。

字素那一列的數字是人眼數出來的個數。它與碼位計數之間的差距,正是掃描器存在的意義:一個由 12 個碼位構成的名字,可以顯示成 12 個字母,也可以顯示成 3 個字母夾著 9 個不可見記號,還可以顯示成一個表情符號。

那些你看不見的字元

零寬空格不佔寬度、不改變字形,卻是一個有真實碼位的真實字元。兩個在螢幕上看起來一模一樣的識別符號,可能就差了其中一個,於是一個列表裡的名字就可能是冒名頂替者 —— 它的排序、搜尋和認證結果都與旁邊那個名字不同。雙向控制符也是同理,它們能讓字串的顯示順序與書寫順序不同 —— 這是一個被用來偽裝檔名的老把戲。

第三個模式裡的掃描器會把它們連同位置一起全部列出:零寬與連線字元、雙向控制符、變體選擇符、不常見的空格、C0 與 C1 控制字元、私用碼位,以及不屬於任何合法代理對的代理項 —— 那根本不是一個字元。本頁靠上的類別表說明了每一類會做什麼,剔除按鈕則會去掉這些字元的一份副本,而不會動你的輸入。

本頁要抓的那些錯誤
  • 不規範化就直接比較兩個字串。 'é' === 'e\u0301' 是 false。比較之前請先用同一種形式規範化兩側,並在你要比較的索引裡也使用同一種形式。
  • 只規範化一側。 一個只規範化查詢條件、卻不管儲存值的查詢會什麼都查不到,而且看起來像是資料出了問題,而不是規範化的問題。
  • 用相容形式給表單做「整理」,還聲稱它是 NFC 安全的。 對密碼套用 NFKC 會縮小可能密碼的空間,並把本該區分開的輸入合併到一起。
  • 以為規範化形式能修好仿冒問題。 它去不掉零寬連線符、變體選擇符或同形字 —— 西里爾字母的 а 與拉丁字母的 a 是不同的碼位,NFC 對它們不做任何處理。
  • 輕信字元計數。 使用者看到的個數、資料庫列強制的個數、位元組上限強制的個數,是三個不同的數字,上面幾行已經展示了它們能差多遠。
  • 以為你拿到的一定是 NFC。 從輸入法、檔案或別的系統送來的文本,是那個系統產生什麼形式就是什麼形式。本頁預設停在 NFC,是因為這是 Web 應該使用的形式,而不是因為你的輸入本來就是它。
快速問答

“為什麼這兩個字串看起來一樣卻不相等?”

因為其中一個用了組合字元,另一個用了基字母加組合記號;或者其中一個含有一個根本不畫出任何東西的字元。切到「比較」模式,兩個框各放一個字串:本頁會報告規範化後第一個不同的位置,以及各自在該位置上是什麼。

“在應用裡我應該用哪一種形式?”

凡是給人讀、顯示、儲存和搜尋的內容都用 NFC,這也是 W3C 為 Web 推薦的形式。當你背後的程式碼想要基字母與獨立記號時用 NFD,這在文本處理以及 macOS 上的某些檔案系統裡很常見。只有在進行匹配而不是儲存時才用 NFKC 或 NFKD:搜尋索引、關鍵詞過濾、重複檢測。

“已經規範化的字串,拼接之後還規範嗎?”

不一定。規範化不會被拼接操作保持:把兩段 NFC 字串接起來,可能得到一段 NFC 本來會寫得不一樣的序列,因為第二個字串開頭的組合記號會掛到第一個字串末尾的字元上。請在組裝之後規範化,而不是隻在組裝之前。

“規範化會去掉不可見字元嗎?”

不會,只有一個半例外:NFKC 與 NFKD 會把不換行空格換成普通空格,這是一次相容摺疊帶來的可見結果。其餘的 —— 零寬空格、連線符、雙向控制符、變體選擇符 —— 在四種形式下都會原樣通過。這正是第三個模式存在的意義。

“為什麼字元數沒變,位元組數卻變了?”

因為兩種形式由不同的碼位構成,而這些碼位在 UTF-8 裡佔用的位元組數不同。預組合的 é 是兩個位元組;分解後的那一對是三個位元組。上面的計數表把輸入和每種形式的四個數字並排打印出來,所以差別是看得見的,而不是靠描述。

“我怎麼知道本頁的結果是對的?”

按自檢。它會規範化本頁資料檔案裡的每一個案例,按文件中記載的關係重新核對四種形式之間的關係(NFC 是冪等的、某個 NFC 字串的 NFD 與原始字串的 NFD 相同,等等),把不可見與控制字元表裡點名的每個字元都過一遍掃描器,並把字素案例與 Intl.Segmenter 對照計數。

標準與致謝

四種形式及其定義來自 Unicode 標準附件 15(Unicode 規範化形式)。用作分解示例的碼位是標準所指定的那些;本頁並不自己實現分解表 —— 它調用瀏覽器自帶的 String.prototype.normalize,這與平臺在文本輸入、排序和比較時使用的是同一份實現,所以這裡報告的結果,就是系統其他部分對同一字符串會做的事。字素簇在瀏覽器支持時來自 Intl.Segmenter,不支持時來自一個文檔化的近似算法。

本頁正文由本站撰寫。頁面上的每一個示例都由本頁自己的程式碼生成,並與資料檔案記錄的案例清單核對過,而該清單又針對相同字元與 Unicode NormalizationTest 資料核對過。不可見與危險字元表是為人類可讀的警告而精心挑選的子集,並不是完整的 Unicode 安全概要(UTS #39),本頁也明確說明了這一點,而不會暗示自己具備並不具備的完整性。