サロゲートペアの入った文字列を処理する場合の注意点
サロゲートペア
「𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。」
ほっけの ひらきを いざかやで ちゅうもんして、いちじかん たつが まだこない。 ぞくな きゃくが てんいんを ひきとめて なじる からだ。
年末の東京らしい風景を彷彿とさせるこの一文にはサロゲートペアの文字がふたつ含まれている。
ひとつは「𩸽」という文字、
もうひとつは「𠈻」という文字である。
JavaScriptでのサロゲートペア文字列のメモ - Qiita
この記事に詳しく書かれているが、サロゲートペアは通常の文字列処理が通用しない厄介者である。
Goの文字列の扱い
Goでは文字をruneという型で処理する決まりらしい。
いつもの『プログラミング言語Go』の記述を引用すると、
Unicodeは世界のすべての書記体系のすべての文字、アクセントや他の発音区別記号、タブやキャリッジリターンなどの制御コード、多数の難解な文字を集めており、各文字にUnicodeコードポイント(Unicode code point)あるいはGo用語ではルーン(rune)と呼ばれる規格番号を割り当てています。
つまり、文字をruneとして取り出すと、番号で管理するそうだ。
実際、「ほっけ」と「ぞく」の文字をruneのリテラルで表すと、
package main import "fmt" func main() { var hokke, zok rune hokke, zok = '\U00029E3D', '\U0002023B' fmt.Printf("%c, %c", hokke, zok) // 𩸽, 𠈻 }
このように、16進数の番号を書く。
stringをrangeに渡すと[]runeに置き換えてくれる
Goでは文字列をrangeに渡すとちゃんとruneのスライスに置き換えてくれる。
このため、「一文字ずつ」ループを当たり前のようにできる。
exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" for _, x := range exp { fmt.Printf("%c", x) } fmt.Println()
これで変数expに書いた通りの文章がコンソールに出力される。
もしこれをbyteのスライスにかえてしまうと都合が悪い。
exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字 for _, x := range []byte(exp) { fmt.Printf("%c", x) }
こっちを動かすと、画像のようにめちゃくちゃな出力になってしまう。
utf8.DecodeRuneを使うとrange同様、安全に文字を取り出せる。
// import "unicode/utf8" exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" var xs []byte xs = []byte(exp) for len(xs) > 0 { r, size := utf8.DecodeRune(xs) fmt.Printf("%c", r) xs = xs[size:] } fmt.Println()
lenで文字数を数えたいか、バイト数を数えたいか
冒頭の文は全部で46文字あるが、単純に文字列をlenで調べてしまうとバイト数を返す。
これは、Pythonでlen(unicode)は文字数、len(str)はバイト数を返す話と似ている。
正しく文字数を取得したい場合は[]runeにキャストしてlenに渡すか、utf8.RuneCountInStringを使う。
exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字 fmt.Println(exp) fmt.Println( len(exp), // 138 utf8.RuneCountInString(exp), // 46 len([]rune(exp))) // 46
サンプルコード
package main import ( "fmt" "unicode/utf8" ) func main() { exp := "𩸽のひらきを居酒屋で注文して、1時間経つがまだ来ない。𠈻な客が店員を引き止めてなじるからだ。" // 46文字 fmt.Println(exp) fmt.Println( len(exp), // 138 utf8.RuneCountInString(exp), // 46 len([]rune(exp))) // 46 for _, x := range exp { fmt.Printf("%c", x) } fmt.Println() // ここで改行を挟まないと何故か1回めのループの「𩸽」の文字が化けて表示される。 var xs []byte xs = []byte(exp) for len(xs) > 0 { r, size := utf8.DecodeRune(xs) fmt.Printf("%c", r) xs = xs[size:] } fmt.Println() // DecodeLastRuneだと逆から反復 xs = []byte(exp) for len(xs) > 0 { r, size := utf8.DecodeLastRune(xs) fmt.Printf("%c", r) xs = xs[:len(xs)-size] } }