🤡

軽量&高速なCSV処理!csv-parserのNode.js/Next.jsでの活用法まとめ

に公開

csv-parserとは

Node.jsでCSVファイルを読み込むときに使用するNode.jsライブラリです。
軽量でストリームベースで動作するのでファイルの容量が大きいものでもメモリ効率良く処理できます。
通常、Node.jsでCSVを処理する際はJSONに変換する処理が必要になりますが、その処理速度は1秒当たり約9万行処理されます。(1行当たりのデータ量にもよる)
https://github.com/mafintosh/csv-parser

実装アプローチ

CSVファイルを処理する実装方法には、主に以下の2つのアプローチがあります。

ファイルシステムからの直接読み込み

import fs from "fs";
import csv from "csv-parser";

const parseCsv = async (filePath) => {
  const results = [];

  try {
    await new Promise((resolve, reject) => {
      fs.createReadStream(filePath)
        .pipe(csv())
        .on("data", (data) => results.push(data))
        .on("end", () => resolve())
        .on("error", (error) => reject(error));
    });

    console.log("results:", results);
  } catch (error) {
    console.error("CSV parsing error:", error);
  }
};

parseCsv("./data.csv");

このアプローチは以下の場合に適しています:

  • ローカルファイルシステム上のCSVファイルを処理する場合
  • バッチ処理やCLIツールとして実装する場合
  • 大容量のCSVファイルを処理する場合

導入以降での解説ではこちらを使用して解説しております。

バッファからの読み込み(ブラウザからのアップロードなど)

import { Readable } from 'stream';
import csv from 'csv-parser';

const parseCsv = async (buffer) => {
  const results = [];

  try {
    await new Promise((resolve, reject) => {
      Readable.from(buffer)
        .pipe(csv())
        .on('data', (data) => results.push(data))
        .on('end', () => resolve())
        .on('error', (error) => reject(error));
    });

    return results;
  } catch (error) {
    console.error('CSV parsing error:', error);
  }
};

const buffer = Buffer.from(csvString); // ブラウザからアップロードされてきた想定
parseCsv(buffer);

このアプローチは以下の場合に適しています:

  • ブラウザからアップロードされたCSVファイルを処理する場合
  • HTTPリクエストで受け取ったファイルを処理する場合
  • メモリ上のバッファデータを処理する場合

導入

前提環境

  • Node.js v18以上(ESModules対応)

公式ドキュメント(Github)ではCommonJSの記法で説明されていますが、今回はNext.js、React等のESModulesのプロジェクトでの使用を想定しているためESModulesでの記述になっています。

インストール

npm
npm install csv-parser
yarn
yarn add csv-parser
pnpm
pnpm install csv-parser

ESModules設定

以下をpackage.jsonに追加

package.json
{
  "type": "module"
}

使用方法

以下のデータを含むCSVファイルを処理していきます。

data.csv
name,age,gender
田中,23,男性
佐藤,30,女性
加藤,19,女性

以下コードはファイルシステムからの直接CSVを読み込み、配列に変換しconsole.logとして出力する処理です。

index.js
import fs from "fs";
import csv from "csv-parser";

const parseCsv = async (filePath) => {
  const results = [];

  try {
    await new Promise((resolve, reject) => {
      fs.createReadStream(filePath)
        .pipe(csv())
        .on("data", (data) => results.push(data))
        .on("end", () => resolve())
        .on("error", (error) => reject(error));
    });

    console.log("results:", results);
  } catch (error) {
    console.error("CSV parsing error:", error);
  }
};

parseCsv("./data.csv");

以下コマンドでファイルの実行するとCSVファイルが配列として変換されていることが確認できます。

node index.js
ログ
results: [
  { name: '田中', age: '23', gender: '男性' },
  { name: '佐藤', age: '30', gender: '女性' },
  { name: '加藤', age: '19', gender: '女性' }
]

csv-parserの処理内容の解説

上記のサンプルコードで以下の記述がcsv-parserのメイン処理になります。
一行ずつ解説していきます。

fs.createReadStream(filePath)
  .pipe(csv())
  .on("data", (data) => results.push(data))
  .on("end", () => {})

fs.createReadStream(filePath)

指定したファイルをストリーム(少しずつ処理)として読み込む処理です。一度に全体を読み込むのではなく、小さな塊ごとに読み込むため、ファイルサイズが大きくてもメモリ負荷が少ないです。

.pipe(csv())

.pipe()とはストリームを次の処理に渡す関数です。
ここではcsv()にデータを渡してCSVファイルの各行をオブジェクトに変換します。
更にcsv()ではオプションを付け処理をすることができます。(オプションはそれぞれ併用することができます)

csv()のよく使用するオプション
  1. separator
    CSVファイルはデフォルトでは,(カンマ)ですが、カンマ以外の区切りが使われている場合に指定できます。
.pipe(csv({ separator: '\t' })) // タブ区切り



2. headers
CSVのヘッダー(カラム名)をどう扱うかを指定できます。
以下のようにheader: falseを指定するとヘッダーの読み取りを無効化し、各行はインデックス(0, 1, 2, ...)をキーとして扱います。

.pipe(csv({ headers: false }))
ログ
{ '0': '田中', '1': '23', '2': '男性' }
{ '0': '佐藤', '1': '30', '2': '女性' }
{ '0': '加藤', '1': '19', '2': '女性' }

また、以下のようにヘッダーを明示的に指定することができます。

.pipe(csv({ headers: ['名前', '年齢', '性別'] }))
ログ
{ '名前': '田中', '年齢': '23', '性別': '男性' }
{ '名前': '佐藤', '年齢': '30', '性別': '女性' }
{ '名前': '加藤', '年齢': '19', '性別': '女性' }



3. skipLines
先頭から特定の行をスキップすることができます。

.pipe(csv({ skipLines: 1 })) // 先頭1行をスキップ
ログ
// 先頭行(ヘッダー)である"name,age,gender"がスキップされ、"田中,23,男性"が先頭行(ヘッダー)として処理されている
{ '田中': '佐藤', '23': '30', '男性': '女性' }
{ '田中': '加藤', '23': '19', '男性': '女性' }



4. mapHeaders
ヘッダーを変換する関数を指定できます。(大文字変換や小文字変換、英語変換など)

.pipe(csv({
  mapHeaders: ({ header }) => header.toUpperCase() // 大文字変換
}))
ログ
{ NAME: '田中', AGE: '23', GENDER: '男性' }
{ NAME: '佐藤', AGE: '30', GENDER: '女性' }
{ NAME: '加藤', AGE: '19', GENDER: '女性' }



5. mapValues
各セルの値を変換することができます。(数値に変換や空白除去など)

// ageカラムの値を全て数値に変換
.pipe(csv({
  mapValues: ({ header, index, value }) => {
    if (header === "age") return Number(value);
    return value;
  },
}))
ログ
{ name: '田中', age: 23, gender: '男性' }
{ name: '佐藤', age: 30, gender: '女性' }
{ name: '加藤', age: 19, gender: '女性' }

.on("第1引数", 第2引数)

.on()ではイベントリスナーを登録します。
第1引数にイベント名(ここでは"data")、第2引数にコールバック関数を指定します。
ここで言うdataとはCSVの1行をパースして、JavaScriptのオブジェクト化したものになります。
CSVの全ての行のパースが完了するまで繰り返し処理されます。

.on("data", (data) => {
  console.log("1行ずつ出力される:", data);
});
ログ
1行ずつ出力される: { name: '田中', age: '23', gender: '男性' }
1行ずつ出力される: { name: '佐藤', age: '30', gender: '女性' }
1行ずつ出力される: { name: '加藤', age: '19', gender: '女性' }

処理したオブジェクトに関しては以下のようにresults.push(data)をすることで配列に処理内容を保持することもできます。

const results = [];

fs.createReadStream(filePath)
  .pipe(csv())
  .on("data", (data) => results.push(data))
  .on("end", () => {})

console.log("results:", results);
ログ
results: [
  { name: '田中', age: '23', gender: '男性' },
  { name: '佐藤', age: '30', gender: '女性' },
  { name: '加藤', age: '19', gender: '女性' }
]

また、data内の値を動的に変更したい場合や特定の行のみを配列に格納することもできます。

const results = [];

fs.createReadStream(filePath)
  .pipe(csv())
  .on("data", (data) => {
    if (data.gender === "女性") { // フィルタリング
      data.name += "さん"; // 値の変更
      results.push(data);
    }
  })
  .on("end", () => {})

console.log("results:", results);
ログ
results: [
  { name: '佐藤さん', age: '30', gender: '女性' },
  { name: '加藤さん', age: '19', gender: '女性' }
]

.on("end", () => {})

CSVファイルの全ての行を処理し終えたときに発火します。
非同期処理で記述する場合はここでresolve()を呼び出すことでPromiseを完了させ、非同期処理が成功終了します。
完了ログを出したい場合などでも使用できます。

const parseCsv = async (filePath) => {
  const results = [];

  try {
    await new Promise((resolve, reject) => {
      fs.createReadStream(filePath)
        .pipe(csv())
        .on("data", (data) => results.push(data))
        .on("end", () => {
          console.log("処理が完了しました"); // 完了ログ
          resolve();
        })
        .on("error", (error) => reject(error));
    });
  } catch (error) {
    console.error("CSV parsing error:", error);
  }
};
ログ
処理が完了しました

実務で使えるヤツ(随時追加するかも)

ヘッダーの余白の除去

ExcelやGoogleスプレッドシートなどからエクスポートされたCSVファイルでは、ヘッダー行に余計な空白が含まれていることがあります。そのままでは正しくキーとして認識されない場合があり、エラーの原因になるため、余白を取り除く処理を行います。

data.csv(ヘッダーに余白入り)
 name , age , gender
田中,23,男性
佐藤,30,女性
加藤,19,女性
index.js
const parseCsv = async (filePath) => {
  const results = [];

  try {
    await new Promise((resolve, reject) => {
      fs.createReadStream(filePath)
        .pipe(
          csv({
            mapHeaders: ({ header }) => header.trim(), // ヘッダーの余白を除去
          })
        )
        .on("data", (data) => results.push(data))
        .on("end", () => resolve())
        .on("error", (error) => reject(error));
    });

    console.log("results:", results);
  } catch (error) {
    console.error("CSV parsing error:", error);
  }
};
ログ
results: [
  { name: '田中', age: '23', gender: '男性' },
  { name: '佐藤', age: '30', gender: '女性' },
  { name: '加藤', age: '19', gender: '女性' }
]

Next.jsでの実装

先に説明した2つのアプローチのうち、以下の実装ではブラウザからのアップロードファイル処理を使用します。

今回は、ブラウザ上からCSVファイルをアップロードし、表として描画するアプリケーションを実装します。このユースケースではAPIルートでの実装が最適です。

主な機能は以下の通りです:

  1. フロントエンドでのファイルアップロード機能
  2. APIルートでのCSVパース処理(Readable.from(buffer)を使用)
  3. パースしたデータの表形式での表示

APIルートを選択した主な理由:

  • クライアントサイドからのファイルアップロード処理に最適
  • サーバーレス環境での動作が可能
  • ストリーム処理による効率的なメモリ使用
  • Next.jsの機能を最大限に活用できる

実装の詳細はGithubリポジトリをご参照ください。
https://github.com/seino914/csv-parser_demo

また、こちらはデモアプリになります。
https://csv-parser-demosite.netlify.app/

オワリに

本記事では、Node.js環境でCSVファイルを効率的に扱うためのライブラリとしてcsv-parserを紹介しました。CSVの取り扱いには他にもfast-csvやPapaParse(ブラウザ向け)、csvtojsonなど多くのライブラリが存在します。それぞれ特徴があり、例えばfast-csvは高速処理に特化しており、csvtojsonは変換機能が充実しています。

その中でもcsv-parserは、ストリームベースで非常に軽量かつシンプルに使えるのが最大の魅力です。数万〜数十万行といった大きなCSVファイルでもメモリ消費を抑えつつ処理できるため、パフォーマンスと実装のバランスが取れており、サーバーサイド処理において非常に実用的です。

また、pipe()と組み合わせたシンプルなAPI構造により、Node.jsのストリーム処理に慣れているエンジニアであればすぐに導入・活用できるのも利点のひとつです。

用途に応じて他のライブラリを検討するのも良い選択ですが、「軽量で速く、扱いやすい」という点でcsv-parserは現在もなお、多くのプロジェクトで信頼されている選択肢の一つです。

アリガトウ☆ゴザイマシタ(人''▽`)☆彡

https://x.com/tono__marvel

Discussion