JavaScript- 正則表達式匹配漢字

原文鏈接:https://zhuanlan.zhihu.com/p/33335629

此文發佈於2018-01-26-知乎(畢竟技術在不斷的變化,請根據實際情況去寫自己需要的代碼)

一個可能有 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-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

截至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 下使用:

<input type="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])/'

 

總結

  1. `/[\u4e00-\u9fa5]/`是錯的,不要用二十年前的正則表達式了
  2. `/\p{Unified_Ideograph}/u`是正確的,不需要維護,匹配所有漢字。這裏`\p`是 Unicode 屬性轉義正則表達式。
  3. `/\p{Ideographic}/u` 和 `/\p{Script=Han}/u` 匹配了除了漢字以外的其他一些字符,在「漢字匹配正則表達式」這個需求下,是錯的。
  4. 目前只有 Chrome 支持 Unicode 屬性轉義正則表達式。對其他環境,使用`@babel/plugin-proposal-unicode-property-regex` 和 `regexpu-core` 進行優雅降級。

參考資料

[1] Unicode 10.0.0 第六章第一節,書寫系統 http://www.unicode.org/versions/Unicode10.0.0/ch06.pdf

[2] Unicode 10.0.0 第十八章第一節,東亞 http://www.unicode.org/versions/Unicode10.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/tr44/tr44-20.html#Property_Definitions

[5] UAX #24 第 27 版 http://www.unicode.org/reports/tr24/tr24-27.html#Introduction

[6] HTML 標準中`input`元素的`pattern`屬性 https://html.spec.whatwg.org/multipage/input.html#the-pattern-attribute

[7] 給`ngPattern`施加`unicode`標誌 https://github.com/angular/angular/pull/20819

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章