Ruby の csv ライブラリーにおける空フィールドの扱い

# はじめに

Ruby には CSV 形式の読み書きができる，その名もずばり csv というライブラリーが標準添付されている。

http://docs.ruby-lang.org/ja/2.3.0/library/csv.html

この記事では，読み込む CSV テキストに**空のフィールドがあった場合**の注意点を述べる。

あ，不正確なところがあったらツッコミを入れてください。

# CSV おさらい

CSV をよく知ってる人は飛ばしてください。

CSV は n 行 m 列に並んだ文字列群を表現するフォーマットの一つで，列の区切りにカンマを使い，行の区切りに改行を使う。

要するに

```
FORTRAN,1954
LISP,1958
Ruby,1995
```

のようなやつ。

フィールドをカンマで区切るからそういう名前になっている。日本語でも「カンマ区切りテキスト」などと呼ぶ。

単純そうだけど，「**フィールド値にカンマが含まれてたら**どう表現すんの？」とか「**フィールド値に改行を含む**ことは可能か」というようなことを考え出すと，そう単純でもない。

極めて重要かつ頻用されるフォーマットなのに，JIS とか ISO とかにはなっていない。

その代わり **RFC4180** という文書が存在して，これに CSV の仕様が書かれている。

http://www.ietf.org/rfc/rfc4180.txt

邦訳例：
https://github.com/ruby/ruby/blob/trunk/lib/csv.rb#L1905

RFC という言葉は Request for Comments の頭字語で，初期の RFC が「〇〇について仕様書を書いてみたんで，コメントくれや」という感じだったことに由来するらしい。

ま，それはともかく，CSV の仕様書で他にコレというものは無いようで，たぶん多くの人が「CSV って RFC4180 に基づくのがイイんじゃね？」と思っているんではないかと勝手に期待している。
（あー，このさいエクセルくんのことは放っておきましょう）

## RFC4180 の中身

とりあえず以下のことだけ知っといてくだされ。

各フィールドはダブルクオートで囲っても囲まなくてもいい。以下の二つは同じデータを表している。

```
foo,bar
```

```
foo,"bar"
```

カンマや改行を含むフィールドはダブルクオートで囲まなくてはならない。

以下の CSV は 2 行 2 列だ。

```
"foo,bar",baz
"foo
bar",baz
```

また，ダブルクオートを含むフィールドもダブルクオートで囲まなくてはならないし，囲んだ中のダブルクオートは二重のダブルクオートによって表す。

つまりこんなふうに書く。

```
foo,"bar""baz"
```


# Ruby の csv ライブラリーの使い方

Ruby の csv ライブラリーはもちろん RFC4180 に従った CSV の読み書きができる。

CSV ファイルの読み込みも，CSV テキストのパースもできる。第 1 行をヘッダー（フィールド名の行）とみなして，各行の各フィールドの値をフィールド名でハッシュのように得たりもできる。

いろんな使い方はリファレンスマニュアルを見ていただくとして，記事の説明に必要な最低限の使い方として以下のコードサンプルを挙げておく。

```rb
require "csv"

p CSV.parse("foo,bar\nhoge,fuga")

#=> [["foo", "bar"], ["hoge", "fuga"]]
```

えー，実は RFC4180 では，行の区切りは CR+LF と決まっているんだが，csv ライブラリーでは LF でもいいことになっている。ほどよくゆるいのだ。

# 空フィールドの問題

前置きが長くなったけど，ここでようやく本題に入る。

## 空フィールド問題とは

以下のような CSV の行があったとする。第 2 フィールドの値はなんだろう？

```
foo,,bar
```

当然，空文字列を期待するところだ。

ところがそうならない。

```rb
p CSV.parse("foo,,bar")

#=> [["foo", nil, "bar"]]
```

`nil` なんである。

では，次の CSV は？

```
foo,"",bar
```

これの場合は空文字列になる。

```rb
p CSV.parse('foo,"",bar')

#=> [["foo", "", "bar"]]
```

この動作はどうやら

https://github.com/ruby/ruby/blob/ruby_2_3/lib/csv.rb#L1887

でやっているようだ。つまり，ダブルクオートで囲まれていない場合，空文字列を**わざわざ** `nil` に置き換えているんである。

RFC4180 は何年か前に読んだけど，上記のような動作の根拠になりそうなことは書いてなかったと思う。

まあ世の中には，

> CSV のフィールドに値が無いことと値が空文字列であることを区別したい

という需要があるのかもしれない。ふーん。

それに，csv ライブラリーが `nil` を与えたとしても後処理で**煮る**なり焼くなりすればいいではないか，という考え方もあるのだろうな（ハイ，笑うとこですョ）

## nil でなく空文字列が欲しいんじゃ

では，`nil` でなくとにかく空フィールドは空文字列になってほしいとき，どうするのがベストか。

csv ライブラリーにはコンバーターという概念がある。CSV を読み込むとき，各フィールドの値に何か処理を施すためのもの。

例えば，数字しか書いてない CSV で，フィールド値を `Integer` で得るには以下のような書き方ができる。

```rb
p CSV.parse("3,4", converters: ->v{v.to_i})

#=> [3, 4]
```

コンバーターを使えば `to_s` で `nil` を `""` に変えられるのでは？

```rb
p CSV.parse("foo,,bar", converters: ->v{v.to_s})
```

このやり方は，実際，Ruby 2.1 まではうまくいっていた。

```rb
p CSV.parse("foo,,bar", converters: ->v{v.to_s})

#=> [["foo", "", "bar"]]
```


ところが，Ruby 2.2 で csv ライブラリーの動作がちょっと変わり，このコードでも `nil` になるようになった。

```rb
p CSV.parse("foo,,bar", converters: ->v{v.to_s})

#=> [["foo", nil, "bar"]]
```

Ruby 2.2 以降は，〈フィールドが `nil` ならコンバーターは通さない〉という仕様だ。

涙ちょちょぎれ！

（追記：2018-04-06）空フィールドでコンバーターが効かない問題は修正された。記事末尾の追記の章を参照。

# 結局どうすれば

きっと読み込みオプションとか何かあるはず，と思って探したけど，どうやら無い。

読み込んだ後で自分でどうにかするしかなさそうだ。とほほ。

空フィールドを空文字列で得るという**当たり前の幸せ**を得るためにわざわざコードを書かないといけないなんて面倒だし，巨大な CSV の読み込みで，ライブラリーがわざわざ空文字列から `nil` に置き換えたものをまた `to_s` するなんて無駄だよね。

# （追記：2018-04-06）空フィールドでコンバーターが効かない問題は解消

どのバージョンでかはよく分からなかったが，空フィールドでコンバーターが効かない問題はバグと認識されたようで，修正された。
https://bugs.ruby-lang.org/issues/11126

現在の csv ライブラリーは，標準添付ではあるものの，gem の形で配布されている。
確認したところ，csv 1.0.0（2017-12-13 リリース）で既に直っているようだ。
（てことは，Ruby 2.5 で直ったってことかな）

しかし，私の「空フィールドは空文字列になってほしい（オプションでもいい）」という願いはかなえられなかった。
https://bugs.ruby-lang.org/issues/12839

# （追記：2018-04-07）コンバーターは速度の敵だった

コンバーターを使って `nil` を空文字列に変換するのは速度的にどうなんだろう？と疑問に思っていたが，簡単なベンチマークをやってみた。

```rb
gem "benchmark-ips"
require "benchmark/ips"
require "csv"

csv_text = <<EOT
foo,bar,"",baz
hoge,"",temo,""
roo,goo,por,kosh
EOT

conv = ->(s){ s || "" }

Benchmark.ips 20 do |r|
  r.report "without converter" do
    CSV.parse csv_text
  end

  r.report "with converter" do
    CSV.parse csv_text, converters: conv
  end

  r.compare!
end
```

結果：

```
Comparison:
  without converter:     9940.9 i/s
     with converter:     8657.0 i/s - 1.15x  slower
```

コンバーターをかますと 15% も遅くなることが分かった。

本文に既に書いたけど，フィールドが空だった場合，csv ライブラリーは**わざわざ空文字列を `nil` に変換**している（[このへん](https://github.com/ruby/ruby/blob/ruby_2_5/lib/csv.rb#L1916)）。それをさらにユーザーがコンバーターで空文字列にする（戻す）。無駄だよね。

#（2018-07-04追記）`nil_value` オプションで切り替えられるように

@OwlQiita さんのコメントにあるように，`nil_value` オプションが導入され，これに空文字列を指定すれば私の望む動作にできるようになりました。

`nil_value` は 2018 年 5 月 3 日リリースの CSV 1.0.2 で導入されたようです。

#（2018-07-20追記）`nil_value: ""` の空文字列の同一性に気をつけろ

なんかハマりやすそうな点に気付いたので，追記します。

以下のように，空フィールドが空文字列になるようにしてみます。

```rb
gem "csv", ">= 1.0.2"
require "csv"

ary = CSV.parse("a,,,a", nil_value: "")
p ary
# => [["a", "", "", "a"]]
```

結果として文字列の配列の配列が得られます。空文字列が二つありますね。
この二つの空文字列は同じオブジェクトです。`nil_value` に与えた文字列そのものです。コピーはされないようです。

さきほどのコードに続けて以下を実行して確かめてみましょう。

```rb
# "" は同じオブジェクト
puts ary[0][1].equal?(ary[0][2]) # => true

# "a" は別のオブジェクト
puts ary[0][0].equal?(ary[0][3]) # => false
```

二つの `"a"` は別のオブジェクトであるのに対し，二つの `""` は同じオブジェクトであることが分かります。

ということは，この空文字列に破壊的に加工を施すと，思わぬ影響が出そうですね。
つまりこういうことです。

```rb
ary[0][1] << "hoge"

p ary # => [["a", "hoge", "hoge", "a"]]
```

一つ目の空文字列の末尾に `"hoge"` を追加したのですが，次のフィールドの値も同じオブジェクトなので当然 `"hoge"` になっているわけですね。

思わぬバグを招きそう。

まあこれに限らず，文字列に破壊的変更を加えるときは，その文字列オブジェクトの由来をきちんと把握することが大事なわけですが。