【Node.js】Bufferの使い方|バイナリ・エンコード変換・バイト長の罠

【Node.js】Bufferの使い方|バイナリ・エンコード変換・バイト長の罠 Node.js

Bufferは、Node.jsでバイナリデータ(バイトの並び)を扱うための仕組みです。画像や音声などのファイル、ネットワークから受け取った生データ、暗号化やエンコード処理などで登場します。fsでファイルを読むと中身はBufferで返り、child_processでコマンドを実行した結果もBufferです。Node.jsを使ううえで避けて通れない基本データ型です。

つまずきやすいのが、Bufferの.lengthは「文字数」ではなく「バイト数」だという点です。日本語はUTF-8で1文字3バイトになるため、"あいう"のBufferのlength9になります。また、Bufferを作るBuffer.allocBuffer.allocUnsafeには中身が初期化されるかどうかの違いがあります。この記事では、実機のNode.jsで確認しながら、Bufferの扱いを整理します。

先に結論

  • 文字列→BufferはBuffer.from("文字列", "utf8")、Buffer→文字列はbuf.toString("utf8")
  • toString("hex")toString("base64")エンコード変換ができます。
  • .lengthはバイト数。日本語はUTF-8で1文字3バイト("あ"=3)。
  • Bufferの新規作成はBuffer.alloc(n)(ゼロ埋め・安全)を使います。
  • Buffer.allocUnsafe(n)は高速ですが中身が初期化されません
  • 結合はBuffer.concat([...])、各バイトはbuf[0]でアクセスできます。

ファイルの読み書きはfsでファイルを読み書きする、コマンド出力の受け取りはchild_processfetchのバイナリ取得はfetchでAPIを叩くもあわせて参考になります。

スポンサーリンク

Bufferとは(どこで出てくるか)

Bufferは「固定長のバイト列」です。普段は意識しなくても、ファイルやネットワークの生データを扱うと自然に出てきます。たとえば次のような場面です。

Bufferが返る場面
const fs = require("node:fs");

// ファイルをエンコード指定なしで読むと Buffer が返る
const data = fs.readFileSync("image.png");
console.log(Buffer.isBuffer(data));   // true

// エンコードを指定すると文字列になる
const text = fs.readFileSync("memo.txt", "utf8");   // これは文字列

// child_process の実行結果も Buffer
// const out = execSync("node --version");  // Buffer → toString() で文字列に

fs.readFileSyncでエンコードを指定せずに読むと、中身はBufferとして返ります(Buffer.isBuffer()で確認できます)。エンコード("utf8"など)を指定すると文字列になります。画像やバイナリファイルはBufferのまま扱い、テキストは文字列に変換する、と使い分けます。

文字列とBufferの相互変換

文字列からBufferを作るにはBuffer.from("文字列", エンコード)、Bufferを文字列に戻すにはbuf.toString(エンコード)です。エンコードを変えれば、同じデータをhexやbase64として取り出すこともできます。

文字列⇔Buffer・エンコード変換
const buf = Buffer.from("Hello", "utf8");

console.log(buf.toString("utf8"));     // Hello
console.log(buf.toString("hex"));      // 48656c6c6f(16進数)
console.log(buf.toString("base64"));   // SGVsbG8=(Base64)

// 逆向き(hex や base64 から文字列に戻す)
console.log(Buffer.from("48656c6c6f", "hex").toString());    // Hello
console.log(Buffer.from("SGVsbG8=", "base64").toString());   // Hello

実機でも、"Hello"のBufferはtoString("hex")48656c6c6ftoString("base64")SGVsbG8=になりました。逆に、hexやbase64の文字列をBuffer.from(..., "hex")などで読み込めば、元のデータに戻せます。Bufferは「バイト列」という共通の形を経由することで、文字列・16進数・Base64の間を自由に変換できるのが便利な点です。

.lengthはバイト長(文字数ではない)

もっとも注意したいのがこれです。Bufferの.lengthは文字数ではなく「バイト数」を表します。ASCII(半角英数)は1文字1バイトですが、日本語はUTF-8で1文字3バイトになるため、文字列のlengthとBufferのlengthがずれます。

文字数とバイト数の違い
const str = "あいう";

console.log(str.length);                          // 3(文字数)
console.log(Buffer.from(str, "utf8").length);     // 9(バイト数!)

// バイト数だけ知りたいなら byteLength(Bufferを作らずに済む)
console.log(Buffer.byteLength("あ", "utf8"));      // 3
console.log(Buffer.byteLength("a", "utf8"));       // 1
日本語1文字は3バイト(UTF-8)

実機で確認したところ、"あいう"文字列のlength3(文字数)、Bufferのlength9(バイト数)になりました。日本語はUTF-8で1文字あたり3バイトを使うためです。この違いを知らないと、「文字数で確保したバッファに日本語が入りきらない」「ファイルサイズの計算が合わない」といったバグになります。文字数がほしいなら文字列の.length、データのバイト数がほしいならBufferの.lengthBuffer.byteLength()を使い分けてください。Buffer.byteLength()は、Bufferを実際に作らずにバイト数だけ計算できるので、サイズの確認に便利です。なお、絵文字など一部の文字は4バイトになることもあります。

Bufferを作る alloc と allocUnsafe

空のBufferを新しく作るにはBuffer.alloc(サイズ)を使います。似たものにBuffer.allocUnsafe(サイズ)がありますが、こちらは中身が初期化されないため、扱いに注意が必要です。

alloc と allocUnsafe
// alloc: ゼロで初期化される(安全・推奨)
const safe = Buffer.alloc(4);
console.log(safe.toString("hex"));   // 00000000(ゼロ埋め)

// allocUnsafe: 初期化されない(高速だが中身は不定)
const unsafe = Buffer.allocUnsafe(4);
//  → 以前メモリにあったデータが残っている可能性がある
//    必ず全体を上書きしてから使うこと

// 文字列から作る場合は Buffer.from
const fromStr = Buffer.from("Hello");
基本はalloc、allocUnsafeは上書き前提

実機で確認したところ、Buffer.alloc(4)00000000すべてゼロで初期化されていました。一方、Buffer.allocUnsafe(4)初期化されず、メモリに以前あったデータがそのまま残っている可能性があります(その分、確保が高速です)。allocUnsafeで作ったBufferを初期化せずに送信・保存すると、意図しない古いデータが混ざる恐れがあり、セキュリティ上の問題にもなり得ます。特別な理由がなければBuffer.allocを使うのが安全です。allocUnsafeを使うのは、直後に必ず全体を書き込むことが分かっている、性能が重要な場面に限ります。古い書き方のnew Buffer()は非推奨なので使わないでください。

結合・バイトアクセス・スライス

複数のBufferをつなぐにはBuffer.concat()、各バイトには配列のようにbuf[0]でアクセスできます。一部を取り出すsubarray()も使えます。

結合とバイトアクセス
// 複数の Buffer を結合
const joined = Buffer.concat([Buffer.from("AB"), Buffer.from("CD")]);
console.log(joined.toString());   // ABCD

// 各バイトにアクセス(数値で取れる)
const buf = Buffer.from("Hello");
console.log(buf[0]);   // 72('H' の文字コード)

// 一部を取り出す(元のメモリを共有する点に注意)
console.log(buf.subarray(0, 3).toString());   // Hel

実機でも、Buffer.concat([...])ABCDを結合してABCDに、buf[0]で先頭バイト72Hの文字コード)が取得できました。buf[i]0〜255の数値を返します。subarray()で一部を切り出せますが、元のBufferとメモリを共有するため、切り出したものを変更すると元にも影響します。独立したコピーがほしいときは、新しいBufferにBuffer.from()でコピーします。

主なメソッドまとめ

Bufferでよく使うものをまとめます。

書き方 働き
Buffer.from(str, enc) 文字列からBufferを作る
buf.toString(enc) Bufferを文字列に(utf8 / hex / base64)
buf.length バイト数(文字数ではない)
Buffer.byteLength(str) 文字列のバイト数を計算
Buffer.alloc(n) ゼロ埋めのBufferを作る(安全)
Buffer.concat([...]) 複数のBufferを結合
buf[i] / buf.subarray() バイトアクセス / 部分取得

よくある失敗

.lengthを文字数だと思う

Bufferの.lengthはバイト数です。日本語は1文字3バイトです。文字数は文字列の.lengthを使います。

allocUnsafeを初期化せず使う

古いデータが残っている可能性があります。基本はBuffer.allocを使います。

new Buffer()を使う

非推奨です。Buffer.fromBuffer.allocを使います。

subarrayの結果を独立コピーだと思う

元とメモリを共有します。独立コピーはBuffer.from()で作ります。

エンコード指定を間違える

作成時と変換時のエンコードをそろえます。違うと文字化けします。

よくある質問

QBufferとは何ですか?
ANode.jsでバイナリデータ(バイトの並び)を扱うためのデータ型です。ファイルの読み込みやネットワーク通信、エンコード変換などで登場します。fs.readFileSyncでエンコードを指定せずに読むと、中身はBufferで返ります。文字列にしたいときはtoString()を使います。
QBuffer.lengthが文字数と合いません。
ABufferの.lengthは文字数ではなくバイト数だからです。日本語はUTF-8で1文字3バイトのため、"あいう"のBufferのlength9になります。文字数がほしいときは文字列の.length、バイト数がほしいときはBufferの.lengthBuffer.byteLength()を使います。
QallocとallocUnsafeの違いは?
ABuffer.alloc(n)は中身をゼロで初期化するため安全です。Buffer.allocUnsafe(n)は初期化しないぶん高速ですが、メモリに以前あったデータが残っている可能性があります。特別な理由がなければBuffer.allocを使ってください。
Q文字列をBase64や16進数に変換するには?
ABuffer.from("文字列").toString("base64")でBase64に、toString("hex")で16進数に変換できます。逆にBuffer.from("SGVsbG8=", "base64").toString()で元に戻せます。Bufferを経由することで、各形式の間を自由に変換できます。
Qnew Buffer()は使ってよいですか?
Anew Buffer()は非推奨で、使うべきではありません。文字列から作るならBuffer.from()、空のBufferを作るならBuffer.alloc()を使ってください。古いコードでnew Buffer()を見かけたら、これらに置き換えます。

まとめ

  • Bufferはバイナリデータ用の型。fsやchild_processの結果として自然に登場します。
  • 変換はBuffer.from(str, enc)buf.toString(enc)。hex・base64にもできます。
  • .lengthはバイト数。日本語はUTF-8で1文字3バイトです。
  • 新規作成はBuffer.alloc(ゼロ埋め・安全)を基本に。allocUnsafeは上書き前提。
  • 結合はBuffer.concat、バイトはbuf[i]でアクセスします。

Bufferは、ファイルやネットワークの生データを扱うときに欠かせない存在です。「.lengthはバイト数」「基本はBuffer.alloc」という2点を押さえれば、日本語のバイト数のずれや初期化漏れといった定番のミスを避けられます。エンコード変換の便利さも、ぜひ活用してみてください。