JSでバイナリを扱う 第1回 バイナリとは何か

「バイナリ」と「テキスト」はまったく別物だと感じていませんか。実はその境界は、思っているほど明確ではありません。シリーズ初回は、すべてのファイルがバイト列であるという出発点から、「バイナリ」という言葉の正体を捉え直します。

発行

著者 藤田 智朗 フロントエンド・エンジニア
JSでバイナリを扱う シリーズの記事一覧
無料公開中
公開翌日4時まで

はじめに

このシリーズでは、JavaScriptでバイナリデータを扱うための知識を、複数回に分けて解説します。ArrayBufferUint8ArrayBlobといった、JavaScriptに標準で組み込まれたオブジェクト(ビルトインオブジェクト)は、ファイルのアップロードや画像処理の場面で出てきますが、「なんとなくコピペで動かしている」という人も多いのではないでしょうか。

これらをきちんと理解するには、その前に「そもそもバイナリとは何か」をはっきりさせておくのが近道です。第1回はコードにはほとんど触れず、概念の整理に時間を使います。具体的には、「バイナリ」と「テキスト」は対立する2種類のデータではない、という見かたを手に入れることがゴールです。

この見かたが手に入ると、次回で扱うオブジェクトの話も、その先で扱うファイルや画像の話も、ひとつの地続きの知識として理解できるようになります。

すべてのファイルはバイト列である

まず出発点を確認します。コンピューターが扱うデータは、最終的にはすべて0と1の並びです。この0/1の最小単位をビットと呼び、8ビットをまとめた単位をバイトと呼びます。

1バイトは8ビットなので、表現できる値は2の8乗で256通り、つまり0から255までの整数1つに対応します。私たちが普段目にするファイル――テキスト、画像、音声、実行ファイル――は、種類が何であれ、このバイトが一列に並んだものにすぎません。これをバイト列と呼びます。

ここがこのシリーズの土台です。.txt.png.mp3も、ディスクの上では「0255の数値がずらっと並んだもの」でしかありません。違いは中身の数値の並びかたであって、「テキストという独立した存在」と「バイナリという独立した存在」が別々にあるわけではないのです。

「テキストファイル」とは何だったのか

では、私たちが「テキストファイル」と呼んでいるものは何なのでしょうか。

バイト列はただの数値の並びです。その数値を「文字」として読むには、文字とバイト列をどう対応づけるかを定めた規則が必要です。この規則のことを文字コードと呼びます。たとえば、数値65Aに、66Bに対応させる、といった具合です。

つまりテキストファイルとは、たまたま文字コードの規則で読むと意味の通る文章になるバイト列のことです。バイト列そのものに「これはテキストだ」という印が付いているわけではありません。バイト列を文字コードの規則で解釈したとき、人間に読める文章として立ち上がる――それが「テキスト」の正体です。

同じ文字でも、コードが違えばバイトが変わる

文字コードにはいくつか種類があり、現在のWebで主流なのはUTF-8です。UTF-8には、ASCIIと呼ばれる古い文字コードと互換性があるという特徴があります。

UTF-8では、アルファベットや数字といったASCIIの範囲の文字は1文字が1バイトで表されます。たとえばA0x41(10進数で65)という1バイトです。0xは16進数を表す接頭辞で、バイトの値は16進数2桁で書く習慣があります。

一方、日本語のような文字は1文字が複数バイトになります。たとえば「あ」はUTF-8では0xE3 0x81 0x82という3バイトです。同じ「あ」でも、別の文字コードを使えばバイトの並びは変わります。「文字」と「バイト列」は1対1で固定されているのではなく、どの文字コードで変換するか次第で対応が変わるのだ、という点を押さえておいてください。

補足:「文字としての数字」と「数値そのもの」は別物

同じ「数字」でも、文字列の"255"と数値の255は別物です。前者は文字コードの表を通って読まれる文字、後者は文字コードとは無関係の数値です。この区別は、次回バイト列に数値を読み書きするときに効いてきます。

補足:BOMや改行コードもバイト列の一部

ファイルの先頭に付くことがあるBOM(UTF-8では0xEF 0xBB 0xBFの3バイト)や、行の区切りを表す改行コードLF0x0ACRLF0x0D 0x0A)も、目には見えませんが立派なバイト列の一部です。なおBOMはUTF-8では必須ではなく、一部のエディタが付与するものです。「文字化け」や「謎の空行」の原因がこうした不可視のバイトにある、というのはよくある話です。

バイナリエディタで覗いてみる

この「ファイルはバイト列である」という感覚は、バイナリエディタでファイルを開いてみると一気に実感できます。バイナリエディタは、ファイルの中身を文字としてではなく、バイトの値そのもの(16進数)として表示するツールです。

たとえば、Helloという5文字だけを書いた.txtファイルをバイナリエディタで開くと、次のように見えます。

「Hello」をバイト列として見る

48 65 6C 6C 6F
H  e  l  l  o

48H65e……というように、1バイトずつ文字に対応しているのがわかります。テキストエディタが裏でやっているのは、このバイト列を文字コードの規則に従って文字として表示することだったのです。

では逆に、.pngのような画像ファイルをテキストエディタで無理やり開くとどうなるでしょうか。ところどころ読める文字が混じることはあっても、全体としては意味をなさない記号の羅列――いわゆる文字化けになります。これは、画像のバイト列を文字コードの規則で解釈しようとした結果です。画像のバイト列は文字として読まれることを想定していないので、規則に当てはめても意味のある文章にはならないのです。

「バイナリファイル」という言葉の正体

ここまで来ると、「バイナリファイル」という言葉が指しているものも整理できます。

すべてのファイルはバイト列なのですから、厳密にいえばすべてのファイルがバイナリです。それでも私たちが特定のファイルを「バイナリファイル」と呼ぶのは、文字コードで解釈しても意味をなさないバイト列だからです。画像や音声、実行ファイルは、文字として読むことを目的に作られていません。だからテキストエディタで開くと文字化けする。その「文字として読んでも意味がない」という性質を指して、私たちは「バイナリ」と呼んでいるのです。

つまり、「テキスト」と「バイナリ」は本質的に対立する2種類のデータではなく、同じバイト列を文字コードの規則で読んで意味が通るか通らないかという、見かたの違いにすぎません。これがこのシリーズを貫く中心的な考えかたです。

JavaScriptへの橋渡し

最後に、ここまでの話をJavaScriptのコードで一度だけ確かめておきましょう。

文字列を「文字コードで変換した結果のバイト列」に変換するには、TextEncoderを使います。先ほど「あ」はUTF-8で0xE3 0x81 0x82の3バイトだと述べましたが、それを実際に確認してみます。

文字列をバイト列に変換する

const bytes = new TextEncoder().encode('あ');
console.log(bytes);
// Uint8Array(3) [ 227, 129, 130 ]
// 227=0xE3, 129=0x81, 130=0x82

返ってきたUint8Array(3)は「3バイトのバイト列」という意味で、中身の227, 129, 130はそれぞれ0xE3, 0x81, 0x82を10進数で表したものです。文章として書いた「あ」が、たしかに3つのバイトに姿を変えました。

ここで登場したUint8Arrayこそ、JavaScriptでバイト列を扱うための基本的なオブジェクトです。名前は一見とっつきにくいですが、3つのパーツに分けると意味が見えてきます。

  • UintUnsigned integer、つまり符号なし整数(マイナスを含まない、0以上の整数)
  • 8:1要素を8ビット=1バイトで表すこと。8ビットで表せる範囲はちょうど0255
  • Array:それらが並んだ配列

つなげると「符号なし8ビット整数の配列」、すなわち0255の値が並んだバイト列そのものです。今はまだ「バイト列を入れる器」くらいの理解でかまいません。次回、この器の正体を掘り下げていきます。

まとめ

第1回では、次のことを確認しました。

  • すべてのファイルは0255の数値が並んだバイト列である
  • テキストとは、文字コードの規則で読むと意味が通るバイト列のこと
  • バイナリとは、文字コードで読んでも意味をなさないバイト列のこと
  • 両者は対立する2種類のデータではなく、同じバイト列に対する見かたの違いにすぎない

このシリーズでは縦糸として、「選んだ画像をグレースケール(白黒)に変換する」小さなデモを、シリーズを通して少しずつ材料を揃えながら完成させる予定です。画像すら0〜255の数値の並びであり、その数値を書き換えれば見た目が変わる――今回見たバイト列の感覚が、そのデモの土台になります。

次回は、TextEncoderが返したUint8Arrayを入り口に、ArrayBufferTypedArrayDataViewという、バイト列を扱うためのオブジェクトを見ていきます。名前も役割も似たオブジェクトがなぜいくつもあり、どう使い分けるのか――その勘どころをつかむのが、次回のゴールです。