RubyでShift JISやCP932などのCSVをUTF-8に変換して読み込む

## 結論

おそらくこれだけで十分。詳しい説明や検証結果は後述。

```ruby
require 'charlock_holmes'
require 'csv'

# CSVファイルのパスを指定
path = 'path/to/file.csv'

# ファイルをすべて読み込んで（大きなファイルは、メモリに優しくない）
# エンコードを推測（あくまで推測）
detection = CharlockHolmes::EncodingDetector.detect(File.read(path))
# => {:type=>:text, :encoding=>"Shift_JIS", :ruby_encoding=>"Shift_JIS", :confidence=>100, :language=>"ja"}

# CharlockHolmes::EncodingDetectorのディテクション結果は、
# CP932であるShift JIS拡張文字コードを含む場合にもShift_JISと見なされてしまう為、CP932を優先して利用する。
# もしそのままShift JISを指定すれば、CSV.foreach()で変換エラーが出る原因となる。
# アップサイドは拡張文字コードを変換できることで、ダウンサイドは7種類の記号文字の見た目が完全に一致しないこと。
encoding = detection[:encoding] == 'Shift_JIS' ? 'CP932' : detection[:encoding]

# CSVを1行ずつUTF-8として読み込む（メモリに優しい！）
CSV.foreach(path,
            encoding: "#{encoding}:UTF-8",
            headers: true) do |row|
  p row.inspect
end
```

フローとしては、

1. ファイル全体を読み込み（メモリを食う方法）： `File.read(path)`
1. 文字コードを判定する
1. 文字コードの判定結果の修正： `Shift JIS`拡張である`CP932`を優先する
1. CSVを1行ずつ読み込む（メモリを食わない方法）：`CSV.foreach`

## 説明

### 大前提

* CharlockHolmesをインストールしてある事
* Ruby 2.2.3で動作を確認済み

### 利用ファイルの読み込み

```ruby
require 'charlock_holmes'
require 'csv'
```

### CSVの読み込みと判定

```ruby
detection = CharlockHolmes::EncodingDetector.detect(File.read(path))

# => "{:type=>:text, :encoding=>\"Shift_JIS\", :ruby_encoding=>\"Shift_JIS\", :confidence=>100, :language=>\"ja\"}"
```

ここではCharlockHolmesを使ってエンコードを見分けています。（ただし、推測に過ぎない）

`File.read(path)`ではファイル全体を読み込む（はず）ため、大容量のファイルを読み込む場合は注意が必要です。Webサイトの場合、通常HTTPリクエストの上限バイト数が設定されていると思いますので、ひとまずこれでよしとしました。

### エンコードの修正

```ruby
encoding = detection[:encoding] == 'Shift_JIS' ? 'CP932' : detection[:encoding]
```
ここが勘所。

#### Shift_JISの代わりにCP932をエンコードとして指定する

この後使う`CSV.foreach`で`CSV.foreach("/path/to/file", encoding: "Shift_JIS:UTF-8")`のように`Shift_JIS`を指定すると、含まれる文字コードによってはエラー`Encoding::UndefinedConversionError`が出てしまいます。

調べたところ`CP932`は`Shift_JIS`から派生した拡張版であるため、`CP932`を指定しておくのが無難です。又、以下の参考資料によればShift_JISで作られたファイルがCP932かShift_JISかを文字コードから見分けることは不可能だと思われます。

代わりに、`CSV.foreach("/path/to/file", encoding: "CP932:UTF-8")`を使えば問題ないでしょう。

##### 参考資料

* [Shift_JIS と Windows-31J (MS932) の違いを整理してみよう](https://weblabo.oscasierra.net/shift_jis-windows31j/)
* [MySQLのsjisとcp932の違い](https://tmtms.hatenablog.com/entry/201805/mysql-sjis-cp932)
* [Shift-JISなCSVを読み込む・書き出しするときにエラーを起こさない数少ない方法](https://qiita.com/yuuna/items/7e4e06a1b881d85697e9#csvreadcsvforeach%E3%81%A7sjis%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%9F%E3%81%84) （私は'CP932'を使い、モンキーパッチを利用しない方針をとりました）

#### 問題になりそうな文字コードの変換をテストする

尚、私は[Shift_JISのダメ文字](https://sites.google.com/site/fudist/Home/grep/sjis-damemoji-jp)を参考にして「ダメ文字」と呼ばれる文字を含ませたところ、`Shift_JIS`を指定した際は想定通りのエラーが出て、`CP932`を指定したところ、今のところエラーが確認できておらず`UTF-8`で扱うことができています。

ただし、私は文字コードにはそこまで詳しくなく、高精度な変換が求められる場合はより厳密なテストが必要でしょう。


#### さらに詳しくテストしてみる：Shift_JISとCP932の違い

こちらのテーブルはMySQLの[sjisとcp932の違い](https://tmtms.hatenablog.com/entry/201805/mysql-sjis-cp932)から引用させていただきました。

以下の7つの文字の見た目が若干異なるようです。下の`8161`と`8191`はそれなりに見た目が異なりますが、文字コードかどちらか分からない場合は`CP932`として読み込んでしまうのは妥協点ではないかと思われます。（そうでなければ、ファイルの提供者にエンコードを教えてもらうか、両方で表示して好きな方を選んで頂く必要がありそうです）

```
+------+------+-------+
| code | sjis | cp932 |
+------+------+-------+
| 815F | \    | ＼    |
| 8160 | 〜   | ～    |
| 8161 | ‖    | ∥     |
| 817C | −    | －    |
| 8191 | ¢    | ￠    |
| 8192 | £    | ￡    |
| 81CA | ¬    | ￢    |
| 8740 | ?    | ①     |
| 8741 | ?    | ②     |
| 8742 | ?    | ③     |
| 8743 | ?    | ④     |
```

実際にこのファイルを2通りで保存してみました。（IntelliJ IDEAを使用）

* UTF-8からShift JISに変換して保存した場合、CP932の文字は文字化けします。
* 逆にUTF-8からWindows-31j(CP932)として保存した場合、sjisの文字は文字化けします。

この2つのファイルを`CharlockHolmes::EncodingDetector.detect`したところ、どちらも`windows-1250`と検知されます。少しひらがなを追記した程度では結果は変わらず、漢字などを書き足してみたところ、どちらも`Shift-JIS`と判定されるようになりますが、実際には片方が`CP932`なのです。このように文字コードが判定に頼るしかない場合、やはり`CP932`と仮定した方が安全です。

### CSVを1行ずつ読み込む

```ruby
CSV.foreach(path,
            encoding: "#{encoding}:UTF-8",
            headers: true) do |row|
  p row.inspect
end
```

最後になりましたが、ここではCSVを1行ずつ読み込んでいます。

* `path`でファイルへのパスを渡しています
* `encoding: "#{encoding}:UTF-8"`では、判定された文字コードをUTF-8に変換しながら読み込むことを指定しています。
* `headers: true`を指定しているので、`row[1]`の代わりに`row[:name]`のように扱うことが可能です。処理中に利用するメモリも少なくなるはずです。
* `p row.inspect`でテストとして、コンソールなどにrowの中身を出力しています

参考：[Importing Massive CSV Data Into Rails](https://mattboldt.com/importing-massive-data-into-rails/)

## おまけ

### RailsでBULK INSERT

[Importing Massive CSV Data Into Rails](https://mattboldt.com/importing-massive-data-into-rails/)からの引用ですが、Railsで複数のINSERTクエリをまとめる場合は`ActiveRecord.import`を利用することでパフォーマンスが向上します。

```ruby
items = []
CSV.foreach('link/to/file.csv', headers: true) do |row|
  items << Item.new(row.to_h)
end
Item.import(items)
```

こちらについてはQiitaにもわかりやすい記事があります。
→[ActiveRecordで複数レコード、BULK INSERTする方法とパフォーマンスについて](https://qiita.com/xend/items/79184ded56158ea1b97a)

### CharDet

日本語いっぱいのShift_JISのファイルでも以下のような結果になってしまうので、こちらはあまり実用的ではなさそうです。

```ruby
detection2 = CharDet.detect(File.read(path))
p detection2.inspect

# => "{\"encoding\"=>\"ISO-8859-2\", \"confidence\"=>0.20525522026784734}"
```


以上です。
何かお気づきの点がありましたら、是非コメントをお願いします！