SE_BOKUのまとめノート的ブログ

SE_BOKUが知ってること・勉強したこと・考えたことetc

GO言語(golang)1.20:日本語を含むマルチバイト文字列操作

f:id:arakan_no_boku:20210412005751p:plain

目次

GO言語(golang)1.20:日本語を含むマルチバイト文字列操作

GO言語で、日本語文字を含む文字列(マルチバイト文字列)を扱うには、若干注意が必要な点があるので、それを整理します。

マルチバイト文字を扱う注意点

GO言語で、文字列を扱う主なパッケージとしては以下があります。

GO言語の符号化方式は「UTF-8」です。

なので、これらで「日本語を含むマルチバイト文字列」は普通に使えます。

ただ「文字数のカウント」と「文字列のスライス」については注意が必要です。

UTF-8は、文字によってバイト数が異なります。

例えば半角の英数字だと「1バイト」、日本語文字だと「3バイト」、特定の絵文字とかだと「4バイト」ですが、内部的に何バイトで表現されていようと「文字」として扱う限りは「1文字」でカウントしたいです。

GO言語ではこういう風に「文字」を単位とすることが「code point単位で扱う」などと表現されるのですが、残念ながら、GO言語の標準パッケージでは「code point単位」ではなく「1バイト」単位でカウントします。

つまり、3バイトで表現された1文字は「3」とカウントされてしまいます。

そこを回避するため「rune」が導入されています。

rune

文字を「code point」単位で扱うための仕組みが「rune」です。

文字列(string)を「rune」に変換することで文字単位(code point単位)で扱うことができるようになります。

rune:マルチバイト文字数カウント

標準関数「len()」を使うと、バイト数が返されます。

var s_short = "漢字12"

fmt.Printf("%d", len(s_short))

の結果は8(漢と字が3バイトずつの6バイトで、12が2バイトの計算)になります。

これをちゃんと「文字数」でカウントするには「rune」に変換する必要があります。

fmt.Printf("%d", len(rune(s_short)))

こうすれば文字数「4」が返ります。

rune:マルチバイト文字列のスライス

文字列もスライスできます。

ただし、文字列は単なる「byte配列」なので、str[0]でとれるのは1文字目ではなくて、1バイト目のコードです。

日本で使うならマルチバイト文字が常にはいってくる可能性があるので、「rune」に変換してスライスして、それを「string」に戻す手間を加える必要があります。

例えば「漢字12」という文字列から「漢字」だけ切り出すのは以下のようになります。

var s_short = "漢字12"

fmt.Println(string(rune(s_short)[0:2]))

スライスは[start:end]でstartからend-1まで切り出します。

文字列の1文字目は[0]なので、[0:2」で文字列の1文字目から2文字目が取れます。

漢字

上記以外なら、マルチバイト文字列は普通につかえる

繰り返しになりますが、GO言語の符号化形式は「utf-8」なので、「日本語を含むマルチバイト文字列」は普通に使えます。

その例をつらつら書こうと思ったのですが、結構見やすい以下のサイトがあったので、ダブって書く気にもなれず、リンクだけはっておきます。

ashitani.jp

あと、補足として、プラスアルファ情報を書いておきます。

補足情報:TrimSpaceは「全角空白」も削除できる

strings.TrimSpacesは、全角空白・半角空白のどちらも削除してくれます。

例えば。

var s_long = "  漢字123456abcdefGHIJKLひらがなカタカナハンカナ混在 "
fmt.Println(strings.TrimSpace(s_long))

とすると、前後の「半角空白」「全角空白」両方削除されて以下の結果になります。

漢字123456abcdefGHIJKLひらがなカタカナハンカナ混在

補足情報:文字列連結は「join」のほうが「+」演算子より速い

文字列を単純に連結するなら「+」演算子でできます。

s = "aaaaaa" + "bbbbbb"

でも、遅いので、ループを回して「+=」でたくさんの文字列を連結しないといけない場合は、「strings.Join」を使うべきです。

strings.joinを使うには、連結する文字列を配列に収めておく必要があります。

例えば、

社員番号
名前
生年月日
郵便番号

と収めてある配列「spout」をカンマ区切りで「社員番号,名前,生年月日,郵便番号」のように連結しなおすなら

strings.Join(spout, ",")

とします。

補足情報:文字列の出現位置を文字数で取得するには一工夫必要

文字列の中に指定した文字列が現れる最初の位置は「strings.Index」、最後の位置は「strings.LastIndex」で取得できます。

例えば、以下の文字列があったとして

var s_long = "  漢ここに漢字123456観abcdefGHIJKL漢字ひらがなカタカナハンカナ混在 "

この中で「漢字」という文字列の出現位置を取得するには以下のようにします。

first_index := strings.Index(s_long, "漢字") // 最初の出現位置
last_index := strings.LastIndex(s_long, "漢字") // 最後の出現位置

この結果を使って先頭から「漢字」までの文字列を取り出すには

tmp_str := s_long[0:first_index]
tmp_str2 := s_long[0:last_index]

この結果が以下のようになります。

  漢ここに
  漢ここに漢字123456観abcdefGHIJKL

これだけ見ると問題ないのですが、これで帰ってくる位置情報は「byte単位」です。

なので「出現位置を文字数で取得」したかったら、上記の「漢字が出現位置までの文字列」を文字数でカウントしてやるとかしないといけないことに注意が必要です。

例えば。

tmp_str := s_long[0:first_index]

len([]rune(tmp_str))

これで「6」が取得できるので「漢字」が最初に出現するのは「6+1」の7文字目ということになります。

補足情報:全角・半角変換は「golang.org/x/text/width」を使う

文字エンコーディングやテキスト変換、ロケール特有のテキスト処理など、国際化(i18n)や地域化(l10n)に関連するテキスト関連処理パッケージを集めたリポジトリで「golang.org/x/text」があります。pkg.go.dev

使うには、インストールが必要なので、初回は以下のいずれかでインストールします。

  • go get  golang.org/x/text:  指定したパッケージをインストールする
  • go get   : importに記載されたパッケージをインストールする
  • go mod tidy :  import記載パッケージのインストールと不要なパッケージの削除

このリポジトリの「width」パッケージで全角・半角変換ができます。

golang.org/x/text/widthをimportします。

文字列の全角・半角変更は3種類あります。

  • Widen :半角英字・数字・カナ・空白を全角英字・数字・カナに変換します。
  • Narrow :全角英字・数字・カナ・空白を半角英字・数字・カナに変換します。
  • Fold :漢字・ひらがな・カタカナを全角に、英数字・空白を半角に変換します

実行サンプルのソースです。

import (
	"fmt"

	"golang.org/x/text/width"
)

func main() {
	var s_long = "  漢ここに漢字123456観abcdefGHIJKL漢字ひらがなカタカナハンカナ混在 "
	ps := width.Widen.String(s_long)
	fmt.Println(ps)
	ps1 := width.Narrow.String(s_long)
	fmt.Println(ps1)
	ps2 := width.Fold.String(s_long)
	fmt.Println(ps2)
}

実行結果は以下です。 

Widen

  漢ここに漢字123456観abcdefGHIJKL漢字ひらがなカタカナハンカナ混在

 
Narrow

  漢ここに漢字123456観abcdefGHIJKL漢字ひらがなカタカナハンカナ混在

 

Fold

  漢ここに漢字123456観abcdefGHIJKL漢字ひらがなカタカナハンカナ混在

補足情報:文字列の比較の前に「Fold」するのはおすすめ

上記の「Fold」とかは文字列比較とかで重宝します。

GO言語では単純に文字列を比較するだけなら「==」でいけるのですけど、以下の例のように全角と半角は当然ながら区別します。

	var sa1 = "123漢字かなカナ"
	var sa2 = "123漢字かなカナ"
	var sb = "123漢字かなカナ"
	if sa1 == sa2 {
		fmt.Println("OK")
	} else {
		fmt.Println("NG")
	}

	if sa1 == sb {
		fmt.Println("OK")
	} else {
		fmt.Println("NG")
	}

 

でも、全角・半角を無視して比較したいときもあります。

そんなときに「Fold」で変換してフォーマットをあわせてやるといけます。

	var sa1 = "123漢字かなカナ"
	var sb = "123漢字かなカナ"

	psa1 := width.Fold.String(sa1)
	psb := width.Fold.String(sb)

	if psa1 == psb {
		fmt.Println("OK2")
	} else {
		fmt.Println("NG2")
	}

こうすれば、結果は「OK2」になります。

補足情報:strings.EqualFoldは全角・半角の考慮をしない

stringsパッケージに「strings.EqualFold」というのがあります。

名前をみるとなんとなく「Fold」で変換して「==」で比較するのと同じようなことができそうに思えますが、無理です。

全角・半角まで考慮はしてくれません。

以下のように、半角の大文字・小文字の違いだけなら無視できます。

	var a = "abcですよ"
	var b = "ABCですよ"

	if strings.EqualFold(a, b) {
		fmt.Println("OK3")
	} else {
		fmt.Println("NG3")
	}

 

とりあえず、今回はこのくらいですかね。

ではでは。