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 可以使用 IsHan
、script=Han
或 sc=Han
的方式來指定特性,例如測試漢字(Han
包含了正體中文、簡體中文,以及日、韓、越南文的全部漢字):
jshell> "a".matches("\\p{Ll}");
$7 ==> true
jshell> "a".matches("\\p{Lu}");
jshell> "林".matches("\\p{IsHan}");
$8 ==> true
對於 Unicode 碼點區塊(block) ,可以使用 InCJKUnifiedIdeographs
、block=CJKUnifiedIdeographs
或 blk=CJKUnifiedIdeographs
,例如,測試中文時常用的 Unicode 碼點範圍為 U+4E00 到 U+9FFF,也就是 CJK Unified Ideographs 的範圍:
jshell> "林".matches("\\p{InCJKUnifiedIdeographs}");
$9 ==> true
Unicode 大小寫與字元類
在〈Pattern 物件〉談過,Pattern.UNICODE_CASE
、Pattern.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
。