如何压缩URL参数

假设我有一个使用第三方API进行内容的单页应用程序 。 该应用程序的逻辑仅在浏览器中,并且没有可以写入的后端。

为了允许深入链接到应用程序的状态,我使用pushState来跟踪确定应用程序状态的一些variables(请注意,Ubersicht的公开版本尚未执行此操作)。 在这种情况下, show_openlabelsmilestonesusernameshow_open (布尔)和with_comments (布尔)和with_comments (布尔)。 url格式是?label=label_1,label_2,label_3&repos=repo_1… 值通常是犯罪嫌疑人,粗略[a-zA-Z][a-zA-Z0-9_-]或任何布尔指标。

到现在为止还挺好。 现在,由于查询string可能有点长,笨重,我想能够传递像http://espy.github.io/ubersicht/?state=SOMOPAQUETOKENTHATLOSSLESSLYDECOMPRESSESINTOTHEORIGINALVALUES#hoodiehqurl,越短越好。

我第一次尝试是使用一些类似zlib的algorithm( https://github.com/imaya/zlib.js )和@ flipzagging指向antirez / smaz(https // github.com / antirez / smaz)听起来更适合短string(JavaScript版本在https://github.com/personalcomputer/smaz.js )。

既然=&没有在https://github.com/personalcomputer/smaz.js/blob/master/lib/smaz.js#L9中专门处理,我们可能会稍微调整一下。

此外,还有一个选项可以在一个固定的表中对值进行编码,例如参数的顺序是预定义的,我们需要跟踪的是实际值。 例如,在smaz压缩之前,将a=hamster&b=cat变成7hamster3cat (length + chars)或仓鼠| cat(value + | )。

还有什么我应该找的?

一个工作的解决scheme,把各种好的(或者我认为)想法放在一起

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

但是,如果您计划探索类似的path,这可能会节省一些时间。

Burrows-Wheeler +移动到前面+霍夫曼变换

我不太确定BWT是否最适合您的input。
这不是常规文本,因此重复模式可能不会像源代码或简单英文那样频繁发生。

此外,dynamic的霍夫曼码必须与编码的数据一起传递,对于非常短的input串,会严重损害压缩增益。

我很可能是错的,在这种情况下,我很乐意看到有人certificate我是。

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

一般原则

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 

| 充当string和/或字段终止符,而布尔值不需要。

2)根据预期的平均input定义符号的静态重新分区,并导出静态霍夫曼码

由于传输一个dynamic表会占用比初始string更多的空间,我认为唯一的办法就是拥有一个静态的哈夫曼表。

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

您可以从英文或其他语言的信件再分配开始,并input一定比例的数字和其他标点符号。

用dynamic霍夫曼编码进行testing,我看到压缩率为30到50%。

这意味着使用静态表格,您可以预期可能会有.6的压缩因子(将数据的长度减less1/3),而不是更多。

3)将这个二进制Huffmann代码转换成一个URI可以处理的东西

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

 !'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz 

会给你一个约30%的扩展因子,实际上不比base64编码更好。

一个30%的扩展会破坏静态霍夫曼压缩的收益,所以这不是一个select!

但是,由于您控制了编码客户端和服务器端,因此您可以使用任何不是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 

所支持的每种语言都有一个所有使用字母的频率表

数字和其他电脑符号,如-._将具有全球频率,不pipe语言如何

分隔符(和| )频率将根据结构中列表和字段的数量进行计算。

所有其他的“外来”字符将被转义为一个特定的代码,并编码为普通的UTF-8。

履行

双向转换path如下:

字段列表< – > UTF-8数据stream< – > huffman代码< – > 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" } 

在这个例子中,input被打包成64个Unicode字符,input长度约为100,产生1/3的缩小。

等效的string:

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

将被一个dynamic的霍夫曼表压缩到59个字符。 没有太大的区别。

毫无疑问,明智的数据重新sorting会减less,但是你需要将dynamic表传递给…

中国人去救援?

根据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个亚洲字符:

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

但是,正如您所看到的,平均表意字符的绝对大小使得string实际上更长(像素方式),所以即使这个想法是有希望的,结果也是相当令人失望的。

select更薄的字形

另一方面,您可以尝试select“瘦”字符作为URI编码的基础。 例如:

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

代替

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

这将使比例字体缩小一半,包括在浏览器地址栏中。

我迄今为止最好的256个“瘦”字形候选集:

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

结论

这个实现应该移植到JavaScript以允许客户端 – 服务器交换。
您还应该提供一种方法来与客户共享结构和霍夫曼编码。

这并不难,也很有趣,但这意味着更多的工作:)。

霍夫曼的人物收益约为30%。

当然,这些字符大部分是多字节的,但是如果你的目标是最短的URI,这并不重要。
除了容易打包到1位的布尔值之外,那些讨厌的string似乎不太愿意被压缩。
有可能更好地调整频率,但我怀疑你会得到超过50%的压缩率。

另一方面,挑选细字形实际上更多的是缩小string。

因此,两者的结合可能确实实现了一些目标,尽pipe对于一个适中的结果来说这是一个很大的工作。

就像你自己提出的那样,我首先要摆脱那些没有任何信息的angular色,因为他们是“格式”的一部分。

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

然后使用具有固定概率向量的Huffmann编码(导致从字符到可变长度比特串的固定映射 – 最可能的字符映射到较短的比特串,而较不可能的字符映射到较长的比特串)。

你甚至可以为不同的参数使用不同的概率向量。 例如在参数“标签”中字母字符的概率很高,但在“资料库”参数中,数字字符的概率最高。 如果你这样做,你应该考虑分隔符“|” 前面参数的一部分。

最后把long bittring(这是连接字符映射到的所有位串)转换成你可以通过base64url编码到URL中的东西。

如果你可以给我一个有代表性的参数列表,我可以通过一个Huffmann编码器来运行它们,看它们压缩的程度。

概率向量(或者等价地从字符到位串的映射)应该被编码为常量数组到JavaScript函数中,并发送给浏览器。

当然,你可以走得更远 – 例如 – 尝试获得可能的标签列表。 然后你可以用Huffmann编码将整个标签映射成比特串。 这会给你更好的压缩效果,但是对于那些新的标签(例如回落到单字符编码)你将会有额外的工作,当然映射(如上所述)是Javascript函数中的一个常量数组)将会大得多。

我有一个狡猾的计划! (和一杯杜松子酒补品)

您似乎不关心字节stream的长度,而是关于所得字形的长度,例如显示给用户的string是什么。

浏览器在将IRI转换为底层的[URI] [2]的同时仍然可以在地址栏中显示IRI。 IRI有更多的可能的字符,而你可能的字符集是相当有限的。

这意味着你可以将你的字符(aa,ab,ac,…,zz和特殊字符)的bigrams编码成完整的unicode频谱的一个字符。 假设你有80个可能的ASCII字符:两个字符的可能组合数量是6400.在Unicode分配的字符中很容易find这些字符,例如在统一的CJK频谱中:

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

我select了CJK,因为如果目标字符被分配在unicode中,并且已经在主要浏览器和操作系统上分配了字形,则这只是(略微)合理的。 由于这个原因,私人使用区域已经不存在,使用卦(其可能的组合可以使用所有的Unicode 1114112可能的代码点)的效率更高。

回顾一下:底层字节仍然存在,并且 – 给定UTF-8编码 – 甚至可能更长,但用户看到并显示的string缩短了50%。

好吧,好吧,原因,为什么这个解决scheme是疯了:

  • IRIs不完美。 许多较现代浏览器较小的工具都有问题。

  • 该algorithm显然需要更多的工作。 你需要一个将bigrams映射到目标字符的函数。 在算术上应该更好地工作,以避免内存中的大哈希表。

  • 目标字符应该被检查,如果他们被分配,如果他们是简单的字符,而不是奇特的单一的东西,如结合字符或东西在Unicode规范化的某处丢失。 另外,如果目标区域是带字形的分配字符的连续范围。

  • 浏览器有时会对IRIs保持警惕。 出于好的理由,给予IDN同形异义词攻击。 他们在地址栏中使用所有这些非ASCII字符都行吗?

  • 最大的问题是,人们在记忆他们不知道的脚本中的人物时,是非常糟糕的。 他们在尝试(重新)input这些字符时更糟糕。 而copy'n'paste可能会出现许多不同的点击错误。 有一个原因URL缩短使用Base64甚至更小的字母。

…其中说:这将是我的解决scheme。 将链接的缩短工作卸载给用户,或通过API集成goo.gl或bit.ly。

小提示: parseIntNumber#toString支持基数参数。 尝试使用36的基数来编码URL中的数字(或索引到列表)。

为什么不使用协议缓冲区 ?

协议缓冲区是用于序列化结构化数据的一种灵活,高效的自动化机制 – 思考XML,但更小,更快,更简单。 您可以定义一次数据的结构,然后使用专门生成的源代码轻松地将结构化数据写入各种数据stream,或使用各种语言读取结构化数据。 您甚至可以更新您的数据结构,而不会破坏针对“旧”格式编译的部署程序。

ProtoBuf.js将对象转换为协议缓冲区消息,并且反之 。

以下对象转换为: CgFhCgFiCgFjEgFkEgFlEgFmGgFnGgFoGgFpIgNqZ2I=

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

以下示例使用require.js构build 。 试试这个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') }); 

这个问题有两个主要方面:编码和压缩。

通用压缩似乎不适用于小string。 由于浏览器不提供任何API来压缩string,所以还需要加载源代码,这可能是巨大的。

很多字符可以通过使用有效的编码来保存。 我写了一个名为μ的库来处理编码和解码部分。 这个想法是指定尽可能多的关于url参数的结构和域的信息作为一个规范。 这个规范可以用来驱动编码和解码。 例如,布尔值可以使用一位编码,整数可以转换为不同的基数(64),从而减less所需的字符数,对象键不需要编码,因为它可以从规范推断,枚举可以编码使用日志2 (numberOfAllowedValues)位。

It looks like the Github APIs have numeric IDs for many things (looks like repos and users have them, but labels don't) under the covers. It might be possible to use those numbers instead of names wherever advantageous. You then have to figure out how to best encode those in something that'll survive in a query string, eg something like base64(url).

For example, your hoodie.js repository has ID 4780572 .

Packing that into a big-endian unsigned int (as many bytes as we need) gets us \x00H\xf2\x1c .

We'll just toss the leading zero, we can always restore that later, now we have H\xf2\x1c .

Encode as URL-safe base64, and you have SPIc (toss any padding you might get).

Going from hoodiehq/hoodie.js to SPIc seems like a good-sized win!

More generally, if you're willing to invest the time, you can try to exploit a bunch of redudancies in your query strings. Other ideas are along the lines of packing the two boolean params into a single character, possibly along with other state (like what fields are included). If you use base64-encoding (which seems the best option here due to the URL-safe version — I looked at base85, but it has a bunch of characters that won't survive in a URL), that gets you 6 bits of entropy per character… there's a lot you can do with that.

To add to Thomas Fuchs' note, yes, if there's some kind of inherent, immutable ordering in some of things you're encoding, than that would obviously also help. However, that seems hard for both the labels and the milestones.

Perhaps you can find a url shortener with a jsonp API, that way you could make all the URLs really short automatically.

http://yourls.org/ even has jsonp support.

Why not use a third party link shortener?

(I am assuming you don't have a problem with URI length limits since you mentioned this is an existing application.)

It looks like you're writing a Greasemonkey script or thereabouts, so perhaps you have access to GM_xmlhttpRequest() , which would allow use of a third party link shortener.

Otherwise, you'd need to use XMLHttpRequest() and host your own link shortening service on the same server to avoid crossing the same-origin policy boundary. A quick online search for hosting your own shorteners supplied me with a list of 7 free/open source PHP link shortener scripts and one more on GitHub, though the question likely excludes this kind of approach since "The app's logic is in-browser only, and there is no backend I can write to."

You can see example code implementing this kind of thing in the URL Shortener UserScript (for Greasemonkey), which pops up a shortened version of the current page's URL when you press SHIFT+T.

Of course, shorteners will redirect users to the long form URL, but this would be a problem in any non-server-side solution. At least a shortener can theoretically proxy (like Apache's RewriteRule with [P]) or use a <frame> tag.

Some more tips:

  • Base64 encodes with a..zA..Z0..9+/= , and un-encoded URI characters are a..zA..Z0..9-_.~ . So Base64 results only need to swap +/= for -_. and it won't expand URIs.
  • You could keep an array of key names, so that objects could be represented with the first character being the offset in the array, eg {foo:3,bar:{g:'hi'}} becomes a3,b{c'hi'} given key array ['foo','bar','g']

Interesting libraries:

  • JSUrl specifically encodes JSON so it can be put in a URL without changes, even though it uses more characters than specified in the RFC. {"name":"John Doe","age":42,"children":["Mary","Bill"]} becomes ~(name~'John*20Doe~age~42~children~(~'Mary~'Bill)) and with a key dictionary ['name','age','children'] that could be ~(0~'John*20Doe~1~42~2~(~'Mary~'Bill)) , thus going from 101 bytes URI encoded to 38.
    • Small footprint, fast, reasonable compression.
  • lz-string uses an LZW-based algorithm to compress strings to UTF16 for storing in localStorage. It also has a compressToEncodedURIComponent() function to produce URI-safe output.
    • Still only a few KB of code, pretty fast, good/great compression.

So basically I'd recommend picking one of these two libraries and consider the problem solved.

Maybe any simple JS minifier will help you. You'll need only to integrate it on serialization and deserialization points only. I think it'd be the easiest solution.