LZ4という解凍特化で高速に圧縮展開できる大変便利なフォーマットがあります。C#でこの圧縮形式を利用できるライブラリはいくつかあるのですが、圧縮したファイルを他言語で読ませるときにハマりました。Pythonとやり取りする場合を例に、問題点と解決方法を検討します。
環境:Python3.6(64bit)、lz4(0.21.6)、u-msgpack-python(2.4.1)
.NET Framework 4.6、lz4net(v1.0.15.93 - 2017/3/18)、MessagePack(v1.7.3.4 - 2018/1/30)
VisualStudio 2017 Community
##LZ4 Frame Formatを使わない問題点
PythonのLZ4ライブラリである「python-lz4(パッケージ名はlz4)」を見ていたら興味深いことが書いてありました。
The bindings provided in this package cover the frame format and the block format specifications. The frame format bindings are the recommended ones to use, as this guarantees interoperability with other implementations and language bindings.
要約すると、「Frame FormatとBlock Formatがあり、他言語とやりとりするなら相互運用性を保証できるFrame Formatを使いなさい」ということ。読んでみると、確かにFrame Formatのほうがヘッダーが厳密に決まっているため、相互運用時のトラブルが起きづらそうに見えます。
C#のLZ4ライブラリでダウンロード数が最多でおそらく一番有名な「lz4net」を見ると、Frame Formatを明示的に指定できないのですよね。リポジトリをFrameで検索してもそれっぽいものが出てきませんでした。似た名前のライブラリがありますが同じ結果になります。
例えば、lz4netを使って簡単なテキストを圧縮してPythonに渡してみます。
using System.Text;
using System.Threading.Tasks;
using System.IO;
using LZ4;
class Program
{
static void Main(string[] args)
{
var str = "じゅげむ じゅげむ ごこうのすりきれ かいじゃりすいぎょの すいぎょうまつ うんらいまつ ふうらいまつ くうねるところにすむところ やぶらこうじのぶらこうじ ぱいぽ ぱいぽ ぱいぽのしゅーりんがん しゅーりんがんのぐーりんだい ぐーりんだいのぽんぽこぴーの ぽんぽこなーの ちょうきゅうめいのちょうすけ";
using (var fs = new FileStream("jyugemu.lz4", FileMode.Create, FileAccess.Write))
using (var lz4Stream = new LZ4Stream(fs, LZ4StreamMode.Compress))
{
var encode = new UTF8Encoding(false);//BOMなしUTF8
var binary = encode.GetBytes(str);
lz4Stream.Write(binary, 0, binary.Length);
}
}
}
このjyugemu.lz4をバイナリーエディタで見ると次のようになります。
Frame Formatの最初4バイトのマジックナンバー「0x184D2204」(詳しくはドキュメントを参照してください)が付与されていません。
Pythonのスクリプトのあるディレクトリにコピーし、Pythonで読んでみます。当然Frame Formatで解凍するとエラーになります。あらかじめlz4のモジュールをインストールしておきます。
import lz4.frame
with lz4.frame.open("jyugemu.lz4", "r") as fp:
decompress = fp.read()
print(decompress)
# decompress = fp.read()
# File "C:\Program Files\Python36\lib\site-packages\lz4\frame\__init__.py", line
# 509, in read
# return self._buffer.read(size)
# File "C:\Program Files\Python36\lib\_compression.py", line 103, in read
# data = self._decompressor.decompress(rawblock, size)
# File "C:\Program Files\Python36\lib\site-packages\lz4\frame\__init__.py", line
# 297, in decompress
# return_bytearray=self._return_bytearray,
#RuntimeError: LZ4F_decompress failed with code: #ERROR_frameType_unknown
じゃあBlock Formatで解凍すればいいのかと言ったらそう上手くは行きませんでした。
import lz4.block
import struct
with open("jyugemu.lz4", "rb") as fp:
binary = fp.read()
decompress = lz4.block.decompress(binary)
print(decompress)
# decompress = lz4.block.decompress(binary)
#ValueError: Decompressor wrote 18446744073709551614 bytes, but 2315502849 bytes
#expected from header
バイト数が明らかにおかしい。やはりドキュメントにある通り、他言語と相互運用するならFrame Formatを使うべきでしょう。
MessagePack for C#のLZ4シリアライザーも注意
MessagePack for C#という大変便利なライブラリがあります。このライブラリのオプションで、MessagePackにLZ4圧縮をかける、つまり高速なシリアライザーにさらに高速の圧縮をかけるという、シリアライザーの頂点のようなことができます。
これを他言語との相互運用に使うというケースはそうないと思いますが、このLZ4シリアライザーもv1.7.3.4の2018年1月末現在、Frame Formatに対応していないようです。(LZ4を使わずにMessagePackだけ使う場合は問題ありません)
例えば
using System.IO;
using MessagePack;
class Program
{
static void Main(string[] args)
{
var str = "じゅげむ じゅげむ ごこうのすりきれ かいじゃりすいぎょの すいぎょうまつ うんらいまつ ふうらいまつ くうねるところにすむところ やぶらこうじのぶらこうじ ぱいぽ ぱいぽ ぱいぽのしゅーりんがん しゅーりんがんのぐーりんだい ぐーりんだいのぽんぽこぴーの ぽんぽこなーの ちょうきゅうめいのちょうすけ";
//MessagePackのLZ4
using (var fs = new FileStream("jyugemu.msg.lz4", FileMode.Create, FileAccess.Write))
{
var binary = LZ4MessagePackSerializer.Serialize<string>(str);
fs.Write(binary, 0, binary.Length);
}
}
}
とMessagePackを挟むとヘッダーは次のようになります。
同様にPythonで読めません。C#の間でやり取りするならいいんですけどね。
##解決法:LZ4圧縮に「IonKiwi.lz4.net」を使う
###C#→Python
Frame Formatが使えるLZ4ライブラリがありました。DL数もlz4netの1/100ぐらいであまり有名ではないのですが、「IonKiwi.lz4.net」です。ライブラリの説明から
This package is a .NET wrapper arround the C lz4 library (v1.8.0), compliant with the LZ4 Framing Format.
CのLZ4のラッパーとのこと。Framing Formatが使えます。早速試してみます。せっかくなのでMessagePackを挟んでやってみます(もちろんわざわざMessagePackを挟まなくても使えます)。
using System.IO;
using lz4;
using MessagePack;
class Program
{
static void Main(string[] args)
{
var str = "じゅげむ じゅげむ ごこうのすりきれ かいじゃりすいぎょの すいぎょうまつ うんらいまつ ふうらいまつ くうねるところにすむところ やぶらこうじのぶらこうじ ぱいぽ ぱいぽ ぱいぽのしゅーりんがん しゅーりんがんのぐーりんだい ぐーりんだいのぽんぽこぴーの ぽんぽこなーの ちょうきゅうめいのちょうすけ";
var strSplit = str.Split(' ');
var data = Enumerable.Range(0, strSplit.Length)
.Select(x => new { id = x + 1, str = strSplit[x] }).ToArray();
//MessagePackでPack(匿名型もいける)
var msgpackBinary = MessagePackSerializer.Serialize<dynamic>(data);
//IonKiwi.lz4.netでFrame Fomat圧縮
using (var fs = new FileStream("frameformat.msg.lz4", FileMode.Create, FileAccess.Write))
using (var lz4Stream = LZ4Stream.CreateCompressor(fs, LZ4StreamMode.Write))
{
lz4Stream.Write(msgpackBinary, 0, msgpackBinary.Length);
}
}
}
frameformat.msg.lz4をPythonのスクリプトのあるのディレクトリにコピーします。MessagePackのモジュール(u-msgpack-python)もインストールしておきます。
import lz4.frame
import umsgpack
with lz4.frame.open("frameformat.msg.lz4", mode="rb") as fp:
unpack = umsgpack.unpack(fp)
print(unpack)
#[{'id': 1, 'str': 'じゅげむ'}, {'id': 2, 'str': 'じゅげむ'}, {'id': 3, 'str': '
#ごこうのすりきれ'}, {'id': 4, 'str': 'かいじゃりすいぎょの'}, {'id': 5, 'str': '
#すいぎょうまつ'}, {'id': 6, 'str': 'うんらいまつ'}, {'id': 7, 'str': 'ふうらいま
#つ'}, {'id': 8, 'str': 'くうねるところにすむところ'}, {'id': 9, 'str': 'やぶらこ
#うじのぶらこうじ'}, {'id': 10, 'str': 'ぱいぽ'}, {'id': 11, 'str': 'ぱいぽ'}, {'
#id': 12, 'str': 'ぱいぽのしゅーりんがん'}, {'id': 13, 'str': 'しゅーりんがんのぐ
#ーりんだい'}, {'id': 14, 'str': 'ぐーりんだいのぽんぽこぴーの'}, {'id': 15, 'str
#': 'ぽんぽこなーの'}, {'id': 16, 'str': 'ちょうきゅうめいのちょうすけ'}]
無事読むことができました。
###Python→C#
せっかくなので逆もやってみましょう。
import lz4.frame
import umsgpack
#オブジェクトの作成
str = "じゅげむ じゅげむ ごこうのすりきれ かいじゃりすいぎょの すいぎょうまつ うんらいまつ ふうらいまつ くうねるところにすむところ やぶらこうじのぶらこうじ ぱいぽ ぱいぽ ぱいぽのしゅーりんがん しゅーりんがんのぐーりんだい ぐーりんだいのぽんぽこぴーの ぽんぽこなーの ちょうきゅうめいのちょうすけ"
str_split = str.split(" ")
dic = [{"id":(x+1), "str":str_split[x]} for x in range(len(str_split))]
#MessagePack
msgpack = umsgpack.packb(dic)
#LZ4に保存
with lz4.frame.open("to csharp.msg.lz4", mode = "wb") as fp:
fp.write(msgpack)
できたファイルをC#の実行ディレクトリにコピーします。
using System.IO;
using lz4;
using MessagePack;
class Program
{
static void Main(string[] args)
{
//IonKiwi.lz4.netでFrame Fomat解凍
byte[] decompBinary;
using (var fs = new FileStream("to csharp.msg.lz4", FileMode.Open, FileAccess.Read))
using (var lz4Stream = LZ4Stream.CreateDecompressor(fs, LZ4StreamMode.Read))
using (var ms = new MemoryStream())
{
lz4Stream.CopyTo(ms);
decompBinary = ms.ToArray();
}
//MessagePackのUnpack
var obj = MessagePackSerializer.Deserialize<dynamic>(decompBinary);
foreach(var o in obj)
{
foreach(var pair in o)
{
Console.Write(pair.Key + ":" + pair.Value + " ");
}
Console.WriteLine();
}
}
}
//id:1 str:じゅげむ
//id:2 str:じゅげむ
//id:3 str:ごこうのすりきれ
//id:4 str:かいじゃりすいぎょの
//id:5 str:すいぎょうまつ
//id:6 str:うんらいまつ
//id:7 str:ふうらいまつ
//id:8 str:くうねるところにすむところ
//id:9 str:やぶらこうじのぶらこうじ
//id:10 str:ぱいぽ
//id:11 str:ぱいぽ
//id:12 str:ぱいぽのしゅーりんがん
//id:13 str:しゅーりんがんのぐーりんだい
//id:14 str:ぐーりんだいのぽんぽこぴーの
//id:15 str:ぽんぽこなーの
//id:16 str:ちょうきゅうめいのちょうすけ
Unpackした後の扱い方については他に方法あると思いますが、LZ4の受け渡しはFrame Formatにしてしまえば簡単ですね。バイナリのまま動かすと管理が楽そうです。