node.js ファイルの読み込みの詳細

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

以下は、指定された中国語のテキストを日本語に翻訳したものです。


Node.jsの中でfsモジュールを使ってファイルを操作するのは非常に一般的な操作ですが、スループットが高い状況では、IOに関する操作には注意が必要です。例えば、ファイルを読み取る際に非常に一般的なコードは以下のようになります:

const fs = require('fs')
fs.readFile('./text.txt', (err, data) => {
  if (!err) {
    console.log(data.toString())
  }
})

この書き方は小さなファイルを読み込む際には特に問題ありませんが、ファイルが大きすぎるとメモリのフットプリントが大きくなる可能性があります。Node.jsでは、プラットフォームの整数ポインタの長さに基づいて最大バッファサイズが決まります。

console.log(buffer.constants.MAX_LENGTH)
// 4294967296 = 4GB

つまり、ファイルが4GBを超える場合、上記の書き方では問題が発生し、Node.jsは直接エラーをスローします。

ストリームを使った操作

経験豊富な開発者は、ファイルサイズによる問題を避けるために、通常fs.createReadStreamを用いてファイルを操作します。readFileとの最大の違いは、ストリームを使うことで大きなファイルをいくつかのチャンクに分割できる点です。各チャンクは数十KBのサイズとなります。ネットワークサービスにおいて、ストリームを通じてデータを渡すのは非常に自然なことです。ストリームを使えば、ブラウザが部分的なHTMLコンテンツを受け取った時点で、全てのコンテンツが受信されるのを待つことなくレンダリングを開始できます。

createReadStreamを使った改訂は以下のようになります:

const fs = require('fs')
let data = ''

const stream = fs.createReadStream('./text.txt')

stream.on('data', chunk => {
  data += chunk
})

stream.on('end', () => {
  console.log(data)
})

一見すると特に問題はないように見え、プログラムも非常によく動作します。ほとんどのファイル処理に適用できるでしょう。しかし、data += chunkをよく見てみると、何かおかしいことに気づくかもしれません。ある開発者はchunkを自然に文字列として処理するかもしれませんが、ストリームから返されるデータは実際にはbufferです。したがって、data += chunkは、実際にはchunk.toString()の後に結合されることになります。ここまで来ると、警鐘を鳴らす開発者もいるかもしれません。

そうです!文字列において最も重要なのはエンコーディングの問題です。デフォルトでは、バッファから文字列への変換はUTF-8を使用します。したがって、data += chunkの書き方は正しくない結果を引き起こす可能性があります。なぜならUTF-8は1、2、3、4バイトを使って1文字を表すことができるからです。便利に示すために、highWaterMarkを5バイトに設定しました。

// text.txt
これはブログの投稿です
const fs = require('fs')
let data = ''
const stream = fs.createReadStream('./text.txt', { highWaterMark: 5 })
stream.on('data', chunk => {
	data += chunk
})
stream.on('end', () => {
	console.log(data)
})

表示結果は以下のようになります:

これ��ブロ���投稿��

チャンクの最大サイズは5バイトのため、chunk.toString()の際に不正確なエンコーディングが発生する可能性があります。なぜなら、全てのデータが受信されていないからです。

正しい結合方法:Buffer.concat

バッファを正しく使用するには、Node.jsが提供するAPIを使って操作し、文字列に変換するのが最良です。これによりエンコーディングの問題を回避できます。

const fs = require('fs')
let data = []
let size = 0
const stream = fs.createReadStream('./text.txt', { highWaterMark: 5 })

stream.on('data', chunk => {
	data.push(chunk)
    size += chunk.length
})

stream.on('end', () => {
  Buffer.concat(data, size).toString()
})

これによりエンコーディングの問題を回避できます。しかし、こう書くのは正直言って面倒です。単純な分析をするだけなら、readFilereadFileSyncを使っても何ら問題ありません。ただし、大きなファイルの分析や高スループットが求められる場合には、これらの細かい点に注意を払う必要があります。(愚痴:ただし、このような場合には他の言語を使って書くことになるかもしれません)

結論

大きなファイルを操作する際は、メモリに一度に全てを入れ出しするのではなく、可能な限りストリームを使って操作することが重要です。また、データを送信する際はBufferを用いてスループットを向上させ、不要なエンコーディングの問題を避けるようにしましょう。ただし、エンコーディング時の関連する操作にも注意が必要です。

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee