【JavaScript】GZIP/ZLIB/DEFLATE データをダウンロードして Compression Streams API で解凍する

[2023-12-02 更新]

データの圧縮や解凍といえば ZIP や 7Z などのファイルフォーマットを思い浮かべるのではないでしょうか。ファイルではありませんが、通信データの圧縮と解凍は HTTP 通信でも行われており、ウェブブラウザーはデータの圧縮解凍の機能を内部的に備えています。この内部機能を JavaScript からでも利用できるようにしたのが Compression Streams API です。

Compression Streams API はデータの圧縮と解凍の両方の機能を備えていますが、この記事では GZIP ファイルを fetch でダウンロードし、それを Compression Streams API で解凍する手順を解説します。本記事で紹介するコードは、Chrome, Edge, Firefox, Safari で動作します。

Compression Streams API の概要

Compression Streams API がサポートしている圧縮方式は、ZLIB, DEFLATE, GZIP の 3 種類です。多くの人になじみのあるファイル圧縮に使う ZIP や 7Z などではないので、少し期待外れだったかもしれません。

ZLIB および GZIP はデータ圧縮フォーマットのことです。これらフォーマットは HTTP プロトコルにおける転送データの圧縮にも使われています。一方、DEFLATE は、ZLIB や GZIP が使う圧縮アルゴリズムのことです。分かりやすく言うと、DEFLATE で圧縮したデータに付加情報を加えたものが ZLIB や GZIP です。そして、ZLIB と GZIP は圧縮アルゴリズムは同じながらも付加情報が異なるフォーマットということです。ZLIB は RFC1950 で、DEFLATE は RFC1951 で、GZIP は RFC1952 で規定されています。

Compression Streams API は名前に “Streams” とあるように、データをストリームとして扱う API です。つまり、Streams API に基づいたストリームとしてデータを扱います。例えば、サーバーとのデータの送受信で、データをリアルタイムに圧縮しながら送信、または、解凍しながら受信するといったケースに親和性が高いと言えます。もちろん、その場合はサーバー側もデータの圧縮・解凍をサポートしなければいけません。

Streams API の概要についてはぜひ記事「Streams API を使って fetch の逐次ダウンロード & ファイル保存」もご覧ください。

なお、ZGIP は圧縮データをファイルに保存することも可能なフォーマットです。本来、Compression Streams API は圧縮ファイルを扱う API ではありませんが、GZIP に関してはファイルデータの解凍が可能です。しかし、制限があります。GZIP ファイルには、圧縮されたファイルのファイル名などの情報も含まれているはずですが、それを取り出すことはできません。もし Compression Streams API で GZIP ファイルを解凍する場合、事前に GZIP ファイルの中にどのようなファイルが圧縮されているのかを知っている必要がありますので注意してください。

圧縮データを解凍する DecompressionStream クラス

圧縮データの解凍を担うのは DecompressionStream クラスです。このクラスから Streams API の TransformStream オブジェクトを生成します。以下のコードでは変数 streamTransformStream オブジェクトです。

const stream = new DecompressionStream('gzip');

DecompressionStream クラスには圧縮方式を表すキーワードを引き渡しますが、サポートされている値とそれに対応する圧縮方式は次の通りです。

キーワード圧縮方式
deflateZLIB
deflate-rawDEFLATE
gzipGZIP

deflate が ZLIB を表し、deflate-raw が DEFLATE を表しています。間違いやすいポイントですので注意してください。

GZIP ファイルをダウンロードして解凍するサンプル

DecompressionStream クラスから生成した TransformStream オブジェクトを単独で使うことはできません。実際には、他の ReadableStream と連結して使います。分かりやすい例を挙げるなら、fetch のデータ受信で得られる ReadableStream オブジェクトに、DecompressionStream クラスから生成した TransformStream オブジェクトを連結するケースが考えられます。ReadableStream オブジェクトに TransformStream オブジェクトを連結するには、ReadableStream オブジェクトの pipeThrough() メソッドを使います。

以下のサンプルコードは fetch で GZIP 圧縮ファイル text.gzip をダウンロードしながらデータを解凍する ReadableStream オブジェクトを生成し、そこから得られたデータをコンソールに出力します。text.gzip は Shift_JIS のテキストファイルを圧縮したものとします。なお、以降のコードは async ブロックの中を前提にしています。

// GZIP ファイルを fetch でダウンロード開始
const response = await fetch('text.gzip');

// fetch の ReadableStream (response.body) に
//   GZIP の DecompressionStream を連結
const rstream = response.body.pipeThrough(
    new DecompressionStream('gzip')
);

// ReadableStreamDefaultReader オブジェクト
const reader = await rstream.getReader();
while (true) {
    // チャンクを取得
    const { done, value } = await reader.read();

    // ストリームが終わったらループを抜ける
    if (done === true) {
        console.log('終了');
        break;
    }

    // value は Uint8Array オブジェクト
    console.log(value);
}

これで圧縮データの解凍はできたものの、この ReadableStream オブジェクトから得られるデータはバイナリーデータです。実際には Uint8Array オブジェクトが得られることになります。元の圧縮データがテキストファイルとはいえ、ブラウザーからは単なるバイナリーデータにしか見えず、文字列として認識されません。これを解決するのが次のコードです。

// GZIP ファイルを fetch でダウンロード開始
const response = await fetch('text.gzip');

// fetch の ReadableStream (response.body) に
//   GZIP の DecompressionStream を連結し、
//   さらに SHIFT_JIS の TextDecoderStream を連結
const rstream = response.body.pipeThrough(
    new DecompressionStream('gzip')
).pipeThrough(
    new TextDecoderStream('shift_jis')
);

// ReadableStreamDefaultReader オブジェクト
const reader = await rstream.getReader();

while (true) {
    // チャンクを取得
    const { done, value } = await reader.read();

    // ストリームが終わったらループを抜ける
    if (done === true) {
        console.log('終了');
        break;
    }

    // value は文字列
    console.log(value);
}

ReadableStream オブジェクトの pipeThrough() を 2 回連結している点に注目してください。このように ReadableStreampipeThrough() を使って様々な役割を持つ TransformStream をいくつでも連結することができます。今回はデータの解凍に加え、TextDecoderStream を連結しています。この TextDecoderStream では、 Shift_JIS エンコーディングの外部文字列(ブラウザーは単なるバイナリーデータとして認識)をブラウザーの内部文字列として扱えるよう変換(デコード)します。このおかげで、上記コードでは、チャンクの読み取り結果が Uint8Array オブジェクトではなく文字列になります。

なお、上記サンプルで、たとえテキストファイルのエンコーディングが UTF-8 だったとしても、TextDecoderStream を使って内部文字列にデコードしないと文字列として認識されませんので注意してください。

まとめ

筆者自身、Compression Streams API の使いどころがなかなか思いつかず、今回の記事では GZIP ファイルのダウンロードと解凍を例に挙げてしまいましたが、本来、Compression Streams API は圧縮ファイルを扱う API ではないため、この API の本質を紹介できていないのが悔やまれます。何か他に適切な利用例を思いついたら、改めて紹介しようと思います。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share