Ruby内部で使用される文字コードの概要とIO#getsが動かない話

Rubyの IO#gets でひらがなを入力しようとしたら例外を吐いたので調べた結果、Ruby内部で使用される文字コードに少し詳しくなったので、備忘録を残しておく。

Ruby内部の文字コード 概要

プログラミング言語の多言語処理を実現させる方法として、UCS(Universal Character Set)方式、CSI(Character Set Independent)方式などがある。


UCS方式

あらかじめプログラムで使用する文字コードを決めておき、外部から文字を取りこむ段階で、その文字コードへの変換を行う。たとえば内部でUTF-8を使用すると決めた場合、どのような文字コードで入力を行っても、いったんUTF-8に変換して取りこむ。


CSI方式

バイト列と符号化方式を持つオブジェクトを文字のオブジェクトとして使用する。

RubyではCSI方式を採用しており、 String#encoding でそのStringオブジェクトの符号化方式を、 String#bytes でバイト列を参照可能。

hoge_jis = "ほげ".encode(Encoding::SHIFT_JIS)
p hoge_jis.encoding # #<Encoding:Shift_JIS>
p hoge_jis.bytes # [130, 217, 130, 176]

hoge_utf = "ほげ"
p hoge_utf.encoding # #<Encoding:UTF-8>
p hoge_utf.bytes # [227, 129, 187, 227, 129, 146]


まとめると、特定の文字コードのバイト列を「文字」として扱うのがUCS方式で、任意のバイト列と符号化方式のペアを「文字」として扱うのがCSI方式。Rubyは後者。



環境

  • Windows Version 10.0.19044.2251
  • Ruby 3.1.2



IO#getsでひらがなを受け取りたい (未解決)

p gets # Encoding::InvalidByteSequenceError

IO#gets で標準入力から入力を受け取る際、「ほげ」と入力すると Encoding::InvalidByteSequenceError が発生した。その解決を試みたが、解決できなかった。


docs.ruby-lang.org

このページによると、rubyコマンドを実行する際に -E もしくは --encoding オプションを使用して、入力されるバイト列をどの符号化方式として解釈するかを選択できる。符号化方式を選択しなかった場合には、 Encoding.default_external に入っている、デフォルトの符号化方式として認識される。

p Encoding.default_external # #<Encoding:UTF-8>

僕の環境では Encoding.default_external の中にはUTF-8のEncodingオブジェクトが入っている。コマンドプロンプトのデフォルトの文字コードはCP932なので、つまりCP932で符号化されたバイト列をUTF-8として解釈しようとしている。だから例外が発生していた。

これを解決するためには、 -E オプションの値にCP932を指定し、「入力はCP932で符号化したバイト列として与えられる」という情報をrubyに伝えれば良い。


docs.ruby-lang.org

-E オプションには、上記の Encoding の定数が使用できる。CP932を使用したい場合、 ruby -E "CP932" hoge.rb で実行する。


line = gets
p line.encoding # #<Encoding:Windows-31J>
puts line # ル�ほげ

上記は -E オプションを使用した上で、 gets の入力として「ほげ」と入力した結果。入力されるバイト列を正しくCP932のものと解釈できるようになり、例外を出さずに入力を受け取れるようになった。

ただ、最初に謎の文字が入ってしまっている。


line = gets
p line.bytes # [217, 255, 130, 217, 130, 176, 10]
p "ほげ".encode(Encoding::CP932).bytes # [130, 217, 130, 176]

同じ「ほげ」という入力で、入力されたバイト列を確認してみた。すると、最初に217 255(0xd9 0xff)という、本来であれば無いはずの2バイトが含まれているのが分かった。

この2バイトの正体と、その消し方は調べても情報を見つけられなかった。


詳しい方がいましたら教えてください。この記事のコメントか、TwitterのDMによろしくお願いします。