JavaScript (ES6 対応) による CSV パーサの実装
副業で Google Apps Script (GAS) を使っているのですが、CSV のパースに困っていました。
GAS には Utilities.parseCsv という CSV パーサがあるのですが, 本記事執筆時点で「同一セルに改行区切りで複数値が入っていても解釈できない」という仕様になっており、自分でパーサを実装する必要に迫られたためです。
とは言え先人が (目的は違いますが) 実装を試みてくれており, 検索すれば JS の CSV パーサの実装は見つかります。
ただし今の私ではその実装を見てすぐに理解できなかったので, 今回はそのコードを ES6 対応にし, 処理が理解しやすいよう書き換えました。
参考にさせていただいたコードは, Qiita に掲載されていた weal さんの記事 にあるものです (weal さん, ありがとうございました!)。
結局このコードは実際に使う必要はなくなったのですが, 勉強になったし折角なのでここに供養します。
GAS の Utilities.parseCsv でパースできる・できない CSV の例
つまり, GAS でパースできる CSV とパースできない CSV は以下の様になります。
- パースできる:
- 単一セル単一値のもの
- 単一セル複数値だが “” で括ったもの
- パースできない: 同一セルに改行区切りで複数値が入ったもの
function parseCsvTest() {
const CSV1 = `col1,col2,col3
foo,bar,aaa`;
const CSV2 = `col1,col2,col3
foo,bar,"foos,bars"`;
const CSV3 = `col1,col2,col3
foo,bar,"foos\nbars"`;
console.log(Utilities.parseCsv(CSV1));
console.log(Utilities.parseCsv(CSV2));
console.log(Utilities.parseCsv(CSV3));
}
結果
// CSV1
[ [ 'col1', 'col2', 'col3' ], [ 'foo', 'bar', 'aaa' ] ]
// CSV2
[ [ 'col1', 'col2', 'col3' ], [ 'foo', 'bar', 'foos,bars' ] ]
// CSV3
[ [ 'col1', 'col2', 'col3' ],
[ 'foo', 'bar', 'foos' ],
[ 'bars"', '', '' ] ]
改行区切り複数値対応の CSV パーサの実装
※詳細に検証したものではないので, 本コードの利用は自己責任でお願いします
function parseMultiValuesCsv(csvStrings) {
// セルに改行コードを含むデータは Utilities.parseCsv() では parse できないため実装
// ref. https://qiita.com/weal/items/5aa94235c40d60ef2f0c
let i, c, r, q, v, j;
let rows = [], row = [];
const dataLen = csvStrings.length;
const retCode = csvStrings.indexOf('\r\n') === -1 ? (csvStrings.indexOf('\r') === -1 ? '\n' : '\r') : '\r\n'; // 改行コードを取得
const retCodeLen = retCode.length;
for (i = 0, c = r = -1; i < dataLen; i++) {
if (csvStrings.charAt(i) === '"') { // quoted
for (j = 0, q = i + 1; q < dataLen; j++, q++) { // 閉 quote を探す
let qIndex = csvStrings.indexOf('"', q);
q = qIndex === -1 ? dataLen + 1 : qIndex; // quote の位置, 無いなら末尾まで
if (csvStrings.charAt(++q) !== '"') {
break; // "" なら継続
}
}
row.push((v = csvStrings.substring(i + 1, (i = q) - 1), j) ? v.replace(/""/g, '"') : v);
} else { // not quoted
if (c < i) {
let commaIndex = csvStrings.indexOf(',', i);
c = commaIndex === -1 ? dataLen : commaIndex; // 直近のカンマ位置と
}
if (r < i) {
let retCodecIndex = csvStrings.indexOf(retCode, i);
r = retCodecIndex === -1 ? dataLen : retCodecIndex; // 直近の改行位置を調べ
}
row.push(csvStrings.substring(i, (i = c < r ? c : r))); // そこまでを値とする
}
if (i === r || retCode === (retCodeLen > 1 ? csvStrings.substr(i, retCodeLen) : csvStrings.charAt(i))) {
rows.push(row);
row = [];
i += retCodeLen - 1;
}
}
csvStrings.charAt(i - 1) === ',' && row.push(''); // , で終わる
row.length && rows.push(row);
return rows;
}
function parseCsvTest_orig() {
console.log(parseMultiValuesCsv(CSV1));
console.log(parseMultiValuesCsv(CSV2));
console.log(parseMultiValuesCsv(CSV3));
}
parseCsvTest_orig() 実行結果
// CSV1
[ [ 'col1', 'col2', 'col3' ], [ 'foo', 'bar', 'aaa' ] ]
// CSV2
[ [ 'col1', 'col2', 'col3' ], [ 'foo', 'bar', 'foos,bars' ] ]
// CSV3
[ [ 'col1', 'col2', 'col3' ], [ 'foo', 'bar', 'foos\nbars' ] ]
最後に
冒頭に書いた通り, このコードは weal さんの実装を ES6 対応にし, 処理の流れを追いやすいよう書き換えただけのものです (書き換えの過程で私の理解不足で破壊してしまったところもあるかもしれません)。
ただ GAS での CSV パースに私と同じように苦しんだ方もいるかなと思い, 記事を残しておきます1。
繰り返しますが, コードの利用は自己責任でお願いします。
自分でパーサの実装は極力したくない2ので, GAS に同一セル改行区切り複数値対応の CSV パーサが早く実装されることを願っています3。