LoginSignup
29
27

More than 5 years have passed since last update.

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

Posted at

概要

先日の投稿

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

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

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

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

先にオチ

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

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

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

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

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

CP932(MS-ExcelでCSV出力するときの文字コード)で読み込む
File.read(path,encoding: 'cp932')
文字コードをUTF-8に変換。変換できない文字は"?"に置き換え
encode("UTF-8",:invalid => :replace)

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

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

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

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))

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

実行例

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

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

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

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

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

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

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

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

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で、特定の文字コードに変換することも可能。

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

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

なので

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で開く。というのもありかも。

文字コードを推定したもので開く
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
実行例
$ ruby read_nkf_name.rb fake.csv 
エンコード名 Shift_JIS
["品名", "種類", "値段"]
["棍棒", "武器", "60"]
["薬草", "drug", "10"]
["毒草", "drug", "1"]
[nil, "(空欄)", "2000"]
["いつもの", "何か", "45くらい"]

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

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

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

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

29
27
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
27