URI.decode
まわりを見ていて、いい勉強になったので記しておきます。
ざっくり言うと
- Ruby 2.3.0 でマルチバイト文字を含んだ URL を
URI.decode
でうまくデコードできなかった場合はCGI.unescape
などの利用を検討されると良いと思います - CarrierWave (執筆時 0.10.0 が最新) を Ruby 2.3.0 と組み合わせた際に、Fog 経由でのファイル名を期待したエンコーディングで取得できない問題について CarrierWave の HEAD ではなおったと思います
-
URI.decode
(URI.unescape
),URI.encode
(URI.escape
) は非推奨
URI::Generic#to_s によるエンコーディングの違い
URI::Generic#to_s
について、Ruby 2.2.4 と Ruby 2.3.0 で振る舞いが変わっているようです。
require 'uri'
str = URI::Generic.build(scheme: 'http', host: 'localhost', path: '/test', query: 'language=日本語').to_s
p URI.decode(str)
p URI.decode(str).encoding
Ruby 2.2.4 での実行結果。
"http://localhost/test?language=日本語"
#<Encoding:UTF-8>
Ruby 2.3.0 での実行結果。デコードしても ASCII-8BIT のままなので、いわゆる文字化けが発生するようです。
"http://localhost/test?language=\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"
#<Encoding:ASCII-8BIT>
いちおう force_encoding('UTF-8')
すれば期待する文字列を手に入れることができます。
URI.decode(str).force_encoding('UTF-8')
=> "http://localhost/test?language=日本語"
git で変更を追ってみる
調査したい対象はこちらです。どのタイミングで変更されたかをみてみます。
$ git blame lib/uri/generic.rb
$ git show f2b9563b
frozen string literal 対応された際 に動きが変わったようです。
- str = ''
+ str = String.new
frozen なリテラル文字列だと、<<
のような破壊的な操作はエラーになるため対応がされておりました。
空文字列のエンコーディングの違い
この文字列の生成方法で何がどう変わったというのが最初分かりませんでした。
Ruby 2.2.4 でもそうなっていますが、 ''
と String.new
でエンコーディングに違いがあるようです。
> ''.encoding
=> #<Encoding:UTF-8>
> String.new.encoding
=> #<Encoding:ASCII-8BIT>
これは盲点でした。
エスケープされた文字列は ASCII-8BIT、デコードした文字列は UTF-8
URI::Generic#to_s
の方を Ruby 2.2.4 と互換のあるエンコーディングの形で Pull Request を出した ところ、 @narse さん からお返事を頂いてなるほどとなりました。
The encoding of escaped URI should be US-ASCII, and maybe URI.decode should set the resulted encoding as UTF-8.
そうなれば今回の URI::Generic#to_s
については、ASCII-8BIT で困ることはなさそう。
URI.decode
も CGI.unescape
みたいに force_encoding をする形とかになるとよいのかな。。。 (ここよく分かっていない) 今後の Ruby へのコミットとか追って行きたいところです。
puts と p
再現テストを行う際にputs URI.decode(str)
では、URI.decode
で期待しているいわゆる文字化けが起こらずなんだろうと思ったら、 puts
を使っていたことが起因のようでした。デバッグ用途なら p
でしたね。なので、p URI.decode(str)
が正解。
ここは @kunitoo にアドバイスしてもらいました。感謝。
URI.decode と CGI.unescape
そもそもが URI.decode
は非推奨 ということで、CGI.unescape
を使ってみたところ期待の UTF-8 でエンコーディングされた文字列が返ってきました。
$ pry
> require 'cgi'
> show-method CGI.unescape
(snip)
def unescape(string,encoding=@@accept_charset)
str=string.tr('+', ' ').b.gsub(/((?:%[0-9a-fA-F]{2})+)/) do |m|
[m.delete('%')].pack('H*')
end.force_encoding(encoding)
str.valid_encoding? ? str : str.force_encoding(string.encoding)
end
なるほど。force_encoding
しているんですね。
Ruby 2.3.0 でマルチバイト文字を含んだ URL を URI.decode
でうまくデコードできなかった場合は CGI.unescape
などの利用を検討されると良いと思います。
この問題で困ったライブラリ
自分が困ったというよりは Facebook で知り合いが困っていたのがきっかけですが、CarrierWave (執筆時 0.10.0 が最新) を Ruby 2.3.0 と組み合わせた際に、Fog 経由でのファイル名を期待したエンコーディングで取得できない問題がありました。
このコミットでなおったと思いますので、この現象に出会ったら見てみてください (@thomasfedb 氏に感謝) 。
後記
ちょうど、この記事を書いていたところ URI::Generic#to_s
の Ruby 2.2 への互換性について trunk に取り込んで頂けたので (ありがとうございます!) 、Ruby 2.3.1 (?) あたりでいくつかの検討課題と含めていい感じの動きになるかなと思っています。