不完全過ぎる zip2tar、あるいは、zip なんかなくなってしまえばいいのに

出来合いのソフトウェアを探し回って疲れ果てるくらいなら作ってしまった方が早い、と考えるタイプの、なおかつ、毎度短時間で書いてしまって整理しないので何度も書くハメになる、あるいは zip なんかこの世から消えてしまいたまえ。

探せばありそうな気は毎度するんだけどね、Windows であってもMSYS生活なワタシにとっては、スクリプト書いちゃえばいいじゃん、という類の、「zip だったものを別のアーカイバで再圧縮」なタスク。

だって毎度10分かからんで書けてしまいますもの、シェルスクリプトで。普段ならこんなかな:

aaa.sh
 1 #! /bin/sh
 2 trap 'rm -fr .tmp' 0 1 2 3 15
 3 
 4 for i in "$@" ; do
 5     rm -fr ".tmp"
 6     bn=`basename $i .zip`
 7     mkdir ".tmp"
 8     unzip "$i" -d ".tmp"
 9     (  # sub-shell
10       cd ".tmp"
11       tar cvf ../"${bn}".tar * && bzip2 -9v ../"${bn}".tar
12     ) && rm -fv "$i"
13 done

今ほんとに10分以内で書いた。正確に測ってないけど、多分5分くらい。だからこそ整理しないまま下手すりゃ、というか確実に「紛失」し、忘れ去り、何ヶ月も何年も経ってから「出来合いのソフトウェアを探し回って疲れ果てるくらいなら作ってしまった方が早い」を繰り返す。最初から残すつもりないんだもん、aaa.sh なんて名前にするのは、ほんとに毎度。あぁバッドサイクル。

でさ、一生このままバッドサイクル繰り返したって別に死にやしないと思うんだけど、今回立体地形図を作りたいてことから、国土地理院の基盤地図情報を結構大量にダウンロードしていて、「今でしょ」なタイミングかなと思って。何せ非力なネット環境で苦労してダウンロードしてるもんだから、もったいなくて消せないけれど、圧縮率のとっても低い zip だもんで、かさばってしょーがないし。これからも興味のある地域のをダウンロードし続けるからさ、さすがに今回は「残る」ものがいい。

マイクロソフトが Windows XP で正式搭載してからというもの、残念ながら「zip が最もポータブルなアーカイバ」になってしまった。デファクトスタンダードといってよい。なんであれ標準めいたものを決定付けたマイクロソフトの功績は大きいとは思うものの、いかんせん、「なんでよりによって zip」。ほんとうは、真に「エンドユーザ目線」では、圧縮率を重視して欲しい。「いつでもどこででも使える」というポータビリティは、多くの場合はフリーソフトウェア群が「追いついて来る」から、どこかの大きな組織が「決断」さえしてしまえば、ユーザは結構平気で付いてくるはずである。(zip の日本語ファイル問題も致命傷だと思うんだけどなぁ。)

てわけで、「国土地理院の基盤地図情報」なんてさぁ、「デラ・でっかい」わけっす。メジャーじゃなくても高圧縮なものを使う、て決断、出来ないもんなんだろうか?

とはいえなにゆえにそういう決断ができないのかは想像がつく。一つには「エンドユーザのため」(思い込み)。一つには「標準だから」(あなたの決断次第で世界は変わるかもしれないのに)。一つには「標準インフラで作れるから」(スキルがないだけ)。一つには「周囲を説得できないから」(する気もないくせに)。たぶんきっとおそらく確実に絶対に、エンドユーザ不在の決断しか出来ないわけだ。

なんでエンドユーザ目線では「圧縮率最優先」と思うのか? 20年前じゃあるまいし。いや、それは違う。ディスクを湯水のように使えるようになれば、「人はほんとうに湯水のように使う」ので、ファイルサイズは大きくなる一方だ。ネットワークが高速になれば、「人は転送サイズに無頓着になる」ので、取り回しに苦労する大きなサイズのファイルは増える一方だ。ところが一方では、最近はコストパフォーマンスの良さやタブレット端末の普及により、光回線よりも無線回線が選ばれることが多く、むしろ存分に快適とは言えないネット環境を持つユーザは、一昔前とそんなに変わらなくなって来てるんじゃないか?

企業が業務で使うデータが「大きい」場合に一番困るのは、最初にワールドワイドなネットから持ってくる際ではない。企業内 LAN 内の「ファイルサーバ」との行き来が大変なのだ。どこでもかしこでも高速回線で繋いでいるわけではない。大抵の企業では、1GB のファイルを移動するだけでも大変なことになるであろう。ある程度のサイズになってくると、自PCで長時間かけて「圧縮」してから長時間かけて移動した方が、圧縮しないまま移動するよりも何百倍も早い場合がある。例えば圧縮に1時間かけて10分で移動出来るのに、そのまま移動すると10時間かかる、なんてザラだ。

つまりファイルが大きければ大きいほど、LAN 環境が普通であれば普通であるほど、「圧縮率こそが高速化の鍵」というのは、至極もっともな話なのである。移動に10時間かかるファイルを2つ、なんて、丸一日作業ぶっとびかねんぞ。そんなヘビィな移動、PCの CPU パワーも喰らうしな。

てわけで、zip なんかなくなってしまえばいいのに。






なんて極端な議論はまぁ脇に置いといて。だいたいにして、Microsoft だけじゃなくて、zip 依存のもの、多過ぎるもん。Python の egg とか wheel もだし、Java の jar もだし。実際なくなったらワタシだって泣くよ。

最初に書いたシェルスクリプトじゃなくて、整理して残しておこうと思ったのは Python で作ったヤツ。MSYS古いやつを使ってると unzip は標準で入ってないしね。Python だと zip, tar, gzip, bzip2 が標準で使えるし。

てわけで、「不完全過ぎる zip2tar」:

zip2tar.py
 1 #! /bin/env python
 2 # -*- coding: utf-8 -*-
 3 import sys
 4 import os
 5 import tarfile
 6 import zipfile
 7 import StringIO
 8 import calendar
 9 import datetime
10 import argparse
11 
12 
13 _ts = int((datetime.datetime.now() -
14            datetime.datetime.utcnow()).total_seconds())
15 
16 
17 def _process_one_zip(
18     zfn,
19     remove_original_zip,
20     compressor,
21     compresslevel):
22 
23     if compressor == "bz2":
24         op = tarfile.TarFile.bz2open
25         ext = ".bz2"
26     else:
27         op = tarfile.TarFile.gzopen
28         ext = ".gz"
29 
30     with zipfile.ZipFile(zfn, 'r') as zf:
31         with op(
32             os.path.splitext(zfn)[0] + ".tar" + ext,
33             mode="w",
34             compresslevel=compresslevel) as tf:
35 
36             for zi in zf.infolist():
37                 d = zf.read(zi)
38                 ti = tarfile.TarInfo(zi.filename)
39                 ti.size = zi.file_size
40                 ti.uname = os.environ["USERNAME"]
41                 ti.gname = os.environ["USERNAME"]
42                 ti.mtime = calendar.timegm(zi.date_time) - _ts
43                 cont = StringIO.StringIO(d)
44                 tf.addfile(ti, cont)
45                 print("%s: %s" % (zi.file_size, zi.filename))
46     if remove_original_zip:
47         try:
48             os.unlink(zfn)
49             print("remove %s" % zfn)
50         except IOError as e:
51             print(str(e))
52 
53 
54 if __name__ == '__main__':
55     parser = argparse.ArgumentParser()
56     parser.add_argument('zipfile', nargs='+')
57     parser.add_argument(
58         '--remove-original-zip', action='store_true', default=False)
59     parser.add_argument(
60         '--level', type=int, default=9)
61     parser.add_argument(
62         '--compressor', choices=["bz2", "gz"], default="bz2")
63     args = parser.parse_args(sys.argv[1:])
64 
65     for zfn in args.zipfile:
66         _process_one_zip(
67             zfn,
68             args.remove_original_zip,
69             args.compressor,
70             args.level)

tarfile、zipfile は毎日使うもんではないんで、「一瞬で」は書けてないけど、多分初版は1時間以内で書いたと思う。欲が出て「gzip にも出来るようにしとこう」「圧縮率も変えたい」とやってたら1時間は超えた。こうなってくると「毎度毎度」書きたくはない。ちゃんと残ってないとイヤン。

どんだけ「不完全」か? 簡単に列挙しとく。

  • zip内がフォルダ階層を持ってる場合のテストを一切してない(ダメかもしんない)
  • 日本語ファイル名が含まれてたらお亡くなることでせう。
  • でもどーせzip内の日本語ファイル名、ぶっ壊れるから、いいんでね? (utf8だったりcp932だったりするがそれを区別する方法がない
  • ↑に関連し、PAX_FORMATについての戦略を考えるべきなのね、ほんとはね。
  • undocument な仕様(tarfile.TarFile.bz2open、tarfile.TarFile.gzopen)への依存。
  • しかもそれって Python 3.x では違う場所。
  • ていうか Python 2.7 でしか動きません。
  • エントリの属性(日付・ユーザなど)にはかなり無頓着。これは Windows と Unix 両方に色目使うと致し方ないところもあるので許せ。
  • といいつつ Windows の MSYS をベースに作ったので、環境変数 USERNAME は Unix ではアヤシイ。これ、確か Windows 固有だったような? (今手許ですぐさま linux 動かす気にはなれないので確認はしません。
  • 属性に使える情報取得には「Unixでしか使えない」os関数が必要だったりすんのよ。

なんてこと書くと全然使い物にならんほど酷いものに感じるかもしれんけど、別にそうでもないよ。今のワタシには十分。必要に応じて育てればいいことであるし。






ところでほんとは「毎度探し回るのがイヤで」というのは、今回は少しウソ。ちょっとだけ探ったの。zip⇔7zipを CUI でやりたくてさ。公式 7zip はコマンドラインから使いにくいし、ってことで、Python から使える 7zip のラッパーはないかしら、と。これはあったんだけれど、libarchive という Unix 生まれのライブラリへの ctypes ラッパーだった。ということはつまり、libarchive の Windows 版が必要、ということになる。うーん、後にしよう、と。

その Python ラッパーはサンプルコードを見る限り、とても使いやすそうだったので、興味はあるんだけれども、libarchive のハードルの高さがわからんのでね。こういうの、ダメなときは全然ダメだからねぇ。






さて。「立体地形図を作りたい」でこれまでダウンロードしてきた「基盤地図情報 数値標高モデル」なんだけれども、気付いたら 1337 個も持っていた。これでもせいぜい関東と東海北陸の一部と宮城県のみ、なのね、すげーサイズになっておる。これを件のスクリプトで、tar + bzip2 (圧縮レベル9) に全部再圧縮してみた。なお、これをするのに起こったのがこれ

せっかくなので、zip 時点でのファイルサイズを記録しておき、再圧縮後のファイルサイズも記録した。感覚的にはだいたいいつでも半分くらいのサイズにはなるのはわかっているのだが、実際の正確なところを見ておきたい。サイズの記録はこんなよ:

例によって MSYS 前提
1 me@host: ~$ stat -c '%n: %s' *.zip | sort > size_zip.txt
2 me@host: ~$ stat -c '%n: %s' *.bz2 | sort > size_bz2.txt

結果の抜粋:

1 FG-GML-4839-56-DEM5A.zip: 580748
2 FG-GML-4939-46-DEM5A.zip: 1312806
3 FG-GML-4939-55-DEM5A.zip: 655821
4 FG-GML-4939-56-DEM5A.zip: 4394515
5 FG-GML-5039-64-DEM5A.zip: 1879913
6 FG-GML-5039-65-DEM5A.zip: 120052
7 FG-GML-5137-75-DEM5A.zip: 152167
1 FG-GML-4839-56-DEM5A.tar.bz2: 362235
2 FG-GML-4939-46-DEM5A.tar.bz2: 839239
3 FG-GML-4939-55-DEM5A.tar.bz2: 416133
4 FG-GML-4939-56-DEM5A.tar.bz2: 2824203
5 FG-GML-5039-64-DEM5A.tar.bz2: 1249817
6 FG-GML-5039-65-DEM5A.tar.bz2: 68765
7 FG-GML-5137-75-DEM5A.tar.bz2: 75430

抜粋した範囲では、2/3~1/2くらい? これを正確に見たいわけ。

中身は全部が同じ形式の XML (GML) なわけなので、「圧縮率のバリエーション」の真の姿を現すものではないけれど、このサイズ比を可視化して、わかった気になってみたいな、と思ってさ。

ヒストグラムでも作ってみるか:

size_viz.py
 1 # -*- coding: utf-8 -*-
 2 # size_zip.txt
 3 # FG-GML-4839-56-DEM5A.zip: 580748
 4 #
 5 # size_bz2.txt
 6 # FG-GML-4839-56-DEM5A.tar.bz2: 362235
 7 d = {}
 8 for fn in ("size_zip.txt", "size_bz2.txt"):
 9     with open(fn, "r") as fi:
10         for line in fi.readlines():
11             line = line.strip()
12             if not line:
13                 break
14             n, s = line.split(": ")
15             n = n.split(".")[0].replace(
16                 "FG-GML-", "").replace(
17                 "DEM", "")
18             if n not in d:
19                 d[n] = [int(s)]
20             else:
21                 d[n].append(float(s))
22 
23 import numpy as np
24 import matplotlib.pyplot as plt
25 
26 num_bins = 50
27 
28 n, bins, patches = plt.hist(
29     [100 * d[k][1] / d[k][0] for k in d],
30     num_bins,
31     #normed=1,
32     facecolor='green',
33     alpha=0.5)
34 
35 bz2total = sum((d[k][1] for k in d))
36 ziptotal = sum((d[k][0] for k in d))
37 plt.title(
38     "bz2 total: {:.1f} [MBytes]\nzip total: {:.1f} [MBytes]\n{:.1f} [%]".format(
39         bz2total / (1024.**2),
40         ziptotal / (1024.**2),
41         bz2total / ziptotal * 100))
42 
43 plt.grid(True)
44 plt.xlabel('(size of bz2) / (size of zip) [%]')
45 plt.ylabel('Occurs')
46 #plt.subplots_adjust(left=0.15)
47 #plt.show()
48 plt.savefig("size_vis_hist.png", bbox_inches="tight")

ふーん、凄いね。最悪でも 75% のサイズにはなってる。1337 個だからそれなりの母集団。これまでの感覚には合うし、物によっては 1/5 のサイズにさえなってるね。全体ではおよそ 2/3 か。半分にはならない、のは、ピークが 65% あたりにあることから腑に落ちますな。

なんにしても bz2 の圧縮率が高いというよりは、zip の圧縮率が低い、てことね。

上の方で書いた「過激な文句」では触れなかったんだけれど、なんかね、基盤地図情報ダウンロードのサイト自体が重いみたいで、「あたしんち」のネットワーク環境に問題がなくてもとんでもない速度のことが多いのね。2MB ごときのダウンロードに10分かかっちゃう、みたいなことが頻発すんの。日によって、時間によっては数秒なのに。これ、随分前から「あたしんち」以外からもそうなので、そういうサイトなんだと思う。だからこそ、2MB が 1MB になるのはとんでもなく大きいわけで。だって10分が5分だよ? 10個ダウンロードするのに、100分が50分。全然違うでしょう。tar + bz2 でも 7zip でもいいんだけど、とにかくこういう「大きなファイルを大量に」配布するようなサービスは、ファイルを小さくする努力をして欲しいわけさ。

あんまり意味はないんだけれど、散布図も書いてみる:

size_viz2.py
 1 # -*- coding: utf-8 -*-
 2 # size_zip.txt
 3 # FG-GML-4839-56-DEM5A.zip: 580748
 4 #
 5 # size_bz2.txt
 6 # FG-GML-4839-56-DEM5A.tar.bz2: 362235
 7 d = {}
 8 for fn in ("size_zip.txt", "size_bz2.txt"):
 9     with open(fn, "r") as fi:
10         for line in fi.readlines():
11             line = line.strip()
12             if not line:
13                 break
14             n, s = line.split(": ")
15             n = n.split(".")[0].replace(
16                 "FG-GML-", "").replace(
17                 "DEM", "")
18             if n not in d:
19                 d[n] = [int(s)]
20             else:
21                 d[n].append(float(s))
22 
23 import numpy as np
24 import matplotlib.pyplot as plt
25 from matplotlib.ticker import FormatStrFormatter
26 
27 fig, ax = plt.subplots()
28 ax.xaxis.set_major_formatter(FormatStrFormatter('%.1f'))
29 
30 ax.scatter(
31     [d[k][0] / (1024.**2) for k in d],
32     [100 * d[k][1] / d[k][0] for k in d])
33 
34 ax.grid(True)
35 ax.set_xlabel('size of zip [MBytes]')
36 ax.set_ylabel('(size of bz2) / (size of zip) [%]')
37 
38 #plt.show()
39 plt.savefig("size_vis_scatter.png", bbox_inches="tight")

何がわかるって、特に何もわからない気はするけれどもね。大きくなるほど圧縮しにくくはなるけれど、なんでしょうか、2~5MB あたりのサイズが苦手なのかしら? ちょっと特徴的な散布図だね。まぁ当たり前だけどファイルの中身に依存することなので、基盤地図情報数値標高モデルだけの集合では、特に何かが言えるわけではありませんよ。ので、色んなサンプルで試してみたくはなるよね。






えーっと、で、結局何が言いたかったんだっけか。

あ、「hoge命名はやめよう」だった。(ちげーよ。)