簡體   English   中英

如何壓縮 URL 參數

[英]How to compress URL parameters

假設我有一個使用第三方 API 獲取內容的單頁應用程序 該應用程序的邏輯僅在瀏覽器中,並且沒有我可以寫入的后端。

為了允許深入鏈接到應用程序的狀態,我使用 pushState 來跟蹤一些決定應用程序狀態的變量(注意 Ubersicht 的公共版本還沒有這樣做)。 在這種情況下reposlabelsmilestonesusernameshow_open (布爾)和with_comments (布爾)和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)聽起來更適合短字符串( https://github.com/personalcomputer/smaz.js 上的JavaScript 版本)。

由於=&沒有在https://github.com/personalcomputer/smaz.js/blob/master/lib/smaz.js#L9 中專門處理,我們也許可以在那里稍微調整一下。

此外,還有一個選項可以對固定表中的值進行編碼,例如參數的順序是預定義的,我們需要跟蹤的只是實際值。 例如,可能在 smaz 壓縮之前,將a=hamster&b=cat變成7hamster3cat (長度+字符)或 hamster|cat(值 + | )。

還有什么我應該尋找的嗎?

一個有效的解決方案,將各種好的(或者我認為的)想法放在一起

我這樣做是為了好玩,主要是因為它讓我有機會在 PHP 中實現 Huffman 編碼器,但我找不到令人滿意的現有實現。

但是,如果您打算探索類似的路徑,這可能會為您節省一些時間。

Burrows-Wheeler+move-to-front+Huffman 變換

我不太確定 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%。

這意味着對於靜態表,您可以期望壓縮因子為 0.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"
}

在該示例中,輸入被打包成 64 個 unicode 字符,輸入長度約為 100,減少了 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% 的壓縮率。

另一方面,選擇細的字形實際上更能縮小字符串。

因此,總而言之,兩者的結合確實可能會有所作為,盡管要獲得適度的結果需要做很多工作。

正如您自己建議的那樣,我會首先擺脫所有不攜帶任何信息的字符,因為它們是“格式”的一部分。

例如,將“labels=open,ssl,cypher&repository=275643&username=ryanbrg&milestones=&with_comment=yes”轉為“open,ssl,cyper|275643|ryanbrg||yes”。

然后使用具有固定概率向量的霍夫曼編碼(導致從字符到可變長度位串的固定映射 - 最可能的字符映射到較短的位串,而不太可能的字符映射到較長的位串)。

您甚至可以為不同的參數使用不同的概率向量。 例如,在參數“labels”中,字母字符的概率很高,但在“repository”參數中,數字字符的概率最高。 如果你這樣做,你應該考慮分隔符“|” 前面參數的一部分。

最后將長位串(即字符映射到的所有位串的串聯)轉換為可以通過 base64url 編碼放入 URL 的內容。

如果您可以向我發送一組代表性參數列表,我可以通過 Huffmann 編碼器運行它們以查看它們的壓縮情況。

概率向量(或等效地從字符到位串的映射)應該作為常量數組編碼到發送到瀏覽器的 Javascript 函數中。

當然,您可以走得更遠,例如 - 嘗試獲取可能的標簽及其概率的列表。 然后您可以使用霍夫曼編碼將整個標簽映射到位串。 這將為您提供更好的壓縮,但是對於那些新標簽(例如回退到單字符編碼),當然還有映射(如上所述,它是 Javascript 函數中的常量數組)需要額外的工作) 會大很多。

為什么不使用協議緩沖區

協議緩沖區是一種用於序列化結構化數據的靈活、高效、自動化的機制——想想 XML,但更小、更快、更簡單。 您可以定義一次數據的結構化方式,然后您可以使用特殊生成的源代碼輕松地使用各種語言在各種數據流中寫入和讀取結構化數據。 您甚至可以在不破壞針對“舊”格式編譯的已部署程序的情況下更新數據結構。

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')
});

我有一個狡猾的計划! (還有一杯杜松子酒)

您似乎並不關心字節流的長度,而是關心結果字形的長度,例如向用戶顯示的字符串。

瀏覽器可以很好地將IRI轉換為底層 [URI][2],同時仍然在地址欄中顯示 IRI。 IRI 具有更多可能的字符庫,而您的可能字符集卻相當有限。

這意味着您可以將字符(aa、ab、ac、...、zz 和特殊字符)的雙字符編碼為完整 unicode 范圍的一個字符。 假設您有 80 個可能的 ASCII 字符:兩個字符的可能組合數為 6400。在指定字符的 Unicode 中很容易找到,例如在漢統一 CJK 譜中:

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

我選擇 CJK 是因為如果目標字符以 unicode 分配並且在主要瀏覽器和操作系統上分配了字形,這只是(稍微)合理。 出於這個原因,私人使用區域已被淘汰,而使用三元組(其可能的組合可以使用所有 Unicode 1114112 可能的代碼點)的更有效的版本已被淘汰。

回顧一下:底層字節仍然存在,並且——給定 UTF-8 編碼——可能更長,但用戶看到和復制的顯示字符字符串縮短了 50%。

好的,好的,原因,為什么這個解決方案是瘋狂的:

  • IRI 並不完美。 許多比現代瀏覽器小的工具都有其問題。

  • 該算法顯然需要更多的工作。 您將需要一個將二元組映射到目標字符並返回的函數。 並且最好在算術上工作以避免內存中的大哈希表。

  • 應該檢查目標字符是否已分配,以及它們是否是簡單字符,而不是花哨的 unicodian 東西,例如組合字符或在 Unicode 規范化中丟失的東西。 此外,如果目標區域是帶有字形的指定字符的連續跨度。

  • 瀏覽器有時會警惕 IRI。 有充分的理由,鑒於 IDN 同形異義詞攻擊。 他們可以在地址欄中使用所有這些非 ASCII 字符嗎?

  • 最重要的是:眾所周知,人們在記住他們不知道的腳本中的角色方面非常糟糕。 他們在嘗試(重新)輸入這些字符時更糟糕。 在許多不同的點擊中,copy'n'paste 可能會出錯。 URL 縮短器使用 Base64 甚至更小的字母是有原因的。

......說到這:那將是我的解決方案。 將縮短鏈接的工作卸載給用戶或通過其 API 集成 goo.gl 或 bit.ly。

小提示: parseIntNumber#toString支持基數參數。 嘗試使用 36 的基數來編碼 URL 中的數字(或索引到列表中)。

更新:我發布了一個包含更多優化的 NPM 包,請參閱https://www.npmjs.com/package/@yaska-eu/jsurl2

還有一些提示:

  • Base64 編碼為a..zA..Z0..9+/=未編碼的 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 的算法將字符串壓縮為 UTF16 以存儲在 localStorage 中。 它還有一個compressToEncodedURIComponent()函數來生成 URI 安全的輸出。
    • 仍然只有幾 KB 的代碼,相當快,壓縮得很好/很好。

所以基本上我建議選擇這兩個庫之一並考慮解決問題。

這個問題有兩個主要方面:編碼和壓縮。

通用壓縮似乎不適用於小字符串。 由於瀏覽器不提供任何 api 來壓縮字符串,因此您還需要加載源代碼,這可能是巨大的。

使用有效的編碼可以保存大量字符。 我編寫了一個名為μ的庫來處理編碼和解碼部分。 這個想法是指定盡可能多的關於 url 參數的結構和域的信息作為規范。 然后可以使用該規范來驅動編碼和解碼。 例如,布爾值可以僅使用一位編碼,整數可以轉換為不同的 base(64) 從而減少所需的字符數,對象鍵不需要編碼,因為它可以從規范中推斷出來,枚舉可以使用編碼記錄2 (numberOfAllowedValues) 位。

看起來 Github API 有很多東西的數字 ID(看起來像 repos 和用戶有它們,但標簽沒有)在封面下。 有可能在任何有利的地方使用這些數字而不是名稱。 然后,您必須弄清楚如何最好地將這些編碼為可以在查詢字符串中保留的內容,例如 base64(url) 之類的內容。

例如,您的 hoodie.js 存儲庫的 ID 為4780572

將它打包成一個 big-endian unsigned int(我們需要的字節數)得到我們\\x00H\\xf2\\x1c

我們只會拋出前導零,我們可以稍后恢復它,現在我們有了H\\xf2\\x1c

編碼為 URL 安全的 base64,並且您擁有SPIc (扔掉您可能獲得的任何填充)。

hoodiehq/hoodie.jsSPIc似乎是一個很大的勝利!

更一般地說,如果您願意投入時間,您可以嘗試利用查詢字符串中的大量冗余。 其他想法是將兩個布爾參數打包成一個字符,可能還有其他狀態(比如包含哪些字段)。 如果您使用 base64 編碼(由於 URL 安全版本,這似乎是這里的最佳選擇——我查看了 base85,但它有一堆無法在 URL 中存活的字符),這將為您提供 6 位每個字符的熵...你可以用它做很多事情。

補充一下 Thomas Fuchs 的說明,是的,如果在您編碼的某些內容中存在某種固有的、不可變的排序,那么這顯然也會有所幫助。 然而,對於標簽和里程碑來說,這似乎很難。

也許你可以找到一個帶有 jsonp API 的 url 縮短器,這樣你就可以讓所有的 URL 自動變得很短。

http://yours.org/甚至有 jsonp 支持。

為什么不使用第三方鏈接縮短器?

(我假設您對URI 長度限制沒有問題,因為您提到這是一個現有的應用程序。)

看起來您正在編寫Greasemonkey腳本或類似腳本,因此您可能有權訪問GM_xmlhttpRequest() ,這將允許使用第三方鏈接縮短程序。

否則,您需要使用XMLHttpRequest()並在同一台服務器上托管您自己的鏈接縮短服務,以避免跨越同源策略邊界。 快速在線搜索托管您自己的縮短器為我提供了7 個免費/開源 PHP 鏈接縮短器腳本的列表和 GitHub 上的另外一個,盡管該問題可能排除了這種方法,因為“應用程序的邏輯僅在瀏覽器中,並且沒有我可以寫入的后端。”

您可以在URL Shortener UserScript(用於 Greasemonkey)中看到實現這種事情的示例代碼,當您按 SHIFT+T 時,它會彈出當前頁面 URL 的縮短版本。

當然,縮短器會將用戶重定向到長格式的 URL,但這在任何非服務器端解決方案中都是一個問題。 至少一個縮短器理論上可以代理(如帶有 [P] 的 Apache 的RewriteRule )或使用 <frame> 標簽。

也許任何簡單的 JS 壓縮器都會幫助你。 您只需要在序列化和反序列化點上集成它。 我認為這將是最簡單的解決方案。

短的

使用 URL 打包方案,例如我自己的,僅從 URL 的 params 部分開始。

更長

正如其他人指出的那樣,典型的壓縮系統不適用於短字符串。 但是,重要的是要認識到 URLs 和 Params 是數據模型的序列化格式:具有特定部分的文本人類可讀格式 - 我們知道該方案是第一個,主機在之后直接找到,端口是隱含的,但可以被覆蓋等...

使用底層概念數據模型,可以使用位效率更高的序列化方案進行序列化。 事實上,我自己創建了這樣一個序列化文件,壓縮率約為 50%:參見http://blog.alivate.com.au/packed-url/

從概念上講,我的方案是在考慮概念數據模型的情況下編寫的,它不會將 URL 反序列化為該概念模型作為一個獨特的步驟。 但是,這是可能的,並且這種正式方法可能會產生更高的效率,其中位不需要與字符串 URL 的順序相同。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM