Edited at

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

More than 1 year has passed since last update.


概要

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"]]