ファイル圧縮の形式は世にいろいろありますが、昨今はzipがもっとも一般的でしょうか。
 zipは圧縮・展開にかかる時間もほどほど、圧縮率もほどほど、そしてWindowsでもLinuxでも対応ソフトウェアあり、など扱いやすさの面でリードしています。
そんななか、特定分野で人気なのがXZ形式。圧縮にはパワーも時間も使いますが、zipよりも圧縮率が高くかつ短時間で伸長できる、という性質があります。圧縮は一度で、何度も伸長する、という例えばソースコード一式やインストーラの配布といった用途に向いていると言えるでしょう。
 あくまで圧縮形式、なのでアーカイブ機能はありません。
今回はWindows向けにXZ形式の圧縮を実装します。アーカイブ形式としてはTarを使います。
もくじ:
はじめに
単に使えればよい、という側面もありますが圧縮時の負荷が大きいという点からは、やはり少しでも速いほうがいいですよね。Python (3.x系)ではlzmaモジュールで使えるようになっていますが、今回はNimでも実装して速度を比較してみます。
XZの圧縮機能、ツールは開発元からSDK (XZ Utils)が提供されており、世に出回っているソフトウェアの多くはそれに依存しているもよう。本体はパブリックドメイン、ツール類を含めるとGPLでライセンスされているようです。
検証した環境はWindows 10 Pro, Python 3.6, Nim 0.18.0です。
PythonでのXZ圧縮
直にlzmaモジュールを叩いてもいいのですが、tar作成の関係もありshutilモジュールを利用します。内部的にはlzmaモジュールとtarfileモジュールに依存しているようです。
実装はshutilモジュールがかなり抽象化しているので、1行です。
| 1 2 | import shutil res = shutil.make_archive("path/to/destfile", "xztar", "path/to/sourcedir") | 
もとのディレクトリは"path/to/sourcedir"で、サブディレクトリ以下のファイルも含まれます。
 返り値(上の例ではres)には作成された結果のファイルのパスが入り、この場合は"path/to/destfile.tar.xz"のように拡張子が付加されたものになります。
第2引数はアーカイブの形式を指定する文字列で、環境依存ですが他にもzip, bztar, gztarなどが利用できます。
NimでのXZ圧縮
実装、といってもスクラッチで作るのは面倒なので、素直にXZ Utilsを使います。
 Windows環境なのでビルド済みのDLLを使ってNimから呼び出すようにします。
NimbleにもLZMAのラッパはあるのですが、動かしてみるとどうやらこれはバグっている様子。参考にしつつもファイル圧縮のところのみ抜き出して実装します。
Tar作成には別記事の実装を利用します。中間ファイルを吐かずオンザフライに作るよう実装するのが面倒だったので、一旦tarファイルを作成したのち、XZ形式で圧縮することにします。
 XZのエンコーダは圧縮の始めから終わりまでステートフルになっており、ちゃんとオンザフライ(中間ファイルを作成しないやり方)にするならtarファイルを抽象化するストリームで実装することになると思われます。
実際試してみると分かりますが、計算処理の負荷で言えば、tarファイルを作成する処理よりも圧倒的にXZ圧縮の処理のほうが重くなります。
 XZ圧縮のプログラム例については後ほど記載します。
速度比較
気になるのは速度ですね。そもそも想定する用途が違う(もちろんshutilのほうが汎用的に作られています)のでapple to appleな比較にはなりませんが、まあご参考までに、ということで。
このテストでは、そこらへんにあったディレクトリを圧縮の対象としました。もとのファイル(tar作成時点で)は約120MB、圧縮をかけると約80MBになりました。
 XZの圧縮オプション(preset)はデフォルト(6)、誤り訂正オプションはCRC(64)です。
複数回(7回)実行させた平均値は下記のようになりました。計測のためPythonではtime.time()、Nimではtimes.cpuTime()を使っています。あまり精度は良くないようですが、試行間の差は0~3秒程度でした。
| Python 3.6 | Nim (liblzma.dll) | |
|---|---|---|
| Time (7-times avg.) | 50.0[sec] | 34.3[sec] | 
Pythonでの実装(スクリプト)とNimでの実装(バイナリ)を用意しておき、PowerShellからInvoke-Expressionでランダムに呼び出しています。
さすが静的型付け言語、Nimのほうが速いですね。もちろん世のプロフェッショナルによる枯れた実装があるなら、わざわざ素人が自作する必要はありません。先達に感謝を捧げつつ利用するのみ。しかしながら、かゆい所を色々試してみるのは楽しいものです。
Pythonの高速化と言えばNumbaやCythonがありますが、外部のモジュールに依存している場合やうまく処理を切り出せる場合、細かくチューニングしたい場合など、静的言語を使うのもアリかもしれませんね。
Nimでのプログラム例
詳細は省略しますが、Nimでliblzma.dllのラッパを実装した例(というか上記のパクリ)が下記です。
 xzプロシージャにファイルパスを渡すと、対象のファイルと同じ場所でXZ圧縮をかけます。元のファイルは削除しません。
NimからDLLで定義されている関数を呼んでいるだけなので、Linuxでもshared objectを用意してコンパイルすれば動くと思います。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import os type   XZStream {.final, pure.} = object     nextIn*: cstring     availIn*: int     totalIn*: int     nextOut*: cstring     availOut*: int     totalOut*: int     allocator*: pointer     internal*: pointer     reservedPtr1*: pointer     reservedPtr2*: pointer     reservedPtr3*: pointer     reservedPtr4*: pointer     seekPos*: int     reservedInt2*: int     reservedInt3*: int     reservedInt4*: int     reservedEnum1*: int32     reservedEnum2*: int32   XZlibStreamError = object of Exception const   liblzma = "libs/liblzma.dll" # specify path to your library   LZMA_PRESET_DEFAULT = 6.int32  ## Default compression level   LZMA_STREAM_INIT = XZStream(     nextIn: nil,     availIn: 0,     totalIn: 0,     nextOut: nil,     availOut: 0,     totalOut: 0,     allocator: nil,     internal: nil,     reservedPtr1: nil,     reservedPtr2: nil,     reservedPtr3: nil,     reservedPtr4: nil,     seekPos: 0,     reservedInt2: 0,     reservedInt3: 0,     reservedInt4: 0,     reservedEnum1: 0,     reservedEnum2: 0)           ## Initialize stream   # returns   LZMA_OK* = 0                    ## Operation completed successfully   LZMA_STREAM_END* = 1            ## End of stream was reached   # checks   LZMA_CHECK_NONE* = 0.int32      ## No Check is calculated   LZMA_CHECK_CRC32* = 1.int32     ## CRC32 using the polynomial from the IEEE 802.3 standard   LZMA_CHECK_CRC64* = 4.int32     ## CRC64 using the polynomial from the ECMA-182 standard   LZMA_CHECK_SHA256* = 10.int32   ## SHA256   # actions   LZMA_RUN* = 0.int32             ## Continue coding   LZMA_FINISH* = 3.int32          ## Finish the coding operation proc lzma_code(   strm: var XZStream, action: int32): int {.cdecl, dynlib: liblzma, importc: "lzma_code".} proc lzma_end(   strm: var XZStream) {.cdecl, dynlib: liblzma, importc: "lzma_end".} proc lzma_easy_encoder(   strm: var XZStream,   preset, check: int32): int {.cdecl, dynlib: liblzma, importc: "lzma_easy_encoder".} proc xz*(source: string): string =   if not source.existsFile():     raise newException(OSError, "source file missing :" & source)   const XZ_BUFFER = 1024 * 512   let     preset = LZMA_PRESET_DEFAULT     check = LZMA_CHECK_CRC64   var     fin = open(source)     fout = open(source & ".xz", fmWrite) ## result as xz appended path     inbuf: array[XZ_BUFFER, char]     outbuf: array[XZ_BUFFER, char]     strm = LZMA_STREAM_INIT     stat = lzma_easy_encoder(strm, preset, check)     ret: int     action = LZMA_RUN   defer:     fin.close()     fout.close()   if stat != LZMA_OK:     raise newException(XZlibStreamError, "failed to initialize :" & $stat)   while true: # input loop     strm.nextIn = inbuf.addr     strm.availIn = fin.readBuffer(inbuf[0].addr, XZ_BUFFER)     if strm.availIn == 0:       action = LZMA_FINISH     while true: # output loop       strm.nextOut = outbuf.addr       strm.availOut = XZ_BUFFER       ret = lzma_code(strm, action)       # handle returned code       if ret != LZMA_OK and ret != LZMA_STREAM_END:         raise newException(XZlibStreamError, "failed to encode :" & $ret)       # write to output file       discard fout.writeBuffer(outbuf[0].addr, XZ_BUFFER - strm.availOut)       if strm.availOut != 0 or ret == LZMA_STREAM_END:         break     if action == LZMA_FINISH:       break   lzma_end(strm)   return source & ".xz" | 
おわり。