T-CREATOR

Node.js で CSV・JSON ファイルを自在に操作する

Node.js で CSV・JSON ファイルを自在に操作する

現代のデータ駆動型アプリケーション開発において、CSV・JSON ファイルの操作は避けて通れない重要なスキルです。Web 開発者として成長していく中で、「データを自在に操れるようになりたい」という思いを抱いたことはありませんか?

本記事では、Node.js を使って CSV・JSON ファイルを効率的に操作する方法を、基礎から応用まで段階的に学んでいきます。実際のエラーコードやトラブルシューティングも含めて、実践的な内容をお届けします。

背景

なぜ CSV・JSON ファイル操作が重要なのか

現代の Web アプリケーション開発では、以下のような場面で頻繁にファイル操作が必要になります。

#場面必要な操作
1データ移行CSV ファイルからデータベースへの一括インポート
2API 連携JSON レスポンスの加工・変換
3レポート生成データベースの内容を CSV で出力
4設定管理環境設定を JSON ファイルで管理

これらの操作を効率的に行えるようになることで、開発生産性が大幅に向上します。特に、手作業では時間がかかる大量データの処理を自動化できるようになると、あなたの開発者としての価値が格段に上がるでしょう。

データ処理における現代的課題

現代の Web アプリケーションでは、以下のような課題に直面することが多くなっています。

  • データ量の増大: 数万件から数十万件のデータを処理する必要がある
  • リアルタイム性: ユーザーの操作に対してすぐに結果を返したい
  • データ品質: 不正なデータや欠損値への対応が必要
  • パフォーマンス: メモリ効率と CPU 効率の両立が求められる

これらの課題を解決するためには、適切なツールと技術の選択が不可欠です。

課題

従来のファイル操作で直面する問題

多くの開発者が経験する、ファイル操作における典型的な問題を見てみましょう。

メモリ不足エラー

大きなファイルを一度に読み込もうとすると、以下のようなエラーが発生します。

javascript// 問題のあるコード例
const fs = require('fs');

try {
  const data = fs.readFileSync('large-file.csv', 'utf8');
  console.log(data);
} catch (error) {
  console.error('Error:', error.message);
}

実際によく発生するエラーメッセージ:

arduinoError: ENOMEM: not enough memory, read

このエラーは、ファイルサイズが Node.js のメモリ制限(通常約 1.4GB)を超えた場合に発生します。

文字化けとエンコーディング問題

日本語を含む CSV ファイルを扱う際によく発生する問題です。

javascript// 文字化けが発生するコード
const fs = require('fs');

const data = fs.readFileSync('japanese-data.csv', 'utf8');
console.log(data); // 文字化けが発生

実際のエラーメッセージ:

javascriptError: Invalid character in header

非同期処理の複雑化

複数のファイルを同時に処理する際に発生する問題です。

javascript// コールバック地獄の例
const fs = require('fs');

fs.readFile('file1.json', 'utf8', (err1, data1) => {
  if (err1) throw err1;

  fs.readFile('file2.json', 'utf8', (err2, data2) => {
    if (err2) throw err2;

    // さらに処理が続く...
  });
});

このような問題を解決するためには、適切なアプローチが必要です。

解決策

Node.js での効率的なアプローチ

上記の課題を解決するために、Node.js が提供する以下の機能を活用します。

1. ストリーミング処理によるメモリ効率化

メモリ不足を回避するために、ストリーミング処理を使用します。

javascriptconst fs = require('fs');
const { Transform } = require('stream');

// ストリーミング処理のベースコード
const processStream = fs
  .createReadStream('large-file.csv')
  .pipe(
    new Transform({
      transform(chunk, encoding, callback) {
        // チャンクごとの処理
        this.push(chunk);
        callback();
      },
    })
  )
  .pipe(fs.createWriteStream('output.csv'));

2. 適切なエンコーディング指定

文字化けを防ぐために、適切なエンコーディングを指定します。

javascriptconst fs = require('fs');
const iconv = require('iconv-lite');

// Shift_JISファイルの正しい読み込み
const buffer = fs.readFileSync('japanese-data.csv');
const content = iconv.decode(buffer, 'Shift_JIS');
console.log(content); // 正しく表示される

3. async/await による非同期処理の簡素化

コールバック地獄を避けるために、async/await を使用します。

javascriptconst fs = require('fs').promises;

async function processFiles() {
  try {
    const data1 = await fs.readFile('file1.json', 'utf8');
    const data2 = await fs.readFile('file2.json', 'utf8');

    // 処理を続ける
    return [JSON.parse(data1), JSON.parse(data2)];
  } catch (error) {
    console.error('Error:', error.message);
    throw error;
  }
}

これらの基本的なアプローチを踏まえて、具体的な操作方法を見ていきましょう。

CSV ファイルの基本操作

必要なパッケージのインストール

まず、CSV 操作に必要なパッケージをインストールします。

bashyarn add csv-parser csv-writer
yarn add -D @types/csv-parser @types/csv-writer

CSV ファイルの読み込み

CSV ファイルを読み込む基本的な方法から始めましょう。

javascriptconst fs = require('fs');
const csv = require('csv-parser');

// CSVファイルの基本的な読み込み
function readCSV(filePath) {
  return new Promise((resolve, reject) => {
    const results = [];

    fs.createReadStream(filePath)
      .pipe(csv())
      .on('data', (data) => results.push(data))
      .on('end', () => resolve(results))
      .on('error', reject);
  });
}

// 使用例
async function main() {
  try {
    const data = await readCSV('sample.csv');
    console.log('読み込み完了:', data.length, '件');
  } catch (error) {
    console.error('読み込みエラー:', error.message);
  }
}

この基本的な読み込み方法で、小さなファイルであれば十分に処理できます。

CSV ファイルの書き込み

次に、データを CSV ファイルに書き込む方法を見てみましょう。

javascriptconst createCsvWriter =
  require('csv-writer').createObjectCsvWriter;

// CSVファイルの書き込み設定
const csvWriter = createCsvWriter({
  path: 'output.csv',
  header: [
    { id: 'name', title: '名前' },
    { id: 'age', title: '年齢' },
    { id: 'email', title: 'メールアドレス' },
  ],
  encoding: 'utf8',
});

// データの準備と書き込み
const records = [
  {
    name: '田中太郎',
    age: 30,
    email: 'tanaka@example.com',
  },
  { name: '佐藤花子', age: 25, email: 'sato@example.com' },
  {
    name: '鈴木一郎',
    age: 35,
    email: 'suzuki@example.com',
  },
];

async function writeCSV() {
  try {
    await csvWriter.writeRecords(records);
    console.log('CSVファイルの書き込み完了');
  } catch (error) {
    console.error('書き込みエラー:', error.message);
  }
}

この方法により、JavaScript オブジェクトから簡単に CSV ファイルを生成できます。

CSV ファイルの変換とフィルタリング

実際の業務では、CSV ファイルの内容を変換したり、条件に応じてフィルタリングしたりすることが多くあります。

javascriptconst fs = require('fs');
const csv = require('csv-parser');
const createCsvWriter =
  require('csv-writer').createObjectCsvWriter;

// CSVデータの変換とフィルタリング
async function transformCSV(inputPath, outputPath) {
  const results = [];

  return new Promise((resolve, reject) => {
    fs.createReadStream(inputPath)
      .pipe(csv())
      .on('data', (data) => {
        // 年齢が30歳以上のデータのみ抽出
        if (parseInt(data.age) >= 30) {
          // データを変換
          const transformed = {
            fullName: data.name,
            ageGroup:
              parseInt(data.age) >= 40
                ? 'シニア'
                : 'ミドル',
            contact: data.email,
          };
          results.push(transformed);
        }
      })
      .on('end', async () => {
        try {
          // 変換されたデータを書き込み
          const writer = createCsvWriter({
            path: outputPath,
            header: [
              { id: 'fullName', title: '氏名' },
              { id: 'ageGroup', title: '年齢層' },
              { id: 'contact', title: '連絡先' },
            ],
          });

          await writer.writeRecords(results);
          resolve(results);
        } catch (error) {
          reject(error);
        }
      })
      .on('error', reject);
  });
}

この例では、元の CSV ファイルから条件に合うデータを抽出し、新しい形式に変換して保存しています。

JSON ファイルの基本操作

JSON ファイルの読み込みと書き込み

JSON ファイルの操作は、Node.js の標準機能で基本的な処理が可能です。

javascriptconst fs = require('fs').promises;

// JSONファイルの読み込み
async function readJSON(filePath) {
  try {
    const data = await fs.readFile(filePath, 'utf8');
    return JSON.parse(data);
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.error(
        `ファイルが見つかりません: ${filePath}`
      );
    } else if (error instanceof SyntaxError) {
      console.error(`無効なJSON形式: ${error.message}`);
    } else {
      console.error(`読み込みエラー: ${error.message}`);
    }
    throw error;
  }
}

// JSONファイルの書き込み
async function writeJSON(filePath, data) {
  try {
    const jsonString = JSON.stringify(data, null, 2);
    await fs.writeFile(filePath, jsonString, 'utf8');
    console.log(`JSONファイルを保存しました: ${filePath}`);
  } catch (error) {
    console.error(`書き込みエラー: ${error.message}`);
    throw error;
  }
}

JSON データの操作と変換

実際の開発では、JSON データの構造を変換したり、特定の条件でフィルタリングしたりすることが多くあります。

javascript// 複雑なJSONデータの変換例
async function transformUserData(inputPath, outputPath) {
  try {
    const userData = await readJSON(inputPath);

    // データの変換と集計
    const transformedData = {
      summary: {
        totalUsers: userData.users.length,
        activeUsers: userData.users.filter(
          (user) => user.isActive
        ).length,
        averageAge:
          userData.users.reduce(
            (sum, user) => sum + user.age,
            0
          ) / userData.users.length,
      },
      usersByDepartment: {},
      recentActivity: [],
    };

    // 部門別ユーザー数の集計
    userData.users.forEach((user) => {
      const dept = user.department;
      if (!transformedData.usersByDepartment[dept]) {
        transformedData.usersByDepartment[dept] = 0;
      }
      transformedData.usersByDepartment[dept]++;
    });

    // 最近のアクティビティを抽出
    transformedData.recentActivity = userData.users
      .filter((user) => user.lastLogin)
      .sort(
        (a, b) =>
          new Date(b.lastLogin) - new Date(a.lastLogin)
      )
      .slice(0, 10)
      .map((user) => ({
        name: user.name,
        lastLogin: user.lastLogin,
        department: user.department,
      }));

    await writeJSON(outputPath, transformedData);
    return transformedData;
  } catch (error) {
    console.error('データ変換エラー:', error.message);
    throw error;
  }
}

この例では、ユーザーデータから統計情報を生成し、新しい JSON 形式で保存しています。

データ変換とフォーマット変更

CSV ⇔ JSON 変換

実際の開発では、CSV と JSON の相互変換が頻繁に必要になります。

javascriptconst fs = require('fs');
const csv = require('csv-parser');
const createCsvWriter =
  require('csv-writer').createObjectCsvWriter;

// CSVからJSONへの変換
async function csvToJson(csvPath, jsonPath) {
  const results = [];

  return new Promise((resolve, reject) => {
    fs.createReadStream(csvPath)
      .pipe(csv())
      .on('data', (data) => results.push(data))
      .on('end', async () => {
        try {
          const jsonData = {
            metadata: {
              convertedAt: new Date().toISOString(),
              totalRecords: results.length,
            },
            data: results,
          };

          await fs.promises.writeFile(
            jsonPath,
            JSON.stringify(jsonData, null, 2)
          );
          console.log(
            `${results.length}件のデータをJSONに変換しました`
          );
          resolve(jsonData);
        } catch (error) {
          reject(error);
        }
      })
      .on('error', reject);
  });
}

// JSONからCSVへの変換
async function jsonToCsv(jsonPath, csvPath) {
  try {
    const jsonData = JSON.parse(
      await fs.promises.readFile(jsonPath, 'utf8')
    );

    // データの配列を取得(構造に応じて調整)
    const data = Array.isArray(jsonData)
      ? jsonData
      : jsonData.data || [];

    if (data.length === 0) {
      throw new Error('変換対象のデータがありません');
    }

    // ヘッダーを自動生成
    const headers = Object.keys(data[0]).map((key) => ({
      id: key,
      title: key,
    }));

    const csvWriter = createCsvWriter({
      path: csvPath,
      header: headers,
    });

    await csvWriter.writeRecords(data);
    console.log(
      `${data.length}件のデータをCSVに変換しました`
    );
  } catch (error) {
    console.error('JSON to CSV変換エラー:', error.message);
    throw error;
  }
}

複雑なデータ構造の変換

実際のプロジェクトでは、より複雑なデータ構造の変換が必要になることがあります。

javascript// ネストしたJSONデータのフラット化
function flattenObject(obj, prefix = '') {
  const flattened = {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const newKey = prefix ? `${prefix}.${key}` : key;

      if (
        typeof obj[key] === 'object' &&
        obj[key] !== null &&
        !Array.isArray(obj[key])
      ) {
        // オブジェクトの場合は再帰的に処理
        Object.assign(
          flattened,
          flattenObject(obj[key], newKey)
        );
      } else if (Array.isArray(obj[key])) {
        // 配列の場合は文字列に変換
        flattened[newKey] = obj[key].join(', ');
      } else {
        flattened[newKey] = obj[key];
      }
    }
  }

  return flattened;
}

// 使用例
async function processNestedData(inputPath, outputPath) {
  try {
    const nestedData = await readJSON(inputPath);
    const flattenedData = nestedData.map((item) =>
      flattenObject(item)
    );

    // フラット化されたデータをCSVに保存
    if (flattenedData.length > 0) {
      const headers = Object.keys(flattenedData[0]).map(
        (key) => ({
          id: key,
          title: key,
        })
      );

      const csvWriter = createCsvWriter({
        path: outputPath,
        header: headers,
      });

      await csvWriter.writeRecords(flattenedData);
      console.log('ネストしたデータをCSVに変換しました');
    }
  } catch (error) {
    console.error('データ処理エラー:', error.message);
    throw error;
  }
}

この機能により、複雑な JSON 構造も CSV ファイルで扱えるようになります。

大容量ファイルの処理技術

ストリーミング処理の実装

大容量ファイルを効率的に処理するために、ストリーミング処理を実装します。

javascriptconst fs = require('fs');
const { Transform } = require('stream');
const csv = require('csv-parser');

// メモリ効率的なCSV処理
class CSVProcessor extends Transform {
  constructor(options = {}) {
    super({ objectMode: true });
    this.processedCount = 0;
    this.batchSize = options.batchSize || 1000;
    this.onProgress = options.onProgress || (() => {});
  }

  _transform(chunk, encoding, callback) {
    try {
      // データの処理ロジック
      const processed = this.processChunk(chunk);

      this.processedCount++;

      // 進捗報告
      if (this.processedCount % this.batchSize === 0) {
        this.onProgress(this.processedCount);
      }

      this.push(processed);
      callback();
    } catch (error) {
      callback(error);
    }
  }

  processChunk(data) {
    // 実際の処理ロジック
    return {
      ...data,
      processedAt: new Date().toISOString(),
      id: this.processedCount,
    };
  }
}

// 使用例
async function processLargeCSV(inputPath, outputPath) {
  return new Promise((resolve, reject) => {
    const processor = new CSVProcessor({
      batchSize: 1000,
      onProgress: (count) => {
        console.log(`処理済み: ${count}件`);
      },
    });

    let output = [];

    fs.createReadStream(inputPath)
      .pipe(csv())
      .pipe(processor)
      .on('data', (data) => output.push(data))
      .on('end', () => {
        console.log(`処理完了: ${output.length}件`);
        // 結果を保存
        fs.writeFileSync(
          outputPath,
          JSON.stringify(output, null, 2)
        );
        resolve(output);
      })
      .on('error', reject);
  });
}

並列処理によるパフォーマンス向上

複数のファイルを並列で処理することで、全体的な処理時間を短縮できます。

javascriptconst cluster = require('cluster');
const os = require('os');

// ワーカープロセスでの処理
if (cluster.isMaster) {
  console.log(`マスタープロセス ${process.pid} を開始`);

  // CPUコア数に応じてワーカーを作成
  const numCPUs = os.cpus().length;
  const files = [
    'file1.csv',
    'file2.csv',
    'file3.csv',
    'file4.csv',
  ];

  for (
    let i = 0;
    i < Math.min(numCPUs, files.length);
    i++
  ) {
    const worker = cluster.fork();
    worker.send({ filePath: files[i], workerId: i });
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(
      `ワーカー ${worker.process.pid} が終了しました`
    );
  });
} else {
  // ワーカープロセスでの処理
  process.on('message', async (msg) => {
    try {
      console.log(
        `ワーカー ${process.pid}${msg.filePath} を処理開始`
      );

      await processLargeCSV(
        msg.filePath,
        `output_${msg.workerId}.json`
      );

      console.log(`ワーカー ${process.pid} が処理完了`);
      process.exit(0);
    } catch (error) {
      console.error('ワーカーエラー:', error.message);
      process.exit(1);
    }
  });
}

この並列処理により、大容量ファイルの処理時間を大幅に短縮できます。

エラーハンドリングとバリデーション

包括的なエラーハンドリング

実際の運用では、様々なエラーケースに対応する必要があります。

javascriptconst fs = require('fs').promises;
const { promisify } = require('util');

// カスタムエラークラス
class FileProcessingError extends Error {
  constructor(message, code, fileName) {
    super(message);
    this.name = 'FileProcessingError';
    this.code = code;
    this.fileName = fileName;
  }
}

// 堅牢なファイル読み込み
async function safeReadFile(filePath) {
  try {
    const stats = await fs.stat(filePath);

    // ファイルサイズチェック(100MB制限)
    if (stats.size > 100 * 1024 * 1024) {
      throw new FileProcessingError(
        'ファイルサイズが大きすぎます',
        'FILE_TOO_LARGE',
        filePath
      );
    }

    const content = await fs.readFile(filePath, 'utf8');
    return content;
  } catch (error) {
    // エラータイプに応じた処理
    switch (error.code) {
      case 'ENOENT':
        throw new FileProcessingError(
          `ファイルが見つかりません: ${filePath}`,
          'FILE_NOT_FOUND',
          filePath
        );
      case 'EACCES':
        throw new FileProcessingError(
          `ファイルへのアクセス権限がありません: ${filePath}`,
          'PERMISSION_DENIED',
          filePath
        );
      case 'EMFILE':
        throw new FileProcessingError(
          'ファイル記述子の上限に達しました',
          'TOO_MANY_OPEN_FILES',
          filePath
        );
      default:
        if (error instanceof FileProcessingError) {
          throw error;
        }
        throw new FileProcessingError(
          `予期しないエラーが発生しました: ${error.message}`,
          'UNEXPECTED_ERROR',
          filePath
        );
    }
  }
}

データバリデーション

データの整合性を保つために、バリデーション機能を実装します。

javascript// データバリデーション関数
function validateCSVData(data) {
  const errors = [];

  // 必須フィールドのチェック
  const requiredFields = ['name', 'email', 'age'];
  requiredFields.forEach((field) => {
    if (!data[field] || data[field].trim() === '') {
      errors.push(`必須フィールド '${field}' が空です`);
    }
  });

  // メールアドレスの形式チェック
  if (data.email && !isValidEmail(data.email)) {
    errors.push(`無効なメールアドレス: ${data.email}`);
  }

  // 年齢の妥当性チェック
  if (
    data.age &&
    (isNaN(data.age) || data.age < 0 || data.age > 150)
  ) {
    errors.push(`無効な年齢: ${data.age}`);
  }

  return {
    isValid: errors.length === 0,
    errors: errors,
  };
}

function isValidEmail(email) {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// バリデーション付きCSV処理
async function processCSVWithValidation(
  inputPath,
  outputPath
) {
  const validData = [];
  const invalidData = [];

  return new Promise((resolve, reject) => {
    fs.createReadStream(inputPath)
      .pipe(csv())
      .on('data', (data) => {
        const validation = validateCSVData(data);

        if (validation.isValid) {
          validData.push(data);
        } else {
          invalidData.push({
            data: data,
            errors: validation.errors,
          });
        }
      })
      .on('end', async () => {
        try {
          // 有効なデータを保存
          if (validData.length > 0) {
            await writeJSON(outputPath, validData);
          }

          // 無効なデータのログ出力
          if (invalidData.length > 0) {
            console.log(
              `無効なデータ: ${invalidData.length}件`
            );
            await writeJSON(
              outputPath.replace('.json', '_invalid.json'),
              invalidData
            );
          }

          resolve({
            validCount: validData.length,
            invalidCount: invalidData.length,
          });
        } catch (error) {
          reject(error);
        }
      })
      .on('error', reject);
  });
}

リトライ機能の実装

ネットワークエラーや一時的な問題に対応するため、リトライ機能を実装します。

javascript// リトライ付きファイル処理
async function processWithRetry(
  operation,
  maxRetries = 3,
  delay = 1000
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await operation();
      return result;
    } catch (error) {
      console.log(
        `試行 ${attempt}/${maxRetries} 失敗:`,
        error.message
      );

      if (attempt === maxRetries) {
        throw new Error(
          `${maxRetries}回の試行後も処理に失敗しました: ${error.message}`
        );
      }

      // 指数バックオフ
      const waitTime = delay * Math.pow(2, attempt - 1);
      console.log(`${waitTime}ms 待機後、再試行します...`);
      await new Promise((resolve) =>
        setTimeout(resolve, waitTime)
      );
    }
  }
}

// 使用例
async function robustFileProcessing(inputPath, outputPath) {
  return await processWithRetry(async () => {
    const data = await safeReadFile(inputPath);
    const processed = JSON.parse(data);
    await writeJSON(outputPath, processed);
    return processed;
  });
}

まとめ

この記事では、Node.js を使った CSV・JSON ファイル操作について、基礎から応用まで幅広く解説しました。

重要なポイントをまとめると:

技術的な学び

#技術領域重要なポイント
1基本操作fs、csv-parser、csv-writer の効果的な使い方
2メモリ効率ストリーミング処理による大容量ファイル対応
3エラー処理包括的なエラーハンドリングとバリデーション
4パフォーマンス並列処理とクラスター機能の活用
5実用性実際のプロジェクトで使える実践的なコード

開発者として成長するために

この記事で学んだ技術は、単なるファイル操作以上の価値があります。データを自在に操れるようになることで、あなたの開発者としての市場価値が向上し、より複雑で挑戦的なプロジェクトに取り組める基盤ができました。

特に、大容量データの処理やエラーハンドリングのスキルは、エンタープライズレベルの開発でも重要な要素です。これらの技術を習得することで、信頼性の高いアプリケーションを構築できる開発者として成長できるでしょう。

今後の発展

さらなるスキルアップのために、以下の分野にも挑戦してみてください:

  • データベース連携: PostgreSQL、MongoDB との連携
  • API 開発: RESTful API、GraphQL でのデータ処理
  • リアルタイム処理: WebSocket や Server-Sent Events の活用
  • クラウド連携: AWS S3、Google Cloud Storage との統合

データ処理のスキルは、現代の Web 開発において欠かせない能力です。この記事で学んだ知識を基に、ぜひ実際のプロジェクトで活用してみてください。

関連リンク