カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

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
這是一篇部落格PO文
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)
})

表示される結果は次のようになります:

這��一���部落��PO��

1つのチャンクは最大で5バイトなので、chunk.toString()で正しくないエンコーディングが発生する可能性があります。なぜなら、すべてのデータを受信していないからです。

正しい連結方法:Buffer.concat

Bufferを正しく使用する場合は、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を使用し、不要なエンコーディングを避けることも重要ですが、エンコーディング時の関連操作にも注意が必要です。

次の記事

2021 雑談

前の記事

avr-libc 中的 ATOMIC_BLOCK

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください