it-roy-ru.com

Как сжать параметры URL

Скажем, у меня есть одностраничное приложение , которое использует сторонний 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_-], или любой логический индикатор.

Все идет нормально. Теперь, поскольку строка запроса может быть немного длинной и громоздкой, и я хотел бы иметь возможность передавать URL-адреса, такие как http://espy.github.io/ubersicht/?state=SOMOPAQUETOKENTHATLOSSLESSLYDECOMPRESSESINTOTHEORIGINALVALUES#hoodiehq, чем короче, тем лучше.

Моя первая попытка была использовать некоторый zlib-подобный алгоритм для этого ( https://github.com/imaya/zlib.js ), а @flipzagging указывал на antirez/smaz (https // github.com/antirez/smaz), что звучит более подходящим для коротких строк (версия JavaScript на https://github.com/personalcomputer/smaz.js ).

Так как = и & специально не обрабатываются в https://github.com/personalcomputer/smaz.js/blob/master/lib/smaz.js#L9 , мы могли бы немного там подправить.

Кроме того, существует опция для кодирования значений в фиксированной таблице, например, порядок аргументов предопределен, и все, что нам нужно отслеживать, это фактическое значение. Например. превратить a=hamster&b=cat в 7hamster3cat (длина + символы) или hamster | cat (значение + |), потенциально до сжатия smaz.

Есть ли что-то еще, что я должен искать?

47
Jan Lehnardt

Рабочее решение, объединяющее воедино несколько хороших (или, я так думаю, идей)

Я сделал это для удовольствия, главным образом потому, что он дал мне возможность реализовать кодировщик Хаффмана в PHP, и я не смог найти удовлетворительную существующую реализацию.

Однако это может сэкономить вам время, если вы планируете исследовать аналогичный путь.

Барроуз-Уилер + движение вперед + преобразование Хаффмана

Я не совсем уверен, что BWT лучше всего подойдет для вашего вклада.
Это не обычный текст, поэтому повторяющиеся шаблоны, вероятно, встречаются не так часто, как в исходном коде или на простом английском языке.

Кроме того, динамический код Хаффмана должен был бы передаваться вместе с закодированными данными, что для очень коротких входных строк сильно повредило бы коэффициент сжатия.

Я вполне могу ошибаться, и в этом случае я с радостью увижу, как кто-то доказывает, что я такой.

Во всяком случае, я решил попробовать другой подход.

Основной принцип

1) определить структуру для ваших параметров URL и удалить постоянную часть

например, начиная с:

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

экстракт:

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

где , и | действуют как разделители строк и/или полей, в то время как логические значения не нужны.

2) определить статическое перераспределение символов на основе ожидаемого среднего ввода и вывести статический код Хаффмана

Поскольку передача динамической таблицы заняла бы больше места, чем ваша исходная строка, я думаю, что единственный способ добиться какого-либо сжатия вообще - это иметь статическую таблицу Хаффмана.

Однако вы можете использовать структуру своих данных в своих интересах для вычисления разумных вероятностей.

Вы можете начать с перераспределения букв на английском или других языках и добавить определенный процент цифр и других знаков препинания.

Тестируя с динамическим кодированием Хаффмана, я увидел степень сжатия от 30 до 50%.

Это означает, что со статической таблицей вы можете ожидать коэффициент сжатия, равный 0,6 (уменьшая длину ваших данных на 1/3), не намного больше.

3) преобразовать этот двоичный код Хаффмана в то, что может обработать URI

70 обычных ASCII 7-битовых символов в этом списке

!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

даст вам коэффициент расширения около 30%, практически не лучше, чем кодирование base64.

Расширение на 30% разрушило бы усиление статического сжатия Хаффмана, так что это вряд ли вариант!

Однако, поскольку вы управляете клиентом и сервером кодирования, вы можете использовать все, что не является зарезервированным символом URI.

Интересная возможность состоит в том, чтобы дополнить вышеприведенный набор до 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 символа Юникода для длины ввода около 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ìíîïĩīĭįıĵĺļłţŧſƚƫƭǐǰȉȋțȴȷɉɨɪɫɬɭʇʈʝːˑ˸;·ϳіїјӏ᠇ᴉᵵᵻᶅᶖḭḯḷḹḻḽṫṭṯṱẗẛỉị⁞⎺⎻⎼⎽ⱡⱦ꞉༈ǁ‖༅༚ᵑᵝᵡᵦᵪา᠑⫶ᶞᚁᚆᚋᚐᚕᵒᵔᵕᶱₒⵗˣₓᶹๅʶˠ᛫ᵛᵥᶺᴊ


Это не сложно и довольно весело, но это означает еще больше работы :).

Прибыль Хаффмана в терминах символов составляет около 30%.

.



С другой стороны, выбор тонких глифов на самом деле делает больше для сокращения строки.

Таким образом, в целом комбинация обоих может действительно чего-то достичь, хотя для скромного результата это большая работа.

So all in all the combination of both might indeed achieve something, though it's a lot of work for a modest result.

41
kuroi neko

Так же, как вы сами предлагаете, я бы сначала избавился от всех персонажей, которые не несут никакой информации, потому что они являются частью «формата».

Например. включите "tags = open, ssl, cypher & repository = 275643 & username = ryanbrg & milestones = & with_comment = yes" в "open, ssl, cyper | 275643 | ryanbrg || yes".

Затем используйте кодировку Хаффмана с фиксированным вектором вероятности (что приводит к фиксированному отображению символов в цепочки битов переменной длины - с наиболее вероятными символами, сопоставленными с более короткими цепочками битов, и менее вероятными символами, сопоставленными с более длинными цепочками битов).

Вы даже можете использовать разные векторы вероятности для разных параметров. Например, в параметре «метки» альфа-символы будут иметь высокую вероятность, но в параметре «хранилище» числовые символы будут иметь наибольшую вероятность. Если вы сделаете это, вы должны рассмотреть разделитель "|" часть предыдущего параметра.

И, наконец, превратите длинную цепочку битов (которая является объединением всех цепочек битов, с которыми были сопоставлены символы) во что-то, что вы можете поместить в URL, закодировав его с помощью base64url.

Если бы вы могли прислать мне набор репрезентативных списков параметров, я мог бы запустить их через кодер Хаффмана, чтобы увидеть, насколько хорошо они сжимаются.

Вектор вероятности (или, что то же самое, отображение символов в цепочки битов) должен быть закодирован как постоянные массивы в функцию Javascript, отправляемую в браузер.

Конечно, вы можете пойти еще дальше и, например, попытаться получить список возможных ярлыков с их вероятностями. Затем вы можете отобразить все метки в цепочки битов с кодировкой Хаффмана. Это даст вам лучшее сжатие, но у вас будет дополнительная работа с новыми надписями (например, возврат к кодированию одного символа) и, конечно, с отображением (которое, как упоминалось выше, является постоянным массивом в функции Javascript). ) будет намного больше.

17
mschoenert

У меня есть хитрый план! (И выпить джин-тоник)

Вы, кажется, не заботитесь о длине потока, но о длине получающихся глифов, например что за строка, которая отображается пользователю.

Браузер довольно хорошо преобразует IRI в базовый [URI] [2], в то же время отображая IRI в адресной строке. IRI имеют больший репертуар возможных символов, в то время как ваш набор возможных символов довольно ограничен.

Это означает, что вы можете закодировать биграммы ваших символов (aa, ab, ac, ..., zz и специальные символы) в один символ полного спектра Unicode. Допустим, у вас есть 80 возможных ASCII символов: количество возможных комбинаций двух символов составляет 6400. Их легко найти в назначенных символах Unicodes, например, в едином спектре ХАН:

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

Я выбрал CJK, потому что это (мало-мальски) разумно, если целевые символы назначаются в Юникоде и им назначены глифы в основных браузерах и операционных системах. По этой причине закрыта область частного использования, а более эффективная версия, использующая триграммы (чьи возможные комбинации могут использовать все возможные кодовые точки Unicodes 1114112), отсутствует.

Напомним: лежащие в основе байты все еще там и - учитывая кодировку UTF-8 - возможны еще дольше, но строка отображаемых символов, которые пользователь видит и копирует, на 50% короче.

Хорошо, хорошо, причины, почему это решение безумно:

  • ИРИ не идеальны. У многих меньших инструментов, чем у современного браузера, есть свои проблемы.

  • Алгоритм, очевидно, требует много дополнительной работы. Вам понадобится функция, которая отображает биграммы на целевые символы и обратно. И это должно предпочтительно работать арифметически, чтобы избежать больших хеш-таблиц в памяти.

  • Целевые символы должны быть проверены, если они назначены, и являются ли они простыми символами и не представляют собой причудливые юникодовские вещи, такие как объединение символов или вещи, которые потерялись где-то в нормализации Юникода. Также, если целевой областью является непрерывный диапазон назначенных символов с глифами.

  • Браузер иногда настороженно относится к IRI. По понятной причине, учитывая IDN омограф атак. Они в порядке со всеми этими не-ASCII-символами в их адресной строке?

  • И самое важное: люди, как известно, плохо запоминают персонажей в сценариях, которых они не знают. Они даже хуже пытаются (повторно) набрать эти символы. И copy'n'paste может пойти не так во многих различных кликах. Существует причина, по которой сокращатели URL используют Base64 и даже меньшие алфавиты. 

... говоря о котором: это было бы моим решением. Разгрузка работы по сокращению ссылок либо для пользователя, либо для интеграции goo.gl или bit.ly через их API.

11
ttepasse

Небольшой совет: и parseInt, и Number#toString поддерживают аргументы radix. Попробуйте использовать основание 36 для кодирования чисел (или индексов в списки) в URL.

9
thomasfuchs

Почему бы не использовать протокол-буферы ?

Буферы протокола - это гибкий, эффективный, автоматизированный механизм для сериализации структурированных данных - представьте 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')
});
8
jgb

Есть два основных аспекта проблемы: кодирование и сжатие. 

Сжатие общего назначения не очень хорошо работает на маленьких строках. Поскольку браузеры не предоставляют API для сжатия строк, вам также необходимо загрузить исходный код, который может быть огромным.

Много символов может быть сохранено с помощью эффективной кодировки. Я написал библиотеку с именем μ для обработки части кодирования и декодирования. Идея состоит в том, чтобы указать столько информации, сколько имеется информации о структуре и домене параметров URL, как спецификацию. Эта спецификация может затем использоваться для управления кодированием и декодированием. Например, логическое значение может быть закодировано с использованием только одного бита, целое число может быть преобразовано в другое основание (64), тем самым уменьшая количество требуемых символов, нет необходимости кодировать ключи объекта, поскольку это может быть выведено из спецификации, перечисления могут быть закодированы с использованием журнал2(numberOfAllowedValues) биты.

4
Anantha Kumaran

Обновление: я выпустил пакет 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, поэтому его можно поместить в 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-безопасного вывода .
    • Всего лишь несколько КБ кода, довольно быстрое, хорошее/отличное сжатие.

Поэтому я бы порекомендовал выбрать одну из этих двух библиотек и решил, что проблема решена.

3
w00t

Возможно, вы можете найти сокращатель URL-адреса с помощью API-интерфейса jsonp, и тогда все URL-адреса будут очень короткими автоматически.

http://yourls.org/ даже есть поддержка JSONP.

2
Jeena

Похоже, что API-интерфейсы Github имеют числовые идентификаторы для многих вещей (похоже, что у репозиториев есть и у пользователей, а у меток нет) под обложками. Можно было бы использовать эти числа вместо имен везде, где это выгодно. Затем вам нужно выяснить, как лучше всего кодировать их в чем-то, что будет существовать в строке запроса, например, что-то вроде base64 (url).

Например, ваш репозиторий hoodie.js имеет идентификатор 4780572.

Упаковка этого в целое число без знака с прямым порядком байтов (столько байтов, сколько нам нужно) дает нам \x00H\xf2\x1c.

Мы просто выбрасываем начальный ноль, мы всегда можем восстановить это позже, теперь у нас есть H\xf2\x1c.

Кодируйте как URL-безопасный base64, и у вас будет SPIc (отбросьте любой заполнитель, который вы можете получить).

Переход от hoodiehq/hoodie.js к SPIc выглядит как крупный выигрыш!

В целом, если вы готовы тратить время, вы можете попытаться использовать кучу избыточностей в строках запроса. Другие идеи в том, что касается упаковки двух логических параметров в один символ, возможно, вместе с другим состоянием (например, какие поля включены). Если вы используете base64-кодировку (которая кажется наилучшим вариантом здесь из-за URL-безопасной версии - я посмотрел на base85, но в ней есть набор символов, которые не выживут в URL), то вы получите 6 битов энтропия на персонажа ... с этим можно многое сделать.

Если добавить к примечанию Томаса Фукса, да, если в некоторых вещах, которые вы кодируете, есть какое-то неотъемлемое, неизменное упорядочение, то это, очевидно, также поможет. Однако это кажется трудным как для лейблов, так и для основных этапов.

2
djc

Почему бы не использовать стороннее сокращение ссылок?

(Я предполагаю, что у вас нет проблем с ограничениями длины URI так как вы упомянули, что это существующее приложение.)

Похоже, что вы пишете Greasemonkey script или около того, поэтому, возможно, у вас есть доступ к GM_xmlhttpRequest () , который позволит использовать стороннее средство сокращения ссылок.

В противном случае вам нужно будет использовать XMLHttpRequest () и разместить собственную службу сокращения ссылок на том же сервере, чтобы избежать пересечения политики того же источника границы. Быстрый онлайн-поиск для размещения ваших собственных средств сокращения предоставил мне список из 7 бесплатных/открытых источников PHP сценариев сокращения ссылок и еще один на GitHub, хотя этот вопрос, вероятно, исключает такого рода подход, так как «логика приложения только в браузере, и нет никакого бэкэнда, в который я мог бы написать».

Вы можете увидеть пример кода, реализующего подобные вещи, в URL Shortener UserScript (для Greasemonkey), который выдает укороченную версию URL текущей страницы, когда вы нажимаете SHIFT + T.

Конечно, средства сокращения будут перенаправлять пользователей на URL-адрес длинной формы, но это будет проблемой в любом решении, не относящемся к серверу. По крайней мере, укорочитель может теоретически прокси (например, Apache RewriteRule с [P]) или использовать тег <frame>.

2
Adam Katz

Может быть, вам поможет любой простой JS minifier. Вам нужно будет только интегрировать его только в точки сериализации и десериализации. Я думаю, что это будет самое простое решение.

1
not-found.404

Короткий

Используйте схему упаковки URL, такую ​​как моя, начиная только с раздела params вашего URL.

Длиннее

Как уже отмечали другие, типичные системы сжатия не работают для коротких строк. Но важно понимать, что URL-адреса и параметры являются форматом сериализации модели данных: текстовый формат, понятный человеку, с конкретными разделами - мы знаем, что схема первая, хост находится сразу после, порт подразумевается, но может быть переопределенным и т.д ...

С исходной моделью данных можно сериализовать с помощью более эффективной битовой схемы сериализации. Фактически, я сам создал такую ​​сериализацию, которая архивирует примерно на 50% сжатие: см. http://blog.alivate.com.au/packed-url/

0
Todd