感謝 @黃俊亮 的投稿,原文:JavaScript 正則表達式匹配漢字。轉載請註明作者和出處。
一個可能有 20 年歷史的正則表達式在谷歌搜索「JavaScript 正則表達式匹配漢字」的時候,前幾條結果全都是 /[\u4e00-\u9fa5]/。沒有人懷疑這個正則表達式有什麼問題,那麼在 2018 年的今天,讓我們站在 Chrome 64 的肩膀上,放飛一下自我。
漢文(Han Script)是漢語、日本語、朝鮮語、韓國語的書寫系統中的一種文字(Script),越南語在早期也曾在書寫系統中使用漢文[1]。漢字(CJK Ideograph)是漢文的基本單元。各國都對漢字提出了自己的編碼標準,Unicode 將這些標準加總在一起進行統一編碼,力求實現原標準與 Unicode 編碼之間的無損轉換。Unicode 從語義(semantic)、抽象字形(abstract shape),具體字形(typeface)三個維度[2]出發,把不同編碼標準裡「起源相同、本義相同、形狀一樣或稍異」的漢字賦予相同編碼,這些被編碼的字符稱為中日韓統一表意文字(下文我們提到的「漢字」,如果不加說明,均指代中日韓統一表意文字)。如果把它們全部列舉出來寫成正則表達式,那麼就是技術上完整的匹配漢字的正則表達式了。
正則表達式 /[\u4e00-\u9fa5]/的意思是匹配所有從 U+4E00, cjk unified ideograph-4e00 到 U+9FA5, cjk unified ideograph-9fa5 的字符。這一段區域對應的是 Unicode 1.0.1 就收錄進來的中日韓統一表意文字(CJK Unified Ideographs)區塊,在 Unicode 3.0 加入擴展 A 區以前,這個正則表達式確實給出了所有漢字的編碼。換言之,從1992年到1999年,這個正則表達式確實是正確的,想必這個表達式已經有20年歷史了。
匹配所有統一表意文字然而時光飛逝,Unicode 在2017年6月發布了10.0.0版本。在這20年間,Unicode 添加了許多漢字。比如 Unicode 8.0 添加的 109 號化學元素「䥑(⿰⻐麥)」,其碼點是 9FCF,不在這個正則表達式範圍中。而如果我們期望程序裡的 /[\u4e00-\u9fa5]/可以與時俱進匹配最新的 Unicode 標準,顯然是不現實的事情。因此,我們需要換一個思路,寫一個無需維護的正則表達式:
/\p{Unified_Ideograph}/u
其中 \u是 ECMAScript 2015 定義的正則表達式標誌,意味著將表達式作為 Unicode 碼點序列。 \p是正在提案階段的正則表達式 Unicode 屬性轉義,它賦予了我們根據 Unicode 字符的屬性數據[3]構造表達式的能力。 Unified_Ideograph是 Unicode字符的一個二值屬性,對於漢字,其取值為 Yes,否則為 No。因此 \p{Unified_Ideograph}匹配所有滿足 Unified_Ideograph=yes的 Unicode 字符,而它的底層實現由運行時所依賴的 Unicode 版本決定,開發者不需要知道漢字的具體 Unicode 碼點範圍。
容易混淆的其他 Unicode 屬性轉義表達式/\p{Ideographic}/u
這個表達式匹配所有滿足 Ideographic=yes的 Unicode 字符。我們先看一下 UAX #44 對這個屬性的解釋[4] :
Characters considered to be CJKV (Chinese, Japanese, Korean, and Vietnamese) or other siniform (Chinese writing-related) ideographs. This property roughly defines the class of "Chinese characters" and does not include characters of other logographic scripts such as Cuneiform or Egyptian Hieroglyphs.
這個屬性表明該字符屬於 CJKV 表意文字或者與漢語書寫相關的其他表意文字(如西夏文、女書),這個屬性粗略地定義了「中文字符」的分類。我們查看Unicode 10.0.0 字符屬性列表可以知道,在 Unicode 10.0.0 中,Ideographic 屬性為 yes 的字符有
3006 ; Ideographic # Lo IDEOGRAPHIC CLOSING MARK
3007 ; Ideographic # Nl IDEOGRAPHIC NUMBER ZERO
3021..3029 ; Ideographic # Nl [9] HANGZHOU NUMERAL ONE..HANGZHOU NUMERAL NINE
3038..303A ; Ideographic # Nl [3] HANGZHOU NUMERAL TEN..HANGZHOU NUMERAL THIRTY
3400..4DB5 ; Ideographic # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5
4E00..9FEA ; Ideographic # Lo [20971] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FEA
F900..FA6D ; Ideographic # Lo [366] CJK COMPATIBILITY IDEOGRAPH-F900..CJK COMPATIBILITY IDEOGRAPH-FA6D
FA70..FAD9 ; Ideographic # Lo [106] CJK COMPATIBILITY IDEOGRAPH-FA70..CJK COMPATIBILITY IDEOGRAPH-FAD9
17000..187EC ; Ideographic # Lo [6125] TANGUT IDEOGRAPH-17000..TANGUT IDEOGRAPH-187EC
18800..18AF2 ; Ideographic # Lo [755] TANGUT COMPONENT-001..TANGUT COMPONENT-755
1B170..1B2FB ; Ideographic # Lo [396] NUSHU CHARACTER-1B170..NUSHU CHARACTER-1B2FB
20000..2A6D6 ; Ideographic # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6
2A700..2B734 ; Ideographic # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734
2B740..2B81D ; Ideographic # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D
2B820..2CEA1 ; Ideographic # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA12CEB0..2EBE0 ; Ideographic # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0
2F800..2FA1D ; Ideographic # Lo [542] CJK COMPATIBILITY IDEOGRAPH-2F800..CJK COMPATIBILITY IDEOGRAPH-2FA1D
Total code points: 96174
它們囊括了所有統一表意文字、西夏文及其組件、女書、中日韓兼容性字符、蘇州碼子、「〇」以及日本語中的書信結尾標誌「〆」。使用 /\p{Ideographic}/u來匹配漢字會過於寬泛。一是包含了西夏文、女書,二是只用於編碼轉換用的兼容字符也納入其中。
/\p{Script=Han}/u
Script 屬性[5]用來篩選滿足下麵條件的一組字符:
字符的書寫形式具有共同的圖像特徵與文字流變
該組字符全部用來表達某個書寫系統內的文本信息(textual information)
我們查看Unicode 10.0.0 Scripts可以知道,滿足 Script=Han的字符有
2E80..2E99 ; Han # So [26] CJK RADICAL REPEAT..CJK RADICAL RAP
2E9B..2EF3 ; Han # So [89] CJK RADICAL CHOKE..CJK RADICAL C-SIMPLIFIED TURTLE
2F00..2FD5 ; Han # So [214] KANGXI RADICAL ONE..KANGXI RADICAL FLUTE
3005 ; Han # Lm IDEOGRAPHIC ITERATION MARK
3007 ; Han # Nl IDEOGRAPHIC NUMBER ZERO
3021..3029 ; Han # Nl [9] HANGZHOU NUMERAL ONE..HANGZHOU NUMERAL NINE
3038..303A ; Han # Nl [3] HANGZHOU NUMERAL TEN..HANGZHOU NUMERAL THIRTY
303B ; Han # Lm VERTICAL IDEOGRAPHIC ITERATION MARK
3400..4DB5 ; Han # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5
4E00..9FEA ; Han # Lo [20971] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FEA
F900..FA6D ; Han # Lo [366] CJK COMPATIBILITY IDEOGRAPH-F900..CJK COMPATIBILITY IDEOGRAPH-FA6D
FA70..FAD9 ; Han # Lo [106] CJK COMPATIBILITY IDEOGRAPH-FA70..CJK COMPATIBILITY IDEOGRAPH-FAD9
20000..2A6D6 ; Han # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6
2A700..2B734 ; Han # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734
2B740..2B81D ; Han # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D
2B820..2CEA1 ; Han # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1
2CEB0..2EBE0 ; Han # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0
2F800..2FA1D ; Han # Lo [542] CJK COMPATIBILITY IDEOGRAPH-2F800..CJK COMPATIBILITY IDEOGRAPH-2FA1D
# Total code points: 89228
它們囊括了所有統一表意文字、中日韓兼容性字符、蘇州碼子、「〇」、「〆」、「々」以及字典常用的部首。從前面漢文(Han Script)與漢字(CJK Ideograph)的關係我們可以知道, /\p{Script=Han}/u匹配的是漢文作為一個字符集裡面的所有字符,因此它包括了部首、「々」等字符,這些字符要麼當它們獨立存在的時候沒有語言意義(部首獨立存在是一個符號),要麼無法獨立存在(「々」依賴於所修飾的漢字)。所以漢字是漢文的一個單元,漢文除了包含漢字以外,還包括這些符號、數字、修飾符。因此使用 /\p{Script=Han}/u來匹配漢字是混淆了漢文與漢字的概念範圍。
瀏覽器兼容性支持JavaScript截至2018年1月,只有 Chrome 64 支持正則表達式 Unicode 屬性轉義。對於其他瀏覽器,我們需要用 babel轉譯插件@babel/plugin-proposal-unicode-property-regex的底層將帶有屬性轉義的正則表達式轉為 Unicode 碼點正則表達式或者 ES 5 的正則表達式。轉譯結果的在線演示可以在這裡查看,用戶可以自己在上面轉譯其他的 Unicode 屬性轉義正則表達式。我們在這裡列舉 /\p{Unified_Ideograph}/u轉譯成Unicode 碼點正則表達式的結果:
const regex = /\p{Unified_Ideograph}/u;
// transpiled to ES6:
const regex = /[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29\u{20000}-\u{2A6D6}\u{2A700}-\u{2B734}\u{2B740}-\u{2B81D}\u{2B820}-\u{2CEA1}\u{2CEB0}-\u{2EBE0}]/u;
從上面這個正則表達式可以知道,轉譯的結果嚴格跟 Unicode 10.0.0 中 Unified_Ideograph 屬性為 yes 的字符
3400..4DB5 ; Unified_Ideograph # Lo [6582] CJK UNIFIED IDEOGRAPH-3400..CJK UNIFIED IDEOGRAPH-4DB5
4E00..9FEA ; Unified_Ideograph # Lo [20971] CJK UNIFIED IDEOGRAPH-4E00..CJK UNIFIED IDEOGRAPH-9FEA
FA0E..FA0F ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA0E..CJK COMPATIBILITY IDEOGRAPH-FA0F
FA11 ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA11
FA13..FA14 ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA13..CJK COMPATIBILITY IDEOGRAPH-FA14
FA1F ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA1F
FA21 ; Unified_Ideograph # Lo CJK COMPATIBILITY IDEOGRAPH-FA21
FA23..FA24 ; Unified_Ideograph # Lo [2] CJK COMPATIBILITY IDEOGRAPH-FA23..CJK COMPATIBILITY IDEOGRAPH-FA24
FA27..FA29 ; Unified_Ideograph # Lo [3] CJK COMPATIBILITY IDEOGRAPH-FA27..CJK COMPATIBILITY IDEOGRAPH-FA29
20000..2A6D6 ; Unified_Ideograph # Lo [42711] CJK UNIFIED IDEOGRAPH-20000..CJK UNIFIED IDEOGRAPH-2A6D6
2A700..2B734 ; Unified_Ideograph # Lo [4149] CJK UNIFIED IDEOGRAPH-2A700..CJK UNIFIED IDEOGRAPH-2B734
2B740..2B81D ; Unified_Ideograph # Lo [222] CJK UNIFIED IDEOGRAPH-2B740..CJK UNIFIED IDEOGRAPH-2B81D
2B820..2CEA1 ; Unified_Ideograph # Lo [5762] CJK UNIFIED IDEOGRAPH-2B820..CJK UNIFIED IDEOGRAPH-2CEA1
2CEB0..2EBE0 ; Unified_Ideograph # Lo [7473] CJK UNIFIED IDEOGRAPH-2CEB0..CJK UNIFIED IDEOGRAPH-2EBE0
# Total code points: 87882
嚴格對應。因此轉譯是正確的。
該插件還可以使用
{
"plugins": [
["@babel/plugin-proposal-unicode-property-regex", { "useUnicodeFlag": false }]
]
}
配置將表達式轉成 ES5 的傳統的以字符的 UTF16 表示為序列的字符串,這裡不再贅述。
input 元素的 pattern 屬性在前端技術中,除了JavaScript會用到正則表達式,HTML 裡 <input>元素的 pattern屬性也會用到正則表達式。與 JavaScript 相比, pattern不支持設置正則表達式的標誌位,因此 HTML 標準中強制規定了 input 元素的 pattern 屬性需要施加 unicode標誌 [6]。目前只有 Chrome 53+, Firefox 遵循了這一標準,其他的瀏覽器暫未支持。
在 React/Angular/Vue.js 三大前端框架中,Angular 提供了近似於 pattern 的指令 ngPattern。目前 ngPattern尚未施加 unicode標誌 [7]。AngularJS 的 ngPattern directive 仍未施加。
在大部分情況,是否施加 unicode標誌不會對正則表達式產生語義區別。主要的差別在於,在使用 \u{10000}表示 Unicode 碼點字符情形,正則表達式 /\u{10000}/代表匹配 u一萬次, /\u{10000}/u匹配字符 \u{10000}一次; /./只匹配 BMP 平面的字符, /./u匹配所有平面的字符。
由於 Unicode 屬性轉義正則表達式依賴於標識位 \u,因此下面的用法目前只能在 Chrome 下使用:
<inputtype="text"pattern="\p{Unified_Ideograph}">
因此,如果需要兼容其他瀏覽器,可以使用轉譯插件的底層庫regexpu-core在 js 層轉換正則表達式,再把轉換結果輸送到 HTML 模版中。
const rewritePattern = require("regexpu-core");
rewritePattern('\\p{Unified_Ideograph}', 'u', {
'unicodePropertyEscape': true,
'useUnicodeFlag': false
});
// → '/(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])/'
總結/[\u4e00-\u9fa5]/是錯的,不要用二十年前的正則表達式了
/\p{Unified_Ideograph}/u是正確的,不需要維護,匹配所有漢字。這裡 \p是 Unicode 屬性轉義正則表達式。
/\p{Ideographic}/u 和 /\p{Script=Han}/u 匹配了除了漢字以外的其他一些字符,在「漢字匹配正則表達式」這個需求下,是錯的。
目前只有 Chrome 支持 Unicode 屬性轉義正則表達式。對其他環境,使用 @babel/plugin-proposal-unicode-property-regex 和 regexpu-core 進行優雅降級。
參考資料[1] Unicode 10.0.0 第六章第一節,書寫系統
[2] Unicode 10.0.0 第十八章第一節,東亞
[3] Unicode 10.0.0 字符屬性列表
[4] UAX #44 第 20 版的屬性說明
[5] UAX #24 第 27 版
[6] HTML 標準中 input元素的 pattern屬性
[7] 給 ngPattern施加 unicode標誌