web-dev-qa-db-ja.com

URLパラメーターを圧縮する方法

コンテンツにサードパーティのAPIを使用する シングルページアプリケーション があるとします。アプリのロジックはブラウザー内のみであり、書き込み可能なバックエンドはありません。

アプリの状態へのディープリンクを許可するために、pushStateを使用して、アプリの状態を決定するいくつかの変数を追跡します(Ubersichtのパブリックバージョンはまだこれを行いません)。この場合、reposlabelsmilestonesおよびusernameshow_open(bool)およびwith_comments(bool)およびwithout_comments(ブール値)。 URL形式は?label=label_1,label_2,label_3&repos=repo_1…です。値は通常の容疑者、大まかな[a-zA-Z][a-zA-Z0-9_-]、または任意のブールインジケータです。

ここまでは順調ですね。クエリ文字列は少し長くて扱いにくいので、http://espy.github.io/ubersicht/?state=SOMOPAQUETOKENTHATLOSSLESSLYDECOMPRESSESINTOTHEORIGINALVALUES#hoodiehqのようなURLを渡すことができるようにしたいので、短いほど良いです。

私の最初の試みは、このためにzlibのようなアルゴリズムを使用することでした( https://github.com/imaya/zlib.js )および@flipzaggingはantirez/smaz(https // github。 com/antirez/smaz)これは短い文字列により適しています(JavaScriptバージョン https://github.com/personalcomputer/smaz.js )。

=&https://github.com/personalcomputer/smaz.js/blob/master/lib/smaz.js#L9 で特に処理されないため、少し微調整できるかもしれません。

さらに、固定テーブルの値をエンコードするオプションがあります。引数の順序は事前に定義されており、追跡する必要があるのは実際の値だけです。例えば。潜在的にsmaz圧縮の前に、a=hamster&b=cat7hamster3cat(長さ+文字)またはhamster | cat(値+ |)に変換します。

他に探しているものはありますか?

53
Jan Lehnardt

さまざまな優れた(または私が思うに)アイデアをまとめる実用的なソリューション

主に、ハフマンエンコーダーをPHPで実装する機会が与えられ、満足できる既存の実装が見つからなかったため、これを楽しみのために行いました。

ただし、同様のパスを探索する場合は、これにより時間を節約できます。

バロウズウィーラー+最前面へ移動+ハフマン変換

BWTがあなたの種類の入力に最適かどうかはわかりません。
これは通常のテキストではないため、ソースコードや平易な英語ほど頻繁に繰り返されるパターンはおそらく発生しません。

また、エンコードされたデータとともに動的なハフマンコードを渡す必要があり、入力文字列が非常に短い場合、圧縮ゲインが大幅に低下します。

私は間違っているかもしれませんが、その場合、誰かが私を証明するのを喜んで見るでしょう。

とにかく、私は別のアプローチを試すことにしました。

一般原則

1)URLパラメーターの構造を定義し、定数部分を削除します

たとえば、次から始まる:

repos=aaa,bbb,ccc&
labels=ddd,eee,fff&
milestones=ggg,hhh,iii&
username=kkk&
show_open=0&
show_closed=1&
show_commented=1&
show_uncommented=0

エキス:

aaa,bbb,ccc|ddd,eee,fff|ggg,hhh,iii|kkk|0110

ここで、,|は文字列やフィールドターミネータとして機能しますが、ブール値には何も必要ありません。

2)予想される平均入力に基づいてシンボルの静的再パーティションを定義し、静的ハフマンコードを導出する

動的テーブルを送信すると、最初の文字列よりも多くのスペースが必要になるため、圧縮を実現する唯一の方法は静的ハフマンテーブルを持つことだと思います。

ただし、データの構造を有利に使用して、合理的な確率を計算できます。

英語または他の言語での文字の再分割から始めて、数字および他の句読点記号の特定の割合を投入できます。

動的なハフマンコーディングによるテストでは、圧縮率が30〜50%でした。

これは、静的テーブルでは、おそらく.6の圧縮係数(データの長さを1/3に減らす)を期待できることを意味します。

3)このバイナリHuffmannコードをURIが処理できるものに変換する

そのリスト内の70個のレギュラーASCII 7ビット文字

!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

展開係数は約30%になり、実際にはbase64エンコードよりも良くありません。

30%の拡張は、静的なハフマン圧縮からのゲインを台無しにするので、これはほとんどオプションではありません!

ただし、エンコード側のクライアントとサーバー側を制御するため、URIの予約文字ではないあらゆるものを使用できます。

興味深い可能性は、Unicodeグリフを使用して上記の256までの設定を完了することです。これにより、同じ数のURI準拠の文字でバイナリデータをエンコードできるため、長い整数分割の苦痛で遅い束を稲妻に置き換えることができます。高速テーブル検索。

構造説明

コーデックはクライアント側とサーバー側の両方で使用されることを意図しているため、サーバーとクライアントが共通のデータ構造定義を共有することが不可欠です。

インターフェイスは進化する可能性が高いため、上位互換性のためにバージョン番号を保存することをお勧めします。

インターフェイス定義では、次のように非常に最小限の記述言語を使用します。

v   1               // version number (between 0 and 63)
a   en              // alphabet used (English)
o   10              // 10% of digits and other punctuation characters
f   1               // 1% of uncompressed "foreign" characters
s 15:3 repos        // list of expeced 3 strings of average length 15
s 10:3 labels
s 8:3  milestones
s 10   username     // single string of average length 10
b      show_open    // boolean value
b      show_closed
b      show_commented
b      show_uncommented

サポートされる各言語には、使用されるすべての文字の頻度表があります

数字と-.、または_のような他のコンピューターのような記号は、言語に関係なくグローバルな頻度を持ちます

区切り記号(,および|)頻度は、構造内に存在するリストおよびフィールドの数に従って計算されます。

他のすべての「外来」文字は、特定のコードでエスケープされ、プレーンなUTF-8としてエンコードされます。

実装

双方向変換パスは次のとおりです。

フィールドのリスト<-> UTF-8データストリーム<->ハフマンコード<-> URI

これがメインのコーデックです

include ('class.huffman.codec.php');
class IRI_prm_codec
{
    // available characters for IRI translation
    static private $translator = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅ";

    const VERSION_LEN = 6; // version number between 0 and 63

    // ========================================================================
    // constructs an encoder
    // ========================================================================
    public function __construct ($config)
    {
        $num_record_terminators = 0;
        $num_record_separators = 0;
        $num_text_sym = 0;

        // parse config file
        $lines = file($config, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line)
        {
            list ($code, $val) = preg_split('/\s+/', $line, 2);
            switch ($code)
            {
            case 'v': $this->version = intval($val); break;
            case 'a': $alphabet = $val; break;
            case 'o': $percent_others = $val; break;
            case 'f': $percent_foreign = $val; break;
            case 'b':
                $this->type[$val] = 'b';
                break;
            case 's':
                list ($val, $field) = preg_split('/\s+/u', $val, 2);
                @list ($len,$num) = explode (':', $val);
                if (!$num) $num=1;
                $this->type[$field] = 's';
                $num_record_terminators++;
                $num_record_separators+=$num-1;
                $num_text_sym += $num*$len;
                break;

            default: throw new Exception ("Invalid config parameter $code");
            }
        }

        // compute symbol frequencies           
        $total = $num_record_terminators + $num_record_separators + $num_text_sym + 1;

        $num_chars = $num_text_sym * (100-($percent_others+$percent_foreign))/100;
        $num_sym = $num_text_sym * $percent_others/100;
        $num_foreign = $num_text_sym * $percent_foreign/100;

        $this->get_frequencies ($alphabet, $num_chars/$total);
        $this->set_frequencies (" .-_0123456789", $num_sym/$total);
        $this->set_frequencies ("|", $num_record_terminators/$total);
        $this->set_frequencies (",", $num_record_separators/$total);
        $this->set_frequencies ("\1", $num_foreign/$total);
        $this->set_frequencies ("\0", 1/$total);

        // create Huffman codec
        $this->huffman = new Huffman_codec();
        $this->huffman->make_code ($this->frequency);
    }

    // ------------------------------------------------------------------------
    // grab letter frequencies for a given language
    // ------------------------------------------------------------------------
    private function get_frequencies ($lang, $coef)
    {
        $coef /= 100;
        $frequs = file("$lang.dat", FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES);
        foreach ($frequs as $line)
        {
            $vals = explode (" ", $line);
            $this->frequency[$vals[0]] = floatval ($vals[1]) * $coef;
        }
    }

    // ------------------------------------------------------------------------
    // set a given frequency for a group of symbols
    // ------------------------------------------------------------------------
    private function set_frequencies ($symbols, $coef)
    {
        $coef /= strlen ($symbols);
        for ($i = 0 ; $i != strlen($symbols) ; $i++) $this->frequency[$symbols[$i]] = $coef;
    }

    // ========================================================================
    // encodes a parameter block
    // ========================================================================
    public function encode($input)
    {
        // get back input values
        $bools = '';
        foreach (get_object_vars($input) as $prop => $val)
        {
            if (!isset ($this->type[$prop])) throw new Exception ("unknown property $prop");
            switch ($this->type[$prop])
            {
            case 'b': $bools .= $val ? '1' : '0'; break;
            case 's': $strings[] = $val; break;
            default: throw new Exception ("Uh oh... type ".$this->type[$prop]." not handled ?!?");
            }
        }

        // set version number and boolean values in front
        $prefix = sprintf ("%0".self::VERSION_LEN."b$bools", $this->version);

        // pass strings to our Huffman encoder
        $strings = implode ("|", $strings);
        $huff = $this->huffman->encode ($strings, $prefix, "UTF-8");

        // translate into IRI characters
        mb_internal_encoding("UTF-8");
        $res = '';
        for ($i = 0 ; $i != strlen($huff) ; $i++) $res .= mb_substr (self::$translator, ord($huff[$i]), 1);

        // done
        return $res;
    }

    // ========================================================================
    // decodes an IRI string into a lambda object
    // ========================================================================
    public function decode($input)
    {
        // convert IRI characters to binary
        mb_internal_encoding("UTF-8");
        $raw = '';
        $len = mb_strlen ($input);
        for ($i = 0 ; $i != $len ; $i++)
        {
            $c = mb_substr ($input, 0, 1);
            $input = mb_substr ($input, 1);
            $raw .= chr(mb_strpos (self::$translator, $c));
        }

        $this->bin = '';        

        // check version
        $version = $this->read_bits ($raw, self::VERSION_LEN);
        if ($version != $this->version) throw new Exception ("Version mismatch: expected {$this->version}, found $version");

        // read booleans
        foreach ($this->type as $field => $type)
            if ($type == 'b')
                $res->$field = $this->read_bits ($raw, 1) != 0;

        // decode strings
        $strings = explode ('|', $this->huffman->decode ($raw, $this->bin));
        $i = 0;
        foreach ($this->type as $field => $type) 
            if ($type == 's')
                $res->$field = $strings[$i++];

        // done
        return $res;
    }

    // ------------------------------------------------------------------------
    // reads raw bit blocks from a binary string
    // ------------------------------------------------------------------------
    private function read_bits (&$raw, $len)
    {
        while (strlen($this->bin) < $len)
        {
            if ($raw == '') throw new Exception ("premature end of input"); 
            $this->bin .= sprintf ("%08b", ord($raw[0]));
            $raw = substr($raw, 1);
        }
        $res = bindec (substr($this->bin, 0, $len));
        $this->bin = substr ($this->bin, $len);
        return $res;
    }
}

基礎となるハフマンコーデック

include ('class.huffman.dict.php');

class Huffman_codec
{
    public  $dict = null;

    // ========================================================================
    // encodes a string in a given string encoding (default: UTF-8)
    // ========================================================================
    public function encode($input, $prefix='', $encoding="UTF-8")
    {
        mb_internal_encoding($encoding);
        $bin = $prefix;
        $res = '';
        $input .= "\0";
        $len = mb_strlen ($input);
        while ($len--)
        {
            // get next input character
            $c = mb_substr ($input, 0, 1);
            $input = substr($input, strlen($c)); // avoid playing Schlemiel the Painter

            // check for foreign characters
            if (isset($this->dict->code[$c]))
            {
                // output huffman code
                $bin .= $this->dict->code[$c];
            }
            else // foreign character
            {
                // escape sequence
                $lc = strlen($c);
                $bin .= $this->dict->code["\1"] 
                     . sprintf("%02b", $lc-1); // character length (1 to 4)

                // output plain character
                for ($i=0 ; $i != $lc ; $i++) $bin .= sprintf("%08b", ord($c[$i]));
            }

            // convert code to binary
            while (strlen($bin) >= 8)
            {
                $res .= chr(bindec(substr ($bin, 0, 8)));
                $bin = substr($bin, 8);
            }
        }

        // output last byte if needed
        if (strlen($bin) > 0)
        {
            $bin .= str_repeat ('0', 8-strlen($bin));
            $res .= chr(bindec($bin));
        }

        // done
        return $res;
    }

    // ========================================================================
    // decodes a string (will be in the string encoding used during encoding)
    // ========================================================================
    public function decode($input, $prefix='')
    {
        $bin = $prefix;
        $res = '';
        $len = strlen($input);
        for ($i=0 ;;)
        {
            $c = $this->dict->symbol($bin);

            switch ((string)$c)
            {
            case "\0": // end of input
                break 2;

            case "\1": // plain character

                // get char byte size
                if (strlen($bin) < 2)
                {
                    if ($i == $len) throw new Exception ("incomplete escape sequence"); 
                    $bin .= sprintf ("%08b", ord($input[$i++]));
                }
                $lc = 1 + bindec(substr($bin,0,2));
                $bin = substr($bin,2);
                // get char bytes
                while ($lc--)
                {
                    if ($i == $len) throw new Exception ("incomplete escape sequence"); 
                    $bin .= sprintf ("%08b", ord($input[$i++]));
                    $res .= chr(bindec(substr($bin, 0, 8)));
                    $bin = substr ($bin, 8);
                }
                break;

            case null: // not enough bits do decode further

                // get more input
                if ($i == $len) throw new Exception ("no end of input mark found"); 
                $bin .= sprintf ("%08b", ord($input[$i++]));
                break;

            default:  // huffman encoded

                $res .= $c;
                break;          
            }
        }

        if (bindec ($bin) != 0) throw new Exception ("trailing bits in input");
        return $res;
    }

    // ========================================================================
    // builds a huffman code from an input string or frequency table
    // ========================================================================
    public function make_code ($input, $encoding="UTF-8")
    {
        if (is_string ($input))
        {
            // make dynamic table from the input message
            mb_internal_encoding($encoding);
            $frequency = array();
            while ($input != '')
            {
                $c = mb_substr ($input, 0, 1);
                $input = mb_substr ($input, 1);
                if (isset ($frequency[$c])) $frequency[$c]++; else $frequency[$c]=1;
            }
            $this->dict = new Huffman_dict ($frequency);
        }
        else // assume $input is an array of symbol-indexed frequencies
        {
            $this->dict = new Huffman_dict ($input);
        }
    }
}

ハフマン辞書

class Huffman_dict
{
    public  $code = array();

    // ========================================================================
    // constructs a dictionnary from an array of frequencies indexed by symbols
    // ========================================================================
    public function __construct ($frequency = array())
    {
        // add terminator and escape symbols
        if (!isset ($frequency["\0"])) $frequency["\0"] = 1e-100;
        if (!isset ($frequency["\1"])) $frequency["\1"] = 1e-100;

        // sort symbols by increasing frequencies
        asort ($frequency);

        // create an initial array of (frequency, symbol) pairs
        foreach ($frequency as $symbol => $frequence) $occurences[] = array ($frequence, $symbol);

        while (count($occurences) > 1)
        {
            $leaf1 = array_shift($occurences);
            $leaf2 = array_shift($occurences);
            $occurences[] = array($leaf1[0] + $leaf2[0], array($leaf1, $leaf2));
            sort($occurences);
        }
        $this->tree = $this->build($occurences[0], '');

    }

    // -----------------------------------------------------------
    // recursive build of lookup tree and symbol[code] table
    // -----------------------------------------------------------
    private function build ($node, $prefix)
    {
        if (is_array($node[1]))
        {
            return array (
                '0' => $this->build ($node[1][0], $prefix.'0'),
                '1' => $this->build ($node[1][1], $prefix.'1'));
        }
        else
        {
            $this->code[$node[1]] = $prefix;
            return $node[1];
        }
    }

    // ===========================================================
    // extracts a symbol from a code stream
    // if found     : updates code stream and returns symbol
    // if not found : returns null and leave stream intact
    // ===========================================================
    public function symbol(&$code_stream)
    {
        list ($symbol, $code) = $this->get_symbol ($this->tree, $code_stream);
        if ($symbol !== null) $code_stream = $code;
        return $symbol;
    }

    // -----------------------------------------------------------
    // recursive search for a symbol from an huffman code
    // -----------------------------------------------------------
    private function get_symbol ($node, $code)
    {
        if (is_array($node))
        {
            if ($code == '') return null;
            return $this->get_symbol ($node[$code[0]], substr($code, 1));
        }
        return array ($node, $code);
    }
}

include ('class.iriprm.codec.php');

$iri = new IRI_prm_codec ("config.txt");
foreach (array (
    'repos' => "discussion,documentation,hoodie-cli",
    'labels' => "enhancement,release-0.3.0,starter",
    'milestones' => "1.0.0,1.1.0,v0.7",
    'username' => "mklappstuhl",
    'show_open' => false,
    'show_closed' => true,
    'show_commented' => true,
    'show_uncommented' => false
) as $prop => $val) $iri_prm->$prop = $val;

$encoded = $iri->encode ($iri_prm);
echo "encoded as $encoded\n";
$decoded = $iri->decode ($encoded);
var_dump($decoded);

出力:

encoded as 5ĶůťÊĕCOĔƀŪļŤłmĄZEÇŽÉįóšüÿjħũÅìÇēOĪäŖÏŅíŻÉĒQmìFOyäŖĞqæŠŹōÍĘÆŤŅËĦ

object(stdClass)#7 (8) {
  ["show_open"]=>
  bool(false)
  ["show_closed"]=>
  bool(true)
  ["show_commented"]=>
  bool(true)
  ["show_uncommented"]=>
  bool(false)
  ["repos"]=>
  string(35) "discussion,documentation,hoodie-cli"
  ["labels"]=>
  string(33) "enhancement,release-0.3.0,starter"
  ["milestones"]=>
  string(16) "1.0.0,1.1.0,v0.7"
  ["username"]=>
  string(11) "mklappstuhl"
}

この例では、入力長が約100の場合、入力は64個のUnicode文字にパックされ、1/3の削減になりました。

同等の文字列:

discussion,documentation,hoodie-cli|enhancement,release-0.3.0,starter|
1.0.0,1.1.0,v0.7|mklappstuhl|0110

動的なハフマンテーブルによって59文字に圧縮されます。大した違いはありません。

間違いなくスマートなデータの並べ替えはそれを減らすだろうが、動的なテーブルを渡す必要があるだろう...

救助に中国人?

ttepasseのアイデアに基づいて、膨大な数のアジア文字を利用して、0x4000(12ビット)の連続値の範囲を見つけ、3バイトを2つのCJK文字にコード化します。

    // translate into IRI characters
    $res = '';
    $len = strlen ($huff);
    for ($i = 0 ; $i != $len ; $i++)
    {
        $byte = ord($huff[$i]);
        $quartet[2*$i  ] = $byte >> 4;
        $quartet[2*$i+1] = $byte &0xF;
    }
    $len *= 2;
    while ($len%3 != 0) $quartet[$len++] = 0;
    $len /= 3;
    for ($i = 0 ; $i != $len ; $i++)
    {
        $utf16 = 0x4E00 // CJK page base, enough range for 2**12 (0x4000) values
               + ($quartet[3*$i+0] << 8)
               + ($quartet[3*$i+1] << 4)
               + ($quartet[3*$i+2] << 0);
        $c = chr ($utf16 >> 8) . chr ($utf16 & 0xFF);
        $res .= $c;
    }
    $res = mb_convert_encoding ($res, "UTF-8", "UTF-16");

帰ってきた:

    // convert IRI characters to binary
    $input = mb_convert_encoding ($input, "UTF-16", "UTF-8");
    $len = strlen ($input)/2;
    for ($i = 0 ; $i != $len ; $i++)
    {
        $val = (ord($input[2*$i  ]) << 8) + ord ($input[2*$i+1]) - 0x4E00;
        $quartet[3*$i+0] = ($val >> 8) &0xF;
        $quartet[3*$i+1] = ($val >> 4) &0xF;
        $quartet[3*$i+2] = ($val >> 0) &0xF;
    }
    $len *= 3;
    while ($len %2) $quartet[$len++] = 0;
    $len /= 2;
    $raw = '';
    for ($i = 0 ; $i != $len ; $i++)
    {
        $raw .= chr (($quartet[2*$i+0] << 4) + $quartet[2*$i+1]);
    }

64個のラテン文字の前の出力

5ĶůťÊĕCOĔƀŪļŤłmĄZEÇŽÉįóšüÿjħũÅìÇēOĪäŖÏŅíŻÉĒQmìFOyäŖĞqæŠŹōÍĘÆŤŅËĦ

42個のアジア文字に「縮小」します。

乙堽孴峴勀垧壩坸冫嚘佰嫚凲咩俇噱刵巋娜奾埵峼圔奌夑啝啯嶼勲婒婅凋凋伓傊厷侖咥匄冯塱僌

ただし、ご覧のとおり、平均表意文字の大部分が文字列を実際に長くしているため(ピクセル単位)、アイデアが有望であったとしても、結果はかなり残念です。

細いグリフを選ぶ

一方、URIエンコーディングのベースとして「細い」文字を選択することもできます。例えば:

█ᑊᵄ′ӏᶟⱦᵋᵎiïᵃᶾ᛬ţᶫꞌᶩ᠇܂اlᶨᶾᛁ⁚ᵉʇȋʇίן᠙ۃῗᥣᵋĭꞌ៲ᛧ༚ƫܙ۔ˀȷˁʇʹĭ∕ٱ;łᶥյ;ᴶ⁚ĩi⁄ʈ█

の代わりに

█5ĶůťÊĕCOĔƀŪļŤłmĄZEÇŽÉįóšüÿjħũÅìÇēOĪäŖÏŅíŻÉĒQmìFOyäŖĞqæŠŹōÍĘÆŤŅËĦ█

これにより、ブラウザのアドレスバーを含め、プロポーショナルフォントで長さが半分になります。

これまでの256個の「細い」グリフの最良の候補セット:

᠊།ᑊʲ་༌ᵎᵢᶤᶩᶪᶦᶧˡ ⁄∕เ'Ꞌꞌ꡶ᶥᵗᶵᶨ|¦ǀᴵ  ᐧᶠᶡ༴ˢᶳ⁏ᶴʳʴʵ։᛬⍮ʹ′ ⁚⁝ᵣ⍘༔⍿ᠵᥣᵋᵌᶟᴶǂˀˁˤ༑,.   ∙Ɩ៲᠙ᵉᵊᵓᶜᶝₑₔյⵏⵑ༝༎՛ᵞᵧᚽᛁᛂᛌᛍᛙᛧᶢᶾ৷⍳ɩΐίιϊᵼἰἱἲἳἴἵἶἷὶίῐῑῒΐῖῗ⎰⎱᠆ᶿ՝ᵟᶫᵃᵄᶻᶼₐ∫ª౹᠔/:;\ijltìíîïĩīĭįıĵĺļłţŧſƚƫƭǐǰȉȋțȴȷɉɨɪɫɬɭʇʈʝːˑ˸;·ϳіїјӏ᠇ᴉᵵᵻᶅᶖḭḯḷḹḻḽṫṭṯṱẗẛỉị⁞⎺⎻⎼⎽ⱡⱦ꞉༈ǁ‖༅༚ᵑᵝᵡᵦᵪา᠑⫶ᶞᚁᚆᚋᚐᚕᵒᵔᵕᶱₒⵗˣₓᶹๅʶˠ᛫ᵛᵥᶺᴊ

結論

この実装は、クライアントとサーバーの交換を可能にするためにJavaScriptに移植する必要があります。
構造とハフマンコードをクライアントと共有する方法も提供する必要があります。

難しいことではなく、楽しいことですが、それはさらに多くの仕事を意味します:)。

文字の項でのハフマンのゲインは約30%です。

もちろん、これらの文字の大部分はマルチバイトですが、最短のURIを目指す場合は重要ではありません。
1ビットに簡単にパックできるブール値を除いて、これらの厄介な文字列は圧縮されるのを嫌がっているようです。
周波数をより適切に調整することは可能かもしれませんが、圧縮率が50%を超えるとは思えません。

一方、細いグリフを選択すると、実際には文字列が縮小されます。

そのため、両方のすべての組み合わせで実際に何かを達成する可能性がありますが、控えめな結果を得るには多大な労力が必要です。

47
kuroi neko

あなた自身が提案するように、情報を運んでいないすべてのキャラクターは「フォーマット」の一部であるため、最初に取り除きます。

例えば。 「labels = open、ssl、cypher&repository = 275643&username = ryanbrg&milestones =&with_comment = yes」を「open、ssl、cyper | 275643 | ryanbrg || yes」に変更します。

次に、固定確率ベクトルを使用したハフマンエンコーディングを使用します(文字から可変長ビット文字列への固定マッピングになります-最も可能性の高い文字はより短いビット文字列に、より可能性の低い文字はより長いビット文字列にマッピングされます)。

パラメータごとに異なる確率ベクトルを使用することもできます。たとえば、パラメータ「ラベル」ではアルファ文字の確率が高くなりますが、「リポジトリ」パラメータでは数値文字の確率が最も高くなります。これを行う場合、セパレーター「|」を考慮する必要があります先行パラメーターの一部。

最後に、長いビット文字列(文字がマップされたすべてのビット文字列の連結)を、base64urlエンコードによってURLに挿入できるものに変換します。

代表的なパラメーターリストのセットを送っていただければ、それらをHuffmannコーダーで実行して、それらがどの程度圧縮されているかを確認できます。

確率ベクトル(または文字からビット文字列へのマッピング)は、ブラウザに送信されるJavascript関数に定数配列としてエンコードする必要があります。

もちろん、さらに先へ進むこともできます。たとえば、可能性のあるラベルとその確率のリストを取得してください。次に、ハフマンエンコーディングを使用して、ラベル全体をビット文字列にマッピングできます。これにより、より良い圧縮が得られますが、新しいラベル(たとえば、単一文字エンコーディングにフォールバック)、およびマッピング(前述のようにJavascript関数の定数配列)に対して余分な作業が必要になります)ははるかに大きくなります。

17
mschoenert

Unningな計画があります! (そしてジントニックのドリンク)

あなたはバイトストリームの長さではなく、結果のグリフの長さを気にしているようです。ユーザーに表示される文字列。

ブラウザは [〜#〜] iri [〜#〜] を基礎となる[URI] [2]に変換しながら、アドレスバーにIRIを表示したままにしておくと非常に優れています。 IRIには可能な文字のレパートリーが多くありますが、可能な文字のセットはかなり限られています。

つまり、文字のバイグラム(aa、ab、ac、…、zzおよび特殊文字)をエンコードして、完全なUnicodeスペクトルの1つの文字にすることができます。 80の可能性があるとしましょうASCII chars:2つの文字の可能な組み合わせの数は6400です。Unicodeで割り当てられた文字、たとえばハン統一CJKスペクトルで簡単に見つけることができます。

aa  →  一
ab  →  丁
ac  →  丂
ad  →  七
…

CJKを選択したのは、ターゲットの文字がUnicodeで割り当てられ、主要なブラウザとオペレーティングシステムでグリフが割り当てられている場合にのみ(わずかに)合理的だからです。そのため、私的使用領域はなくなり、トライグラムを使用したより効率的なバージョン(その組み合わせはUnicode 1114112の可能性のあるすべてのコードポイントを使用できる)がなくなりました。

要約すると、基礎となるバイトはまだそこにあり、UTF-8エンコーディングを使用するとさらに長くなる可能性がありますが、ユーザーが表示およびコピーする表示文字列は50%短くなります。

わかりました、わかりました、理由、このソリューションが非常識な理由:

  • IRIは完全ではありません。最新のブラウザよりも少ないツールの多くには問題があります。

  • アルゴリズムには明らかに多くの作業が必要です。バイグラムをターゲットの文字にマッピングして戻す関数が必要です。また、メモリ内の大きなハッシュテーブルを回避するために、算術的に作業することをお勧めします。

  • ターゲット文字は、割り当てられている場合、および単純な文字であり、Unicode正規化でどこかで失われた文字やものの組み合わせなどの派手なユニコディアのものではない場合、チェックする必要があります。また、ターゲット領域がグリフ付きの割り当てられた文字の連続したスパンである場合。

  • ブラウザは時々IRIに警戒します。正当な理由により、IDNホモグラフ攻撃が与えられました。これらの非ASCII文字をすべてアドレスバーに入れても問題ありませんか?

  • そして最大の問題は、人々が知らないスクリプトの文字を覚えるのが悪名高いことです。これらは、これらの文字を(再)入力しようとするとさらに悪化します。また、copy'n'pasteは多くの異なるクリックで失敗することがあります。 URL短縮サービスがBase64およびさらに小さなアルファベットを使用する理由があります。

…といえば:それが私の解決策でしょう。リンクを短縮する作業をユーザーにオフロードするか、APIを介してgoo.glまたはbit.lyを統合します。

12
ttepasse

protocol-buffers を使用しないのはなぜですか?

プロトコルバッファは、構造化データをシリアル化するための柔軟で効率的な自動化メカニズムです。XMLを考えてください。データを1回構造化する方法を定義したら、特別に生成されたソースコードを使用して、構造化データをさまざまなデータストリームとさまざまな言語で簡単に読み書きできます。 「古い」形式に対してコンパイルされたデプロイ済みプログラムを壊すことなく、データ構造を更新することもできます。

ProtoBuf.js は、オブジェクトをプロトコルバッファメッセージに変換します。

次のオブジェクトはCgFhCgFiCgFjEgFkEgFlEgFmGgFnGgFoGgFpIgNqZ2I=に変換されます

{
    repos : ['a', 'b', 'c'],
    labels: ['d', 'e', 'f'],
    milestones : ['g', 'h', 'i'],
    username : 'jgb'
}

次の例は、 require.js を使用して構築されています。これを試してみてください jsfiddle

require.config({
    paths : {
        'Math/Long'  : '//rawgithub.com/dcodeIO/Long.js/master/Long.min',
        'ByteBuffer' : '//rawgithub.com/dcodeIO/ByteBuffer.js/master/ByteBuffer.min',
        'ProtoBuf'   : '//rawgithub.com/dcodeIO/ProtoBuf.js/master/ProtoBuf.min'
    }
})

require(['message'], function(message) {
    var data = {
        repos : ['a', 'b', 'c'],
        labels: ['d', 'e', 'f'],
        milestones : ['g', 'h', 'i'],
        username : 'jgb'
    }

    var request = new message.arguments(data);

    // Convert request data to base64
    var base64String = request.toBase64();
    console.log(base64String);

    // Convert base64 back
    var decodedRequest = message.arguments.decode64(base64String);
    console.log(decodedRequest);
});

// Protobuf message definition
// Message definition could also be stored in a .proto definition file
// See: https://github.com/dcodeIO/ProtoBuf.js/wiki
define('message', ['ProtoBuf'], function(ProtoBuf) {
    var proto = {
        package : 'message',
        messages : [
            {
                name : 'arguments',
                fields : [
                    {
                        rule : 'repeated',
                        type : 'string',
                        name : 'repos',
                        id : 1
                    },
                    {
                        rule : 'repeated',
                        type : 'string',
                        name : 'labels',
                        id : 2
                    },
                    {
                        rule : 'repeated',
                        type : 'string',
                        name : 'milestones',
                        id : 3
                    },
                    {
                        rule : 'required',
                        type : 'string',
                        name : 'username',
                        id : 4
                    },
                    {
                        rule : 'optional',
                        type : 'bool',
                        name : 'with_comments',
                        id : 5
                    },
                    {
                        rule : 'optional',
                        type : 'bool',
                        name : 'without_comments',
                        id : 6
                    }
                ],
            }
        ]
    };

    return ProtoBuf.loadJson(proto).build('message')
});
10
jgb

小さなヒント:parseIntNumber#toStringは両方とも基数引数をサポートします。 36の基数を使用して、URLの数値(またはリストへのインデックス)をエンコードしてみてください。

9
thomasfuchs

この問題には、エンコードと圧縮という2つの主要な側面があります。

汎用の圧縮は、小さな文字列ではうまく機能しないようです。ブラウザは文字列を圧縮するためのAPIを提供しないため、ソースをロードする必要がありますが、これは巨大になる可能性があります。

効率的なエンコードを使用することで、多くの文字を保存できます。 μ という名前のライブラリを作成して、エンコードとデコードの部分を処理しました。アイデアは、仕様としてurlパラメータの構造とドメインについて利用可能な情報をできるだけ多く指定することです。この仕様を使用して、エンコードとデコードを駆動できます。たとえば、ブール値は1ビットのみを使用してエンコードでき、整数は別のbase(64)に変換できるため、必要な文字数を削減できます。オブジェクトキーは仕様から推測できるため、エンコードする必要はありません。列挙型はログ2(numberOfAllowedValues)ビット。

4
Anantha Kumaran

更新:さらに最適化したNPMパッケージをリリースしました。 https://www.npmjs.com/package/@yaska-eu/jsurl2 を参照してください

さらにいくつかのヒント:

  • _a..zA..Z0..9+/=_を使用したBase64エンコード、および エンコードされていないURI文字 は_a..zA..Z0..9-_.~_です。そのため、Base64の結果は_+/=_を_-_._と交換するだけでよく、URIを展開しません。
  • キー名の配列を保持して、配列内のオフセットである最初の文字でオブジェクトを表すことができます。 _{foo:3,bar:{g:'hi'}}_は、指定されたキー配列_a3,b{c'hi'}_になります_['foo','bar','g']_

興味深いライブラリ:

  • JSUrl は、JSONを具体的にエンコードして、RFCで指定されているよりも多くの文字を使用している場合でも、変更なしでURLに配置できるようにします。 _{"name":"John Doe","age":42,"children":["Mary","Bill"]}_は~(name~'John*20Doe~age~42~children~(~'Mary~'Bill))になり、キー辞書_['name','age','children']_は~(0~'John*20Doe~1~42~2~(~'Mary~'Bill))になる可能性があるため、101バイトのURIから38。にエンコードされます。
    • フットプリントが小さく、高速で合理的な圧縮。
  • lz-string は、LZWベースのアルゴリズムを使用して、localStorageに格納するために文字列をUTF16に圧縮します。 URIセーフな出力を生成するcompressToEncodedURIComponent()関数もあります。
    • それでも、わずか数KBのコードで、非常に高速で、優れた/優れた圧縮が行われます。

したがって、基本的にこれら2つのライブラリのいずれかを選択し、問題を解決することをお勧めします。

4
w00t

おそらく、jsonp APIを使用してURL短縮サービスを見つけることができます。これにより、すべてのURLを自動的に短くすることができます。

http://yourls.org/ jsonpもサポートしています。

2
Jeena

Github APIには、多くの項目の数値IDが含まれているようです(リポジトリのように見え、ユーザーはIDを持っていますが、ラベルは持っていません)。有利な場合には、名前の代わりにこれらの番号を使用することが可能かもしれません。次に、クエリ文字列で生き残るものでそれらを最適にエンコードする方法を把握する必要があります。 base64(url)のようなもの。

たとえば、hoodie.jsリポジトリのIDは4780572です。

これをビッグエンディアンの符号なしint(必要なだけのバイト数)にパックすると、\x00H\xf2\x1cが得られます。

先頭のゼロを投げるだけで、後でいつでも元に戻すことができ、H\xf2\x1cができました。

URLセーフなbase64としてエンコードすると、SPIcがあります(取得される可能性のあるパディングをすべてトスします)。

hoodiehq/hoodie.jsからSPIcに行くのは、かなりの勝ちのようです!

より一般的には、時間をかけたい場合は、クエリ文字列の冗長性を利用してみてください。他のアイデアは、2つのブール型パラメーターを単一の文字にパックするラインに沿っており、他の状態(どのフィールドが含まれているかなど)と一緒かもしれません。 base64エンコードを使用する場合(URLセーフバージョンのため、ここでは最適なオプションと思われます-base85を調べましたが、URLで存続できない文字の束があります)、6ビットの文字ごとのエントロピー...それでできることはたくさんあります。

トーマス・フックスのメモに付け加えると、はい、あなたがエンコードしているもののいくつかに固有の不変の順序がある場合、それは明らかに役立つでしょう。しかし、それはラベルとマイルストーンの両方にとって難しいようです。

2
djc

サードパーティのリンク短縮サービスを使用してみませんか?

(これは既存のアプリケーションであると述べたため、 RIの長さの制限 に問題がないと仮定しています。)

Greasemonkey スクリプトまたはその周辺を書いているように見えるので、おそらく GM_xmlhttpRequest() にアクセスできます。これにより、サードパーティのリンク短縮サービスを使用できます。

それ以外の場合は、 XMLHttpRequest() を使用し、同じサーバーで独自のリンク短縮サービスをホストして、 same-Origin policy 境界を越えないようにする必要があります。独自の短縮サービスをホストするための簡単なオンライン検索で 7個のフリー/オープンソースのリストPHPリンク短縮サービススクリプト および もう1つ GitHubでは、「このアプリのロジックはブラウザ内のみであり、書き込み可能なバックエンドがないため」という質問があるため、この種のアプローチは除外される可能性があります。

RL Shortener UserScript(Greasemonkeyの場合)でこの種のことを実装するサンプルコードを見ることができます。これは、Shift + Tを押すと、現在のページのURLの短縮バージョンをポップアップします。

もちろん、短縮サービスはユーザーを長い形式のURLにリダイレクトしますが、これはサーバーサイド以外のソリューションでは問題になります。少なくとも短縮器は、理論的にはプロキシすることができます(Apacheの RewriteRule with [P]など)または<frame>タグを使用します。

2
Adam Katz

たぶん、簡単なJSミニファイアーが役立ちます。シリアル化および逆シリアル化ポイントでのみ統合する必要があります。それが最も簡単な解決策だと思います。

1
not-found.404

短い

URLのparamsセクションからのみ開始する、独自のURLパッキングスキームを使用します。

長い

ここで他の人が指摘しているように、典型的な圧縮システムは短い文字列では機能しません。ただし、URLとParamsはデータモデルのシリアル化形式であることを認識することが重要です:特定のセクションを含むテキスト形式の人間が読める形式-スキームが最初であり、ホストが直後に検出され、ポートが暗黙的に認識されるが、オーバーライドされるなど.

元のデータモデルを使用すると、ビット効率の高いシリアル化スキームでシリアル化できます。実際、約50%の圧縮をアーカイブするようなシリアル化を自分で作成しました: http://blog.alivate.com.au/packed-url/ を参照してください

0
Todd