Skip to content

Instantly share code, notes, and snippets.

@think49
Last active July 12, 2022 05:36
Show Gist options
  • Save think49/009a8744c147a7013f14ea8913bd9027 to your computer and use it in GitHub Desktop.
Save think49/009a8744c147a7013f14ea8913bd9027 to your computer and use it in GitHub Desktop.
csv.js: ES5 規定の JSON と同じインターフェースを持つCSVパーサ

csv.js

概要

ES5 規定の JSON と同じインターフェースを持つCSVパーサです。 使い方は JSON と同じなので JSON を扱った事のある人は違和感なく使えると思います。

CSV API

CSV

CSV は単純な Object 型であり、関数でもコンストラクタでもありません。

console.log(typeof CSV);  // "object"
CSV();                    // TypeError: CSV is not a function
new CSV();                // TypeError: CSV is not a constructor(

CSV@@toStringTag"CSV" です。 (@@toStringTag は ES6 規定の Symbol 値です。)

console.log(Object.prototype.toString.call(CSV)); // "[object CSV]"

CSV.parse(csvString [, separator, reviver])

第一引数に与えられたCSV文字列をパースし、二次元配列を返します。

var array = CSV.parse('"A1","B1"\r\n"A2","B2"');
console.log(array); // [["A1","B1"],["A2","B2"]]

第二引数 separator (オプション)

第二引数でCSVの区切り文字を指定できます。 第二引数が省略された場合、区切り文字はカンマ(')として扱われます。

var array = CSV.parse('"A1"|"B1"\r\n"A2"|"B2"', '|');
console.log(array); // [["A1","B1"],["A2","B2"]]

ただし、区切り文字は1文字でなければなりません。2文字以上の区切り文字が指定された場合は1文字目が区切り文字として扱われます。 (2文字以上は技術的には可能ですが、CSV.parse の処理速度低下に繋がるのであえて1文字に制限しています。2文字以上の区切り文字は要望が多ければ検討します。)

var array = CSV.parse('A1<>B1\r\nA2<>B2', '<>');
console.log(array); // [["A1",">B1"],["A2",">B2"]]

第三引数 reviver (オプション)

第三引数 reviver を指定すると、セル値を引数にとるコールバック関数 reviver を呼び出した返り値を要素値に代入します。

var array = CSV.parse('A1,B1\r\n1,2', ',', function reviver (value) {
  var number = Number(value);
  return number.toString() === value ? number : value;
});
console.log(array); // [["A1","B1"],[1,2]]

CSVフォーマットのエスケープ規則

  • CSVのセル値はダブルクォートで括らなくても構いません。ダブルクォートで括られなかった場合、「改行、区切り文字」が後述するまで貪欲に消費してセル値として扱われます。
  • CSVのセル値をダブルクォートで括ると改行や区切り文字(デフォルトではカンマ ')を含める事が可能です。
  • CSVのセル値でダブルクォートを表現したい場合、ダブルクォートで括った上で "" を入力する事でエスケープできます。
/**
 * セル値はダブルクォートで括らなくても良い
 */
var array = CSV.parse('A1,B1\r\nA2,B2');
console.log(array); // [["A1","B1"],["A2","B2"]]

/**
 * セル値をダブルクォートで括れば改行や区切り文字を含める事が出来る
 */
var array = CSV.parse('"A1-1\r\nA1-2","B1-1\r\nB1-2"\r\n"A2-1,A2-2","B2-1,B2-2"');
console.log(array); // [["A1-1\r\nA1-2","B1-1\r\nB1-2"],["A2-1,A2-2","B2-1,B2-2"]]

/**
 * セル値をダブルクォートで括った場合、"" でダブルクォート1文字と見なす
 */
var array = CSV.parse('"A1""","B1"""\r\n"""A2""","""B2"""');
console.log(array);  // [["A1\"","B1\""],["\"A2\"","\"B2\""]]

CSV.stringify(array [, separator, replacer])

二次元配列をCSV文字列に変換します。

var csvString = CSV.stringify([['A1','B1'],['A2','B2']]);
console.log(csvString); // "A1","B1"\r\n"A2","B2"

第二引数 separator (オプション)

第二引数でCSVの区切り文字を指定できます。 第二引数が省略された場合、区切り文字はカンマ(')として扱われます。

var csvString = CSV.stringify([['A1','B1'],['A2','B2']], '|');
console.log(csvString); // "A1"|"B1"\r\n"A2"|"B2"

第三引数 replacer (オプション)

第三引数 replacer を指定すると、CSV文字列に変換する前に配列の要素値を引数に撮るコールバック関数 replacer が呼び出され、返り値をセル値とします。

var csvString = CSV.stringify([['A1','B1'],[1,2]], ',', function replacer (value) {
  return typeof value === 'number' ? value : '"' + String(value).replace(/"/g, '""') + '"';
});
console.log(csvString); // "A1","B1"\r\n1,2
/**
* csv.js
* CSV parser.
*
* @version 1.0.3
* @author think49
* @url https://gist.github.com/think49/009a8744c147a7013f14ea8913bd9027
* @license http://www.opensource.org/licenses/mit-license.php (The MIT License)
* @see <a href="http://www.ietf.org/rfc/rfc4180.txt">http://www.ietf.org/rfc/rfc4180.txt</a>
*/
'use strict';
var CSV = (function (String, RegExp) {
var CSV = {};
function generateSeparator (separator) {
separator = String(separator);
return separator.length > 0 ? separator.charAt(0) : ',';
}
function parse (csvString /* [, separator, reviver] */) {
var separator = generateSeparator(arguments.length > 1 ? arguments[1]: ','),
escapedSeparator = separator.replace(/(?=[\\\]])/, '\\'),
regExp = new RegExp('"[^"]*(?:""[^"]*)*"|\r\n|[\r\n' + escapedSeparator + ']|[^' + escapedSeparator + '\r\n]+', 'g'),
csvRows = [],
csvCells = [],
reviver = arguments[2],
result, token, length, complete;
csvString = String(csvString);
length = csvString.length;
if (typeof reviver !== 'function') {
while (result = regExp.exec(csvString)) {
token = result[0];
switch (token.charAt(0)) {
case '"':
csvCells.push(token.slice(1, token.length - 1).replace(/""/g, '"'));
break;
case separator:
break;
case '\r':
case '\n':
csvRows.push(csvCells);
csvCells = [];
break;
default:
csvCells.push(token);
break;
}
complete = regExp.lastIndex === length;
if (complete) {
break;
}
}
} else {
while (result = regExp.exec(csvString)) {
token = result[0];
switch (token.charAt(0)) {
case '"':
csvCells.push(reviver(token.slice(1, token.length - 1).replace(/""/g, '"')));
break;
case separator:
break;
case '\r':
case '\n':
csvRows.push(csvCells);
csvCells = [];
break;
default:
csvCells.push(reviver(token));
break;
}
complete = regExp.lastIndex === length;
if (complete) {
break;
}
}
}
if (!complete) {
throw new SyntaxError('not well-formed CSV');
}
if (csvCells.length > 0) {
csvRows.push(csvCells);
}
return csvRows;
}
function stringify (array2d /* [, separator, replacer] */) {
var separator = generateSeparator(arguments.length > 1 ? arguments[1]: ','),
csvRows = [],
replacer = arguments[2];
if (typeof replacer !== 'function') {
for (var i = 0, l = array2d.length, csvCells; i < l; ++i) {
csvCells = [];
for (var j = 0, array = array2d[i], m = array.length; j < m; ++j) {
csvCells.push('"' + String(array[j]).replace(/"/g, '""') + '"');
}
csvRows.push(csvCells.join(separator));
}
} else {
for (var i = 0, l = array2d.length, csvCells; i < l; ++i) {
csvCells = [];
for (var j = 0, array = array2d[i], m = array.length; j < m; ++j) {
csvCells.push(replacer(array[j]));
}
csvRows.push(csvCells.join(separator));
}
}
return csvRows.join('\r\n');
}
if (typeof Object.defineProperties === 'function') {
Object.defineProperties(CSV, {
parse: {
writable: true,
configurable: true,
value: parse
},
stringify: {
writable: true,
configurable: true,
value: stringify
}
});
} else {
CSV.parse = parse;
CSV.stringify = stringify;
}
if (typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol') {
CSV[Symbol.toStringTag] = 'CSV'; // Symbol.toStringTag === @@toStringTag (ES6)
}
return CSV;
}.call(this, String, RegExp));

更新履歴 (csv.js)

ver 1.0.3 (2016/05/11)

  • CSV.parse() の内部実装を String.prototype.replace から RegExp.prototype.exec に変更した

ver 1.0.2 (2016/05/11)

  • CSV.parse() で第三引数 reviver が指定された場合、配列化する前にセル値を引数にとるコールバック関数 reviver を呼び出すようにした
  • CSV.stringify() で第三引数 replacer が指定された場合、CSV文字列に変換する前に要素値を引数にとるコールバック関数 replacer を呼び出すようにした

ver 1.0.1 (2016/05/10)

  • CSV.stringify() で区切り文字を指定できるようにした
  • 区切り文字は始めの1文字のみとして扱うように動作変更(2文字以上を扱えるようにすると CSV.parse() のパフォーマンスが低下する為。要望が多ければ2文字以上の対応も検討します。)

ver 1.0.0 (2016/05/10)

  • 初版
function testCSV (csvString, separator, reviver, replacer) {
var array;
console.info('test CSV');
console.log(csvString);
switch (arguments.length) {
case 1:
array = CSV.parse(csvString);
console.log(JSON.stringify(array));
console.log(CSV.stringify(array));
break;
case 2:
array = CSV.parse(csvString, separator);
console.log(JSON.stringify(array));
console.log(CSV.stringify(array, separator));
break;
case 3:
array = CSV.parse(csvString, separator, reviver);
console.log(JSON.stringify(array));
console.log(CSV.stringify(array, separator));
break;
case 4:
array = CSV.parse(csvString, separator, reviver);
console.log(JSON.stringify(array));
console.log(CSV.stringify(array, separator, replacer));
break;
}
}
testCSV('A1|B1\r\n"A2"|"B2"', '|');
testCSV('A1\\B1\r\n"A2"\\"B2"', '\\');
testCSV('A1]B1\r\n"A2"]"B2"', ']');
testCSV('A1,B1\r\n"A2","B2"\r\n"A3-1\r\nA3-2","B3-1,B3-2"');
testCSV('A1<>B1\r\nA2<>B2', '<>');
testCSV('A1,B1\r\n1,2', ',', function reviver (value) {
var number = Number(value);
return number.toString() === value ? number : value;
},
function replacer (value) {
return typeof value === 'number' ? value : '"' + String(value).replace(/"/g, '""') + '"';
});
@hterunao
Copy link

hterunao commented Feb 10, 2021

利用させていただきました。ありがとうございます。
ダブルクォートで括られていない空文字列(行頭行末のカンマ、カンマの連続など)が、無視されてしまうようです。
(console.log(CSV.parse(',,a,'))は、[["","","a",""]]となってほしいが、[["a"]]となる)

やっつけで下記のように応急処置してみました。
L35-56

      var cellValue = '';
      while (result = regExp.exec(csvString)) {
        token = result[0];
        
        switch (token.charAt(0)) {
          case '"':
            cellValue += token.slice(1, token.length - 1).replace(/""/g, '"');
            break;
          case separator:
            csvCells.push(cellValue);
            cellValue = '';
            break;
          case '\r':
          case '\n':
            csvCells.push(cellValue);
            cellValue = '';
            csvRows.push(csvCells);
            csvCells = [];
            break;
          default:
            cellValue += token;
            break;
        }

        complete = regExp.lastIndex === length;

        if (complete) {
          break;
        }
      }
      if (cellValue !== '') {
        csvCells.push(cellValue);
      }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment