.tar.gzファイルをcatコマンドで結合できないのはなぜか?
Why can't you combine .tar.gz files with cat? (alexwlchan.net)
要約
この記事は、複数の.tar.gzファイルを結合しようとした際にcatコマンドが機能しない理由を解説しています。tarとgzipの内部構造と動作原理を説明し、なぜ単純なバイト列の連結が失敗するのか、そして安全に結合するための正しい方法(Pythonのtarfileモジュールを使用)を示しています。
全文翻訳
catコマンドで.tar.gzファイルを結合できないのはなぜか?
私は複数の.tar.gzアーカイブを生成し、それらを1つの最終ファイルに結合する必要があるプロジェクトに取り組んでいます。単純にバイトをcatコマンドで結合できると思っていましたが、それはうまくいきませんでした。この一見簡単なタスクは、tarとgzipに対する私の誤った理解を露呈しました。
コードを修正するために、まず私のメンタルモデルを修正する必要がありました。そしてそれは、テープドライブ、特許法、そしてEOFマーカーへと私を導きました。
tarはtape archiveの略
tarは、複数のファイルとそのメタデータ(ファイル名、タイムスタンプ、ディレクトリ構造)を1つのファイルにまとめるファイルアーカイバです。
元々は磁気テープ用に設計されており、そのファイル構造はメディアの物理的な制約に基づいています。
- シーケンシャルリード。 磁気テープは、最初から始めてテープの最後まで再生するのが最も効率的です。
- 追記専用書き込み。 初期のテープは、既存のデータを置き換えるのではなく、レコードの末尾にのみデータを追記できました。
- 固定データサイズ。 テープには固定容量があり、初期のテープは固定データブロックサイズを持っていました。
内部的には、tarアーカイブは、固定サイズのブロックに分割されたファイルのシーケンスです。ファイルにはヘッダーブロック(ファイル名やファイルサイズなどのメタデータを含む)とデータブロック(ファイルの内容)があります。ファイルの後に、すべてゼロで埋められた2つ以上のブロックがあります。これらは、リーダーにアーカイブのそれ以降を無視するように指示するEOFマーカー(End-Of-File marker)を形成します。
tarアーカイブの内部構造を示すアーキテクチャ図。ヘッダーとデータブロックを持つ2つのファイル、2つのゼロブロック、そして2つの無視されるブロックがあります。
この構造は物理的なテープを反映しています。ファイルをシーケンシャルに読み取ったり、末尾に新しいファイルを追加したりできます。このシーケンシャルな設計が、tarがネットワーク経由でのストリーミングに人気がある理由です。完全なアーカイブをダウンロードするのを待たずに、受信したファイルをすぐに処理できます。
この構造を知ることで、以前は混乱していたtarの側面を理解できるようになりました。
- ファイルサイズは事前に宣言する必要があります。 データブロックを書き込む前に、ヘッダーにファイルサイズを書き込む必要があります。Pythonの
TarFile.addfileAPIを使用する際、tarinfo.sizeを設定し忘れることがよくあります。その結果、Pythonはヘッダーに0を書き込み、空のアーカイブを作成します。 - アーカイブには重複するファイル名を含めることができます。 テープ上の既存のブロックを編集または削除できないため、同じファイル名を持つ新しいバージョンを追記してファイルを更新します。アーカイブを展開すると、後のファイルが前のファイルを上書きします。
- EOFマーカー以降はすべて無視されます。 物理テープには固定容量があるため、EOFマーカーはデータの終了位置と空のテープの開始位置を示します。GNU tarのようなツールにはEOFマーカーを過ぎて読み取りを続けるための
--ignore-zerosフラグがありますが、私はデフォルト設定で読み取れるアーカイブを作成したいと考えています。
私はtarアーカイブを単純にcatコマンドで結合しようとしましたが、リーダーが最初のEOFマーカーで停止するため、これは失敗します。代わりに、Pythonのtarfileモジュールを使用してアーカイブを結合しています。各アーカイブを展開し、そのメンバーを新しいアーカイブにコピーします。この新しいアーカイブには、単一のEOFマーカーが設定されます。
import tarfile
def combine_tars(output_file, input_files):
"""
複数のtarアーカイブを1つのアーカイブに結合します。
"""
with tarfile.open(output_file, "w") as out:
for f in input_files:
with tarfile.open(f, "r") as src:
for member in src.getmembers():
out.addfile(member, src.extractfile(member))
combine_tars("numbers.tar", ["one.tar", "two.tar", "three.tar"])
これは生のバイトを連結するよりも多くのコードが必要ですが、特別な設定なしで読み取れるtarアーカイブを作成します。
gzipは単一のデータストリームを圧縮する
gzipは、単一のファイルまたはデータストリームを受け取り、それを小さくするストリーム圧縮器です。圧縮はロスレスなので、元に戻して元のファイルを取得できます。
tarとは異なり、gzipは物理ハードウェアではなく特許法への対応として生まれました。RFC 1952(gzipファイル形式を定義)を読むと、その作成当時の時代を反映した3つの設計上の制約が見られます。
- 特許フリー。 gzipツールは、当時特許で保護されていた基盤となるLZWアルゴリズムを使用していた圧縮ツール
compressのフリーソフトウェア代替として作成されました。 - ストリーム可能。 gzipファイルの圧縮または解凍は、少量で境界のあるメモリのみを使用する必要があります。今日よりもRAMがはるかに希少で高価だった1990年代初頭には、小さな連続したチャンクでデータを処理できる能力が不可欠でした。
- ポータブル。 gzipファイルは、作成されたコンピュータのCPU、OS、ファイルシステム、その他の側面から独立している必要があります。私たちは今日、このようなポータビリティを当然のことと考えていますが、常にそうであったわけではありません。
内部的には、gzipファイルは1つ以上の「メンバー」のシーケンスです。各メンバーにはヘッダー(元のファイル名や変更時刻などのメタデータを含む)、圧縮されたデータ、そしてトレーラー(CRC32チェックサムと非圧縮サイズを含む)があります。ファイルは最後のトレーラーの後に終了します。gzipにはEOFマーカーはありません。
gzipファイルの内部構造を示すアーキテクチャ図。ヘッダー、データブロック、トレーラーを持つ3つのメンバーがそれぞれあります。
概念的には、メンバーをファイルのアナログと見なしたくなりますが、gzipはそうは機能しません。ツールは複数のメンバーを同じデータストリームの一部として扱いますが、個別にリストしたり抽出したりすることはできません。マルチメンバーgzipファイルを解凍すると、単一のストリームしか返されません。
メンバーが次々と続き、EOFマーカーがないため、バイトを単純にcatコマンドで結合することでgzipファイルを連結できます。
echo "one uno eins" | gzip > one.gz
echo "two duo zwei" | gzip > two.gz
echo "three tres drei" | gzip > three.gz
cat one.gz two.gz three.gz > numbers.gz
gunzip --uncompress --to-stdout numbers.gz
.tar.gzアーカイブをどのように結合するか?
tarとgzipは固い絆で結ばれています。tarはディレクトリツリーを単一のストリームに結合し、gzipはそのストリームを小さくします。どちらもシーケンシャルリードをサポートしているため、.tar.gzはネットワーク経由でデータをストリーミングするのに非常に人気があります。これにより、アーカイブ全体をダウンロードする前に個々のファイルの処理を開始できます。
私の間違いは、.tar.gzファイルをcatコマンドで結合しようとしたことです。gzipは圧縮されたメンバーを単一のストリームにうまく結合しますが、tarが解凍されたストリームを読み取ろうとすると、最初のアーカイブのEOFマーカーに遭遇して読み取りを停止します。
.tar.gzファイルを安全に結合するには、基盤となるメンバーを抽出し、それらを新しいファイルに書き込む必要があります。これは、上記のPython関数をプレーンな読み書き(r/w)からgzip圧縮された読み書き(r:gz/w:gz)に変更することを意味します。
import tarfile
def combine_tar_gzs(output_file, input_files):
"""
複数のgzip圧縮tarアーカイブを1つのアーカイブに結合します。
"""
with tarfile.open(output_file, "w:gz") as out:
for f in input_files:
with tarfile.open(f, "r:gz") as src:
for member in src.getmembers():
out.addfile(member, src.extractfile(member))
combine_tar_gzs("numbers.tar.gz", ["one.tar.gz", "two.tar.gz", "three.tar.gz"])
これは混乱するバグから始まりましたが、楽しいサイドクエストになりました。今ではこれらのフォーマットがどのように機能するか、元のコードがなぜ機能しないのか、そしてそれをどのように修正できるのかを理解しています。秘密のショートカットや明らかな最適化を見逃していないという知識を胸に、プロジェクトに戻ることができます。