Node.jsは、サーバーサイドでのデータ処理や自動化ツールの開発において、非常に強力なプラットフォームとして定着しています。

データ分析やシステム間のデータ連携において、CSV(Comma Separated Values)形式のファイルを取り扱う機会は非常に多いものです。

シンプルなテキスト形式であるCSVは、Excelやデータベースとの親和性が高く、軽量であるという利点があります。

しかし、Node.jsでCSVを効率的に読み込むためには、ファイルのサイズや実行環境に応じた最適な手法を選択する必要があります。

本記事では、標準のfsモジュールを用いた基本的な読み込みから、大量のデータを高速に処理するためのストリーム活用術まで、実用的な実装方法を詳しく紹介します。

Node.jsにおけるCSV読み込みの基本アプローチ

Node.jsでCSVファイルを読み込む際、まずは標準モジュールである「fs(File System)」を利用する方法が基本となります。

小規模なファイルであれば、ファイル全体を一括でメモリに読み込む手法が最もシンプルで実装も容易です。

Node.jsのバージョンが進むにつれ、非同期処理の記述にはPromiseベースのAPIを利用することが推奨されるようになっています。

まずは、fs.promisesを使用してCSVファイルを文字列として読み込む基本的なコードを確認しましょう。

JavaScript
const fs = require('node:fs/promises');

async function readSimpleCSV(filePath) {
    try {
        // ファイルの内容をUTF-8文字列として一括読み込み
        const data = await fs.readFile(filePath, 'utf-8');
        
        // 改行コードで分割し、各行を配列に格納
        const lines = data.split('\n');
        
        // 各行をカンマで分割して二次元配列を作成
        const result = lines.map(line => line.split(','));
        
        console.log('読み込み完了:', result);
    } catch (error) {
        console.error('読み込み失敗:', error);
    }
}

readSimpleCSV('data.csv');
実行結果
読み込み完了: [
  ['id', 'name', 'email'],
  ['1', '田中太郎', 'tanaka@example.com'],
  ['2', '佐藤次郎', 'sato@example.com']
]

この手法は非常に直感的ですが、数GBを超えるような巨大なCSVファイルには適していません。

ファイル全体をメモリ上に展開するため、メモリ不足(Heap out of memory)によるプロセス終了のリスクがあるからです。

そのため、実務レベルの開発では、次に紹介する「ストリーム処理」の活用が不可欠となります。

ストリーム処理による高速かつ低メモリな読み込み

Node.jsの最大の強みの一つは、データを細切れにして順次処理する「ストリーム(Stream)」という仕組みです。

ストリームを利用すると、ファイルを少しずつ読み込みながら処理を進めるため、メモリ使用量を一定に保つことができます。

fs.createReadStreamの活用

標準のfs.createReadStreamを使用することで、読み込み専用のストリームを作成できます。

これにイベントリスナーを設定することで、データが届くたびに処理を実行できます。

JavaScript
const fs = require('node:fs');

function streamCSV(filePath) {
    const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });

    stream.on('data', (chunk) => {
        // 読み込まれた一部のデータ(チャンク)がここに届く
        console.log('データの一部を受信:', chunk.length);
    });

    stream.on('end', () => {
        console.log('すべての読み込みが完了しました。');
    });

    stream.on('error', (err) => {
        console.error('エラー発生:', err);
    });
}

streamCSV('large_data.csv');

ただし、標準のストリームだけでは「カンマ区切り」や「ダブルクォート囲み」といったCSV特有の構文を正しく解析(パース)するのは困難です。

複雑なCSVのパースを自前で実装すると、エスケープ処理などでバグが発生しやすくなります。

そこで、信頼性の高いライブラリを組み合わせて利用するのが一般的です。

定番ライブラリ「csv-parse」を使用した実装

Node.jsのエコシステムにおいて、CSV処理のデファクトスタンダードとなっているのがcsv-parseです。

このライブラリは、Node.jsのストリームAPIと完全に互換性があり、非常に高度な設定が可能です。

インストール方法

npmを使用して、プロジェクトにインストールします。

Shell
npm install csv-parse

csv-parseによるパースの実装例

ストリームとパイプライン(pipe)を使用して、効率的にデータをオブジェクト形式に変換します。

JavaScript
const fs = require('node:fs');
const { parse } = require('csv-parse');

const processCSV = () => {
    const parser = parse({
        columns: true, // 1行目をヘッダーとして扱い、オブジェクトを生成
        skip_empty_lines: true // 空行を無視
    });

    fs.createReadStream('users.csv')
        .pipe(parser)
        .on('data', (row) => {
            // 各行がオブジェクトとして渡される
            console.log('ユーザー情報:', row.name);
        })
        .on('end', () => {
            console.log('パース処理が終了しました。');
        })
        .on('error', (err) => {
            console.error('パースエラー:', err.message);
        });
};

processCSV();

この方法であれば、数百万行のデータであってもメモリを圧迫せずに処理を完結させることができます。

また、columns: trueを指定することで、配列のインデックスではなくrow.emailのようにプロパティ名でデータにアクセスできるため、コードの可読性が大幅に向上します。

最新の非同期イテレータによる読み込み

現代的なNode.js開発では、for await...of構文を用いた非同期イテレータが非常に人気です。

イベントリスナー(on dataなど)を使用するよりも、同期処理のような見た目で非同期ストリームを扱えるため、コードがスッキリとします。

JavaScript
const fs = require('node:fs');
const { parse } = require('csv-parse');

async function readCSVWithAsyncIterator() {
    const parser = fs.createReadStream('data.csv').pipe(parse({
        columns: true
    }));

    for await (const record of parser) {
        // 非同期に1行ずつ処理
        console.log('レコード:', record);
    }
    
    console.log('全データの処理が完了しました。');
}

readCSVWithAsyncIterator();

非同期イテレータを使用すると、try-catch文によるエラーハンドリングが容易になるという大きなメリットがあります。

また、ループ内での非同期処理(DB保存など)の順序制御も容易に行えます。

実務で直面する課題と解決策

CSVの読み込みは、単にファイルを読み込むだけでは終わらないことが多いです。

ここでは、日本の開発現場で特によく遭遇する2つの課題とその対策について解説します。

1. 文字コード(Shift-JIS)への対応

Excelで出力されたCSVファイルは、文字コードがUTF-8ではなくShift-JISであることが多々あります。

Node.jsの標準fsモジュールはデフォルトでUTF-8を想定しているため、そのまま読み込むと文字化けが発生します。

このような場合は、iconv-liteライブラリを併用してエンコーディングを変換します。

JavaScript
const fs = require('node:fs');
const { parse } = require('csv-parse');
const iconv = require('iconv-lite');

fs.createReadStream('sjis_data.csv')
    .pipe(iconv.decodeStream('Shift_JIS')) // Shift-JISからUTF-8へ変換
    .pipe(parse())
    .on('data', (row) => {
        console.log(row);
    });

2. 大量データ処理時のバックプレッシャー

読み込み速度が速すぎ、書き込み処理(データベースへのインサートなど)が追いつかない現象をバックプレッシャーと呼びます。

Node.jsのストリームにはこの負荷を調整する機能が備わっていますが、pipeline関数を使用することでより安全に管理できます。

機能メリット
fs.readFile実装が最も簡単。小さな設定ファイル等に最適。
Stream (pipe)メモリ効率が極めて高い。大規模データ向け。
Async Iterator可読性が高く、モダンなJavaScriptの書き方。

まとめ

Node.jsでCSVファイルを読み込む方法は、目的やデータの規模に応じて多様な選択肢が存在します。

設定ファイルや少量のマスタデータを読み込むだけであれば、fs.promises.readFileによる一括読み込みが手軽で便利です。

一方で、業務システムやデータ移行などで扱う大量のCSVに対しては、ストリーム処理とライブラリを組み合わせた実装が必須となります。

特にcsv-parseのような成熟したライブラリを活用することで、複雑なエスケープルールやヘッダー処理を安全に行うことができます。

また、文字コードの問題やバックプレッシャーへの対策を理解しておくことで、本番環境でも安定して動作するアプリケーションを構築できるでしょう。

今回紹介した手法の中から、開発中のプロジェクトに最も適したアプローチを選択し、効率的なデータ処理を実現してください。