JavaScript 正則表達式匹配漢字
本文全文轉載自JavaScript正則表達式匹配漢字,轉載前已經獲得了作者,也就是我,的許可。追求更好的閱讀體驗請看原文。
2019年10月更新:
- 瀏覽器支持數據更新至2019 年10 月。
- 使用7.7版本的
@babel/env
即可自動打開Unicode屬性轉義正則表達式支持
一個可能有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`是ECMAScript 2018定義的正則表達式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-2CEA1
2CEB0..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]用來篩選滿足下麵條件的一組字符:
1. 字符的書寫形式具有共同的圖像特徵與文字流變
2. 該組字符全部用來表達某個書寫系統內的文本訊息(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
截至2019年10月,Chrome 64以上, Safari 11.1以上都支持正則表達式Unicode屬性轉義。對於其他瀏覽器,我們推薦使用7.7 版本的`@babel/env`轉譯配置將帶有屬性轉義的正則表達式轉為Unicode 碼點正則表達式。
{"presets":["@babel/env"]}
就可以根據`browserslist`指定的瀏覽器版本自動決定是否進行此特性的轉譯。注意Babel/env 7.3 - 7.6的版本對此特性的支持產生了問題,該問題已經在PR 10447得到解決,將隨Babel 7.7.0一同發布。
如果您還在使用更老一點的Babel 7.x版本,需要用`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-u4DB5u4E00-u9FEAuFA0EuFA0FuFA11uFA13uFA14uFA1FuFA21uFA23uFA24uFA27-uFA29u{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]。目前除了IE 和Edge 18 以外的主流瀏覽器都已經實現了這一標準。 ( Unicode-aware regular expressions in ES2015 )
在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 屬性轉義正則表達式需要Chrome 64 以上,Safari 11.1 以上,因此下面的用法目前只能在這些瀏覽器中使用:
<inputtype="text"pattern="p{Unified_Ideograph}">
因此,如果需要兼容其他瀏覽器,可以使用轉譯插件的底層庫regexpu-core在js層轉換正則表達式,再把轉換結果輸送到HTML模版中。
constrewritePattern=require("regexpu-core");rewritePattern('p{Unified_Ideograph}','u',{'unicodePropertyEscape':true,'useUnicodeFlag':false});// → '/(?:[u3400-u4DB5u4E00-u9FEAuFA0EuFA0FuFA11uFA13uFA14uFA1FuFA21uFA23uFA24uFA27-uFA29]|[uD840-uD868uD86A-uD86CuD86F-uD872uD874-uD879][uDC00-uDFFF]|uD869[uDC00-uDED6uDF00-uDFFF]|uD86D[uDC00-uDF34uDF40-uDFFF]|uD86E[uDC00-uDC1DuDC20-uDFFF]|uD873[uDC00-uDEA1uDEB0-uDFFF]|uD87A[uDC00-uDFE0])/'
總結
- `/[u4e00-u9fa5]/`是錯的,不要用二十年前的正則表達式了
- `/p{Unified_Ideograph}/u`是正確的,不需要維護,匹配所有漢字。這裡`p`是Unicode 屬性轉義正則表達式。
- `/p{Ideographic}/u` 和`/p{Script=Han}/u` 匹配了除了漢字以外的其他一些字符,在「漢字匹配正則表達式」這個需求下,是錯的。
- 目前Chrome 和Safari 支持Unicode 屬性轉義正則表達式。對其他環境,使用7.7 版本的`@babel/env` 就可以自動根據瀏覽器規定打開支持。
參考資料
[1] Unicode 10.0.0第六章第一節,書寫系統http://www. unicode.org/versions/Un icode10.0.0/ch06.pdf
[2] Unicode 10.0.0第十八章第一節,東亞http://www. unicode.org/versions/Un icode10.0.0/ch18.pdf
[3] Unicode 10.0.0字符屬性列表http://www. unicode.org/Public/10.0 .0/ucd/PropList.txt
[4] UAX #44第20版的屬性說明http://www. unicode.org/reports/tr4 4/tr44-20.html#Property_Definitions
[5] UAX #24第27版http://www. unicode.org/reports/tr2 4/tr24-27.html#Introduction
[6] HTML標準中`input`元素的`pattern`屬性https:// html.spec.whatwg.org/mu ltipage/input.html#the-pattern-attribute
[7]給`ngPattern`施加`unicode`標誌https:// github.com/angular/angu lar/pull/20819