Edited at

Shift-JISなCSVを読み込む・書き出しするときにエラーを起こさない数少ない方法

More than 3 years have passed since last update.


CSV#read,CSV#foreachでSJISを読み込みたい

ECなどいまだにSJIS文化がはびこってるシステムとやりとりをするるとどうしてもSJISを入出力しないといけなくなります。RubyでCSVを読み込むには、CSVと、Fileをつかって読み込む方法がありますが、どちらにしても文字コードの壁がそびえ立っています。Fileをつかわずに、CSVだけで完結する方法をとってみました。


文字コードを指定

大概の場合このケースでいけることが多いです。


csv_sjis.rb

  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にencodinginternal_encodingextranal_encoding以外のオプションがあるとエラーをあげる仕組みになっています。CSVクラスにとっては、直接関係ない読み込みオプションはエラーを出してるみたいです。


モンキーパッチ

csv.rbのコンストラクタにモンキーパッチを当ててundefreplaceのオプションを削除すれば動きそうです。というわけで出来たのが以下のパッチです。


config/initializer/csv.rb

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_sjis.rb

  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にするとエラーが起きてしまうためにさらにエラーが起きないように対応してあります。


config/initializer/string.rb

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_output.rb

sjis_string = "SJISにしたい文字列を入れます"

p sjis_string.sjiable.encode("cp932")

Rails4からview側でロジックかいてあげるとCSVファイルをわざわざつくらなくていいので楽ちんであります。

参考:

http://esoz.blog.fc2.com/blog-entry-59.html

それでは良いSJISライフを!