適当なキーワードでパッケージを探したら本当にあった
Ruby にziplineという gem がある。ファイルのリストを渡すと zip に固めてストリーミングダウンロードしてくれるという Rails 向けの gem だ。今回同じことを Python で行いたく、似たような機能のパッケージ探してみた。
すると全く同じ名前のパッケージを見つけたけど、これziplineアルゴリズムトレードライブラリだ!
しょうがないから google で「python zip stream」あたりのキーワードで適当に検索を掛けてみると、ドンピシャなパッケージpython-zipstreamが一発ヒットしてビビる。
これは使えそうだ
ダミーファイル作成
まずダウンロードテスト用のダミーファイルを作る
dd if=/dev/random of=dummy1.txt bs=1M count=100
for i in {1..10} ; do dd if=/dev/random of=dummy${i}.txt bs=1M count=100; done
これでdummy1.txt
~dummy10.txt
まで 100MB のファイルが 10 個作成された。
zip をストリーミングダウンロード
先に成功したコードを挙げておく。
これで開発環境の場合はhttp://localhost:3030/zip
にアクセスすると、dummy1.txt
~dummy10.txt
を zip したfiles.zip
がストリーミングでダウンロードされる。
import zipstream |
本家のREADME
を読んだ感じ、使い方がよく理解できなかったので軽く解説を入れてみる。
zip = zipstream.ZipFile(mode='w', compression=zipstream.ZIP_DEFLATED) |
まずは ZipFile のインスタンスを作る。zipstream.ZipFile
はzipfile.ZipFile
を継承したラッパークラスなので、共通のインターフェースになっている。ここを読むと分かりやすいかもしれない。
mode='w'
を指定しているが、ここはデフォルトも'w'
だし、'w'
が含まれないとRuntimeError
になる。compression
はデフォルトZIP_STORED
なのでデフォルトだと圧縮されない。取り合えず圧縮したいレベルならZIP_DEFLATED
でお K。
- ZIP_STORED : 圧縮無しでファイルをまとめるだけ
- ZIP_DEFLATED : 通常の ZIP 圧縮、
zlib
モジュールが必要 - ZIP_BZIP2 : BZIP2 での圧縮を行う、
bz2
モジュールが必要 - ZIP_LZMA : LZMA での圧縮を行う、
lzma
モジュールが必要
zip.write((path / 'dummy1.txt').as_posix(), arcname='1.txt') |
ZipFile インスタンスにファイルを渡していく。ここもzipfile.ZipFile#write
とインターフェースは同じで、write('ファイルパス', arcname='アーカイブされるファイル名', compress_type='個別の圧縮指定')
になる。
ここでディレクトリを渡してしまうと空のディレクトリが入ってしまうので注意が必要。
write_iter(arcname, iterable, compress_type)
メソッドではファイルパスではなくイテレータを渡すこともできるようだ。
.as_posix()
してるのはファイルパスとしてpathlib
を受け付けないから…残念!
for chunk in zip: |
zipstream.ZipFile
はイテレータで圧縮済みデータを返してくれる。
具体的にはwrite
したファイルのリストからファイルを順次読み出し、1024 * 8
バイト読んで圧縮して都度データを返す。
zip_filename = 'files.zip' |
最後はbottle
のresponse
部分だ。Content-Type
は'application/zip'
を指定する。Content-Disposition
にはファイルをアタッチすることで即ダウンロードする動きをしてくれる。response.body
はジェネレータを受けることができるので、generator()
関数をそのままセットするとストリーム処理するようになってくれる、ありがたい。
Content-Length は?
Content-Length
を指定しないとダウンロードの残り時間表示が「不明」になってしまうが、圧縮後のサイズが事前に分からないのでここはしょうがない。
試しにContent-Length
に適当な値を設定してみたら、ダウンロードが強制中断になってしまった。
実行環境
- Python 3.6.3
- bottle 0.12.13
- zipstream 1.1.4