web-dev-qa-db-fra.com

Comment compresser les paramètres d'URL

Supposons que je possède une application à page unique qui utilise une API tierce pour le contenu. La logique de l'application est uniquement dans le navigateur, et il n'y a pas de back-end où je puisse écrire.

Pour permettre la création de liens profonds dans l’état de l’application, j’utilise pushState pour suivre quelques variables qui déterminent l’état de l’application (notez que la version publique d’Ubersicht ne le fait pas encore). Dans ce cas, repos, labels, milestones et username, show_open (bool) et with_comments (bool) et without_comments (bool). Le format de l'URL est ?label=label_1,label_2,label_3&repos=repo_1…. Les valeurs sont les suspects habituels, approximativement [a-zA-Z][a-zA-Z0-9_-], ou n’importe quel indicateur booléen.

Jusqu'ici tout va bien. Maintenant, comme la chaîne de requête peut être un peu longue et difficile à manier et que je voudrais pouvoir transmettre des URL comme http://espy.github.io/ubersicht/?state=SOMOPAQUETOKENTHATLOSSLESSLYDECOMPRESSESINTOTHEORIGINALVALUES#hoodiehq, plus court sera le mieux.

Ma première tentative allait utiliser un algorithme de type zlib ( https://github.com/imaya/zlib.js ) et @flipzagging désignait antirez/smaz (https // github.com/antirez/smaz) qui semble plus approprié pour les chaînes courtes (version JavaScript à l’adresse https://github.com/personalcomputer/smaz.js ).

Puisque = et & ne sont pas spécifiquement traités dans https://github.com/personalcomputer/smaz.js/blob/master/lib/smaz.js#L9 , nous pourrions peut-être modifier un peu les choses.

En outre, il existe une option pour coder les valeurs dans une table fixe, par ex. l'ordre des arguments est prédéfini et tout ce dont nous avons besoin de garder trace est la valeur réelle. Par exemple. transformez a=hamster&b=cat en 7hamster3cat (longueur + caractères) ou hamster | cat (valeur + |), éventuellement avant la compression smaz.

Y a-t-il autre chose que je devrais être à la recherche?

47
Jan Lehnardt

Une solution de travail qui rassemble divers morceaux de bonnes idées (ou du moins je pense)

Je l’ai fait pour le plaisir, principalement parce que cela m’a donné l’occasion d’implémenter un encodeur Huffman dans PHP et que je n’ai pas trouvé d’implémentation existante satisfaisante.

Cependant, cela pourrait vous faire gagner un peu de temps si vous envisagez d'explorer un chemin similaire.

Burrows-Wheeler + transformation frontale + transformation de Huffman

Je ne suis pas certain que BWT conviendrait mieux à votre type de contribution.
Ce n’est pas un texte normal, donc des motifs récurrents ne se produiraient probablement pas aussi souvent que dans le code source ou l’anglais brut.

En outre, un code dynamique de Huffman devrait être transmis avec les données codées qui, pour des chaînes d'entrée très courtes, nuiraient gravement au gain de compression.

Je pourrais très bien me tromper, auquel cas je verrais volontiers quelqu'un prouver que je le suis.

Quoi qu'il en soit, j'ai décidé d'essayer une autre approche.

Principe général

1) définissez une structure pour vos paramètres d'URL et supprimez la partie constante

par exemple, à partir de:

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

extrait:

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

, et | agissent comme des terminateurs de chaîne et/ou de champ, alors que les valeurs booléennes n'en ont pas besoin.

2) définir une répartition statique des symboles en fonction de l'entrée moyenne attendue et dériver un code de Huffman statique

Étant donné que la transmission d’une table dynamique prend plus de place que votre chaîne initiale, je pense que la seule façon de réduire la compression est d’avoir une table statique de Huffman.

Cependant, vous pouvez utiliser la structure de vos données à votre avantage pour calculer des probabilités raisonnables.

Vous pouvez commencer avec la répartition des lettres en anglais ou dans d'autres langues et ajouter un certain pourcentage de chiffres et autres signes de ponctuation.

En testant avec un codage dynamique de Huffman, j'ai constaté des taux de compression de 30 à 50%.

Cela signifie qu'avec une table statique, vous pouvez vous attendre à un facteur de compression de 0,6 (réduisant la longueur de vos données de 1/3), pas beaucoup plus.

3) convertir ce code binaire de Huffmann en quelque chose qu'un URI peut gérer

Les 70 caractères ASCII normaux 7 bits de cette liste

!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

vous donnerait un facteur d'expansion d'environ 30%, pratiquement pas mieux qu'un code base64.

Une expansion de 30% ruinerait le gain d'une compression Huffman statique, ce n'est donc pas une option!

Cependant, étant donné que vous contrôlez le côté client et serveur d'encodage, vous pouvez utiliser à peu près tout ce qui n'est pas un caractère réservé à l'URI.

Une possibilité intéressante serait de compléter la configuration ci-dessus à 256 avec tous les glyphes unicode, ce qui permettrait de coder vos données binaires avec le même nombre de caractères conformes à l'URI, remplaçant ainsi un groupe pénible et lent de divisions d'entiers longs par un éclair. consultation rapide de table.

Description de la structure

Le codec est destiné à être utilisé côté client et côté serveur. Il est donc essentiel que le serveur et les clients partagent une définition de structure de données commune.

Étant donné que l'interface est susceptible d'évoluer, il semble judicieux de stocker un numéro de version pour assurer la compatibilité ascendante.

La définition de l'interface utilisera un langage de description très minimaliste, comme ceci:

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

Chaque langue prise en charge aura un tableau de fréquence pour toutes ses lettres utilisées

les chiffres et autres symboles informatiques tels que -, . ou _ auront une fréquence globale, quelles que soient les langues

les séparateurs (, et |) seront calculés en fonction du nombre de listes et de champs présents dans la structure.

Tous les autres caractères "étrangers" seront échappés avec un code spécifique et codés en tant que UTF-8 en clair.

La mise en oeuvre

Le chemin de conversion bidirectionnel est le suivant:

liste des champs <-> flux de données UTF-8 <-> codes de Huffman <-> URI

Voici le codec principal

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

Le codec Huffman sous-jacent

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

Et le dictionnaire Huffman

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

Exemple

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

sortie:

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"
}

Dans cet exemple, l'entrée a été compressée dans 64 caractères Unicode, pour une longueur d'entrée d'environ 100, donnant une réduction de 1/3.

Une chaîne équivalente:

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

Serait compressé par une table dynamique de Huffman à 59 caractères. Pas beaucoup de différence. 

Nul doute que le réordonnancement intelligent des données réduirait cela, mais vous devrez alors passer le tableau dynamique ...

Chinois à la rescousse?

En s’inspirant de l’idée de ttepasse , on pourrait tirer parti du grand nombre de caractères asiatiques pour trouver une plage de valeurs contiguës 0x4000 (12 bits), pour coder 3 octets en 2 caractères CJK, comme suit:

    // 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");

et retour:

    // 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]);
    }

La sortie précédente de 64 caractères latins

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

serait "réduire" à 42 caractères asiatiques:

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

Cependant, comme vous pouvez le constater, la grande majorité de votre idéogramme moyen allonge la chaîne (pixels), de sorte que même si l’idée était prometteuse, le résultat est plutôt décevant.

Choisir des glyphes plus minces

D'autre part, vous pouvez essayer de choisir des caractères "minces" comme base pour le codage URI. Par exemple:

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

au lieu de

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

Cela réduira la longueur de moitié avec les polices proportionnelles, y compris dans une barre d'adresse du navigateur.Mon meilleur ensemble de 256 glyphes "minces" à ce jour:.

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


Ce n'est pas difficile et plutôt amusant à faire, mais cela signifie encore plus de travail :).

Le gain de Huffman en terme de caractères est d’environ 30%.

.



En revanche, la sélection de glyphes minces ne réduit en réalité plus la chaîne.

En résumé, la combinaison des deux pourrait en effet permettre d’atteindre quelque chose, même s’il faut beaucoup de travail pour un résultat modeste.

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

Tout comme vous le proposez, je voudrais d'abord supprimer tous les caractères qui ne portent aucune information, car ils font partie du "format".

Par exemple. tournez "labels = open, ssl, cypher & repository = 275643 & username = ryanbrg & milestones = & with_comment = yes" en "open, ssl, cyper | 275643 | ryanbrg || yes".

Ensuite, utilisez un codage de Huffmann avec un vecteur de probabilité fixe (résultant en un mappage fixe de caractères en chaînes de bits de longueur variable - les caractères les plus probables étant mappés en chaînes de bits plus courtes et les caractères moins probables en cartes de bits plus longues).

Vous pouvez même utiliser différents vecteurs de probabilité pour les différents paramètres. Par exemple, dans le paramètre "étiquettes", les caractères alpha auront une probabilité élevée, mais dans le paramètre "référentiel", les caractères numériques auront la probabilité la plus élevée. Si vous faites cela, vous devriez considérer le séparateur "|" une partie du paramètre précédent.

Enfin, transformez la chaîne de bits longue (qui correspond à la concaténation de toutes les chaînes de caractères auxquelles les caractères ont été mappés) en quelque chose que vous pouvez insérer dans une URL en le codant avec base64url.

Si vous pouviez m'envoyer un ensemble de listes de paramètres représentatifs, je pourrais les passer par un codeur Huffmann pour vérifier leur compression.

Le vecteur de probabilité (ou l'équivalent mappage des caractères en chaînes de bits) doit être codé sous forme de tableaux constants dans la fonction Javascript envoyée au navigateur.

Bien sûr, vous pourriez aller encore plus loin et - par exemple - essayer d'obtenir une liste de libellés possibles avec leurs probabilités. Ensuite, vous pouvez mapper des étiquettes entières en chaînes de bits avec un encodage de Huffmann. Cela vous donnera une meilleure compression, mais vous aurez du travail supplémentaire pour les nouvelles étiquettes (par exemple, revenir au codage à caractère unique), et bien sûr le mappage (qui - comme mentionné ci-dessus - est un tableau constant dans la fonction Javascript ) sera beaucoup plus grande.

17
mschoenert

J'ai un plan rusé! (Et un verre de gin tonic)

Vous ne semblez pas vous soucier de la longueur du flux secondaire, mais de la longueur des glyphes résultants, par exemple. ce que la chaîne qui est affichée à l'utilisateur.

Les navigateurs sont assez efficaces pour convertir un IRI en [URI] [2] sous-jacent tout en affichant l’IRI dans la barre d’adresse. Les IRI ont un plus grand répertoire de caractères possibles, tandis que votre ensemble de caractères possibles est plutôt limité.

Cela signifie que vous pouvez encoder des bigrammes de vos caractères (aa, ab, ac,…, zz et caractères spéciaux) en un seul caractère du spectre Unicode complet. Supposons que vous ayez 80 caractères ASCII possibles: le nombre de combinaisons possibles de deux caractères est 6400. Ce sont des caractères faciles à trouver dans les caractères attribués à Unicodes, par exemple. dans le spectre unifié CJK:

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

J'ai choisi CJK parce que ce n'est (légèrement) raisonnable que si les caractères cibles sont attribués en unicode et se voient attribuer des glyphes sur le navigateur principal et les systèmes d'exploitation. Pour cette raison, la zone d'utilisation privée est supprimée et la version la plus efficace utilisant des trigrammes (dont les combinaisons possibles pourraient utiliser tous les points de code Unicodes 1114112 possibles) est supprimée.

Pour récapituler: les octets sous-jacents sont toujours là et - compte tenu du codage UTF-8 - possibles encore plus longtemps, mais la chaîne de caractères affichés que l'utilisateur voit et copie est réduite de 50%.

Ok, Ok, pourquoi cette solution est folle:

  • Les IRI ne sont pas parfaits. Beaucoup d'outils de moindre qualité que le navigateur moderne ont leurs problèmes.

  • L'algorithme nécessite évidemment beaucoup plus de travail. Vous aurez besoin d’une fonction qui mappe les bigrammes sur les caractères cibles et inversement. Et il est préférable de travailler de manière arithmétique pour éviter les grandes tables de hachage en mémoire.

  • Les caractères cibles doivent être vérifiés s'ils sont attribués et s'il s'agit de simples caractères et non de choses unicodiennes sophistiquées, telles que la combinaison de caractères ou d'éléments perdus quelque part dans la normalisation Unicode. Également si la zone cible est une étendue continue de caractères assignés avec des glyphes.

  • Les navigateurs se méfient parfois des adresses IRI. Pour une bonne raison, étant donné les attaques d'homographes IDN. Sont-ils OK avec tous ces caractères non-ASCII dans leur barre d'adresse?

  • Et le plus gros: les gens sont notoirement mauvais pour se souvenir des caractères dans des scripts qu'ils ne connaissent pas. Ils sont encore plus difficiles à essayer de (re) taper ces caractères. Et copier et coller peut mal tourner en plusieurs clics. Il y a une raison pour laquelle les raccourcisseurs d'URL utilisent Base64 et des alphabets encore plus petits. 

… En parlant de: ça serait ma solution. Décharger le travail de raccourcissement des liens vers l'utilisateur ou intégrer goo.gl ou bit.ly via leurs API.

11
ttepasse

Petit conseil: parseInt et Number#toString prennent en charge les arguments de base. Essayez d’utiliser une base de 36 pour coder des nombres (ou des index dans des listes) dans des URL.

9
thomasfuchs

Pourquoi ne pas utiliser protocol-buffers ?

Les tampons de protocole constituent un mécanisme souple, efficace et automatisé de sérialisation de données structurées. Pensez XML, mais plus petit, plus rapide et plus simple. Vous définissez comment vous voulez que vos données soient structurées une fois, puis vous pouvez utiliser un code source généré spécial pour écrire et lire facilement vos données structurées vers et depuis une variété de flux de données et en utilisant diverses langues. Vous pouvez même mettre à jour votre structure de données sans interrompre les programmes déployés compilés avec le "vieux" format.

ProtoBuf.js convertit les objets en messages de tampon de protocole et vice versa.

L'objet suivant est converti en: CgFhCgFiCgFjEgFkEgFlEgFmGgFnGgFoGgFpIgNqZ2I=

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

Exemple

L'exemple suivant est construit avec require.js . Essayez-le 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

Le problème présente deux aspects principaux: l’encodage et la compression. 

La compression à usage général ne semble pas bien fonctionner sur les petites chaînes. Comme les navigateurs ne fournissent aucune API pour compresser une chaîne, vous devez également charger le code source, ce qui peut être énorme.

Beaucoup de caractères peuvent être sauvegardés en utilisant un encodage efficace. J'ai écrit une bibliothèque nommée μ pour gérer la partie encodage et décodage. L'idée est de spécifier autant que des informations disponibles sur la structure et le domaine des paramètres d'URL sous forme de spécification. Cette spécification peut ensuite être utilisée pour piloter le codage et le décodage. Par exemple, un booléen peut être codé en utilisant un seul bit, un entier peut être converti en une base différente (64), ce qui réduit le nombre de caractères requis. bûche2(numberOfAllowedValues) bits.

4
Anantha Kumaran

Mise à jour: j'ai publié un package NPM avec quelques optimisations supplémentaires, voir https://www.npmjs.com/package/@yaska-eu/jsurl2

Quelques autres conseils:

  • Les codes Base64 avec a..zA..Z0..9+/= et les caractères d'URI non codés sont a..zA..Z0..9-_.~. Ainsi, les résultats Base64 doivent uniquement échanger +/= pour -_. et ne pas développer les URI.
  • Vous pouvez conserver un tableau de noms de clés afin que les objets puissent être représentés avec le premier caractère correspondant au décalage du tableau, par exemple. {foo:3,bar:{g:'hi'}} devient a3,b{c'hi'} étant donné le tableau de clés ['foo','bar','g']

Bibliothèques intéressantes:

  • JSUrl code spécifiquement JSON afin qu'il puisse être placé dans une URL sans modification, même s'il utilise plus de caractères que celui spécifié dans le RFC. {"name":"John Doe","age":42,"children":["Mary","Bill"]} devient ~(name~'John*20Doe~age~42~children~(~'Mary~'Bill)) et avec un dictionnaire de clé ['name','age','children'] qui pourrait être ~(0~'John*20Doe~1~42~2~(~'Mary~'Bill)), passant ainsi de l'URI de 101 octets codé à 38 .
    • Faible encombrement, compression rapide et raisonnable.
  • lz-string utilise un algorithme basé sur LZW pour compresser les chaînes au format UTF16 afin de les stocker dans localStorage. Il a également une fonction compressToEncodedURIComponent() pour produire une sortie URI-safe .
    • Encore que quelques Ko de code, assez rapide, bonne/bonne compression.

Donc, en gros, je vous recommande de choisir l’une de ces deux bibliothèques et de considérer le problème résolu.

3
w00t

Peut-être que vous pouvez trouver un raccourcisseur d’URL avec une API jsonp, de cette façon vous pourriez créer automatiquement toutes les URL très courtes.

http://yourls.org/ a même le support jsonp.

2
Jeena

Il semble que les API Github aient des identifiants numériques pour beaucoup de choses (ça ressemble à des dépôts et les utilisateurs les ont, mais pas les étiquettes) sous les couvertures. Il serait peut-être possible d'utiliser ces numéros au lieu de noms lorsque cela est avantageux. Vous devez ensuite déterminer comment coder au mieux les éléments de quelque chose qui survivra dans une chaîne de requête, par exemple. quelque chose comme base64 (url).

Par exemple, votre référentiel hoodie.js a l'ID 4780572.

Le fait d’emballer cela dans un big-endian unsigned int (autant d’octets que nécessaire) nous donne \x00H\xf2\x1c.

Nous allons simplement lancer le zéro, nous pouvons toujours le restaurer plus tard, nous avons maintenant H\xf2\x1c.

Encodez en tant que base64 sécurisée pour les URL, et vous avez SPIc (lancez le remplissage que vous pourriez obtenir).

Passer de hoodiehq/hoodie.js à SPIc semble être une victoire de bonne taille!

Plus généralement, si vous êtes prêt à investir du temps, vous pouvez essayer d'exploiter une série de redondances dans vos chaînes de requête. D'autres idées vont dans le sens de regrouper les deux paramètres booléens dans un seul caractère, éventuellement avec un autre état (comme quels champs sont inclus). Si vous utilisez le codage base64 (ce qui semble être la meilleure option ici en raison de la version sécurisée contre les URL - j'ai examiné le base85, mais il contient un tas de caractères qui ne survivront pas dans une URL), cela vous donne entropie par personnage ... vous pouvez faire beaucoup avec cela.

Pour ajouter à la note de Thomas Fuchs, oui, s’il existe une sorte d’ordre inhérent et immuable dans certaines des choses que vous encodez, cela aiderait évidemment aussi. Cependant, cela semble difficile à la fois pour les étiquettes et les jalons.

2
djc

Pourquoi ne pas utiliser un raccourcisseur de lien tiers?

(Je suppose que vous n'avez pas de problème avec les limites de longueur URI puisque vous avez mentionné qu'il s'agit d'une application existante.)

On dirait que vous écrivez un script Greasemonkey ou à peu près, alors vous avez peut-être accès à GM_xmlhttpRequest () , ce qui permettrait d'utiliser un raccourcisseur de lien tiers.

Sinon, vous devrez utiliser XMLHttpRequest () et hébergez votre propre service de raccourcissement de liens sur le même serveur pour éviter de franchir la limite same-Origin policy . Une recherche en ligne rapide pour l'hébergement de vos propres raccourcisseurs m'a fourni une liste de 7 scripts de raccourcisseur de liens libres/libres PHP et un plus sur GitHub, bien que la question exclue probablement ce type de approche depuis "La logique de l'application est uniquement dans le navigateur, et il n'y a pas de back-end, je peux écrire."

Vous pouvez voir un exemple de code implémentant ce genre de chose dans le URL Shortener UserScript (pour Greasemonkey), qui affiche une version abrégée de l'URL de la page en cours lorsque vous appuyez sur SHIFT + T.

Bien entendu, les raccourcisseurs redirigeront les utilisateurs vers l'URL de forme longue, mais cela poserait un problème pour toute solution autre que côté serveur. Au moins un raccourcisseur peut théoriquement utiliser un proxy (comme RewriteRule avec Apache dans Apache) ou utiliser une balise <frame>.

2
Adam Katz

Peut-être qu'un simple minificateur JS vous aidera. Vous n'aurez besoin que de l'intégrer aux points de sérialisation et de désérialisation uniquement. Je pense que ce serait la solution la plus simple.

1
not-found.404

Court

Utilisez un schéma d'emballage d'URL tel que le mien, en commençant uniquement par la section params de votre URL.

Plus long

Comme d'autres l'ont souligné ici, les systèmes de compression typiques ne fonctionnent pas pour les chaînes courtes. Mais il est important de reconnaître que les URL et les paramètres sont le format de sérialisation d'un modèle de données: un format lisible par un texte avec des sections spécifiques. être annulé, etc ...

Avec le modèle de données d'origine, il est possible de procéder à une sérialisation avec un schéma de sérialisation plus efficace en bits. En fait, j'ai moi-même créé une telle sérialisation qui archive environ 50% de compression: voir http://blog.alivate.com.au/packed-url/

0
Todd