はじめに
BOM 付き UTF-8 の csv ファイルのカラム名を RSpec で検証しようと思ったときにハマったので、備忘録を残しておきます。
tl;dr
BOM 付き UTF-8 の csv ファイルのカラム名を RSpec で検証するときは、CSV.parse
するときにBOM を取り除く必要がありました。
csv = CSV.parse(csv_content.sub(/^\xEF\xBB\xBF/, ''))
背景
RSpec でアプリ側で作成した csv ファイルが適切に出力されていたか確認するために、csv のカラム名を検証しようとしていました。
サンプルは以下のような形です。
require 'rails_helper'
RSpec.describe Sample do
describe '#method' do
it 'checks if csv columns is valid' do
# Sample.csv_content が csv ファイルを出力するメソッド
csv_content = Sample.csv_content
csv = CSV.parse(csv_content)
expect(csv[0][0]).to eq '日付'
expect(csv[0][1]).to eq '時刻'
end
end
end
Sample.csv_content
は以下のような形です。方法については以下を参考にしました。
(実際の処理は BOM 付き csv ファイルをそのままダウンロードして出力するような処理だったので、BOM を無理やりつけてはいません)
BOM付きUTF-8のCSVを作成する(Excel文字化け対策) - Qiita
class Sample
def self.csv_content
# bom = %w(EF BB BF).map { |e| e.hex.chr }.join
# コメント踏まえ 2019/12/24 修正
bom = "\uFEFF"
content = CSV.generate do |csv|
csv << ['日付', '時刻']
csv << ['2019/12/22', '14:00']
end
bom + content
end
end
出力する csv ファイルは以下の通りです。こちらが BOM つき UTF-8 のエンコーディングになっています。
日付,時刻
2019/12/22,14:00
BOM 付きのままだと検証が失敗する
しかし実際に RSpec を実行しようとすると、expect(csv[0][0]).to eq '日付'
の部分で検証チェックに失敗します。エラーは以下のような形です。
expect(csv[0][0]).to eq '日付' expected: "日付" got: "日付" (compared using ==)<
一見、expected と got が同じ値に見えるので、何が原因なのか最初はわかりづらいですね。。
RSpec 実行中にデバッガーで処理を止め、文字列エンコードを ascii-8bit にして確認すると、
expected: "\xE6\x97\xA5\xE4\xBB\x98"
got: "\xEF\xBB\xBF\xE6\x97\xA5\xE4\xBB\x98"
got のほうが \xEF\xBB\xBF
分だけ長いことがわかりました。これは BOM そのもので、CSV.parse
しても、文字列先頭の BOM はそのままついてくるということでした。。
解決方法
CSV.parse
する対象の csv 文字列から、BOM を取り除く処理を書きます。文字列の最初に表示される BOM のみ sub
で空文字に置換するイメージです。
csv_content = Sample.csv_content
csv = CSV.parse(csv_content.sub(/^\xEF\xBB\xBF/, ''))
そもそも BOM とは
Byte Order Mark の略です。Wikipedia から引用します。
プログラムがテキストデータを読み込む時、その先頭の数バイトからそのデータがUnicodeで表現されていること、また符号化形式(エンコーディング)としてどれを使用しているかを判別できるようにしたものである。
UnicodeがはじまったころはアメリカではASCII、ヨーロッパなどではISO-8859、日本ではShift_JISやEUC-JPが主流であり、使用されている符号化方式がUnicodeであることを明確に区別する必要があった。その方法として、先頭のデータにテキスト以外のデータを入れることが発案された。
復数の国で異なるエンコーディングが使われていた中で、世界共通の文字コードとして扱われる Unicode との区別し、ファイルを開く際に Unicode のエンコーディングかどうかを判別できるようにしたのですね。
終わりに
なんとなく言葉だけ知っていた BOM について、この機会に改めて調べられてよかったです。