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

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

## 文字コードを指定
大概の場合このケースでいけることが多いです。


```ruby: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メソッドをみてみます。

```rb
#@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クラスのインスタンスを、再度オプションと一緒にコンストラクタに渡しています。

```rb
#@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`のオプションを削除すれば動きそうです。というわけで出来たのが以下のパッチです。

```rb: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に入れています。

## 使い方
開くときにオプションをつけて呼び出せます。

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

```rb: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

```

#使い方

```rb:sjis_output.rb
sjis_string = "SJISにしたい文字列を入れます"
p sjis_string.sjiable.encode("cp932")
```

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



参考:
[http://esoz.blog.fc2.com/blog-entry-59.html](http://esoz.blog.fc2.com/blog-entry-59.html)

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