Rubyのエンコーディング

Ruby 1.9 から文字列や正規表現オブジェクトはそれぞれエンコーディング(いわゆる文字コード)を保持するようになりました。

たとえば 0xB1 0xB2 という2バイトは EUC-JP エンコーディングでは「渦」SHIFT_JIS エンコーディングでは「アイ」という文字になります。つまり同じバイト列でもエンコーディングが異なれば異なる文字として解釈されます。

1.8 では文字列はただのバイト列でした。なので、それがどのような文字を表しているのか、つまりエンコーディングが何なのかはプログラムが知っている必要がありました。

1.9 では文字列オブジェクト自身が自分が何のエンコーディングかを知っています。同じ 0xB1 0xB2 というバイト列でも、それが EUC-JP の「渦」なのか SHIFT_JIS の「アイ」なのかは、文字列自身が知っています。

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

スクリプト中の文字列リテラルはスクリプトエンコーディングに従います。
スクリプトエンコーディングは、スクリプトファイルの先頭行のマジックコメントで指定します。

# coding: utf-8
'a'.encoding   #=> #<Encoding:UTF-8>
# coding: cp932
'a'.encoding   #=> #<Encoding:Windows-31J>

マジックコメントがない場合は US-ASCII エンコーディングになります(ただし -K オプション指定時はそれに従います)。

ただし、リテラル中で「\uXXXX」の表記を使用した場合は、その文字列はスクリプトエンコーディングに関わらず UTF-8 エンコーディングになります。

# coding: cp932
"\u3042".encoding   #=> #<Encoding:UTF-8>

スクリプトエンコーディングが US-ASCII では、リテラル中で「\xXX」の表記で非ASCII文字を指定すると、その文字列は ASCII-8BIT エンコーディングになります。

# coding: us-ascii
"\x41".encoding   #=> #<Encoding:US-ASCII>
"\x93".encoding   #=> #<Encoding:ASCII-8BIT>

スクリプトエンコーディングにあわないバイト列を指定するとスクリプトを実行する前にエラーになります。ただし "\xXX" で指定した場合はその時点ではエラーになりません。

# coding: cp932
""             # CP932 なのに UTF-8 で「あ」を記述。エラーになる。
"\xe3\x81\x82"   # これはエラーにならない

正規表現リテラルもスクリプトエンコーディングに従います。

# coding: utf-8
//.encoding   #=> #<Encoding:UTF-8>

/〜/ の後に n, e, s, u を指定することでスクリプトエンコーディングと異なるエンコーディングを指定することができます(n: US-ASCII, e: EUC-JP, s: Windows-31J, u: UTF-8)。

# coding: utf-8
/ABC/s.encoding   #=> #<Encoding:Windows-31J>

正規表現リテラル中でも「\xXX」で直接コードを指定できますが、文字列の場合と異なり指定したコードがエンコーディングにあわなければスクリプトを実行する前にエラーになります。

# coding: utf-8
/\xff/   # invalid multibyte escape: /\xff/

正規表現が ASCII 文字だけで構成されている場合はスクリプトエンコーディングに依らず US-ASCII エンコーディングになります。

# coding: utf-8
/ABC/.encoding   #=> #<Encoding:US-ASCII>

/〜/n も同じ US-ASCII エンコーディングですが、文字列とのマッチング時の振る舞いが異なります。

エンコーディングの変換

String#encode

文字列を指定したエンコーディングに変換した文字列を生成して返します。

# coding: utf-8
str = ''   # UTF-8 で「あ」(0xE3 0x81 0x82)
str2 = str.encode('cp932')
str          # UTF-8 で「あ」(元のまま)
str2         # CP932 で「あ」(0x82 0xA0)

第二引数を指定すると、変換元のエンコーディングとして文字列自身のエンコーディングではなく第二引数で指定したエンコーディングを使用します。

# coding: ascii-8bit
str = "\xe3\x81\x82"           # ASCII-8BIT エンコーディング
str.encode('cp932', 'utf-8')   # UTF-8 の「あ」と見なして CP932 に変換する

文字列中に自身のエンコーディングとして不正な文字が入っていたり、変換先エンコーディングに変換できない文字が入っていた場合は例外が発生します。

# coding: utf-8
str = "\xff"          # 0xFF は UTF-8 として不正な文字
str.encode('cp932')   # "\xFF" on UTF-8 (Encoding::InvalidByteSequenceError)
# coding: utf-8
str = "\xe2\x99\xa1"  # 0xE2 0x99 0xA1(ハートマーク)は CP932 には存在しない文字
str.encode('cp932')   # U+2661 from UTF-8 to Windows-31J (Encoding::UndefinedConversionError)

オプションを指定して例外を発生させないようにでもきます。

# coding: utf-8
str = "\xff"
# 変換元エンコーディングで不正な文字を置換する
str.encode('cp932', :invalid=>:replace)   #=> CP932 で "あ?い"
# coding: utf-8
str = "\xe2\x99\xa1"   # 「あ(ハートマーク)い」
# 変換先エンコーディングに存在しない文字を置換する
str.encode('cp932', :undef=>:replace)   #=> CP932 で "あ?い"
String#force_encoding

文字列のバイト列はそのままでエンコーディングを変更します。encode と異なり文字列オブジェクトのエンコーディングを変更する破壊的メソッドです。

# coding: ascii-8bit
str = "\xe3\x81\x82"   # ASCII-8BIT エンコーディング
str.force_encoding('utf-8')
str.encoding           #=> #<Encoding:UTF-8>

文字列と正規表現のマッチング

文字列と正規表現のエンコーディングが異なっているとマッチングした時に例外が発生します。

# coding: utf-8
"" =~ /./s   # incompatible encoding regexp match (Windows-31J regexp with UTF-8 string) (Encoding::CompatibilityError)

US-ASCII エンコーディングの正規表現は文字列が ASCII 互換のエンコーディングであれば US-ASCII 以外のエンコーディングでも比較することができます。

# coding: utf-8
re = /./
re.encoding   #=> #<Encoding:US-ASCII>
str = ''
str.encoding  #=> #<Encoding:UTF-8>
str =~ re     # 「あ」に適合

/〜/n で生成した正規表現オブジェクトも同じ US-ASCII エンコーディングですが、これは US-ASCII 以外のエンコーディング文字列とのマッチングで warning になります。

# coding: utf-8
re = /./n
re.encoding   #=> #<Encoding:US-ASCII>
"" =~ re    # warning: regexp match /.../n against to UTF-8 string

上記のように /〜/ と /〜/n は微妙に振る舞いが違うのですが、プログラムでは区別できません(おそらく)。

# coding: utf-8
re1 = /ABC/
re2 = /ABC/n
re1.encoding   #=> #<Encoding:US-ASCII>
re2.encoding   #=> #<Encoding:US-ASCII>
re1 == re2     #=> true

[追記] Regexp#options で判別できました。

# coding: utf-8
/ABC/.options    #=> 0 
/ABC/n.options   #=> Regexp::NOENCODING

エンコーディングが同じであったとしても、文字列内にそのエンコーディングで不正なバイトの並びがあると、マッチング時に例外が発生します。

# coding: utf-8
str = "\xff"   # ここではエラーにならない
str =~ /./     # invalid byte sequence in UTF-8 (ArgumentError)

IOのエンコーディング

ファイルなどの IO は入出力時にエンコーディングを変換することがあります。

IO は外部エンコーディングと内部エンコーディングを持ちます。

外部エンコーディング
ファイル内の文字列のエンコーディングを示します。
内部エンコーディング
ファイルから読み込んだ文字列を変換するエンコーディングを示します。

外部エンコーディングはファイルのオープン時に指定するか、IO#set_encoding で指定します。未指定時は Encoding.default_external が使用されます。
Encoding.default_external がプログラム内で指定されていない場合は -E オプション、-K オプション、ロケール(Linux の場合は通常は LC_ALL, LC_CTYPE, LANG 等の環境変数で設定される)の順で決定されます。Encoding.default_external は必ず値を持ちます。

内部エンコーディングもファイルのオープン時に指定するか、IO#set_encoding で指定します。未指定時は Encoding.default_internal が使用されます(読み込み時)。
Encoding.default_internal がプログラム内で指定されていない場合は -E オプションの指定になります。-E も指定されていない場合は nil になります。

読み込み時

読み込みメソッドはテキスト読み込みとバイナリ読み込みの二種類あります。おおまかに言うと、テキスト読み込みメソッドは文字や行単位で読み込むもの、バイナリ読み込みメソッドはバイト単位で読み込むものです。IO#read のように同じメソッドでも引数によってテキスト読み込みとバイナリ読み込みが異なるものもあります。詳しくは http://doc.ruby-lang.org/ja/1.9.3/class/IO.html を参照しましょう。

テキスト読み込みメソッドを使用してデータを読み込むと外部エンコーディングと内部エンコーディングに合わせて文字列が変換されます。

# CP932 で書かれたファイルを読み込む
File.open('cp932.txt', 'r:cp932').gets   # CP932 エンコーディング文字列
# CP932 で書かれたファイルを読み込んで UTF-8 文字列に変換する
File.open('cp932.txt', 'r:cp932:utf-8').gets   # UTF-8 エンコーディング文字列
# CP932 で書かれたファイルを読み込んで UTF-8 文字列に変換する
Encoding.default_external = 'cp932'
Encoding.default_internal = 'utf-8'
File.open('cp932.txt', 'r').gets   # UTF-8 エンコーディング文字列

(最後の例は 1.9.3 では「code converter not found (Windows-31J to UTF-8) (Encoding::ConverterNotFoundError)」という例外になりますがバグのようです http://bugs.ruby-lang.org/issues/5733)

外部エンコーディングで間違ったエンコーディングを指定してもエラーにはなりません。例えば外部エンコーディングに UTF-8 を指定して、CP932 で書かれたファイルから読み込んでもエラーにはならず、UTF-8 エンコーディング文字列が生成されます。ただし、文字列の中身は UTF-8 としては不正なバイト列となっているため、その後 encode メソッド等で変換しようとするとエラーになります。内部エンコーディングや Encoding.default_internal が指定されている場合は、読み込み時に変換されるため、その時点でエラーになります。

書き込み時

書き込み時は、メソッドに依らず、文字列を外部エンコーディングに従って変換します。ただし、外部エンコーディングが ASCII-8BIT の場合は変換されません。外部エンコーディングを指定していなくても、内部エンコーディング または Encoding.default_internal が指定されていると Encoding.default_external に従って変換します。外部エンコーディング、内部エンコーディング、Encoding.default_internal のいずれも指定されていない場合は変換されません。

次の例は、外部エンコーディング、内部エンコーディング、Encoding.default_internal が指定されていないので変換されずに書き込まれます(ただし -E オプションで Encoding.default_internal が指定されている場合は変換されます)。

# coding: utf-8
utf8 =  ''                    # UTF-8 の「あ」
eucjp = ''.encode('euc-jp')   # EUC-JP の「あ」
File.open('out.txt', 'w').puts(utf8, eucjp)   # 変換されずにそのまま書き込まれる

次の例は、ファイルオープン時に外部エンコーディングが CP932 に指定されているので、CP932 に変換されて書き込まれます。

# coding: utf-8
utf8 =  ''                    # UTF-8 の「あ」
eucjp = ''.encode('euc-jp')   # EUC-JP の「あ」
File.open('out.txt', 'w:cp932').puts(utf8, eucjp)  # CP932 エンコーディングで書き込まれる

次の例は、外部エンコーディングが指定されていませんが、Encoding.default_internal が指定されているので、Encoding.default_external に変換されて書き込まれます。Encoding.default_internal の値は変換されるエンコーディングとは関係ありません。値が設定されているかどうかだけで変換するかどうかが決定されます。Encoding.default_external はプログラム中では指定されていないので、-E オプションかロケールに従います。

# coding: utf-8
Encoding.default_internal = 'us-ascii'
utf8 =  ''                    # UTF-8 の「あ」
eucjp = ''.encode('euc-jp')   # EUC-JP の「あ」
File.open('out.txt', 'w').puts(utf8, eucjp)   # default_external エンコーディングで書き込まれる

エンコーディングの注意事項まとめ

文字列はエンコーディングを持っていますが、正しい文字が格納されているとは限りません。外部から読み込んだ文字列や force_encoding でエンコーディングを強制した文字列は、正規表現とのマッチング時に例外が発生する可能性があります。

ファイルオープン時の外部エンコーディングと内部エンコーディングは必ず指定しましょう。-E オプションや環境変数によって思いも依らぬものに変換されてしまう可能性があります。

[追記] 続編あります http://tmtms.hatenablog.com/entry/20120902/ruby_encoding