CSV#read,CSV#foreachでSJISを読み込みたい
ECなどいまだにSJIS文化がはびこってるシステムとやりとりをするるとどうしてもSJISを入出力しないといけなくなります。RubyでCSVを読み込むには、CSV
と、File
をつかって読み込む方法がありますが、どちらにしても文字コードの壁がそびえ立っています。File
をつかわずに、CSV
だけで完結する方法をとってみました。
文字コードを指定
大概の場合このケースでいけることが多いです。
CSV.foreach("/path/to/file", encoding: "Shift_JIS:UTF-8") do |row|
p row #->UTF-8な日本語
end
が。
Encoding::UndefinedConversionError - "\x87U" from Shift_JIS to UTF-8:
とエラーが出てしまいます。
文字コードのオプションを付けてみる
File#openの場合には、
{encoding: "Shift_JIS:UTF-8", undef: :replace}
というオプションをつけて対応できますが、CSV#foreachなど、読み込み系のメソッドではこのオプションに対応しておらずに、エラーが出てしまいます。
ArgumentError - Unknown options: undef.:
というわけで、csvモジュールにモンキーパッチを当ててこのオプションが有効になるようにします。
csv.rbを調査
標準添付のcsv.rbのforeachメソッドをみてみます。
#@1119
def self.foreach(path, options = Hash.new, &block)
return to_enum(__method__, path, options) unless block
open(path, options) do |csv|
csv.each(&block)
end
end
オプションごとopenメソッドに渡しています。
#@1249
def self.open(*args)
# find the +options+ Hash
options = if args.last.is_a? Hash then args.pop else Hash.new end
# wrap a File opened with the remaining +args+ with no newline
# decorator
file_opts = {universal_newline: false}.merge(options)
begin
f = File.open(*args, file_opts)
rescue ArgumentError => e
raise unless /needs binmode/ =~ e.message and args.size == 1
args << "rb"
file_opts = {encoding: Encoding.default_external}.merge(file_opts)
retry
end
csv = new(f, options)
...
openメソッドでは、Fileメソッドuniversal_newline
を追加したオプションにを渡しています。ということはここではエラーは起きずにopenされていることになります。
開いたfileクラスのインスタンスを、再度オプションと一緒にコンストラクタに渡しています。
#@1486
def initialize(data, options = Hash.new)
# build the options for this read/write
options = DEFAULT_OPTIONS.merge(options)
...
init_separators(options)
init_parsers(options)
init_converters(options)
init_headers(options)
init_comments(options)
@force_encoding = !!(encoding || options.delete(:encoding))
options.delete(:internal_encoding)
options.delete(:external_encoding)
unless options.empty?
unless options.empty?
raise ArgumentError, "Unknown options: #{options.keys.join(', ')}."
end
...
ここで、Unknown options
のエラーをあげているようです。追っていくと、optionにencoding
、internal_encoding
、extranal_encoding
以外のオプションがあるとエラーをあげる仕組みになっています。CSVクラスにとっては、直接関係ない読み込みオプションはエラーを出してるみたいです。
モンキーパッチ
csv.rbのコンストラクタにモンキーパッチを当ててundef
とreplace
のオプションを削除すれば動きそうです。というわけで出来たのが以下のパッチです。
module CSVEncodingExtension
def initialize(data, options = Hash.new)
options.delete(:replace)
options.delete(:undef)
super
end
end
CSV.send(:prepend, CSVEncodingExtension)
optionsからCSVクラスに直接不要なオプションを削除することで対応しました。
railsで利用しているのでconfig/initializerに入れています。
使い方
開くときにオプションをつけて呼び出せます。
CSV.foreach("/path/to/file", encoding: "Shift_JIS:UTF-8", undef: :replace, replace: "*") do |row|
p row #->UTF-8な日本語
end
これでどんなSJISがきても大丈夫(なはず)。
#SJISでCSVを書き出す
SJISであって、UTF-8にない文字コードのためにエラーがおきます。
String
にメソッドを追加して、他の文字コードに置き換えてしまうのが一般的です。ただ、SJIS→UTF-8→SJISの場合、代替文字であるU+FFFD
をSJISにするとエラーが起きてしまうためにさらにエラーが起きないように対応してあります。
class String
def sjisable
str = self
str = str.exchange("U+301C", "U+FF5E")
str = str.exchange("U+2212", "U+FF0D")
str = str.exchange("U+00A2", "U+FFE0")
str = str.exchange("U+00A3", "U+FFE1")
str = str.exchange("U+00AC", "U+FFE2")
str = str.exchange("U+2014", "U+2015")
str = str.exchange("U+2016", "U+2225")
str = str.exchange("U+FFFD", "U+30FB")
end
def exchange(before_str,after_str)
self.gsub( before_str.to_code.chr('UTF-8'),
after_str.to_code.chr('UTF-8') )
end
def to_code
return $1.to_i(16) if self =~ /U\+(\w+)/
raise ArgumentError, "Invalid argument: #{self}"
end
end
#使い方
sjis_string = "SJISにしたい文字列を入れます"
p sjis_string.sjiable.encode("cp932")
Rails4からview側でロジックかいてあげるとCSVファイルをわざわざつくらなくていいので楽ちんであります。
参考:
http://esoz.blog.fc2.com/blog-entry-59.html
それでは良いSJISライフを!