Java 與 Unicode 規則表示式

July 2, 2022

在往下看之前,請先看看〈認識 Unicode〉,瞭解 Unicode 的發展、碼元與碼點的不同。

char 與 String

Java 支援 Unicode,char 型態佔 2 個位元組,對於碼點在 U+0000 至 U+FFFF 範圍內的字元,例如 '林',原始碼中可使用以下方式表示:

char fstName1 = '林';
char fstName2 = '\u6797';

認識 Unicode〉談過,U+0000 至 U+FFFF 範圍內的字元,Unicode 歸類為BMP(Basic Multilingual Plane),碼點與碼元一對一對應,現在問題來了,若字元不在 BMP 範圍內呢?例如高音譜記號 𝄞 的 Unicode 碼點為 U+1D11E,顯然無法只用一個 \uhhhh 來表示,也無法儲存在 char 型態的空間。

程式中若真的要表示 𝄞,必須使用字串儲存,而 " " 在編譯時會轉換為 "\uD834\uDD1E" 來表示,分別表示 UTF-16 編碼時的高低碼元,這稱為代理對(Surrogate pair)。

也就是說,在 Java 中,字串裡的 \uhhhh 的表示法,在 BMP 範圍內才是代表碼點,若超過 BMP 範圍,就只是在指定碼元,而不是碼點。

規則表示式的 \uhhhh

字元表示、字元類〉看過,規則表示式中也可以使用 \uhhhh,BMP 範圍代表碼點,例如:

jshell> "林".matches("\\u6797");
$1 ==> true

jshell> "\u6797".matches("\\u6797");
$1 ==> true

別將字串裡的 \uhhhh 與規則表示鄉的 \uhhhh 搞混了!"\u6797",而 matches 裡的 "\\u6797" 代表規則表示式 \u6797

超過 BMP 範圍的話,Java 裡也可以採用代理對表示,也就是 \uhhhh 這時代表了碼元。例如比對高音譜記號 𝄞:

jshell> "\uD834\uDD1E".matches("\\uD834\\uDD1E");
$1 ==> true

使用代理對並不方便,Java 的話可以用 \x{h…h} 來表示,h…h 表示碼點,\x{h…h} 要用來表示 BMP 範圍內的碼點也是可以的:

jshell> "\uD834\uDD1E".matches("\\x{1D11E}")
$1 ==> true

jshell> "林".matches("\\x{6797}")
$2 ==> true

Unicode 特性轉譯

在〈認識 Unicode〉談過,在 Unicode 規範中,每個 Unicode 字元會隸屬於某個分類。

Java 在 Unicode 特性的支援上,使用 \p\P 的方式,\p 表示具備某特性(Properties),而 \P 表示不具備某特性。

例如,\p{L} 表示字母(Letter),\p{N} 表示數字(Number)等,可以進一步指定子特性,例如 \p{Lu} 表示大寫字母、\p{Ll} 表示小寫字母:

jshell> "a".matches("\\p{Ll}");
$5 ==> true

jshell> "a".matches("\\p{Lu}");
$6 ==> false

來個有趣的測試吧!²³¹¼½¾㉛㉜㉝ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ都是數字,底下程式片段會顯示true:

System.out.println(
    (
        "²³¹¼½¾" + 
        "㉛㉜㉝" +
        "ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯ" + 
        "ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹ"
    )
    .matches("\\pN*")
);

也可以加上 Is 來表示二元特性,例如 \p{IsL}\p{IsLu} 等,若是單字元表示特性,例如 \p{L},可以省略 {} 寫為 \pL;也可以使用 \p{general_category=Lu} 或簡寫為 \p{gc=Lu}

在〈認識 Unicode〉談過,有的語言可能會使用多種文字來書寫,例如日語就包含了漢字、平假名、片假名等文字,有的語言只使用一種文字,例如泰文。Unicode 將書寫組織為文字(Script)特性。

Java 可以使用 IsHanscript=Hansc=Han 的方式來指定特性,例如測試漢字(Han 包含了正體中文、簡體中文,以及日、韓、越南文的全部漢字):

jshell> "a".matches("\\p{Ll}");
$7 ==> true

jshell> "a".matches("\\p{Lu}");
jshell> "林".matches("\\p{IsHan}");
$8 ==> true

對於 Unicode 碼點區塊(block) ,可以使用 InCJKUnifiedIdeographsblock=CJKUnifiedIdeographsblk=CJKUnifiedIdeographs,例如,測試中文時常用的 Unicode 碼點範圍為 U+4E00 到 U+9FFF,也就是 CJK Unified Ideographs 的範圍:

jshell> "林".matches("\\p{InCJKUnifiedIdeographs}");
$9 ==> true

Unicode 大小寫與字元類

在〈Pattern 物件〉談過,Pattern.UNICODE_CASEPattern.UNICODE_CHARACTER_CLASS 與規則表示式在 Unicode 方面的支援相關。

首先來看 Pattern.UNICODE_CASE,在設定 Pattern.CASE_INSENSITIVE 時,可以加上 Pattern.UNICODE_CASE 啟用 Unicode 版本的忽略大小寫。例如,比較 Ä(U+00C4)與 ä(U+00E4)其實是同一字母的大小寫:

jshell> var regex1 = Pattern.compile("\u00C4", Pattern.CASE_INSENSITIVE);
regex3 ==> ?

jshell> regex1.matcher("\u00E4").find();
$11 ==> false

jshell> var regex2 = Pattern.compile("\u00C4", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
regex4 ==> ?

jshell> regex2.matcher("\u00E4").find();
$13 ==> true

規則表示式是在後來才支援 Unicode,這就有了個問題,例如預定義字元類沒有考量 Unicode 規範,例如 \w 預設只比對 ASCII 字元,若要令 \w 可以比對 Unicode 字元,可以設置 (?U)(對應 Pattern.UNICODE_CHARACTER_CLASS):

jshell> "林".matches("\\w");
$14 ==> false

jshell> "林".matches("(?U)\\w");
$15 ==> true

例如,𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼 都是十進位數字,然而 "𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼".matches("\\d*") 會是 false,若是使用 "𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼".matches("(?U)\\d*") 會是 true

分享到 LinkedIn 分享到 Facebook 分享到 Twitter