fs.readFile(path, 'utf8', callback) はファイル全体を読み、その中身を文字列としてコールバックに渡します。fs.readFileSync(path, 'utf8') は読み込みが終わるのを待って文字列を直接返し、fs/promises の await readFile(path, 'utf8') は同じことをブロックせずに行います。'utf8' を省くと、テキストではなく生のバイトのBufferが返ります。
先に readFile called が表示されます。readFile は読み込みを開始するだけで、中身の準備ができてからコールバックが動くからです。'utf8', を消して実行し直すと、コールバックはファイルの生のバイト、<Buffer 42 75 79 20 6d 69 6c 6b 0a 43 61 6c 6c 20 41 64 61> を受け取ります。
readFile、readFileSync、fs/promisesの違い
3つとも同じファイルを読みます。違うのは、結果の受け取り方と、プログラムのほかの部分が待たされるかどうかです。
同期版の行が最初に表示されます。それが戻るまでほかには何も実行されないからです。残りの2つはメインのコードの後に、読み込みが終わった順に完了します。
readFileSync | readFile(コールバック) | fs/promises の readFile | |
|---|---|---|---|
| 戻り値 | 中身 | undefined | 中身を返すPromise |
| プログラムをブロックする | する | しない | しない |
| エラー | 投げられる。try/catch を使う | コールバックの第1引数 | rejectされたPromise。await と try/catch |
| 使う場面 | 起動時のコードとスクリプト | 古いコールバックのコード | 新しいコード |
起動時に設定ファイルを読むときや短いスクリプトでは readFileSync を使います。サーバーでは、同期の読み込みはディスクが動いている間ほかのリクエストをすべて止めるので、Promise版を使いましょう。そのAPIの残りはfs/promisesのページで扱っています。
ファイルがないとき(ENOENT)
ファイルが存在しないと、エラーは code: 'ENOENT'(「error, no entry」)を持ちます。err.code を確認すれば、ファイルがないことと、権限の問題のようなほかの失敗とを区別できます。
1回目の呼び出しはファイルが見つからず default settings を使い、2回目は dark mode を読みます。ほかのエラーを投げ直しておけば、本当の問題が黙ってデフォルト値に隠れることはありません。
fs.existsSync:読み込みをそのまま試すべき理由
fs.existsSync(path) は true か false を返すので、読む前に確認したくなります。
if (fs.existsSync('data.txt')) {
const text = fs.readFileSync('data.txt', 'utf8');
}
これでは1回で済むところを2回に分けていて、しかもその間にファイルが消される可能性があるので、読み込みはやはり例外を投げることがあります。上の例のように、読み込みを試して ENOENT を処理しましょう。existsSync が適しているのは、何かがあるかどうかだけを知りたくて開くつもりがないとき、たとえばフォルダを作るかどうかを決めるときです。
JSONファイルを読む
テキストを読んで、JSON.parse でパースします。構文エラーのあるファイルは SyntaxError を投げるので、同じ try の中に入れておきましょう。
3001 と boolean が表示されます。パースした後の値はJSONの型を持っています。require('./config.json') でも読めますが、結果がキャッシュされるので、ファイルが変わった後でも2回目の require は古いオブジェクトを返します。
ファイルを1行ずつ読む
メモリに収まるファイルなら、全体を読んで改行文字で分割します。最後の改行が残す空文字列はフィルターで取り除きます。
分割すると 4 個の要素になり、最後の1つは空なので、ループは番号付きの3行を表示します。Windowsで書かれたファイルは行末が \r\n なので、両方に対応するには /\r?\n/ で分割します。
数ギガバイトのログはメモリに収まりません。代わりにストリームで読めば、メモリに置くのは一度に1行だけです。
const fs = require('fs');
const readline = require('readline');
async function main() {
const rl = readline.createInterface({
input: fs.createReadStream('big.log'),
crlfDelay: Infinity,
});
for await (const line of rl) {
console.log(line);
}
}
main();
よくある間違い
コールバックの外で結果を使う。 const text = fs.readFile(...) は undefined です。中身があるのはコールバックの中か、await の後だけです。
エンコーディングを忘れる。 'utf8' がないとBufferが返り、data.split('\n') は data.split is not a function を投げます。テキストが欲しいときは必ずエンコーディングを渡しましょう。
間違ったフォルダからの相対パス。 'data.txt' は node を起動した場所で探されます。スクリプトの隣に置くファイルには path.join(__dirname, 'data.txt') を使います。
よくある質問
fs.readFileがundefinedを返すのはなぜですか?
fs.readFile は中身を返しません。読み込みを開始し、後で (err, data) を渡してコールバックを呼びます。データはコールバックの中で使うか、readFileSync を呼ぶか、Promise版を await してください。
'data.txt'のような相対パスはどこから探されますか?
現在の作業ディレクトリ、つまり node を実行したフォルダです。スクリプトがあるフォルダではありません。スクリプトの隣にあるファイルを読むなら、path.join(__dirname, 'data.txt') でパスを組み立てます。
fs.readFileでURLのファイルを読めますか?
読めません。読めるのはファイルシステムだけです。HTTPでファイルを取得するなら、fetch を使ってから await res.text() します。
readFileはどのくらいの大きさのファイルまで扱えますか?
ファイル全体をメモリに読み込むので、設定ファイル、テンプレート、小さなデータファイルなら問題ありません。数百メガバイトのファイルは、fs.createReadStream で少しずつ読みましょう。