Rubyで文字コードをUTF-8からShift-jisに変換に詰まったお話
現職でCSV吐き出しの時に文字列のデフォルト文字コードをShift-jisにして欲しいという依頼がありました。
Rubyの文字列のデフォルト文字コードはUTF-8になるので、それをCSV吐き出しの時にShift-jisに変換するだけで実現できるので余裕だと思ったのですが、意外にハマりました。
同じようにハマってる人がいるかもしれないので、解決方法を記載します。
ハマった実装箇所
RubyのライブラリのCSVクラスのgenerateメソッドを利用すればCSV吐き出しは割と簡単に実装できます。
実装イメージ
↓
require "csv"
text =<<-EOS
id,first name,last name,age
1,taro,tanaka,20
2,jiro,suzuki,18
3,ami,sato,19
4,yumi,adachi,21
EOS
csv = CSV.generate(text, headers: true) do |csv|
csv.add_row(["5", "saburo", "kondo", "34"])
end
コード参考:https://docs.ruby-lang.org/ja/latest/method/CSV/s/generate.html
shift-jisに変換したいときは:encoding というキーを使用すると出力のエンコーディングを指定することができます。
CSV.generate(text, headers: true, encoding: "SJIS")
このオプションをつけると、出力されるエンコーディングがutf-8からshift-jis自動変換されます。
こちらで実装をすると、なぜか以下のエラーが発生しました.....
incompatible character encodings: Windows-31J and UTF-8
原因調査
なぜ、エンコードエラーになるのか調査します。
どの文字列がエラーになるのか調べた結果、以下の文字列にエラーが発生していました。
"AAA−0001"
特に違和感のない文字列です、なぜエラーになるのでしょうか?
詳しく調べてみると、以下の文字をshif-jisに変換すると例外エラーになってしまうらしいです。
文字コード(UTF-8) | 文字 | 備考 |
---|---|---|
U+00A2 | ¢ | セント記号(通貨) |
U+00A3 | £ | ポンド記号(通貨) |
U+00AC | ¬ | NOT記号 |
U+2016 | ‖ | Double vertical line |
U+2212 | − | マイナス記号 |
U+301C | 〜 | 波ダッシュ |
参考: https://osa.hatenablog.com/entry/2014/08/21/113602 |
"AAA−0001"
この文字列には−(マイナス記号)が含まれているので、例外エラーが発生したと想定されます。
解決方法
エラー原因はわかりました。ではどう解決すれば良いのでしょうか?
一番簡単な方法はRubyのオープンクラスを利用した文字列クラスの拡張です。
Rubyはクラスの継承に制限がありません。StringクラスやArrayクラスなど、組み込みライブラリのクラスであっても継承して独自のクラスを定義することができます。
なので以下のようにStringクラスにWindows-31Jに変換する時の例外を防止するためのメソッドを追加します。
class String
def sjisable
str = self
#変換テーブル上の文字を下の文字に置換する
from_chr = "\u{301C 2212 00A2 00A3 00AC 2013 2014 2016 203E 00A0 00F8 203A}"
to_chr = "\u{FF5E FF0D FFE0 FFE1 FFE2 FF0D 2015 2225 FFE3 0020 03A6 3009}"
str.tr!(from_chr, to_chr)
#変換テーブルから漏れた不正文字は?に変換し、さらにUTF8に戻すことで今後例外を出さないようにする
str = str.encode("Windows-31J","UTF-8",:invalid => :replace,:undef=>:replace).encode("UTF-8","Windows-31J")
end
end
コード参考:https://qiita.com/yugo-yamamoto/items/0c12488447cb8c2fc018
このメソッドを例外エラーになる箇所で実行することでえ例外エラーが出なくなる。
"AAA−0001".sjisable
オープンクラスを利用したくない場合
オープンクラスやは非常に強力で、うまく使えば開発の効率を高めることができます。
その辺面、Rubyの標準クラスに独自のメソッドを追加したのはいいが、追加した本人以外はコードを読んでも誰がどこで何の目的で定義したメソッドなのかわからず、かえってチーム全体の開発効率を落とす。
または、予期せぬタイミングでエラーが発生するといったデメリットも考えられます。
もしくは一部の人は文字コードをShift_JISに変更することがStringクラスの責務なのか、それはCSVにするときに必要になるのでCSVを扱うクラスの責務ではないのか?という疑問も出ると思います。
なので、もしオープンクラスを利用しない場合はCsvUtilityクラス的なものを作成して、CSVを扱う手続きはそこに集約し、Shift_JISなりUTF-8なりで出力できるようにしてあげる実装にする方がベターになります。