Ruby

ruby 2.4.0 CSV の liberal_parsing オプションについて調査してみた

概要

ruby 2.4.0 から、CSV に liberal_parsing オプションが追加された

これによって、RFC4180を無視してフィールド内に置かれた、エスケープされていないダブルクォートを読み込むことができるようになった

実用に足りるか細かい挙動を調査したので、それをメモ

調査した感想

  • RFC4180に準拠しているフィールドと、そうでないフィールドが混在しているときに使うのを想定している様子
    • たとえば "a""a""a",b"bb みたいな
  • そういう使い方ならばオススメ。 ただし、ダブルクォートのとじ忘れは、今までと同様にパースに失敗する場合がある
  • 文中に改行も無く、ダブルクォートに囲まれたフィールドを期待していないのであれば、String#split(',') を使ったほうが速いし、上記のような問題も発生しない

liberal_parsing オプションの挙動をざっくり説明

ruby 2.4.2 の挙動

  • ダブルクォートに囲まれたフィールドの場合、 liberal_parsing があってもなくても今までと同じ挙動
  • ダブルクォートに囲まれていないフィールドの場合、
    • フィールド内にあるダブルクォートに特別な意味を持たせずにパースする
      • a,bb"b,c ならば ["a", "bb\"b", "c"]
    • フィールドの先頭にダブルクォートがあって末尾にない場合、文中のダブルクォートの数が奇数だとパースに失敗する
      • a,"b"b"b,c
        • 既存のダブルクォートに囲まれた文字列のパースの実装とバッティングしている? バグと言ってよいのかな...?
      • フィールドの先頭にダブルクォートが無くて末尾にある場合は、ダブルクォートの偶数奇数に関わらずパースされる
        • a,b"b"b,c

参考

色々なCSVを liberal_parsing オプションをつけてパースした結果

ダブルクォートに囲まれたフィールドの場合

liberal_parsing オプションに関わらず、今までと同様の挙動

正しくエスケープされたダブルクォートの場合

01.csv
a,"bb""b",c

RFC4180に合わせてダブルクォートが処理される

CSV.parse(csv, liberal_parsing: true) #=> [["a", "bb\"b", "c"]]

文中にエスケープされていないダブルクォートがある場合

01.csv
a,"bb"b",c

例外が発生する

CSV.parse(csv, liberal_parsing: true) #=> Unclosed quoted field on line 1.

ダブルクォートに囲まれていないフィールドの場合

途中にダブルクォートがある場合

01.csv
a,bb"b,c

恐らく、こういうパターンを救いたいが為の liberal_parsing オプション
ダブルクォートに特別な意味を持たせずにパースされる

CSV.parse(csv, liberal_parsing: true) #=> [["a", "bb\"b", "c"]]

途中にエスケープされたダブルクォートがある場合

01.csv
a,bb""b,c

ダブルクォートに特別な意味を持たせないのでエスケープは無視される

CSV.parse(csv, liberal_parsing: true) #=> [["a", "bb\"\"b", "c"]]

先頭にのみダブルクォートがある場合

01.csv
a,"bbb,c

RFC4180に準拠したフィールドであるかどうか確認している途中で末尾に到着して、MalformedCSVError が発生
これを救いたければ、CSVライブラリは使わずに、split(',') した方が良さそう

CSV.parse(csv, liberal_parsing: true) #=> Unclosed quoted field on line 1.

先頭と途中にダブルクォートがあって、ダブルクォートの合計が偶数の場合

01.csv
a,"bb"b,c

この場合は救われる
実装を斜め読みする限り、既存のダブルクォートをパースする処理との共存のためにそうなっている

CSV.parse(csv, liberal_parsing: true) #=> [["a", "\"bb\"b", "c"]]

先頭と途中にダブルクォートがあって、ダブルクォートの合計が奇数の場合

01.csv
a,"b"b"b,c

奇数の場合は例外になる
この辺を救いたければ(ry

CSV.parse(csv, liberal_parsing: true) #=> Unclosed quoted field on line 1.

末尾と途中にダブルクォートがある場合

01.csv
a,bb"b",c

この場合は、フィールド内のダブルクォートが奇数だろうが、偶数だろうが例外は発生しない

CSV.parse(csv, liberal_parsing: true) #=> [["a", "bb\"b\"", "c"]]