Unicode 文字種の判別

Zenn で「やっかいな日本語」なる記事 (ポエム) を書いたが,このブログでは Go における Unicode 文字種の判別に話題を絞って紹介してみる。

Unicode 文字種を判別するには unicode 標準パッケージが使える。

判別用の unicode.RangeTable を用意し,これを参照することで文字種を判別することができる。 このパッケージの中身を見ると分かるが,かなりの数の定義済み unicode.RangeTable テーブルが取り揃えられている。 今回はこの定義済みテーブルのみ使うことにしよう。

図形文字と制御文字

まずは大雑把に「図形文字」と「制御文字」を判別してみよう。

図形文字の判別には unicode.IsGraphic() 関数が,制御文字の判別には unicode.IsControl() 関数が使える。

ただし unicode.IsControl() 関数では U+00FF 以下の ISO 88591 で定義されている制御文字領域しか判別してくれないようで BOM (U+FEFF) などの Unicode 独自の制御文字も含めて判別するのであれば unicode.C テーブルを使う必要がある。

そこで,こんな関数を考えてみる。

import "unicode"

func check(r rune) string {
    switch {
    case unicode.IsGraphic(r):
        return "Graphic"
    case unicode.IsControl(r):
        return "Latin1 Control"
    case unicode.Is(unicode.C, r):
        return "Unicode Control"
    }
    return "Unknown"
}

これを使って実際に文字列をチェックしてみよう。

func main() {
    text := string([]byte{0xef, 0xbb, 0xbf, 0xe3, 0x82, 0x84, 0x09, 0xe3, 0x81, 0x82})
    fmt.Println(text)
    for _, c := range text {
        fmt.Printf("%#U (%v)\n", c, check(c))
    }
}

これを実行すると

$ go run sample1.go
や     あ
U+FEFF (Unicode Control)
U+3084 'や' (Graphic)
U+0009 (Latin1 Control)
U+3042 'あ' (Graphic)

となった。うんうん。

結合子と異体字セレクタ

上述の check() 関数を使って,今度は絵文字の中身を見てみる。

func main() {
    text := "🙇‍♂️"
    for _, c := range text {
        fmt.Printf("%#U (%v)\n", c, check(c))
    }
}

これを実行すると

$ go run sample2.go
U+1F647 '🙇' (Graphic)
U+200D (Unicode Control)
U+2642 '♂' (Graphic)
U+FE0F '️' (Graphic)

となった。

ありゃ。 ZWJ はともかく異体字セレクタって図形文字あつかいなんだ。

これでは大雑把すぎるので check() 関数にいくつか条件を足して

func check(r rune) string {
    switch {
    case unicode.Is(unicode.Sc, r):
        return "Symbol/currency"
    case unicode.Is(unicode.Sk, r):
        return "Symbol/modifier"
    case unicode.Is(unicode.Sm, r):
        return "Symbol/math"
    case unicode.Is(unicode.So, r):
        return "Symbol/other"
    case unicode.Is(unicode.Variation_Selector, r):
        return "Variation Selector"
    case unicode.Is(unicode.Join_Control, r):
        return "Join Control"
    case unicode.IsGraphic(r):
        return "Graphic"
    case unicode.IsControl(r):
        return "Latin1 Control"
    case unicode.Is(unicode.C, r):
        return "Unicode Control"
    }
    return "Unknown"
}

と書き換えてみる。 これを使ってもう一度実行してみると

$ go run sample2.go
U+1F647 '🙇' (Symbol/other)
U+200D (Join Control)
U+2642 '♂' (Symbol/other)
U+FE0F '️' (Variation Selector)

となった。これで結合子や異体字セレクタをきちんと判別できる。 なお,シンボルについて細かく区別しなくていいのなら unicode.IsSymbol() 関数を使う手もある。

漢字と部首

以前「こんな埼「玉」修正してやるぅ」でも書いたが, Unicode では漢字の部首にもコードポイントが割り当てられている。 しかし,幸いなことに unicode 標準パッケージの定義済み unicode.RangeTable テーブルで部首を判別可能である。

具体的には check() 関数を以下のように書き換える。

func check(r rune) string {
    switch {
    case unicode.Is(unicode.Radical, r):
        return "Radical"
    case unicode.Is(unicode.Ideographic, r):
        return "Ideographic"
    case unicode.IsGraphic(r):
        return "Graphic"
    case unicode.IsControl(r):
        return "Latin1 Control"
    case unicode.Is(unicode.C, r):
        return "Unicode Control"
    }
    return "Unknown"
}

これを使えば

func main() {
    text := "⽟玉"
    for _, c := range text {
        fmt.Printf("%#U (%v)\n", c, check(c))
    }
}

の実行結果が

$ go run sample3.go 
U+2F5F '⽟' (Radical)
U+7389 '玉' (Ideographic)

となる。

なお, unicode.Ideographic テーブルで判別できるのは本当に漢字だけなので,全角の英数字・かな文字・記号は,これにかからない。 ちなみに,部首は絵文字と同じくシンボル扱いなので unicode.IsSymbol() 関数でも一応は区別できる。

3羽の「ペンギン」

次は check() 関数をかな文字を判別するよう書き換える。 こんな感じ。

func check(r rune) string {
    switch {
    case unicode.Is(unicode.Katakana, r):
        return "Katakana"
    case unicode.Is(unicode.Hiragana, r):
        return "Hiragana"
    case unicode.Is(unicode.Lm, r):
        return "Letter/modifier"
    case unicode.Is(unicode.Lo, r):
        return "Letter"
    case unicode.Is(unicode.Mc, r):
        return "Mark/spacing combining"
    case unicode.Is(unicode.Me, r):
        return "Mark/enclosing"
    case unicode.Is(unicode.Mn, r):
        return "Mark/nonspacing"
    case unicode.IsGraphic(r):
        return "Graphic"
    case unicode.IsControl(r):
        return "Latin1 Control"
    case unicode.Is(unicode.C, r):
        return "Unicode Control"
    }
    return "Unknown"
}

これを使って以下の文字列を判別してみる。

func main() {
    text := "ペンギンペンギンペンギン"
    for _, c := range text {
        fmt.Printf("%#U (%v)\n", c, check(c))
    }
}

実行結果は以下の通り。

$ go run sample4.go 
U+30DA 'ペ' (Katakana)
U+30F3 'ン' (Katakana)
U+30AE 'ギ' (Katakana)
U+30F3 'ン' (Katakana)
U+30D8 'ヘ' (Katakana)
U+309A '゚' (Mark/nonspacing)
U+30F3 'ン' (Katakana)
U+30AD 'キ' (Katakana)
U+3099 '゙' (Mark/nonspacing)
U+30F3 'ン' (Katakana)
U+FF8D 'ヘ' (Katakana)
U+FF9F '゚' (Letter/modifier)
U+FF9D 'ン' (Katakana)
U+FF77 'キ' (Katakana)
U+FF9E '゙' (Letter/modifier)
U+FF9D 'ン' (Katakana)

濁点や半濁点の文字種が全角と半角で異なっている点に注意。 ホンマ,面倒くさいったら。

面倒な Unicode

unicode 標準パッケージにある定義済み unicode.RangeTable テーブルはよくできてるし,ある程度は日本語も考慮されているけど,細かい制御を行うのであれば用途に応じて専用の unicode.RangeTable テーブルを用意したほうがいいだろう。 量が多くて面倒くさいけどね。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. 8ビット空間の符号化文字集合および文字エンコーディング。国や言語ごとにいくつかのバリエーションがある。最も有名なのはドイツ語やフランス語の文字を含む ISO 8859-1,通称 Latin-1 だろう。日本の JIS X 0201 も ISO 8859 のバリエーションと言える。 ↩︎