カランのブログ

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

四零二曜日電子報上線啦!訂閱訂起來

ソフトウェアエンジニア / 台湾人 / 福岡生活
このブログはRSS Feed をサポートしています。RSSリンクをクリックして設定してください。技術に関する記事はコードがあるのでブログで閲覧することをお勧めします。

今のモード ライト

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

記事のタイトルや概要は自動翻訳であるため(中身は翻訳されてない場合が多い)、変な言葉が出たり、意味伝わらない場合がございます。空いてる時間で翻訳します。

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

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