2
0

【JavaScript】バイナリデータ理解への旅

Last updated at Posted at 2024-07-01

はじめに

バイナリデータを扱うときにちょこちょこ出てくる、Buffer, Blob, Base64, ...
出てくるたびに少し調べて、わかった気になって、忘れたころにまたやってくる...
お前ら一体なんなんだよぉと思ったので、ちゃんとアウトプットしてみた。

そもそもバイナリって?

IT用語辞典によれば、以下とのこと。

バイナリ(binary)とは、一般には、二進数もしくは0と1で表現される数字のこと。

しかし、曖昧な使われ方をしているようで、人間に向けではないコンピュータ用のデータを総じてバイナリデータといったりするみたい。

型付き配列

JavaScriptの世界で、通常の配列は色々な値を入れられるし、拡張なども自由で非常に便利。
しかし、巨大なバイナリデータなどを扱うときは処理速度遅い。。。
型付き配列というものを使おう!!!

  • 通常の配列:型の制限無く自由な値を格納可能。要素数の拡張なども自由。
  • 型付き配列:格納する値の型や機能に制限あり。→ 高速データアクセス

この型付き配列を扱う時に、バッファービューというものを使う!!

役割 実装
バッファー メモリの確保 ArrayBufferクラス
ビュー 値へのアクセス TypedArrayオブジェクト、DataView

※DataViewはここでは割愛します。

ArrayBufferクラス

文字通り、バイトの配列。以下のように書けば、16バイトの配列。

const buffer = new ArrayBuffer(16);

ArrayBuffer の内容(確保したメモリ)を直接操作することはできない。
なら、どうすんねん。→ ビューを使う!!!

TypedArrayオブジェクト

TypedArrayコンストラクタがあるわけではない!!

実際にあるのは、以下のようなサブクラス(ほかにもいろいろある)。

値の範囲 バイト数
Int8Array -128 ~ 127 1
Uint8Array 0 ~ 255 1
Int16Array -32768 ~ 32767 2
Uint16Array 0 ~ 65535 2

Int8: 8bit(1バイト)の整数型。
Uint = Unsigned int なので、符号なし整数型のこと。

ArrayBufferでメモリを確保し、確保したメモリに対して、Uint8Arrayでビューを作成しアクセスしてみる。

const buffer = new ArrayBuffer(16); // 16バイト
const uint8Array = new Uint8Array(buffer, 0, 3) // 第2引数: byteOffset, 第3引数: length
console.log(uint8Array)
// Uint8Array(16) [0, 0, 0]
uint8Array[0] = 255
uint8Array[1] = 257
uint8Array[2] = 'hoge'
console.log(uint8Array)
// Uint8Array(16) [255, 1, 0]

上記から、Uint8Arrayオブジェクト作成直後は、各要素は0で初期化されており、255までの整数を設定できるが、範囲外の値の場合は、下位ビットの値のみが採用されていることがわかる。また、文字列を設定しようとしてもエラーにはならないが無視されるみたい。

型付き配列は、通常配列とどう違うの?

  • Array.isArrayメソッドでfalseとなる
  • pushやpopなどの一部のメソッドが使えない
const normalArray = [0, 1];
const buffer = new ArrayBuffer(16);
const typedArray = new Int16Array(buffer);

console.log(Array.isArray(normalArray));
// true
console.log(Array.isArray(typedArray));
// false

normalArray.push(2);
console.log(normalArray);
// [0, 1, 2]
typedArray.push(2);
// TypeError: typedArray.push is not a function

型付き配列の方が通常配列より速いってほんとに~?

下記のようなスクリプトを用意する。

const TRIAL_COUNT = 3;
const ARRAY_LENGTH = 1024*1024*50;
const DIVISOR = 200; // 各要素に格納する値は0~200になるようにする

//Array:
for(let j = 0; j < TRIAL_COUNT; j++) {
  const startTime = Date.now();
  let arr = Array(ARRAY_LENGTH);
  for(let i = 0; i < arr.length; i++) {
    arr[i] = i % DIVISOR;
  }
  let sum = 0;
  for(let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  console.log('Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum);
  arr = undefined;
}
//Uint8Array
for(let j = 0; j < TRIAL_COUNT; j++) {
  const startTime = Date.now();
  let arr = new Uint8Array(ARRAY_LENGTH);
  for(let i = 0; i < arr.length; i++) {
    arr[i] = i % DIVISOR;
  }
  let sum = 0;
  for(let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  console.log('Uint8Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum, ', byteLen:', arr.byteLength);
  arr = undefined;
}
//Float64Array:
for(let j = 0; j < TRIAL_COUNT; j++) {
  const startTime = Date.now();
  let arr = new Float64Array(ARRAY_LENGTH);
  for(let i = 0; i < arr.length; i++) {
    arr[i] = i % DIVISOR;
  }
  let sum = 0;
  for(let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  console.log('Float64Array:', Date.now() - startTime, '[ms] , length:', arr.length, ', Sum:', sum, ', byteLen:', arr.byteLength);
  arr = undefined;
}

実行すると、以下のように結果が得られた。

Array: 4561 [ms] , length: 52428800 , Sum: 5216665600
Array: 3715 [ms] , length: 52428800 , Sum: 5216665600
Array: 3761 [ms] , length: 52428800 , Sum: 5216665600
Uint8Array: 160 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 52428800
Uint8Array: 175 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 52428800
Uint8Array: 151 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 52428800
Float64Array: 138 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 419430400
Float64Array: 154 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 419430400
Float64Array: 157 [ms] , length: 52428800 , Sum: 5216665600 , byteLen: 419430400

この結果から以下が確認できる。

  • 型付き配列の方が、通常配列よりも速い
  • Float64とUint8では、速度はほとんど変わらない
  • byteLenを見ると、Uint8Arrayの方がFloat64Arrayより少ないメモリで済んでいる

TypedArrayオブジェクト結局どれを使えばいいん?

Uint8ArrayでOK!

理由:

  • ほとんどのバイナリファイルデータが8ビットの符号なし整数(バイト)単位で構成されている
  • 必要最低限のビット幅をもつTypedArrayを使う方がメモリ効率がよい

Blob ってなに?

Blob(Binary Large Object)は、不変の生データであるファイルのようなバイナリデータの大きな塊を効率的に扱うためのオブジェクトらしい。

Blobは以下のように作成できる。Blobコンストラクタには、配列を渡す。配列の中身としては、文字列、ArrayBuffer, TypedArray, Blob などを入れる。

const obj = { hello: "world" };
const blob = new Blob([JSON.stringify(obj, null, 2)], {
  type: "application/json",
});

Blob と File

下記のソースコードは、ファイルをアップロードすると、そのファイルを参照できるURLを生成し、画面表示する。

See the Pen Untitled by Yucca1 (@Yucca1) on CodePen.

  • 上記のJavaScriptソースコードにおける、event.target.files[0]で取得できるものが File オブジェクト。
  • Fileオブジェクトは、特別な種類のBlobオブジェクト。Blobが利用できる場面ではどこでも利用できる。
  • 生成されるURLは、バイナリデータを保持していて、URL.createObjectURL を用いてFileオブジェクトを変換して取得している。

Blob と ArrayBuffer の使い分け

バイナリデータを扱う方法として、BlobとArrayBufferがあることはわかったけど、使い分けはどう考えればいいのか。どうやら以下のように考えてよさそう

  • Blob: ファイルをそのまま操作
  • ArrayBuffer:ファイルの中身をみてみる

例えば、二つのファイルをアップロードし、それらを結合して、ダウンロードするソースを書いてみると以下の通り。ここでは、ファイルのアップロード、ダウンロードはBlobで行い、ファイルの結合はArrayBufferを用いて実施している。

See the Pen BlobSample02 by Yucca1 (@Yucca1) on CodePen.

Base64 とは?

  • Base64は、バイナリデータをテキスト形式で表現するためのエンコーディング方法
  • バイナリデータを64種類のASCII文字(A-Z、a-z、0-9、+、/)で表現するため、テキストとして扱える

Base64 によるデータ転送

HTTPリクエスト・レスポンスボディでバイナリデータを送受信する場合に、Base64エンコーディングを使う。ここでは、HTTPリクエストでファイルデータを送信する準備として、画面上でアップロードしたファイルデータをBase64エンコーディングするところまでやってみる。

See the Pen BlobSample03 by Yucca1 (@Yucca1) on CodePen.

上記コードでは、FileReader オブジェクトを作成し、readAsArrayBufferメソッドを用いることで、ファイルの内容をArrayBufferとしてresult属性にセットしている。
また、btoaメソッド(binary to ASCII)により、バイナリ文字列をBase64エンコーディングしている。

Base64 によるデータ埋め込み

画像をBase64エンコードしてHTMLのタグのsrc属性に直接埋め込むこともできる。

See the Pen BlobSample03 by Yucca1 (@Yucca1) on CodePen.

上記コードでは、FileReaderオブジェクトのreadAsDataURLメソッドを用いることで、ファイルの内容をデータURLとしてresult属性にセットしている。

参考文献

おわりに

人類は思い出した...バイナリは面倒ってことを...

とはいえ、基本的なことから学び、いろんなソースコードを書いてみて、また少しバイナリの扱い方について理解を深められた気がする。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0