Ruby文字エンコードに悩まされ、直接指定やnkfによる推測などを試した


# 概要

## 先日の投稿

CSVと文字エンコードで悩んで、後からUTF-8に変換した、ruby力低い話
http://qiita.com/rojiuratech/items/8e348937960bf76fb783

について、以下のような指摘があったこと、

- 文字エンコードがわかるのなら、指定したほうが良い
- 指定しないとデフォルトの文字コードと認識されてしまう。
- そもそも、最初からUTF-8で与えることはできないのか？

加えてソースコードが汚すぎたので、やり直しすことにしました。

## 先にオチ

- CSVデータの受け取りは、1行のコードで簡単にできる。
- NKFコマンドで強制的に変換することも可能
- 文字コードが不明な場合、NKFで推定してから指定する方法もあり
- LibreOffice使えればいいけど、エンジニア以外ほぼMSOfficeユーザ

# CSVデータを２次元配列として受け取る処理は、１行で済んだ

文字エンコードを指定してファイルを読み込み、UTF-8に変換する処理は１行にまとめられました。

```ruby:１行でUTF-8形式の二次元配列に加工できる
mod_ary = CSV.parse(File.read(path,encoding: 'cp932').encode("UTF-8",:invalid => :replace))
```

処理内容を細かく分けると

```ruby:CP932（MS-ExcelでCSV出力するときの文字コード）で読み込む
File.read(path,encoding: 'cp932')
```

```ruby:文字コードをUTF-8に変換。変換できない文字は"?"に置き換え
encode("UTF-8",:invalid => :replace)
```

参考　encodeメソッドに関する解説

http://ref.xaio.jp/ruby/classes/string/encode#h-1413969796

```ruby:引数dataに相当するものを二次元配列に変換
CSV.parse(data)
```

## 付帯処理を入れても、こんな短いソースに

```ruby:read_sjis_csv.rb
require 'csv'

path = ARGV[0]

# CP932形式で読み込んだCSVファイルを、UTF-8形式に変換した上でCSVにパースする。
mod_ary = CSV.parse(File.read(path,encoding: 'cp932').encode("UTF-8",:invalid => :replace))

# 各セルの値を持つ２次元配列を表示
mod_ary.each do |str|
  p str
end
```

## 実行例

こんな具合の、いかにもDBの中身をダンプしたようなCSVファイルがあるとして

```CSV:fake.csv
品名,種類,値段
棍棒,武器,60
薬草,drug,10
毒草,drug,1
,(空欄）,2000
いつもの,何か,45くらい
```

こんな具合にセルの中身を配列の要素とするものが取れる。
（1行ずつ表示しています）
あとはモデルに書き込むなりyamlなどに変換するなり、ご自由に。

```bash:実行例
$ ruby read_sjis_csv.rb fake.csv 
["品名", "種類", "値段"]
["棍棒", "武器", "60"]
["薬草", "drug", "10"]
["毒草", "drug", "1"]
[nil, "(空欄）", "2000"]
["いつもの", "何か", "45くらい"]
```

# 文字コードを指定しないと、デフォルトの指定になってしまう。

Rubyでは、ファイルを読み込む際の文字コードを
`Encoding.default_external`で決めています（デフォルトではUTF-8）

```ruby:デフォルト文字コード
pry(main)> Encoding.default_external
=> #<Encoding:UTF-8>
```

したがって、ファイルを読み込む際に文字コードを指定しないと、本当は異なる文字コードなのにUTF-8とみなしてしまう危険も。

```ruby:cp932形式のファイルを、UTF-8とみなして処理しているケース
pry(main)> hige = IO.read("fake_sjis.csv")
=> "\x95i\x96\xBC,\x8E\xED\x97\xDE,\x92l\x92i\r\x9E\x9E\x96_,\x95\x90\x8A\xED,60\r\x96\xF2\x91\x90,drug,10\r\x93ő\x90,drug,1\r,(\x8B\u{D74C1}j,2000\r\x82\xA2\x82\u0082\xE0\x82\xCC,\x89\xBD\x82\xA9,45\x82\xAD\x82炢"
pry(main)> hige.encoding
=> #<Encoding:UTF-8>
```

## 文字コードがわからない場合、NKFを利用して、強制的に変換

本当は文字コードを指定しておいたほうがいいんだろうけど
NKFで、特定の文字コードに変換することも可能。

```ruby:nkf命令で文字コードを変換したものを扱う
require 'nkf'
require 'csv'
encoded_sheet = CSV.parse(NKF::nkf('-w',File.read(path)))
```

nkfコマンドの構造はnkf(opt,str) 
- opt コマンド
- str　変換対象　

なので

```ruby:nkf
NKF::nkf('-w',File.read(path))
```

の場合、pathで指定したファイルデータをすべて読み込み、強制的にUTFにする。

nkf（Rubyリファレンスマニュアル）
http://docs.ruby-lang.org/ja/1.9.3/class/NKF.html

# 文字コードが不明なものについてNKF.guessで推測し、そのコードで開く

文字コードが与えられていない場合、推定したコードで開く。
というのもありかもしれない。（File.readのオプションで、不正なコードを置き換えるなどの指定が可能なので）
この場合、NKF.guessで推定していますが、拡張子も判断材料に含め、
拡張子が `.csv` かつ　文字コードが `Shift-JIS`ならMS-Office向きに作成されたものとみなし `cp932`で開く。というのもありかも。


```ruby:文字コードを推定したもので開く
require 'nkf'
require 'csv'

path = ARGV[0]

sample = File.read(path,10)
enc = NKF.guess(sample)

puts "エンコード名 #{enc.name}"

encoded_sheet = CSV.parse(File.read(path,encoding: enc.name).encode("UTF-8",:invalid => :replace ))

encoded_sheet.each do |str|
  p str
end
```

```bash:実行例
$ ruby read_nkf_name.rb fake.csv 
エンコード名 Shift_JIS
["品名", "種類", "値段"]
["棍棒", "武器", "60"]
["薬草", "drug", "10"]
["毒草", "drug", "1"]
[nil, "(空欄）", "2000"]
["いつもの", "何か", "45くらい"]
```

コマンドに関しては、ここも参考にしました
参考：nkfライブラリによる文字コードの推測

http://rubytips86.hatenablog.com/entry/2014/03/17/210612

# そもそも、最初からUTF-8で与えることはできないのか？

LibreOffice使えば、CSVで書き出すことも可能ですが
現在の環境では、Webエンジニアが私しかいない＆布教コストを考えると・・・。

と、こんな感じで文字エンコードに関して長々と書くのでありました。