Node.jsのファイル入出力まとめ

f:id:f-a24:20210312162825p:plain

こんにちは。フロントエンドエンジニアの藤澤(@f_a24_)です。

最近、とある案件にてNode.jsでファイル操作をよくしているので、その中からファイル入出力処理についてまとめたいと思います。

Node.jsとは?

f:id:f-a24:20210312162650p:plain

Node.jsは一言で言うと、サーバサイドで動作するJavaScriptです。
元々はブラウザで動作するJavaScriptというプログラミング言語をサーバサイドでも実行できるようにしたものといったイメージになります。
(サーバサイドで動作するプログラミング言語は他にJavaPHPなどがあります。)

f:id:f-a24:20210316135533p:plain

単にJavaScriptと言う場合はブラウザで動作する方を意味し、サーバサイドで動作する方はNode.jsや省略してNodeと言ったりするのが一般的かと思います。
BiNDupではBiND PressSmoothGrowといった機能で使用されています。

そのNode.jsにはファイル入出力処理のやり方が色々あるのですが、ファイル操作に関する機能は標準モジュールのfsになりますので以下のようにして読み込む必要があります。

const fs = require('fs');

ファイル読み込み

readFileSync:一括で取得(同期)

この方法はパッと見で一番分かりやすい方法です。
ファイルの内容を一度にすべて取得し、変数に格納します。

const text = fs.readFileSync('test.txt', 'utf8');
console.log(text);

readFile:一括で取得(非同期)

上記の方法はファイルを読み込むまで、その後の処理が止まってしまいます。
Node.jsはシングルスレッド/シングルプロセスが原則ですので、こういった処理を長時間行うと後続の処理がなかなか実行されずサービスがまともに提供できなくなる可能性があります。

その対策として非同期で読み込む方法があります。
こちらは、まずファイルにアクセスして読み込んでいる間は後続の処理を行い、読み込みが完了したら完了後の処理を行うといった形です。

fs.readFile('test.txt', 'utf8', (err, text) => {
  // ファイル読み込み完了後の処理
  if (err) throw err;
  console.log(text);
});

Stream:複数回に分けて取得

上記2つの方法はどちらも一括でファイルを読み込むので、巨大なファイルを読み込むとメモリがパンクしてしまうという問題があります。

これを解決するためには複数回に分けてファイルを読み込む必要がありますが、Streamという機能を使うと簡潔に記述することが出来ます。

const stream = fs.createReadStream('test.txt');

let text = '';
// データを取得する度に実行される
stream.on('data', chunk => {
  text += chunk.toString('utf8');
});

// データをすべて読み取り終わったら実行される
stream.on('end', () => {
  console.log(text);
});

// エラー処理
stream.on('error', err=>{
  console.log(err.message);
});

readline:1行ずつ取得

Streamを使用すると指定したバイト数(デフォルト値は65536byte)ずつファイルからデータを取得します。

テキストファイルを処理する際に1行ずつ取得して処理をするといったことはよくあることだと思いますが、Streamと標準モジュールのreadlineを使用すると簡単に書くことが出来ます。

const readline = require('readline');
const stream = fs.createReadStream('test.txt');

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

let i = 1;
reader.on('line', data => {
  console.log(`${i}: ${data}`);
  i++;
});

Promise:一括で取得

最近のfsモジュールはPromiseに対応しています。

fs.promises.readFile('test.txt', 'utf8').then(data => {
  // ファイル読み込み完了後の処理
  console.log(data);
});

const main = async () => {
  const text = await fs.promises.readFile('test.txt', 'utf8');
  console.log(text);
};
main();

最初のreadFilereadFileSyncとあまり変わらない、むしろ記述量が増えていますが、複数ファイルを同時に読み込んで処理をしたい場合にはPromise.allを一緒に使用することで処理が簡単に書けます。

const main = async () => {
  await Promise.all(['test1', 'test2', 'test3'].map(async file => {
    const text = await fs.promises.readFile(`${file}.txt`, 'utf8');
    console.log(`${file} : ${text}`);
  }));
  console.log('All files loaded');
};
main();

ファイル書き込み

writeFileSync:書き込み(同期)

ファイル書き込みも読み込みと似たような形になります。
ファイルが存在しない場合は新規に作成し、ファイルが存在する場合は内容をすべて上書きします。

fs.writeFileSync('file.txt', 'Hello Node');

appendFileSync:追加書き込み(同期)

こちらの方法は内容を上書きせず、末尾に追加して書き込む方法です。
ちなみに先ほどのwriteFileSyncにはオプションがあり、書き込みモードを指定すれば同じ動作になります。

fs.appendFileSync('file.txt', '\nHello Node');
// 同じ
fs.writeFileSync('file.txt', '\nHello Nod', { flag: 'a' });

writeFile:書き込み(非同期)

書き込みも読み込みと同様に非同期で実行することが可能です。

fs.writeFile('file.txt', 'Hello Node', err => {
  if (err) console.log(err.message);
});

appendFile:追加書き込み(非同期)

fs.appendFile('file.txt', '\nHello Node', err => {
  if (err) console.log(err.message);
});
// 同じ
fs.writeFile('file.txt', 'Hello Node', { flag: 'a'}, err => {
  if (err) console.log(err.message);
});

Stream:複数回に分けて書き込む

書き込みを非同期で何度も行うと以下のようなCallback地獄という形になりがちです。

// Callback地獄
fs.writeFile('file1.txt', 'Hello', err => {
  if (err) return console.log(err.message);
  fs.appendFile('file1.txt', 'Node', err => {
    if (err) return console.log(err.message);
    fs.appendFile('file1.txt', '\n', err => {
      if (err) return console.log(err.message);
    });
  });
});

そんなときはStreamを使うと分かりやすく書けます。

const stream = fs.createWriteStream('file.txt');
stream.write('Hello ');
stream.write('Node');
stream.end('\n');

// エラー処理
stream.on('error', err =>{
  if(err) console.log(err.message);
});

Promise:書き込み

書き込みも読み込みと同様にPromise対応されています。

fs.promises.writeFile('test.txt', 'Hello Node with Promise')
  .then(() => {
    console.log('Writing completed');
  });

const main = async () => {
  await fs.promises.writeFile('test.txt', 'Hello Node with Promise');
  console.log('Writing completed');
};
main();

// 複数ファイルを並列処理で書き込む
const write = async () => {
  await Promise.all(['test1', 'test2', 'test3'].map(async file => {
    await fs.promises.writeFile(`${file}.txt`, 'Hello Node with Promise');
    console.log(`${file} : Writing completed`);
  }));
  console.log('All files writing completed');
};
write();

まとめ

今回はNode.jsのファイル入出力処理に関して自分なりにまとめてみました。
それぞれの特徴を抑えた上で、適材適所に使っていきたいですね。

参考ページ

Atsushi Fujisawa カテゴリーの記事一覧 -