おもこん

おもこんは「思いつくままにコンピュターの話し」の省略形です

徒然Ruby(41)エンコーディング

文字列のエンコーディングに頭を悩ませることはほとんどなくなりました。 なぜなら、どのアプリ、システムもUTF-8を使うようになったからです。 Rubyでもエンコーディングの問題が起こることはまず無いでしょう。 ですが、今回はエンコーディングの考え方を整理してみたいと思います。

ASCIIコード

コンピュータの内部では文字を数字に置き換えて記憶しています。 これを文字コードといいます。 初期の有名な文字コードにASCII(アスキー)がありますが、これは7ビットで表すことができます。 ビットとは、メモリーの最小単位で、1と0を区別できるものです。 8個のビットをバイトといい、コンピュータはバイト単位でメモリーを扱います。 1ビットは0と1を表すことができますが、1バイトだと2^8=256個を区別でき、数字としては0から255までを区別できるようになります。 ASCIIは7ビットなので、0から2^7-1=127までの数字に文字が対応します。

ASCII - Wikipedia

例えば大文字のAは16進数の41(10進数の65)小文字のaは16進数の61(10進数の97)などです。 詳しくは上記のリンク先を参照してください。 ASCIIで表せるのは大文字と小文字のアルファベット、ピリオドなどの記号、改行などを表すコントロールコードだけです。 要するに、キーボードで直接入力できる文字だと思えば良いでしょう。

ASCIIは7ビットですが、コンピュータはバイト単位にデータを処理するので、ASCIIも8ビットで処理されることが普通です。 このとき、最上位ビットは0になります。 もしも最上位ビットが1だと、ASCIIの定義外なので、文字としては不定ということになります。 Rubyではこのような1バイト単位で、0から127まではASCIIとして扱うことができるコード体系(エンコード)をASCII_8BITとしています。 主にバイナリデータを扱うのに使われます。

ASCII 以外のコード

日本語にはアルファベット以外に、ひらがな、カタカナ、漢字があります。 他の言語でも、例えばドイツ語ではウムラウトエスツェット(ß)があります。 これらをASCIIで表すことはできません。 そのため、2バイト以上を使って様々な文字と数字(文字コード)を対応させるということが考えられました。 この方法が現在ではUTF-8でほぼ統一されていますが、過去にはSHIFT-JISやEUC-JPなどがありました。 それらをエンコーディングといいます。 つまり、エンコーディングは文字と数字(文字コード)の対応を表すルールなのです。

しかし、UTF-8、SHIFT-JIS、EUC-JPには互換性がありませんので、あるコード体系から別のコード体系には「変換」が必要です。 過去にはWindowsはSHIFT-JISが使われLinuxではEUC-JPが使われていましたので、両者でデータのやりとりをするときには文字コードの変換が必要でした。 また、これらのコードは日本語以外の言語(ASCII以外)の文字サポートがありませんでした。 最終的にはUnicodeという様々な国の言語の文字をサポートするコード体系が生まれ、特にUTF-8が標準的に用いられるようになりました。 現在ではWIndowsLinuxMacUTF-8が標準です。

このようにしてUTF-8がどのシステムでも使われるようになったので、問題は起こらなくなりました。 これらの文字コードのことをエンコーディングといいます。 Rubyでは文字列にエンコーディングが付随していて、UTF-8以外にEUC-JPやSHIFT-JISにも対応できるようになっています。

以下ではRubyエンコーディングが問題になることがらについて説明します。

スクリプトエンコーディングリテラルエンコーディング

Rubyで書いているプログラム自体の文字コードはどのような問題を含むでしょうか? これは「スクリプトエンコーディング」の問題と呼びます。

Rubyのキーワードは、すべてASCIIの範囲にあり、その限りではRubyは正しくスクリプトを解釈してくれます。 UTF-8、SHIFT-JIS、EUC-JPなどは、すべてASCIIの範囲の文字はそのとおりにコードになっています。 例えば「def」の文字コードは16進数で「64 65 66」で、これは上記の3つの文字コードでも同じです。 このようにASCIIの範囲の文字はASCIIと同じコードを使うエンコーディングをASCII互換エンコーディングといいます。 それ以外のエンコーディングはASCII非互換エンコーディングです。

RubyスクリプトにはASCII互換エンコーディングRubyがサポートしている)を使うことができます。 逆にそれ以外、ASCII非互換やRubyがサポートしないエンコーディングは使うことができません。

また、スクリプトエンコーディングを明示的に指定したいときはマジックコメントを使います。 詳しくはRubyのドキュメントの多言語化を参照してください。

文字列リテラル正規表現リテラル、シンボルリテラルには文字列が出てくるので、エンコーディングが関わります。 これらはスクリプトエンコーディングに従います。 ただしバックスラッシュ記法で文字コードを表す場合は他の文字コードに変換されたり、エラーになることがあります。 詳細は多言語化を参照してください。 通常はリテラルエンコーディングスクリプトエンコーディングだと考えれば大丈夫です。

文字列のエンコーディング

Rubyの文字列オブジェクトはエンコーディングを持っていて、encodingメソッドでその文字列のエンコーディングを知ることができます。

p s.encoding #=>#<Encoding:UTF-8>
  • ある文字列を他のエンコーディングの同じ文字列に変更するにはencodeメソッドを使います。
  • ある文字列をその文字列の内容を変えずにエンコーディングを変更するにはforce_encodingメソッドを使います。

この2つは混乱しやすいので注意してください。 encodeメソッドはエンコーディングを変えるだけでなく、文字列の内容(コード)も変更しますが、force_encodingではエンコーディングのみが変更されます。

s = ""
p s.encoding #=>#<Encoding:UTF-8>
t = s.encode(Encoding::EUC_JP)
p t.encoding #=>#<Encoding:EUC-JP>
p s.force_encoding(Encoding::ASCII_8BIT)
p t.force_encoding(Encoding::ASCII_8BIT)

これを実行すると

#<Encoding:UTF-8>
#<Encoding:EUC-JP>
"\xE3\x81\x82"
"\xA4\xA2"

となります。 後半2行から、sとtでは文字コードが変更されていることが分かります。 異なるコードですが、表している文字は両方とも「あ」です。

文字列を==で比較する場合、「等しい」と判定されるのは次の2条件を満たすときです。

したがって、sとtは両方とも「あ」を表しているが、エンコーディングが異なるので、「s==t」はfalseになります。

このような問題は複数のエンコーディングを使っているところから発生するので、ひとつのエンコーディングだけならば、ことは単純になります。

I/Oのエンコーディング

外部から入力するときに、それが文字列であればエンコーディングが問題になります。

テキストの読み込み

テキスト読み込みメソッド、例えばIO.readlinesエンコーディングの影響を受けます。 読み込み元はRubyの外部ですから、Rubyエンコーディングを決めることはできません。 プログラマーが外部のエンコーディングを把握して、Rubyに設定することになります。 このエンコーディングをIOの「外部エンコーディング」といいます。

Rubyの内部で使っているエンコーディングはデフォルトでUTF-8です(変更もできますが)。 これを「内部エンコーディング」といいます。 あるIOに対して「外部エンコーディング」と「内部エンコーディング」がわかっていれば、Rubyは読み込み時に変換してくれます。 これらのエンコーディングを指定するのが「set_encoding」メソッドです。 その引数は、外部エンコーディング、内部エンコーディングの順に指定します。 エンコーディングには文字列またはエンコーディング定数(例えばEncoding::UTF_8)が使えます。

今、「こんにちは」という日本語テキストがEUC-JPで保存されたファイル「gr_euc.txt」があるとします。 これを読むこむときにUTF-8に変換するには次のようにします。

f = File.open("gr_euc.txt", "r")
f.set_encoding("EUC-JP", "UTF-8")
print f.read #=> こんにちは
f.close

詳細はIO のエンコーディングとエンコーディングの変換を参照してください。

Ruby/gtk4を使う場合、RubyでなくGTK 4、より正確にはGIOの入力関数を使うことがあります。 そのとき、エンコーディングが考慮されていないので、Rubyとしてはバイナリ入力のASCII-8BITでエンコーディングを設定することがあります。 そのときには必要なエンコーディングをforce_encodingメソッドで入力文字列に与えることが必要になります。

テキストの書き込み

テキストの書き込みは読み込みよりも単純です。

s = "あいうえお" # UTF-8
f = File.open("gr.txt", "w")
f.write(s) # UTF-8で出力
f.close

f = File.open("gr_euc.txt", "w")
f.set_encoding("EUC-JP")
f.write(s) # EUC-JPで出力
f.close

f = File.open("gr_sjis.txt", "w")
f.set_encoding("SJIS","UTF-8")
f.write(s) # Shift-JISで出力
f.close
標準入出力

ここでは、デフォルトの標準入出力である、キーボード入力と画面出力について扱います。 これらは、オペレーティング・システムによって、どのエンコーディングを使うかが決められます。 UBUNTUなどのLinuxオペレーティング・システムでは現在はほとんどUTF-8です。

ですから、Rubyスクリプトが他のエンコーディングの文字列を持っていて、それを画面出力するときにはUTF-8に変換しなければなりません。 この方法には2つあります。

  • 文字列のエンコーディングUTF-8に変換しておく。これはencodeメソッドでできます。
  • $stdoutの外部エンコーディングはデフォルトでnilになっている(つまり出力時に何の変換もしない)。それをUTF-8に設定すると出力時に自動的に変換をしてくれる。
$stdout.set_encoding(Encoding::UTF_8)