Python でマルチストリームと呼ばれる形式の bzip2 圧縮ファイルを効率的に展開する方法を調べます。
この記事には .NET Framework 版があります。
マルチストリーム
個別に bzip2 で圧縮して連結したデータをマルチストリームと呼びます。
$ echo -n hello | bzip2 > a.bz2
$ echo -n world | bzip2 > b.bz2
$ cat a.bz2 b.bz2 > ab.bz2
bzcat
などのコマンドでそのまま扱えます。
$ bzcat ab.bz2
helloworld
並列圧縮の pbzip2 や Wikipedia のダンプなどで使用されます。
bz2 モジュール
Python の bz2 モジュールはバージョン 3.3 からマルチストリーム(複数のストリーム)をサポートしました。
バージョン 3.3 で変更: 'a' (追記) モードが追加され、複数のストリームの読み込みがサポートされました。
以下、このドキュメントを引用しながら進めます。
一括展開
bz2 ファイルを bz2.open()
で開けば、展開されたデータが読み込めます。
デフォルトがバイナリモードのため、テキスト読み取りでは rt
を指定します。
引数 mode には、バイナリモード用に
'r'
、'rb'
、'w'
、'wb'
、'x'
、'xb'
、'a'
、あるいは'ab'
、テキストモード用に'rt'
、'wt'
、'xt'
、あるいは'at'
を指定できます。デフォルトは'rb'
です。
マルチストリームの例として作った ab.bz2 の展開を試みます。
import bz2
with bz2.open("ab.bz2", "rt", encoding="utf-8") as f:
print(f.read())
helloworld
すべてのストリームが一括で展開されました。
逐次展開
マルチストリームを逐次的に展開する場合、BZ2Decompressor を使います。
BZ2Decompressor
クラスを用いて、複数のストリームからなるデータを展開する場合は、それぞれのストリームについてデコンプレッサオブジェクトを用意してください。
BZ2Decompressor
の処理フローは次のようになります。インスタンスごとに 1 ストリームを処理します。事前に圧縮データのストリームの切れ目が分からなくても、未処理のデータを回収できる仕組みです。
- インスタンスを作成:
decompressor = BZ2Decompressor()
- 圧縮データを渡して展開:
bytes = decompressor.decompress(data)
- ストリームの終端までしかデータは展開されないため、未処理のデータがないか
decompressor.unused_data
を確認。 - 未処理のデータがあれば、1.に戻って次のストリームを展開。
これを実装します。
import bz2
with open("ab.bz2", "rb") as f:
data = f.read()
while data:
decompressor = bz2.BZ2Decompressor()
bytes = decompressor.decompress(data)
data = decompressor.unused_data
print(bytes.decode("utf-8"))
hello
world
ストリームごとに逐次展開できました。
Wikipedia
巨大なファイルの逐次展開を試みます。
Wikipedia のダンプデータがマルチストリームの巨大な bz2 ファイルです。全体を展開しなくてもデータを部分的に取り出せるようにストリームで分割されています。
日本語版だとあまりにも巨大過ぎるため、サイズが 1/100 ほどのアイスランド語版を使用します。
記事執筆時点で入手可能な2020年5月1日版より、以下のファイルを使用します。
- iswiki-20200501-pages-articles-multistream.xml.bz2 45.7 MB
※ ファイルサイズは統計を見て当たりを付けました。
全体読み込み
【注意】このセクションのコードは実験用です。遅いため実用には適しません。
先ほどの ab.bz2 と同じように、最初に圧縮ファイル全体を読み込んでストリームを逐次展開します。
内容が多過ぎるため表示はしないで、ストリーム数と展開後のバイト数を数えます。
import bz2
target = "iswiki-20200501-pages-articles-multistream.xml.bz2"
with open(target, "rb") as f:
data = f.read()
streams = 0
bytes = 0
while data:
decompressor = bz2.BZ2Decompressor()
streams += 1
bytes += len(decompressor.decompress(data))
data = decompressor.unused_data
print(f"streams: {streams:,}, bytes: {bytes:,}")
$ time python multistream-1.py
streams: 1,022, bytes: 207,530,810
real 0m22.503s
user 0m9.531s
sys 0m12.969s
メモリマップ
【注意】このセクションのコードは実験用です。遅いため実用には適しません。
比較のため圧縮ファイルをメモリマップしてみます。
【参考】 mmap --- メモリマップファイル — Python 3 ドキュメント
import bz2, mmap
target = "iswiki-20200501-pages-articles-multistream.xml.bz2"
with open(target, "rb") as f:
with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
data = mm
streams = 0
bytes = 0
while data:
decompressor = bz2.BZ2Decompressor()
streams += 1
bytes += len(decompressor.decompress(data))
data = decompressor.unused_data
print(f"streams: {streams:,}, bytes: {bytes:,}")
$ time python multistream-2.py
streams: 1,022, bytes: 207,530,810
real 0m22.450s
user 0m10.484s
sys 0m11.953s
処理時間はほとんど変わりませんでした。
逐次読み込み
【注意】このセクションのコードは、速度的には実用できます。ただしストリームを逐次展開する必要がなければ、もっと簡単に書けます。
ファイルを一度に読み込まないで、少しずつ読み込みます。
フローがやや複雑になります。decompressor に渡したデータがストリームの終端まで達しているか eof
で確認して、終端に達していなければ同じインスタンスを使い続けます。
import bz2
target = "iswiki-20200501-pages-articles-multistream.xml.bz2"
streams = 0
bytes = 0
size = 1024 * 1024 # 1MB
with open(target, "rb") as f:
decompressor = bz2.BZ2Decompressor()
data = b''
while data or (data := f.read(size)):
bytes += len(decompressor.decompress(data))
data = decompressor.unused_data
if decompressor.eof:
streams += 1
decompressor = bz2.BZ2Decompressor()
print(f"streams: {streams:,}, bytes: {bytes:,}")
$ time python multistream-3.py
streams: 1,022, bytes: 207,530,810
real 0m6.839s
user 0m6.734s
sys 0m0.094s
劇的に高速化しました。どうやら今までは展開以外に時間が掛かっていたようです。
size
を変えて試しましたが、5MB を超えた辺りから明らかに遅くなり始めました。
データ取得
ジェネレーターによってストリームごとに展開したデータを取得する例です。
import bz2
target = "iswiki-20200501-pages-articles-multistream.xml.bz2"
size = 1024 * 1024 # 1MB
def getstreams():
with open(target, "rb") as f:
decompressor = bz2.BZ2Decompressor()
bytes = b''
data = b''
while data or (data := f.read(size)):
bytes += decompressor.decompress(data)
data = decompressor.unused_data
if decompressor.eof:
yield bytes
bytes = b''
decompressor = bz2.BZ2Decompressor()
lens = [len(bytes) for bytes in getstreams()]
print(f"streams: {len(lens):,}, bytes: {sum(lens):,}")
まとめ
【注意】ストリームを逐次展開する必要がなければ、このセクションの一括展開を使用してください。
巨大ファイルをストリームごとに逐次展開する場合、入力する圧縮データも逐次読み込みする必要があります。
unused_data
のスライス時にデータのコピーが発生するため、一度に巨大なデータを渡すと無駄なコピーが大量に発生するためだと思われます。
比較として、ストリームで区切らずに一括展開します。
import bz2
target = "iswiki-20200501-pages-articles-multistream.xml.bz2"
bytes = 0
size = 1024 * 1024 # 1MB
with bz2.open(target, "rb") as f:
while (data := f.read(size)):
bytes += len(data)
print(f"bytes: {bytes:,}")
$ time python multistream-4.py
bytes: 207,530,810
real 0m6.685s
user 0m6.594s
sys 0m0.094s
逐次展開に比べて多少速いです。Wikipedia のようにストリームに意味がある場合を除けば、逐次展開にあまり意味はなさそうです。
関連記事
Wikipedia のダンプについての記事です。
多言語辞書 Wiktionary のダンプからデータを抽出する記事です。Wikipedia の関連プロジェクトでダンプの形式は同じです。
BZ2Decompressor.decompress()
の翻訳について修正を提案しました。
参考
3.3 以前の Python でマルチストリームを扱った記事です。
pbzip2 の速度を計測した記事です。