💎️

zlib gem を試す

2024/11/23に公開

基本形

require "zlib"
value = "foo"
value = Zlib::Deflate.deflate(value)  # => "x\x9CK\xCB\xCF\a\x00\x02\x82\x01E"
Zlib::Inflate.inflate(value)          # => "foo"

deflate は「しぼむ」で、inflate は「ふくらませる」という意味。

文字列しか扱えない

Zlib::Deflate.deflate({a: 1}) rescue $!  # => #<TypeError: no implicit conversion of Hash into String>

汎用的なデータ構造を扱う

to_json で文字列化すればなんとかなる。

require "json"
value = {a: 1}                     # => {:a=>1}
json = value.to_json               # => "{\"a\":1}"
bin = Zlib::Deflate.deflate(json)  # => "x\x9C\xABVJT\xB22\xAC\x05\x00\b*\x02\t"
json = Zlib::Inflate.inflate(bin)  # => "{\"a\":1}"
JSON.load(json)                    # => {"a"=>1}

しかし何もしなければハッシュキーが文字列に戻ったりしてぐだぐだになる。

それが厭なら YAML にする手もある。

require "yaml"
value = {a: 1}                     # => {:a=>1}
yaml = value.to_yaml               # => "---\n:a: 1\n"
bin = Zlib::Deflate.deflate(yaml)  # => "x\x9C\xD3\xD5\xD5\xE5\xB2J\xB4R0\xE4\x02\x00\n\x04\x01\xC2"
yaml = Zlib::Inflate.inflate(bin)  # => "---\n:a: 1\n"
YAML.load(yaml)                    # => {:a=>1}

しかし、Time 型が含まれると復元できなかったりする。

value = {a: Time.now}              # => {:a=>2024-11-18 18:50:29.475275 +0900}
yaml = value.to_yaml               # => "---\n:a: 2024-11-18 18:50:29.475275000 +09:00\n"
bin = Zlib::Deflate.deflate(yaml)  # => "x\x9C\x05\xC1\xC1\r\x00 \b\x03\xC0\xBFS\xF075\x85@\xC0n\xE3\xFEKx\a`\xE9\xC9\x82\x91p\x87\x8F\xF9\xA8\xA8\xB8'\xBB\xA2\x8B\xA4m^\x91\xEB\x03\xC7\xC8\b\x95"
yaml = Zlib::Inflate.inflate(bin)  # => "---\n:a: 2024-11-18 18:50:29.475275000 +09:00\n"
YAML.load(yaml) rescue $!          # => #<Psych::DisallowedClass: Tried to load unspecified class: Time>

どうして JSON も YAML も、こう使いにくいのだろうか。

Rails のセッションにかますとはまる

フォームの入力値をセッションに保存しておきたい場合がある。このとき、セッションにクッキーを使っていると 4096 バイトの制約があり、テキストエリアなどが絡むとすぐに溢れてしまう。そのようなとき (根本的な解決にはならないが意地でもセッションに保存したいのなら) 圧縮して保存する方法があるのだけど次のようにするとエラーになる。

session = {}
value = {a: 1}
json = value.to_json                         # => "{\"a\":1}"
session[:foo] = Zlib::Deflate.deflate(json)  # => "x\x9C\xABVJT\xB22\xAC\x05\x00\b*\x02\t"
json = Zlib::Inflate.inflate(session[:foo])  # => "{\"a\":1}"
value = JSON.load(json)                      # => {"a"=>1}

Rails は session を内部で JSON 化しようとするのでバイナリ文字列が含まれていると死んでしまう。したがって、途中で base64 をかまして次のようにしないといけない。

require "base64"
session = {}
value = {a: 1}                        # => {:a=>1}
json = value.to_json                  # => "{\"a\":1}"
bin = Zlib::Deflate.deflate(json)     # => "x\x9C\xABVJT\xB22\xAC\x05\x00\b*\x02\t"
base64 = Base64.strict_encode64(bin)  # => "eJyrVkpUsjKsBQAIKgIJ"
session[:foo] = base64                # => "eJyrVkpUsjKsBQAIKgIJ"
base64 = session[:foo]                # => "eJyrVkpUsjKsBQAIKgIJ"
bin = Base64.strict_decode64(base64)  # => "x\x9C\xABVJT\xB22\xAC\x05\x00\b*\x02\t"
json = Zlib::Inflate.inflate(bin)     # => "{\"a\":1}"
value = JSON.load(json)               # => {"a"=>1}

圧縮レベルの違い

レベル 1〜9 で指定できるが普通はこのどちらかの定数で指定するのがわかりやすい。

Zlib::BEST_SPEED        # => 1
Zlib::BEST_COMPRESSION  # => 9

あと次の定数もある。

Zlib::DEFAULT_COMPRESSION  # => -1
Zlib::NO_COMPRESSION       # => 0

で、次のコードを用意して、

require "securerandom"
require "active_support/core_ext/benchmark"
@value = 1000.times.collect { SecureRandom.hex }.join
def _(level)
  time = "%4.1f ms" % Benchmark.ms { 100.times { Zlib::Deflate.deflate(@value, level) } }
  bytesize = Zlib::Deflate.deflate(@value, level).bytesize
  [time, bytesize, bytesize.fdiv(@value.bytesize)]
end

比較すると、

@value.bytesize              # => 32000
_ Zlib::BEST_SPEED           # => ["23.6 ms", 18900, 0.590625]
_ Zlib::BEST_COMPRESSION     # => ["53.9 ms", 18587, 0.58084375]
_ Zlib::DEFAULT_COMPRESSION  # => ["54.1 ms", 18587, 0.58084375]
_ Zlib::NO_COMPRESSION       # => [" 1.0 ms", 32011, 1.00034375]
  • 初期値 DEFAULT_COMPRESSION は BEST_COMPRESSION のこと
  • BEST_SPEED は BEST_COMPRESSION に比べて 2.4 倍ほど速い
  • BEST_SPEED でも (上でのデータでは) 圧縮率にほとんど違いなし

などがわかった。

Discussion