base_convertで文字列圧縮

こんにちは、アシアルの岡本です。

とある事情により16進数の文字列を圧縮する必要に迫られました。
URLに載せる必要があるのでバイナリは駄目ですzlib関数は使えません。

「PHPのbase_convert関数を使って36進数に変えれば楽勝!」
と思ったのが運のつき。
長く険しい文字列圧縮の旅(大袈裟!)が始まってしまいました。

base_convert関数は数値の基数を任意に変換するPHPの標準関数です。
こいつを使えば10進数の数字を16進数に変換したり、
16進数の数字を2進数に変換するといったことが一発で行えます。

この関数を活用し「16777216」という10進数を16進数に圧縮した場合、
「ffffff」と8文字から6文字に25%も圧縮できるのです。

しかし、PHPマニュアルの警告には
「大きな数値で base_convert() を使用すると、
精度が失われてしまうことがあります。」
とありました。
試してみましたが、残念ならが事実でした。
80バイトもあるような長い文字列で利用すると、似てるけど少し違う数字になってしまいます。

残念です。

「さて、どうしよう・・・」

自力で進数の変換コードを書く気力のなかった、
文系プログラマの僕はbase_convertに拘って、
文字列を適当な長さ(13桁までいけました)で区切る選択肢を選びました。

そうして作り上げた圧縮クラスが以下のソースコードになります。


<?php
/**
 * compress_by_base_convert 
 * N進数の文字列をもっと大きなM進数に置き換えて文字を圧縮するクラス
 * 
 * @copyright Asial
 * @author Yuki Okamoto <yuki@asial.co.jp> 
 */
class compressByBaseConvert
{
  var $compress_base = 36; 
  var $uncompress_base = 16; 
  var $split = 13; 
  var $delimiter = "-";
  /** 
   * compress 
   * 圧縮します
   * 
   * @param  string $string 
   * @return string
   */
  public function compress ($string)
  {
    // 分割しやすいよう0でパディングして、桁を分割したい数の倍数にする。
    $length = strlen($string);
    $padding_length = $this->split - ($length % $this->split);
    $join_length = ($length + $padding_length);
    $string = str_pad($string, $join_length, "0", STR_PAD_LEFT);
    // 分割し、0パディングを解除する
    $string_array = str_split($string, $this->split);
    $string_array[0] = ltrim($string_array[0], "0");
    // base_convertで圧縮
    foreach ($string_array as $split) {
      $compress = base_convert($split, $this->uncompress_base, $this->compress_base);
      $compress_array[] = $compress;
    }   
    // 連結して返す
    return implode($this->delimiter, $compress_array);
  }
  /** 
   * uncompress 
   * 解凍します
   * 
   * @param  string $string
   * @return string
   */
  public function uncompress($string)
  {
    // 配列に戻す
    $string_array = explode($this->delimiter, $string);
    // base_convertで解凍
    foreach ($string_array as $split) {
      $uncompress = base_convert($split, $this->compress_base, $this->uncompress_base);
      $uncompress_array[] = $uncompress;
    }   
    // 途中の0が落ちないように0パディングして連結
    $uncompress_join = ""; 
    foreach ($uncompress_array as $uncompress) {
       $uncompress_join .= str_pad($uncompress, $this->split, "0", STR_PAD_LEFT);
    }   
    // 上の桁の0は不要なのでtrimして削除
    $uncompress_join = ltrim($uncompress_join, 0); 
    return $uncompress_join;
  }
}
// 実証コード
$compress = new compressByBaseConvert();
$source   = "153733d3cd194c6b60e50f1dcf5cdf703a3a363031302d30352d31382036303a33313a3aa9e9a25ea53fb8a2";
$archive = $compress->compress($source);
$return = $compress->uncompress($archive);
var_dump($source, $archive, $return);

分割は、思ったより簡単ではありませんでした。
分割した左辺に0があった場合は圧縮→解凍したときに0が取れてしまうので、
パディングして直す必要があったり色々大変でした。

このクラスは、完成というには程遠いですが
(元の進数と圧縮に使う進数をsetするメソッドすらない…)
ニーズがあるか知りたいので公開してしまいます。
自由に使ってもらって構いませんが、無保証です。
うん、後で弊社の小川に説教されるかも…

手が空いたら、今度は自分でconvert処理を書くかもしれません。
base_convertでは36進数が限界で62進数が使えないので。

「クラス」といえば
アシアルのスクールにオブジェクト編が追加されました。
PHPオブジェクト指向編 効率的にプログラミングする手法

講師の小川の方が丁寧に正しいオブジェクトの書き方を教えますので、
PHPでオブジェクト使ってみたい方は是非一度ご検討ください。