⛓️

JavaScriptでダブルクォーテーションを含むCSVを配列に変換する

2022/07/14に公開

JavaScriptでCSV処理をちゃんとやるならPapa Parseのようなライブラリを使ったほうが良いです。しかし、

ライブラリは使わずもっと気軽に!
でもカンマ(,)で分割するだけではなくそこそこ厳密にCSV処理したい!

ということがあったため遊びがてらコピペで使える関数を作ってみました。

ちょっとCSV変換を依頼されたときに便利です。

CSV行を配列へ変換

まずはCSVではなく、1行のCSV行で考えます。
下記のparseCsvLine関数は次の特徴を持つCSV行を文字列の配列に変換します。

  • データはカンマ(,)で区切られている
  • データにカンマ(,)もしくはダブルクォーテーション(")を含むときはダブルクォーテーション(")で囲まれている
  • データに含まれるダブルクォーテーション(")はダブルクォーテーション2つ("")にエスケープされている
  • 改行が含まれていない
parseCsvLine関数
const parseCsvLine = line => line.split(',').reduce(([data, isInQuotes], text) => {
  if (isInQuotes) {
    data[data.length - 1] += ',' + text.replace(/\"+/g, m => '"'.repeat(m.length / 2))
    return [data, !(text.match(/\"*$/)[0].length % 2)]
  } else {
    const match = text.match(/^(\"?)((.*?)(\"*))$/)
    data.push(match[1] ? match[2].replace(/\"+/g, m => '"'.repeat(m.length / 2)) : match[2])
    return [data, match[1] && !(match[4].length % 2)]
  }
}, [[]])[0]

入力となるCSV行は正しいことを前提としています。

ちなみに逆に文字列配列をCSV行に戻すのは簡単です。

const toCsvLine = data =>
  data.map(datum => datum.includes(',') ? `"${datum.replaceAll('"', '""')}"` : datum).join(',')

CSVを2次元配列へ変換

CSV行ではなくCSVファイルと考えると、データに改行が含まれていないCSVであれば上記parseCsvLine関数を各行に適用するだけで問題ありません。多くの場合はそれで事足りると思います。
しかし、場合によってCSVはデータに改行を含むことがあります。
下記のparseCsv関数は次の特徴を持つCSVを文字列の2次元配列に変換します。

  • データはカンマ(,)で区切られている
  • データにカンマ(,)、ダブルクォーテーション(")、改行のいずれかを含むときはダブルクォーテーション(")で囲まれている
  • データに含まれるダブルクォーテーション(")はダブルクォーテーション2つ("")にエスケープされている
parseCsv関数
const parseCsv = csv => csv.replace(/\r/g, '').split('\n').reduce(([data, isInQuotes], line) => {
  const [datum, newIsInQuotes] = ((isInQuotes ? '"' : '') + line).split(',').reduce(([datum, isInQuotes], text) => {
    const match = isInQuotes || text.match(/^(\"?)((.*?)(\"*))$/)
    if (isInQuotes) datum[datum.length - 1] += ',' + text.replace(/\"+/g, m => '"'.repeat(m.length / 2))
    else datum.push(match[1] ? match[2].replace(/\"+/g, m => '"'.repeat(m.length / 2)) : match[2])
    return [datum, isInQuotes ? !(text.match(/\"*$/)[0].length % 2) : match[1] && !(match[4].length % 2)]
  }, [[]])
  if (isInQuotes) data[data.length - 1].push(data[data.length - 1].pop() + '\n' + datum[0], ...datum.slice(1))
  else data.push(datum)
  return [data, newIsInQuotes]
}, [[]])[0]

こだわったところ

実行速度や効率を一切考えておらず、行数を少なくすることだけ意識しました。

また、最初にカンマ(,)で分割すること(複数行の場合は改行(\n))にこだわりました。
パーサー風に、一つのCSVを分割せずに1文字ずつ処理すれば実行速度が上がり、もしかしたらもっと行数を短くできたかもしれません。では何故かと聞かれたら、CSVを扱うときはまずカンマ(,)で分割することから考えるので、とにかくそこから始めたかっただけです。

使用にあたって

需要はあまり無いと思いますが、parseCsvLine関数とparseCsv関数は自由にコピペして利用していただいて問題ございません。もし上手く変換できないパターンがあったら修正しますので、コメントいただけますと嬉しいです。

もっと優秀なライブラリが沢山ありますので、npm等での配布はしません。

Discussion