Ribbit's works

ダブルクォートの有無や文字コードを問わず使用できるCSV取込

#javascript
にメンテナンス済み
記事のトップ画像

いきなりですが、CSV 取込って難しくありませんか?

取り込む際は File オブジェクトを取り扱わないといけませんし、string として取得できた後も、データの中にカンマが含まれていたり、ダブルクォートで囲まれてたり囲まれていなかったり…

考慮することが多いです。

JavaScript を使った CSV 取込について検索して調べてみたんですが、古い情報が載っていたり、場合によっては正常に取り込めなかったり。

ということで今回は各文字コードに対応し、かつダブルクォートの有無にかかわらず 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 を使うしかありません。

FileReader であれば UTF-8 も処理できてしまうので Blob.text()を使う必要はなさそうですが、UTF-8 エンコードのファイルからデータを取得する際はこちらが推奨されているので、このようなコードになっています。

扱い方を統一させるため、戻り値はいずれのエンコードの場合もPromise<string | undefined>です。

データを配列に変換する

続いて取得した string 型の変数を二次元配列に変換します。

/**
 @param {File} file
 @param {string} encoding
 @return {string[][]}
*/
const getArrayFromCSV = async (file, encoding = 'Shift_JIS') => {
  const text = await getTextFromCSV(file, encoding);

  if (!text || typeof text !== 'string') {
    throw new Error('ファイルを配列に変換できませんでした');
  }

  const arr = text.split(/\r?\n/).map((row) => {
    const wrapsQuat = row.indexOf('"') === 0 && row.lastIndexOf('"') === row.length - 1;

    if (wrapsQuat) {
      return row.substring(1, row.length - 1).split('","');
    }
    return row.split(',');
  });

  return arr;
};

CSV ファイルの改行コードも CRLF と LF のパターンがあるので、正規表現を使って

str.split(/\r?\n/);

のように設定しています。

時々、特定の行はダブルクォーテーションで覆い、そのほかの行はカンマのみ。といった曲者もいるので、ダブルクォーテーションの判定は行ごとにおこなっています。

row.indexOf('"') === 0 && row.lastIndexOf('"') === row.length - 1;

ダブルクォーテーションで覆われている場合は、まず先頭と末尾を削除し, データ間のダブルクォーテーションはカンマと合わせて削除するようにしています。