如何压缩URL参数

82

假设我有一个单页应用程序,它使用第三方API提供内容。 应用程序的逻辑仅在浏览器中实现; 我无法编写后端。

为了允许深度链接到应用程序的状态,我使用pushState()来跟踪一些变量以确定应用程序的状态。(请注意,Ubersicht的公共版本尚未执行此操作。)

  • 变量: repos, labels, milestones, username, show_open (bool), with_comments (bool)和without_comments (bool)。
  • 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的算法进行压缩。 然后@flipzagging指向了antirez/smaz,它看起来更适合短字符串。(JavaScript版本在这里。)

由于 Javascript 版本没有特别处理 =&(详见主要库文件的第 9 行),我们可以在那里稍微调整一下。

此外,还有一个选项可以对值进行编码到一个固定表中。使用这个选项,参数的顺序是预定义的,我们只需要跟踪实际值即可。例如:将 a=hamster&b=cat 转换为 7hamster3cat(长度+字符)或 hamster|cat(值 + |),可能是在进行 Smaz 压缩之前。

我还需要寻找其他内容吗?


1
使用Base62编码的Packer值得一试。我经常使用deflate()和inflate(),但你需要对deflate的输出进行base64编码... http://danml.com/js/compression.js - dandavis
@OP - 你可以将这些值存储在cookie或隔离存储中,而不是查询字符串中吗? - O.O
13个回答

68

将各种好的(或者我认为是好的)想法放在一起的可行解决方案

我这样做只是为了好玩,主要是因为它给了我一个机会在PHP中实现Huffman编码器,而我找不到一个令人满意的现有实现。

然而,如果您计划探索类似的路径,这可能会为您节省一些时间。

Burrows-Wheeler+move-to-front+Huffman变换

我不确定BWT是否最适合您的输入类型。
这不是常规文本,因此重复模式可能不像源代码或纯英语那样经常出现。

此外,对于非常短的输入字符串,必须传递动态Huffman代码以及编码数据,这将严重损害压缩增益。

我可能是错的,如果是这样,我很乐意看到有人证明我是错的。

无论如何,我决定尝试另一种方法。

一般原则

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)基于预期平均输入定义符号的静态重新分配,并导出静态Huffman码。

由于传输动态表所需的空间比初始字符串更大,因此我认为实现任何压缩的唯一方法是具有静态哈夫曼表。

但是,您可以利用数据的结构来计算合理的概率。

您可以从英语或其他语言中字母的重新分配开始,并投入一定比例的数字和其他标点符号。

使用动态Huffman编码进行测试,我看到了30至50%的压缩率。

这意味着使用静态表,您可能会期望0.6的压缩系数(将数据长度减少1/3),并没有更多。

3)将此二进制Huffmann码转换为URI可以处理的内容

该列表中的70个常规ASCII 7位字符

!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

这将给你一个大约30%的扩展因子,实际上并不比base64编码好多少。

30%的扩展会破坏静态Huffman压缩所获得的收益,因此这几乎不是一个选项!

然而,由于您控制着编码客户端和服务器端,因此您可以使用任何不是URI保留字符的内容。

一个有趣的可能性是将上述设置补充到256个字符,其中包括任何Unicode字符,这将允许使用相同数量的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,以便允许客户端和服务器之间的交换。
您还应该提供一种与客户端共享结构和Huffman编码的方式。

这并不难,而且相当有趣,但这意味着更多的工作:)。

从字符角度来看,Huffman编码可以减少约30%。

当然,这些字符大部分是多字节的,但如果您追求最短的URI,那就无所谓了。
除了布尔值可以轻松压缩为1位之外,那些令人讨厌的字符串似乎不太愿意被压缩。
也许可以更好地调整频率,但我怀疑您能达到50%以上的压缩率。

另一方面,选择细小的字形确实可以更好地缩小字符串。

总而言之,两者的组合可能确实可以取得一些成果,尽管这需要很多工作才能获得适度的结果。


4
你的帖子也值得奖励 - 我会回来。 - Sven
谢谢。我不是为了钱在这里 :). 我可以将相关部分移植到JavaScript中,以便做出更适合的响应,但无论如何,我认为Google协议缓冲区的JS端口并不是完成此任务的正确工具。你需要编写成千上万行的JavaScript代码来解决一个小问题,而最终生成的“压缩”字符串实际上会比你简化后的消息负载更长。 - kuroi neko
真的很好奇这个贴子为什么会被踩...难道违反了法律吗? - kuroi neko
这是您的赏金。不确定200是否太多,但SO只允许您逐步授予奖励,因此没有额外的100赏金。另一方面,您的答案获得了比其他答案更多的赞同,我认为这是观众的积极投票。 - Sven
哈哈,晚来总比不来好吧。我很高兴它对你有帮助。虽然我从未抽出时间编写客户端代码,但如果你真的想使用它,我可以看一下。 - kuroi neko
1
有没有 JavaScript 的实现? - rraallvv

17

为什么不使用Protocol Buffers

Protocol Buffers是一种灵活、高效、自动化的机制,用于序列化结构化数据 - 类似于XML,但更小、更快、更简单。您只需定义一次想要如何结构化数据,然后就可以使用特殊生成的源代码,轻松地将结构化数据编写到各种数据流中,并从各种语言中读取它们。您甚至可以更新您的数据结构,而不会破坏已针对“旧”格式编译的程序。

ProtoBuf.js可将对象转换为Protocol Buffer消息,反之亦然。

以下对象转换为: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')
});

5
奖励赏金是为了在Javascript中拥有真正的实现,看起来像是缩短数据并将其返回。 - Sven
1
协议缓冲区:4000行JavaScript。Require.js:2000行JavaScript。扩展因子大于1。如果这是一个解决方案,那么问题是什么?;) - kuroi neko
@kuroineko,require.js是可选的。Long.js、ByteBuffer.js和ProtoBuf.js被压缩并gziped为20Kb(在128 Kbps连接上下载时间为1秒)。这个提案是直接的,并且向后兼容未来的更改(旧书签)。 - jgb
我不是来争论的,但这种兼容性取决于 Google 库的外部端口,这些库仅维护 C++、Python 和 Ruby。在我看来,这种解决方案的缺陷在于将所有东西都嵌入客户端,而服务器端工具可以完成 90% 的工作,并仅向客户端传递最小的数据(参数结构和压缩字符串),将客户端代码减少到一个小的编码器/解码器(比 base64 编码更高效,否则“压缩”字符串实际上会比原始数据更长!)。 - kuroi neko
3
jsfiddle出现故障。访问 https://rawgit.com/dcodeIO/protobuf.js/master/dist/protobuf-light.min.js 时会得到404错误。请检查链接是否正确并尝试修复该问题。 - Ryan

17

正如你所建议的那样,我会首先摆脱所有不携带任何信息的字符,因为它们是“格式”的一部分。

例如,将“labels=open,ssl,cypher&repository=275643&username=ryanbrg&milestones=&with_comment=yes”转换为“open,ssl,cyper|275643|ryanbrg||yes”。

然后使用具有固定概率向量的Huffmann编码(从字符到可变长度位串的固定映射 - 最可能的字符映射到较短的位串,可能性较小的字符映射到较长的位串)。

您甚至可以为不同的参数使用不同的概率向量。例如,在参数“labels”中,字母字符将具有高概率,但在“repository”参数中,数字字符将具有最高概率。如果这样做,您应该考虑分隔符“ |”作为前一个参数的一部分。

最后,通过对所有字符映射到的位串进行连接,将长位串转换为可以放入URL的内容,并对其进行base64url编码。

如果您可以向我发送一组代表性的参数列表,我可以运行它们通过Huffmann编码获得压缩效果。

概率向量(或等效地,从字符到位串的映射)应编码为常量数组,嵌入到发送到浏览器的JavaScript函数中。

当然,您甚至可以进一步努力,例如尝试获取可能标签的列表及其概率。然后,您可以将整个标签映射到带有Huffmann编码的位串。这将给您更好的压缩效果,但对于那些新标签会有额外的工作(例如回退到单个字符编码),并且映射(如上所述是JavaScript函数中的常量数组)将变得更大。


13

我有一个狡猾的计划!(同时还有一杯金汤力饮料)

你好像不关心字节流的长度,而是关心最终显示给用户的字符数,例如所显示的字符串。

浏览器在将IRI转换为底层[URI][2]时非常出色,同时仍在地址栏中显示IRI。IRI具有更多可能的字符集合,而您的字符集合相对较少。

这意味着您可以将两个字符(aa,ab,ac等等,以及特殊字符)的二元组编码为完整Unicode谱系中的一个字符。假设您有80个可能的ASCII字符:两个字符的可能组合数为6400。这些在已分配Unicode字符中很容易找到,例如在汉字统一CJK谱系中:

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

我选择CJK,因为只有当目标字符在Unicode中被分配并在主要浏览器和操作系统上具有已分配的字形时,这才是(稍微)合理的。因此,私有使用区域不起作用,并且使用三字母组合的更有效版本不起作用(其可能的组合可以使用Unicodes 1114112个可能代码点中的所有内容)。

总之:底层字节仍然存在,而且 - 在给定UTF-8编码的情况下 - 可能会更长,但用户看到并复制的显示字符字符串要缩短50%。

好吧,好吧,为什么这种解决方案是疯狂的原因:

  • IRI并非完美无缺,许多较小的工具比现代浏览器还要出问题。

  • 算法显然需要更多工作。您将需要一个将bigrams映射到目标字符并返回的函数。最好它在算术上可行,以避免在内存中使用大型哈希表。

  • 应检查目标字符是否被分配以及是否为简单字符,而不是像组合字符或在Unicode规范化过程中遗失的那些花哨的Unicode东西。还要检查目标区域是否为具有字形的已分配字符的连续跨度。

  • 浏览器有时会对IRI持谨慎态度。出于良好的原因,鉴于IDN同形异义词攻击。他们是否接受地址栏中所有这些非ASCII字符?

  • 而最大的问题是:人们对不认识的脚本中的字符记忆力很差。他们尝试(重新)输入这些字符的能力甚至更差。复制并粘贴可能会在许多不同的单击中出错。URL缩短器使用Base64甚至更小的字母表有道理。

顺便说一下:那将是我的解决方案。将缩短链接的工作委托给用户或通过其API集成goo.gl或bit.ly。


10

小提示:无论是parseInt 还是 Number#toString,都支持进制参数。尝试使用36进制来在URL中编码数字(或列表中的索引)。


8

更新:我发布了一个NPM包,其中包含一些更多的优化,详见https://www.npmjs.com/package/@yaska-eu/jsurl2

一些其他的提示:

  • Base64使用a..zA..Z0..9+/=进行编码,而未编码的URI字符a..zA..Z0..9-_.~。因此,Base64结果只需要将+/=替换为-_.,就不会扩展URIs。
  • 你可以保留一个关键字名称的数组,这样对象就可以用第一个字符表示数组中的偏移量,例如{foo:3,bar:{g:'hi'}}变成a3,b{c'hi'},给定关键字数组['foo','bar','g']

有趣的库:

  • JSUrl专门对JSON进行编码,以便在URL中使用而不进行更改,即使它使用的字符比RFC规定的多。 {"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的代码,速度相当快,压缩效果良好/极佳。
所以基本上我建议选择这两个库中的一个,问题就解决了。

嘿,只是一个小的侧记:base64编码最终会使字符串变长,特别是如果你一开始使用的是基本ASCII(很多编码空间将保持未使用)。 - Christian

8
问题有两个主要方面:编码和压缩。
通用的压缩在小字符串上似乎不起作用。由于浏览器没有提供任何压缩字符串的API,您还需要加载源代码,这可能非常庞大。
但是,使用高效的编码可以节省很多字符。我编写了一个名为μ的库来处理编码和解码部分。
思路是尽可能指定关于URL参数结构和域的信息作为规范。然后可以使用此规范来驱动编码和解码。例如:
- 布尔值可以使用一个比特位进行编码; - 整数可以转换为base64(从而减少所需的字符数); - 对象键不需要进行编码(因为它们可以从规范中推断出来); - 枚举可以使用log2(numberOfAllowedValues)位进行编码。

2
我喜欢这个实现!在我们的项目中尝试过,看起来运行得很好。现在只需要让它与Angular路由一起工作 :) - RichieRock
它与IE9兼容吗? - Sachin Singh
我没有明确地在IE9上进行测试,但它应该可以工作,因为实现不依赖于任何特定于浏览器的API。 - Anantha Kumaran

3
也许你可以找到一个带有jsonp API的URL缩短服务,这样你就可以自动地将所有URL缩短得非常短。
甚至http://yourls.org/也支持jsonp。

2
我认为这个解决方案在一般情况下是有效的,但由于“没有后端可写入”的限制,无论通信方法多么复杂,它都被排除了。 :) - Sven

2
看起来 Github 的 API 在内部为很多东西(看起来像是仓库和用户,但标签不是)都有数字 ID。在有利的情况下,可以使用这些数字而非名称。然后你需要想办法将它们编码成能在查询字符串中存活的格式,例如类似于 base64(url) 的东西。
例如,您的 hoodie.js 仓库 ID 是 4780572。
将其打包成大端无符号整数(需要多少字节就用多少字节),得到 \x00H\xf2\x1c。
我们只需丢掉开头的零,稍后可以恢复,现在我们有 H\xf2\x1c。
再将其编码为 URL 安全的 base64,就得到 SPIc(去掉可能得到的填充)。
从 hoodiehq/hoodie.js 到 SPIc 看起来是一个不错的优化!
更一般地说,如果愿意投入时间,可以尝试利用查询字符串中的许多重复项。其他想法是将两个布尔参数打包成单个字符,可能还包括其他状态(例如包含哪些字段)。如果使用 base64 编码(由于 URL 安全版本,这似乎是最佳选择,我看了看 base85,但其中有很多字符无法在 URL 中存活),这会为每个字符提供 6 位熵……你可以做很多事情。
补充一下 Thomas Fuchs 的说明,是的,如果某些被编码的东西具有固有的、不可变的排序,则显然也会有所帮助。然而,这对标签和里程碑似乎都很难实现。

2
为什么不使用第三方链接缩短服务呢?(我假设您没有问题URI长度限制,因为您提到这是一项现有的应用程序。)看起来您正在编写一个Greasemonkey脚本或类似的东西,因此也许您可以使用GM_xmlhttpRequest()访问第三方链接缩短服务。
否则,您需要使用XMLHttpRequest()并在同一服务器上托管自己的链接缩短服务,以避免越过同源策略边界。快速在线搜索托管您自己的短链接提供了7个免费/开源PHP链接缩短器脚本列表一个在GitHub上,尽管问题可能不包括这种方法,因为“应用程序的逻辑仅在浏览器中,并且没有后端可以写入。”
您可以在URL缩短器用户脚本(针对Greasemonkey)中查看实现此类操作的示例代码,当您按SHIFT + T时,它会弹出当前页面URL的缩短版本。
当然,缩短器会将用户重定向到长形式的URL,但这在任何非服务器端解决方案中都是一个问题。至少缩短器理论上可以代理(像Apache的RewriteRule与[P])或使用标签。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接