Go 與 Unicode 規則表示式
July 13, 2022其實在 Go 裡要處理文字,開發者一開始就必須了解 Unicode、UTF 的差別,一開始就會面對「字串持有的就是位元組」這個事實,len("Go語言")
的結果會是8,也是因為實際上 "Go語言"
就是長度為 8 的 []byte
,也就是 UTF-8 編碼。
Go 的字串/碼點
在〈Strings, bytes, runes and characters in Go〉談到,「字元」的定義本身就很模糊,而在這模糊的定義上,各語言又去定義「字串」是由字元組成這件事,就更令人搞不清楚了,因此衍生出許多文字處理方面的問題。
為了避免這些問題,Go 的字串從一開始,就以 UTF-8 為設計的中心,開發者在指定索引時,就是指定 UTF-8 碼元(code unit)的位置,當下所取得的結果會是 byte
;此時,我們可以使用 byte[]("Go語言")
取得儲存的編碼位元組,切片操作的結果也會是 []byte
。
許多與字串索引相關的 API 操作,傳回索引值時,指的都是字串持有的 []byte
之位置。例如,strings
套件有不少字串處理 API,名稱有 Index 的字樣,例如,strings.Index
傳回的整數。指的就是 []byte
的索引位置;regex.Regexp
實例上具有 Index 字樣方法,也是如此。
為了避免「字元」定義的模糊問題,Go 沒有所謂的字元對應型態,只有碼點的概念。使用 for range
走訪 "Go語言"
時:
for i, cp := range "Go語言" {
fmt.Printf("%d %q\n", i, cp)
}
最後一個 i
會是 5,原因是:UTF-8 在編碼時,中文字會使用三個碼元(也就是三個位元組),for range
會試著識別一組碼元,確認是否對應至 Unicode 碼點(code point),若是指定給 cp
,而 cp
的型態是 rune
。
rune
為 int32
的別名,可用來儲存 Unicode 碼點,如果將方才範例 %q
改為 %d
,就會看到「語」、「言」的碼點的十進位數字,分別是 35486、35328,在 Go 也可以用 []rune("Go語言")
來取得 []rune
,每個索引位置儲存的都是碼點。
Go 的 rune
,儲存的就是文字的碼點號碼,其型態名稱不使用 codepoint 的原因在於,rune
這個名稱比較簡短,並且來自一類已滅絕的盧恩字母(Runes)(https://en.wikipedia.org/wiki/Runes)。
Go 主要使用 UTF-8 位元組格式,作為字串的底層儲存,不過,也提供了 unicode
套件來協助 Unicode 碼點特性判斷。
例如,unicode/utf8
可用來進行 rune
與 UTF-8 之間的驗證、轉換,unicode/utf16
套件用來進行 rune
與 UTF-16 編碼之間的處理。
如果察看 unicode/utf8
,會發現處理的資料是 []byte
,這是因為 UTF-8 的碼元是八個位元,Go 使用 byte
(也就是uint8
)儲存;UTF-16 編碼的碼元會是十六個位元,Go 使用 uint16
來儲存,因此 unicode/utf16
處理的資料是 []uint16
。
Unicode 與規則表示式
在撰寫 Go 的字串時,可以使用 \Uhhhh
或 \Uhhhhhhhh
來指定碼點,不過在撰寫規則表示式時,是使用 \x{...}
來指定碼點。例如:
package main
import (
"fmt"
"regexp"
)
func main() {
// 比對高音譜記號
matched, _ := regexp.MatchString(`\x{1D11E}`, "\U0001D11E")
fmt.Println(matched)
}
在 Unicode 特性的支援上,使用 \p
、\P
的方式,表示具有或不具有指定的特性,\pN
、\PN
的 N
是單一字母,若要多個字母組合,可以使用 \p{...}
、\P{...}
。
例如〈一般分類特性〉,\pL
表示字母(Letter),\pN
表示數字(Number)等,若要進一步指定子特性,例如 \p{Lu}
表示大寫字母、\p{Ll}
表示小寫字母:
fmt.Println(regexp.MatchString(`\p{Ll}`, "a")) // true <nil>
fmt.Println(regexp.MatchString(`\p{Lu}`, "a")) // false <nil>
來個有趣的比對吧!𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼
都是十進位數字:
fmt.Println(regexp.MatchString(`\p{Nd}`, "𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼")) // true <nil>
數字呢?²³¹¼½¾𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼㉛㉜㉝ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ
都是:
fmt.Println(regexp.MatchString(`\p{N}`, "²³¹¼½¾𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼㉛㉜㉝ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ")) // true <nil>
有的語言可能會使用多種文字來書寫,例如日語就包含了漢字、平假名、片假名等文字,有的語言只使用一種文字,例如泰文。Unicode 將碼點群組為文字(script)特性上,測試時只要寫上文字特性名稱就可以了,例如測試漢字、希臘文:
fmt.Println(regexp.MatchString(`\p{Han}`, "林")) // true <nil>
fmt.Println(regexp.MatchString(`\p{Greek}`, "α")) // true <nil>