[Node.js] ファイルを読み込む様々な方法

Node.jsでは、ファイルからデータを読み込むだけでも様々なアプローチが用意されています。 今回はよく使われる方法についてまとめてみます。

非同期

JavaScriptと言えば非同期処理ですね。 Node.jsはシングルスレッド/シングルプロセスが原則ですので、例えばWebサーバとして稼働させている場合、ファイルの読み書きを同期的に長時間行うと後続の処理がどんどん溜まっていきまともにサービスが提供できなくなる可能性があります。

というわけでまずは非同期でファイルから読み込む方法について取り上げます。

一括で取得 (fs.readFile)

ファイルの内容を一度にすべて取得し、変数に放り込みます。対象が巨大なデータの場合はそのまま死に至りますのでくれぐれもご注意を。

const fs = require("fs");

fs.readFile("data1.txt", "utf-8", (err, data) => {
  if (err) throw err;
  console.log(data);
});

指定バイト分を取得 (fs.read)

fs.read()を利用することで、指定バイト数分を取得できます。引数の値を変更することで読み取る位置などを調整することができますので、ファイルの真ん中あたりから取ってくることなども可能です。

// 定数
const BUFF_SIZE = 100;    // バッファーのサイズ
const BUFF_POS  = 0;      // バッファーの保存開始位置
const READ_SIZE = 3;      // 読み取るサイズ
const READ_POS  = 0;      // 読み取り開始位置

// モジュール
const fs = require("fs");

fs.open("data1.txt", "r", (err, fd) => {
  if (err) throw err;

  // バイナリ保管用の変数を準備
  const buff = Buffer.alloc(BUFF_SIZE);

  // 指定したサイズ分ファイルから読み取る
  fs.read(fd, buff, BUFF_POS, READ_SIZE, READ_POS, (err, bytesRead, buffer) => {
    if (err) throw err;
    console.log(buffer.toString('utf8', 0, READ_SIZE));

    // ファイルを閉じる
    fs.close(fd, (err)=>{
      if(err) throw err;
    });
  });
});

見事にCallback地獄に陥ってますねw

複数回に分けて取得 (Stream1)

fs.readFile()で巨大なデータを一度に取得するとメモリがパンクしてしまいますので、ある程度のボリュームになることが予想される場合は環境/言語を問わず何度かに分けて取得するのがセオリーです。

前述のfs.read()でがんばるのも一つの手ではありますが、Node.jsでは以下のようにStreamの仕組みを利用すると見通しの良いコードが書けます。一度にhighWaterMarkで指定したサイズずつファイルから取得します。メモリに優しいですね。

const fs = require("fs");

// Streamを準備
const stream = fs.createReadStream("data2.txt", {
                    encoding: "utf8",         // 文字コード
                    highWaterMark: 1024       // 一度に取得するbyte数
                });

let count = 0;    // 読み込み回数
let total = 0;    // 合計byte数

// データを取得する度に実行される
stream.on("data", (chunk) => {
  count++;                 // 読み取り回数
  total += chunk.length;   // これまで読み取ったbyte数

  console.log(chunk.toString("utf8"));
});

// データをすべて読み取り終わったら実行される
stream.on("end", () => {
  console.log(`${count}回に分けて取得しました`);
  console.log(`合計${total}byte取得しました`);
});

// エラー処理
stream.on("error", (err)=>{
  console.log(err.message);
});
  • highWaterMarkはデフォルトでは16kbyte(16,384byte)になります。
  • Streamについてここでは詳しくは取り上げません。

複数回に分けて取得 (Stream2)

Streamで読み取る場合、次のようにループさせながら取得することもできます。こちらのほうが他の言語に近い操作感ですね。

const fs = require("fs");
const stream = fs.createReadStream("data2.txt", {
                    encoding: "utf8",         // 文字コード
                    highWaterMark: 1024       // 一度に取得するbyte数
                });

let count = 0;    // 読み込み回数
let total = 0;    // 合計byte数

stream.on("readable", () => {
  let chunk;
  while ( (chunk = stream.read()) !== null ) {
    count++;
    total += chunk.length;
    console.log(chunk.toString("utf8"));
  }
});

stream.on("end", () => {
  console.log(`${count}回に分けて取得しました`);
  console.log(`合計${total}byte取得しました`);
});

1行ずつ取得 (readline)

テキストファイルを処理する際に、1行ずつ取得して処理を行いたいことってよくありますよね。自前で実装することもできますが、標準モジュールであるreadlineを利用すると比較的かんたんに実装できます。

const fs = require("fs");
const readline = require("readline");

// Streamを準備
const stream = fs.createReadStream("data2.txt", {
                  encoding: "utf8",         // 文字コード
                  highWaterMark: 1024       // 一度に取得するbyte数
                });

// readlineにStreamを渡す
const reader = readline.createInterface({ input: stream });

let i = 1;
reader.on("line", (data) => {
  // 行番号を作成
  let num = i.toString().padStart(5, "0");  // 5文字未満は"0"で埋める
  i++;

  console.log(`${num}: ${data}`);
});

同期

Callback版

JavaScriptでのファイル処理は非同期がよく利用されますが、例えばCLIで動かすコマンドやcron等でバッチ処理などを行う場合、あえて同期処理にしたい場合があります。そんなときに利用するのがfs.readFileSync()です。

const fs = require("fs");

try {
  const buff = fs.readFileSync("data1.txt", "utf8");
  console.log(buff);
}
catch(e) {
  console.log(e.message);
}

fs.readFileSync()は読み込み処理が完了してから下の行の処理を行いますので、console.log(buff)にはちゃんとファイルの中身が表示されます。

Promise版

最近のfsモジュールはPromiseに対応しており、async/awaitを利用した書き方が可能です。

const fs = require("fs").promises;

/**
 * ファイルの内容を表示
 *
 * @param {string} file ファイルパス
 */ 
const displayFile = async (file) => {
  try{
    const buff = await fs.readFile(file, "utf-8");
    console.log(buff);
  }
  catch(e){
    console.log(e.message);
  }
};

// 実行
displayFile("data1.txt");

すごく雑に言うとasyncをつけた関数の中で、awaitをつけると処理が終了するまで待ってくれます(=非同期にならない)。このときのエラー処理は通常通りtry〜catch構文で補足できます。

指定バイト分を取得 (fs.readSync)

非同期処理でfs.read()を利用しましたが、この同期版としてfs.readSync()が用意されています。

// 定数
const BUFF_SIZE = 100;    // バッファーのサイズ
const BUFF_POS  = 0;      // バッファーの保存開始位置
const READ_SIZE = 3;      // 読み取るサイズ
const READ_POS  = 0;      // 読み取り開始位置

// モジュール
const fs = require("fs");

// 入れ物準備
const buff = Buffer.alloc(BUFF_SIZE);
let str = "";

// ファイルを同期的に開いて内容を取得
try{
  const fd = fs.openSync("data1.txt", "r");
  fs.readSync(fd, buff, BUFF_POS, READ_SIZE, READ_POS);
  str = buff.toString("utf8", 0, READ_SIZE);
  fs.closeSync(fd);
}
catch(e){
  console.log(e.message);
}

console.log( str );

ファイルを開く際にfs.openではなくfs.openSyncになっている点にも注意が必要です。最初ハマりましたw ちなみにfs.openで開いてreadSyncしようとすると実行時エラーになります。

複数回に分けて取得 (fs.readSync)

今回はfs.readSyncを使って、ちょっとずつファイルから取ってくる処理を書いてみます。

const fs = require("fs");

const displayFile = (file) =>{
  const BUFF_SIZE = 100;    // バッファーのサイズ
  const BUFF_POS  = 0;      // バッファーの保存開始位置
  const READ_SIZE = 1024;   // 読み取るサイズ

  const buff = Buffer.alloc(BUFF_SIZE);
  let str = "";
  let pos = 0;
  let size = 0;

  try{
    const fd = fs.openSync(file, "r");
    while( (size = fs.readSync(fd, buff, BUFF_POS, READ_SIZE, pos)) !== 0 ){
      str = buff.toString("utf8", 0, size);
      pos += size;
      console.log(str);
    }
    fs.closeSync(fd);
  }
  catch(e){
    console.log(e.message);
  }
};

displayFile("data1.txt");

1行ずつ取得 (readline)

Readlineも同期処理に変えてみます。

const fs = require("fs");
const readline = require("readline");

const displayFile = async (file) => {
  const stream = fs.createReadStream(file);
  const rl = readline.createInterface({
    input: stream
  });

  let i = 1;
  for await (const line of rl) {
    // 行番号を作成
    let num = i.toString().padStart(5, "0");  //5文字未満は"0"で埋める
    i++;

    console.log(`${num}: ${line}`);
  }
};

displayFile("data1.txt");

関連ページ

blog.katsubemakito.net

参考ページ

ハンズオンNode.js

ハンズオンNode.js

Amazon