ls /asapon/blog

基本tech、時々多趣味

Rubyで文字コードを扱ってて四苦八苦したのでまとめてみた

TL;DR

文字コードってUTF-8で統一すれば良いんでしょ?くらいの認識しか持っていなかったWebエンジニアマンが、実際にちゃんと文字コードについて学びまとめてみた話

この記事ってどんな人が対象なの?

B2B系の仕事に関わる人は得になるかもしれません(の割にはJavaではなくRubyの話ですが)。
理由としては、仕事をしているとクライアントの使っているOSの状況などで、様々な文字コードに対応することがあるかと思います。私の前職はC2Cで、ユーザが直接打ち込んでくるものに対応することが多かったので、SQLインジェクションやそれに対するエスケープ処理などを気にかけていました。しかし、文字コードに関してなにかしら注意を払うといったことはありませんでした(更に言えば、私は社内の管理画面なども手がけていたので、より気にする必要がなかった)。

そもそも文字コードって

UTF-8
Shift_JIS
ASCⅡ
EUC
みたいなやつらです。

文字はどうやって表現されるのか

バイト列 + エンコーディング情報 = 文字表現
すなわち、あるバイト列は文字コードに関する情報を持つことで初めて文字として表示されるということ。バイト列が文字コードと対応していれば、それにあった文字が表示されます。

Rubyの文字列の扱いをバイト列から探ってみる

Rubyには String#bメソッドとString#bytesメソッドがある。それぞれの役目についてまとめると
String#b ... ASCII-8BITの文字列の複製を返す
String#bytes ... 整数値(10進数)で文字列中のバイトを繰り返し取り出す

このふたつのメソッドを使って実際に文字列を解体してみる。

>> a = 'あ'
=> "あ"
>> a.encoding
=> #<Encoding:UTF-8>
>> a.b
=> "\xE3\x81\x82"
>> a.b.encoding
=> #<Encoding:ASCII-8BIT>

この \xE3\x81\x82 は文字エンコーディングASCII-8BIになる。
ところで、ASCII-8BIとはなんだろう。ASCⅡのお仲間なのかな?と思って調べたところこんな記事がヒットした。引用すると

ASCII-8BITは基本的に現実のエンコーディングではなく、任意のバイトストリーム(0から > 255までの値をとるバイト)を表すものであり、生のバイトストリームに用いたり、文字列のエンコーディングが不明であることを明示したりするときに用いられます。

とのこと。上記は日本語に対してString#bメソッドを使っているので、ASCⅡの範囲外である日本語レシーバはバイト列がそのまんま返ってくるよということである。逆に言えば名前が示す通り、ASCⅡの範囲(1文字を7bitで表している)であれば文字を表示することができる。

>> 'hoge'.b
=> "hoge"

ちなみに String#b は16進数で返ってくる。これは人間でも理解しやすいようにしているためである。

>> a.bytes { |b| puts b.to_s(16) }
e3
81
82
=> "あ"

ここで文字コードの対応表を見てみる。
の部分を確認すると E38182 となっている。つまり上のバイト列はUTF-8に対応したバイト列であることが分かる(UTF-8エンコーディング情報をもって初めて意味をなすということ)。この対応したという部分が符号化文字集合ということである。

>> a.b.force_encoding('UTF-8')
=> "あ"

ちなみにString#encodeUTF-8 を指定しても上手くいかない。
String#encodeは現在のエンコーディングが正しいと仮定した上で動作するものだからである。

>> a = 'あ'
=> "あ"
>> a = a.encode('Windows-31J')
=> "\x{82A0}"
>> a.encode('UTF-8')
=> "あ"

一方ASCII-8BITは、

文字列のエンコーディングが不明であることを明示したりするときに用いられます

ということなので、そもそものエンコーディングが不明状態のバイト列になります。 しかし、UTF-8の並びをしているので、String#force_encodingを用いることで を表示することができます。もし結合をしたい場合はString#force_encodingを用いて対処しましょう。

>> a = 'あ'
=> "あ"
>> b = 'あ'.b
=> "\xE3\x81\x82"
>> a + b
Traceback (most recent call last):
        2: from /Users/asadashougo/.rbenv/versions/2.5.0/bin/irb:11:in `<main>'
        1: from (irb):3
Encoding::CompatibilityError (incompatible character encodings: UTF-8 and ASCII-8BIT)
>> a + b.force_encoding('UTF-8')
=> "ああ"

最初から不正なバイト列はどうなるの?

さきほどの話はそもそもバイト列として正しいものになります。
なぜならちゃんと日本語の からバイト列を作ったからです。

>> 'あ'.bytesize
=> 3
>> 'あ'.b.bytesize
=> 3

どちらも3バイトになっています(ちなみにUTF-8は3バイトで日本語を表記しています)。しかし不正なバイト列ならどうでしょう。

>> a = "\x82\xa0"
=> "\x82\xA0"
>> a.encoding
=> #<Encoding:UTF-8>
>> a.bytesize
=> 2
>> a.valid_encoding?
=> false

UTF-8エンコーディングなのに2バイトなので不正なバイト列だと言うことが分かります。
これに対処するにはString#scrubを利用すると不正なバイト列を別の文字に代替することができます。

>> a.scrub
=> "��"
>> a.scrub.valid_encoding?
=> true

参考