CSV 内の改行コードが混在していると CSV::MalformedCSVError が発生する

こんにちは、ryohei515です。

弊社が提供している Re:lation では LINE 等のチャットの CSV エクスポート機能があります。
そこで CSV::MalformedCSVError というエラーが発生することがありました。

結論、掲題の通りの原因だったのですが、エラー調査時にこのことが書かれた記事等見かけることがなかったので、備忘で残しておこうと思います。

環境

Ruby 3.1.0

実際に発生したエラー

CSV.parse(csv) の処理で、以下の通り発生しました。

/usr/local/lib/ruby/3.1.0/csv/parser.rb:1067:in `parse_quotable_robust': Any value after quoted field isn't allowed in line 1. (CSV::MalformedCSVError)
    from /usr/local/lib/ruby/3.1.0/csv/parser.rb:1007:in `block in parse_quotable_loose'
    from /usr/local/lib/ruby/3.1.0/csv/parser.rb:52:in `block in each_line'
    from /usr/local/lib/ruby/3.1.0/csv/parser.rb:49:in `each_line'
    from /usr/local/lib/ruby/3.1.0/csv/parser.rb:49:in `each_line'
    from /usr/local/lib/ruby/3.1.0/csv/parser.rb:963:in `parse_quotable_loose'
    from /usr/local/lib/ruby/3.1.0/csv/parser.rb:406:in `parse'
    from /usr/local/lib/ruby/3.1.0/csv.rb:2554:in `each'
    from /usr/local/lib/ruby/3.1.0/csv.rb:2554:in `each'
    from /usr/local/lib/ruby/3.1.0/csv.rb:2589:in `to_a'
    from /usr/local/lib/ruby/3.1.0/csv.rb:2589:in `read'
    from /usr/local/lib/ruby/3.1.0/csv.rb:1738:in `parse'
    ...

原因

試行した結果、CSV 内のデータ内の改行コードと、行の終わりの改行コードが統一されていれば正しく parse できるようだと判明しました。

以下の例だと、1行目2列目で fu\nga として LF (\n) で改行をし、行と行の区切りの改行としても LF を用いているため、問題なく parse できます。

irb(main):006:0> csv = "\"hoge\",\"fu\nga\"\n\"foo\",\"bar\""
=> "\"hoge\",\"fu\nga\"\n\"foo\",\"bar\""
irb(main):007:0> CSV.parse(csv)
=> [["hoge", "fu\nga"], ["foo", "bar"]]

しかし、fu\r\nga のように、データ内の改行を CRLF (\r\n)、区切りの改行を LF といった形で混在させると CSV::MalformedCSVError が発生しました。

irb(main):008:0> invalid_csv = "\"hoge\",\"fu\r\nga\"\n\"foo\",\"bar\""
=> "\"hoge\",\"fu\r\nga\"\n\"foo\",\"bar\""
irb(main):009:0> CSV.parse(invalid_csv)
Traceback (most recent call last):
       16: from /usr/bin/irb:23:in `<main>'
       15: from /usr/bin/irb:23:in `load'
       14: from /Library/Ruby/Gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
       13: from (irb):9
       12: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv.rb:685:in `parse'
       11: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv.rb:1245:in `read'
       10: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv.rb:1245:in `to_a'
        9: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv.rb:1236:in `each'
        8: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv.rb:1236:in `each'
        7: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv/parser.rb:303:in `parse'
        6: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv/parser.rb:779:in `parse_quotable_loose'
        5: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv/parser.rb:28:in `each_line'
        4: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv/parser.rb:28:in `each_line'
        3: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv/parser.rb:31:in `block in each_line'
        2: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv/parser.rb:818:in `block in parse_quotable_loose'
        1: from /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/csv/parser.rb:869:in `parse_quotable_robust'
CSV::MalformedCSVError (Any value after quoted field isn't allowed in line 1.)

なお、改行コードを CRLF に統一していた場合も正常に動作するため、混在が NG なのかと思われます。

irb(main):010:0> crlf_csv = "\"hoge\",\"fu\r\nga\"\r\n\"foo\",\"bar\""
=> "\"hoge\",\"fu\r\nga\"\r\n\"foo\",\"bar\""
irb(main):011:0> CSV.parse(crlf_csv)
=> [["hoge", "fu\r\nga"], ["foo", "bar"]]

おわりに

CSV として改行コードが混在してはいけないという決まりはないと思いますし、エラー内容からも原因を読み取れなかったため、特定に少し苦労しました。。

同じエラーで悩まれている方の参考になれば幸いです。