如何压缩 URL 参数

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

为了允许深度链接到应用程序的状态,我使用 pushState()来跟踪一些决定应用程序状态的变量。(请注意,Ubersicht 的公共版本还没有这么做。)

  • 变量: reposlabelsmilestonesusernameshow_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 的算法。然后@flipzag 指向 安提雷兹/< strong > smaz ,它看起来更适合于短字符串。(JavaScript 版本)

由于在 Javascript 版本中没有专门处理 =&(参见 主 lib 文件的第9行) ,我们可以在那里稍微调整一下。

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

我还需要找什么吗?

42009 次浏览

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

看起来 Github API 有很多东西的数字 ID (看起来回购和用户有,但是标签没有)。在有利的地方可以使用这些数字而不是名称。然后,您必须弄清楚如何将这些内容最好地编码到查询字符串中,例如 base64(url)。

例如,您的 hoodie.js 存储库具有 ID 4780572

将它打包成一个 big-endian 无符号 int (需要多少字节就打多少字节) ,就得到了 \x00H\xf2\x1c

我们只需要抛出前导零,我们总是可以恢复以后,现在我们有 H\xf2\x1c

将其编码为 URL 安全的 base64,就得到了 SPIc(丢弃可能得到的任何填充)。

hoodiehq/hoodie.jsSPIc似乎是一个相当大的胜利!

更一般地说,如果愿意投入时间,可以尝试利用查询字符串中的一些冗余。其他想法是将两个布尔参数打包成一个字符,可能还包括其他状态(比如包含哪些字段)。如果你使用 base64编码(这似乎是最好的选择,因为这个版本是 URL 安全的——我看过 base85,但是它有一大堆字符不能在 URL 中存活) ,每个字符可以获得6比特的熵... 你可以用它做很多事情。

对于 Thomas Fuchs 的笔记,是的,如果在你编码的某些东西中,存在某种固有的、不可变的顺序,那么这显然也会有所帮助。然而,这对于标签和里程碑来说似乎都很困难。

我有个绝妙的计划

您似乎并不关心 bytestream 的长度,而是关心产生的字形的长度,例如,显示给用户的字符串是什么。

浏览器在将 IRI转换为底层[ URI ][2]的同时仍然在地址栏中显示 IRI 方面做得很好。IRI 拥有更多的可能字符,而您的可能字符集相当有限。

这意味着您可以将字符的双字符(aa、 ab、 ac、 ... 、 zz 和特殊字符)编码为完整 Unicode 频谱中的一个字符。假设您有80个可能的 ASCII 字符: 两个字符的可能组合的数目是6400。这些字符在 Unicodes 很容易找到,例如在汉统一的中日韩频谱中:

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

我之所以选择 CJK,是因为只有当目标字符以 unicode 格式分配,并且在主要浏览器和操作系统上分配了字形时,这才(稍微)合理。由于这个原因,私人使用区域已经过时,使用三元组的更有效的版本(其可能的组合可以使用所有 Unicode 1114112可能的代码点)已经过时。

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

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

  • IRI 并不完美,很多比现代浏览器更小的工具都有自己的问题。

  • 这个算法显然需要更多的工作。您将需要一个函数来将二元字符映射到目标字符并返回。而且它应该更好地算术工作,以避免在内存中的大散列表。

  • 应该检查目标字符,如果它们被分配,如果它们是简单的字符,而不是像组合字符或在 Unicode 标准化中丢失的东西那样喜欢的 unicodian 字符。此外,如果目标区域是指定的字符与符号的连续跨度。

  • 浏览器有时会提防 IRI。理由很充分,鉴于 IDN 的同形词攻击。它们对地址栏中所有这些非 ASCII 字符都满意吗?

  • 最大的问题是: 众所周知,人们不善于记住他们不认识的剧本中的人物。他们在尝试(重新)输入这些字符时更糟糕。而且复制粘贴可能会在许多不同的点击中出错。URL 缩短服务使用 Base64甚至更小的字母是有原因的。

说到这个,这就是我的解决办法。将缩短链接的工作卸载给用户,或者通过 API 集成 goo.gl 或 bit.ly。

也许您可以找到一个带有 jsonp API 的 URL 缩短程序,这样您就可以使所有的 URL 真正自动地变短。

Http://yourls.org/甚至支持 jsonp。

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

例如,将“ label = open,ssl,cypher & itory = 275643 & username = ryanbrg & Rio ones = & with _ comments = yes”转到 “ Open,ssl,cyper | 275643 | ryanbrg | | yes”。

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

您甚至可以对不同的参数使用不同的概率向量。例如,在参数“ label”中,字母字符具有较高的概率,但在“ itory”参数中,数字字符具有最高的概率。如果这样做,则应将分隔符“ |”视为前面参数的一部分。

最后,将长比特字符串(即所有字符映射到的比特字符串的连接)转换为可以通过 base64url 编码放入 URL 中的内容。

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

概率向量(或者相当于从字符到位字符串的映射)应该作为常量数组编码到发送到浏览器的 Javascript 函数中。

当然,您可以更进一步,例如,尝试获取可能的标签列表及其概率。然后您可以使用 Huffmann 编码将整个标签映射到位字符串。这会给你更好的压缩效果,但是对于那些新的标签,你还有额外的工作要做(比如回到单个字符编码) ,当然,映射(如上所述,是 Javascript 函数中的一个常量数组)会更大。

也许任何简单的 JS 缩小器将帮助您。您只需要在序列化和反序列化点上集成它。我觉得这是最简单的解决办法。

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

我这样做是为了好玩,主要是因为它让我有机会在 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)基于期望的平均输入定义符号的静态重分区,并导出静态哈夫曼码

由于传输一个动态表要比传输初始字符串占用更多的空间,我认为实现任何压缩的唯一方法就是使用一个静态 Huffman 表。

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

你可以从英语或其他语言的字母重新划分开始,然后加入一定比例的数字和其他标点符号。

通过使用动态 Huffman 编码进行测试,我发现压缩率在30% 到50% 之间。

这意味着对于一个静态表,您可以期望得到0.6的压缩因子(将数据的长度减少1/3) ,仅此而已。

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

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

!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

会给你一个大约30% 的扩展因子,实际上并不比基64编码好。

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数据流 <-> Huffman code <-> 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;
}
}

基本的 Huffman 编解码器

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

将被动态 Huffman 表压缩为59个字符。

毫无疑问,智能数据重新排序将减少这种情况,但是接下来您需要传递动态表..。

中国人来救你了?

根据 提帕斯的想法,人们可以利用大量的亚洲字符找到一个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 代码的方法。

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

根据 角色,霍夫曼增益在30% 左右。

当然,这些字符在大多数情况下是多字节的,但是如果您的目标是最短的 URI,那么这并不重要。
除了可以很容易地压缩为1位的布尔值之外,这些讨厌的字符串似乎不太愿意被压缩。
也许可以更好地调整频率,但我怀疑你将获得超过50% 的压缩率。

另一方面,挑选细的象形文字实际上更有助于缩小字符串。

因此,总的来说,两者的结合可能确实会取得一些成果,尽管要取得一个适度的结果需要做大量的工作。

为什么不使用第三方链接缩短程序?

(我假设您在使用 URI 长度限制时没有问题,因为您提到了这是一个现有的应用程序。)

看起来您正在编写一个 油猴子脚本或者类似的东西,所以也许您可以访问 GM _ xmlhttpRequest (),这将允许使用第三方链接缩短程序。

否则,您将需要使用 XMLHttpRequest ()并在同一台服务器上承载您自己的链接缩短服务,以避免跨越 同一原产地政策边界。我在 GitHub 上快速搜索了一下自己的缩短程序,得到了一个 7个免费/开源 PHP 链接缩短脚本的列表再来一杯,不过这个问题可能排除了这种方法,因为“这个应用的逻辑只能在浏览器中使用,而且没有后端可写。”

您可以在 网址缩短服务 UserScript (用于 Greasemonkey)中看到实现此类事情的示例代码,当您按 SHIFT + T 时,它会弹出当前页面 URL 的缩写版本。

当然,缩写程序会将用户重定向到长形式的 URL,但是这在任何非服务器端解决方案中都是一个问题。至少在理论上,缩写程序可以代理(比如 Apache 的 重写规则使用[ P ])或使用 < frame > 标记。

为什么不使用 协议缓冲区

协议缓冲是一种灵活、高效、自动化的结构化数据序列化机制——想想 XML,但更小、更快、更简单。您可以定义如何将数据结构化一次,然后可以使用特殊生成的源代码轻松地在各种数据流和使用各种语言之间编写和读取结构化数据。您甚至可以更新数据结构,而无需中断按“旧”格式编译的已部署程序。

Js 将对象转换为协议缓冲区消息,反之亦然。

下面的对象转换为: CgFhCgFiCgFjEgFkEgFlEgFmGgFnGgFoGgFpIgNqZ2I=

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

例子

下面的示例是使用 需求构建的。

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

更新: 我发布了一个带有更多优化的 NPM 包,请参见 https://www.npmjs.com/package/@yaska-eu/jsurl2

更多提示:

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

有趣的图书馆:

  • 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 的代码,速度很快,压缩效果很好。

因此,基本上我建议从这两个库中选择一个,并认为问题已经解决了。

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

通用压缩似乎不适用于小字符串。由于浏览器不提供任何 API 来压缩字符串,因此您还需要加载源代码,这可能非常庞大。

但是使用有效的编码可以保存大量字符。我已经编写了一个名为 μ 的库来处理编码和解码部分。

其思想是尽可能多地指定关于 URL 参数的结构和域的信息作为规范。然后可以使用此规范驱动编码和解码。例如:

  • 布尔值可以只使用一位进行编码;
  • 整数可以转换为 base64(从而减少所需的字符数) ;
  • 对象键不需要编码(因为它们可以从规范中推断出来) ;
  • 枚举可以使用 log2(numberOfAllowedValues)位进行编码。

很短

使用 URL 打包方案,比如我自己的,仅从 URL 的 params 部分开始。

更久

正如其他人指出的,典型的压缩系统不适用于短字符串。但是,重要的是要认识到 URL 和 Params 是一种数据模型的序列化格式: 一种具有特定部分的文本人类可读格式——我们知道方案是第一个,主机是在后面找到的,端口是隐含的,但是可以被覆盖,等等。.

使用底层的概念数据模型,可以使用比特效率更高的序列化方案进行序列化。事实上,我自己已经创建了这样一个序列化,它存档了大约50% 的压缩: 请参阅 http://blog.alivate.com.au/packed-url/

从概念上讲,我的方案是基于概念数据模型编写的,它并没有将 URL 反序列化到那个概念模型中作为一个独立的步骤。然而,这是可能的,而且这种正式的方法可能会产生更大的效率,在这种情况下,比特不需要与字符串 URL 的顺序相同。