今川館

都内勤務の地味OLです

サロゲートペアの入った文字列を処理する場合の注意点

サロゲートペア

「𩸽のひらきを居酒屋で注文して、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)
}

こっちを動かすと、画像のようにめちゃくちゃな出力になってしまう。

f:id:imagawa_yakata:20161216144357p:plain

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]
	}
}