【Ruby】Excelで書き出したUTF-8のCSVファイルを読み込む

2018年10月12日Ruby

こんにちは、しきゆらです。

今回はExcelで書き出したCSVファイルをRubyで読み込むときに躓いたところをメモしておきます。

 

状況

Excelで管理していたとあるデータをCSVで書き出し、Rubyで変換しつつDBへ突っ込もうと思っていました。

CSVファイルは、先頭にヘッダーとなるタイトルをつけていて、2行目からが実質のデータでした。

 

ExcelでCSVファイルを吐き出す

Excelで書き出すことができるCSVファイルは2種類あります。

  • 「一般的な形式」の中にある「CSV UTF-8 (コンマ区切り) (.csv)」
  • 「特別な形式」の中にある「CSV (コンマ区切り) (.csv)」

「特別な形式」では、文字コードがShift-JISのため「一般的な形式」の方で吐き出しました。

あとは、RubyのCSVライブラリで読み込んでforeachなどですべての行を読み込めば終わり。

・・・と思っていた時代が私にもありました。

このUTF-8が曲者でした。

 

詳しくは、続きから。

 

 

挫折編:ひとまず読み込んで見る

以下のようなスクリプトをサクッと作り、読み込んで見ました。

require 'csv'
 
opt = {
  # CSVファイルの最初の1行目をヘッダーとして扱う
  headers: true,
  # ヘッダーをSymbolに変換
  header_converters: ->(header) { header.to_sym }
}
CSV.foreach("hoge.csv", opt) do |row|
  p row
end
#=> CSV::Rowがたくさん・・・

この辺を簡単にかけるRubyさんはやはり楽ちんですね。

 

さて、pで表示してみるときちんと読み込めているようです。

 

では、日付データを取得してみましょう。

foreachの中身を以下のように変更。

CSV.foreach("hoge.csv", opt) do |row|
   p row[:date]
end
#=> nilがたくさん・・・・

全部nilじゃないか!

おかしいぞ、バグか???

 

と思っていろいろ調べてみると、原因はExcelさんでした。

 

原因:UTF-8 BOM

UTF-8には、バイトオーダーマーク(BOM)と呼ばれるデータが付いているタイプと、ついていないタイプが存在しています。

このBOM、ファイルの先頭に「UTF-8で書かれていますよ」という数バイトの記号を目印としてつけるようです。

バイトオーダーマーク – Wikipedia

https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%83%88%E3%82%AA%E3%83%BC%E3%83%80%E3%83%BC%E3%83%9E%E3%83%BC%E3%82%AF

 

なお、BOM自体は必須のものではなく、ついているものとそうでないものが存在します。

UTF-8が一般的になった(?)最近では、前提として使われているためBOMはついていないですが、

昔はそうでなかったため、BOMをつけて識別していたようですね。

 

さて、そんなBOMですが、Excelはこれを使って文字コードを識別しているようです。

つまり、Excelで吐き出したCSVファイルは「(BOM記号)date,・・・」となっているようです。

これでは、見た目上はdateでもkeyが微妙に違うため、取得できません。

 

解決編:読み込み時にBOMを考慮する

CSVファイルのBOMを削除してもいいのですが、そうするとExcelで開くと文字化けしてしまいます。

別に構わない、という場合はこちらもでいいかもしれません。

 

しかし、今回は何かあったらExcelで確認したかったのでBOMがついたまま処理してみました。

方法としては、BOMはファイルの先頭に数バイトの記号がついているだけなので、それを無視するだけ。

 

では、コードを修正します。

opt = {
  headers: true,
  # headerを変換するときに、BOMを取り払ってからシンボルに直す
  header_converters: ->(header) { header.sub(/\A\uFEFF/, '').to_sym }
}

変更点は、CSVへ渡すオプションの中の「header_converters」です。

これは、ヘッダーを変換する方法を自分の関数で設定できるものです。

class CSV – Ruby 2.5.0リファレンス

https://docs.ruby-lang.org/ja/latest/class/CSV.html#S_NEW

 

headerはファイルの先頭1行だけ。

BOMもファイルの先頭にくっつく記号。

つまり、ここに細工をしてやればいいわけです。

 

今回は、headerの文字列をシンボルに直したかったので、to_symしていました。

その前段階で、headerの文字列の中にあるBOM記号をsubメソッドで置換するようにしました。

 

これにて、問題なくdateでアクセスできるようになりました。

 

 

まとめ

BOMの存在は知っていましたが、特に気にしたことはありませんでした。

調べてみると、単なる厄介者ではなく昔は必要だったのでついているとのこと。

まだまだわからないことや知らないことがありますね。

 

歴史や経緯を知りつつ、今は必要なくなりつつあるんであれば統一してほしいなぁと思った今日このごろ。

今回はBOMをファイルから削除せず、読み込み時に考慮して上げる方法で対応しました。

 

 

今回はここまで。

おわり

Posted by しきゆら