JavaScriptでCSVを取り込む:文字コード(Shift_JIS/UTF-8)とダブルクォートに対応
いきなりですが、CSV 取込って難しくありませんか?
取り込む際は File オブジェクトを取り扱わないといけませんし、string として取得できた後も、データの中にカンマが含まれていたり、ダブルクォートで囲まれてたり囲まれていなかったり…
考慮することが多いです。
そこでこの記事では、文字コード(Shift_JIS / UTF-8 など)の違いと、ダブルクォートの有無や中身(カンマ・改行・エスケープ)に対応して CSV を二次元配列に変換するコードを紹介します。 取り込む CSV データは、
<input type="file" />
上記のような入力から取得できる File オブジェクトを使うことを前提としています。
CSV ファイルからテキストデータを取得する
CSV ファイルから String 型のデータを取得する部分です。
まずコード全文をご覧ください。
/**
@param {File} file
@param {string} encoding
@return {Promise<string | undefined>}
*/
const getTextFromCSV = (file, encoding = 'Shift_JIS') => {
if (encoding === 'UTF-8') {
return file.text();
}
return new Promise((resolve, reject) => {
try {
const reader = new FileReader();
reader.readAsText(file, encoding);
reader.onload = (event) => resolve(event.target?.result);
reader.onerror = (event) => reject(event);
} catch (error) {
reject(error);
}
});
};
CSV ファイルの文字コードが UTF-8 であれば、Blob.text()を使用できます。 File は Blob のラッパークラスなので、そのまま File.text()として使用できます。
文字コードが UTF-8 以外の場合は、イベントベースの FileReader を使うしかありません。
なお Blob.text() は常に UTF-8 として読み込むため、Shift_JIS などの CSV には使えません。UTF-8 の場合に限り、Promise で簡潔に書ける Blob.text() を使っています。
呼び出し側で扱いを統一できるよう、戻り値はいずれの場合も Promise です。
指定できる encoding は、Shift_JIS や UTF-8 などの Encoding Standard のラベルです。日本の業務システムが出力する CSV は Shift_JIS(Windows では CP932)が多いため、既定値を Shift_JIS にしています。
UTF-8 の CSV は先頭に BOM(\uFEFF)が付くことがあります。FileReader は BOM を取り除きますが、念のため次のパース処理でも除去しています。
データを配列に変換する
取得した文字列を二次元配列に変換します。行ごとに split する方法は手軽ですが、フィールド内にカンマや改行、""(エスケープされたダブルクォート)が含まれると壊れてしまいます。そこで 1 文字ずつ読む簡易パーサーにしています(RFC 4180 の主要な仕様に準拠)。
/**
@param {string} text
@return {string[][]}
*/
const parseCSV = (text) => {
const rows = [];
let row = [];
let field = '';
let inQuotes = false;
// BOM を除去
const src = text.replace(/^\uFEFF/, '');
for (let i = 0; i < src.length; i++) {
const char = src[i];
if (inQuotes) {
if (char === '"') {
if (src[i + 1] === '"') {
field += '"'; // エスケープされたダブルクォート
i++;
} else {
inQuotes = false;
}
} else {
field += char; // 引用符内のカンマ・改行もそのまま保持
}
continue;
}
if (char === '"') {
inQuotes = true;
} else if (char === ',') {
row.push(field);
field = '';
} else if (char === '\n' || char === '\r') {
if (char === '\r' && src[i + 1] === '\n') i++; // CRLF
row.push(field);
rows.push(row);
row = [];
field = '';
} else {
field += char;
}
}
// 末尾に改行がないファイルの最終行
if (field !== '' || row.length > 0) {
row.push(field);
rows.push(row);
}
return rows;
};
/**
@param {File} file
@param {string} encoding
@return {Promise<string[][]>}
*/
const getArrayFromCSV = async (file, encoding = 'Shift_JIS') => {
const text = await getTextFromCSV(file, encoding);
if (typeof text !== 'string') {
throw new Error('ファイルを配列に変換できませんでした');
}
return parseCSV(text);
};
ポイントは次のとおりです。
- 改行コード:CRLF・LF・CR のいずれも行の区切りとして扱います。
- ダブルクォート:フィールドごとに判定するため、特定の行や列だけ囲まれている CSV でも読み込めます。囲まれたフィールド内のカンマ・改行は値の一部になります。
- エスケープ:
""は"1 文字として復元します。
使い方
input の change イベントで File を受け取り、配列に変換します。
document.querySelector('input[type="file"]').addEventListener('change', async (event) => {
const [file] = event.target.files;
if (!file) return;
const data = await getArrayFromCSV(file, 'Shift_JIS');
console.log(data); // [['id', 'name'], ['1', 'Taro, Yamada'], ...]
});
読み込んだ値はすべて文字列です。数値や日付として扱う場合は、取り込み後に変換してください。
大容量 CSV や複雑な要件の場合
数十 MB を超える CSV や、区切り文字の自動判定、ヘッダー行をキーにしたオブジェクト化などが必要な場合は、Papa Parse などの実績あるライブラリの利用をおすすめします。Papa Parse でも、Shift_JIS の CSV は FileReader と同様に encoding を指定して読み込む必要があります。