it-swarm.com.de

Wie komprimiert man URL-Parameter?

Angenommen, ich habe eine Einseitenanwendung , die eine Drittanbieter-API für Inhalte verwendet. Die Logik der App ist nur im Browser und es gibt kein Backend, in das ich schreiben kann.

Um tief in den Status der App einzusteigen, verwende ich pushState, um einige Variablen zu verfolgen, die den Status der App bestimmen (beachten Sie, dass die öffentliche Version von Ubersicht dies noch nicht tut). In diesem Fall repos, labels, milestones und username, show_open (bool) und with_comments (bool) und without_comments (bool). Das URL-Format ist ?label=label_1,label_2,label_3&repos=repo_1…. Die Werte sind die üblichen Verdächtigen, grob [a-zA-Z][a-zA-Z0-9_-] oder ein boolescher Indikator.

So weit, ist es gut. Da die Abfragezeichenfolge etwas lang und unhandlich sein kann und ich gerne URLs wie http://espy.github.io/ubersicht/?state=SOMOPAQUETOKENTHATLOSSLESSLYDECOMPRESSESINTOTHEORIGINALVALUES#hoodiehq weitergeben möchte, ist der Ausdruck um so besser, je kürzer.

Mein erster Versuch bestand darin, einen zlib-ähnlichen Algorithmus zu verwenden ( https://github.com/imaya/zlib.js ) und @flipzagging wies auf antirez/smaz (https // github.com/antirez) hin/smaz) was für kurze Zeichenfolgen besser geeignet ist (JavaScript-Version unter https://github.com/personalcomputer/smaz.js ).

Da = und & nicht speziell in https://github.com/personalcomputer/smaz.js/blob/master/lib/smaz.js#L9 behandelt werden, können wir die Dinge dort möglicherweise ein wenig optimieren.

Darüber hinaus gibt es eine Option zum Codieren der Werte in einer festen Tabelle, z. Die Reihenfolge der Argumente ist vordefiniert und wir müssen nur den tatsächlichen Wert nachverfolgen. Z.B. Verwandeln Sie a=hamster&b=cat in 7hamster3cat (Länge + Zeichen) oder Hamster | cat (Wert + |), möglicherweise vor der Smaz-Komprimierung.

Gibt es noch etwas, nach dem ich suchen sollte?

47
Jan Lehnardt

Eine funktionierende Lösung, bei der verschiedene gute Ideen (oder so glaube ich) zusammengefügt werden

Ich habe das aus Spaß gemacht, hauptsächlich, weil es mir die Möglichkeit gab, einen Huffman-Encoder in PHP zu implementieren, und ich konnte keine zufriedenstellende vorhandene Implementierung finden.

Dies kann jedoch einige Zeit einsparen, wenn Sie einen ähnlichen Pfad erkunden möchten.

Burrows-Wheeler + Umzug nach vorne + Huffman-Transformation

Ich bin nicht ganz sicher, ob BWT für Ihre Eingaben am besten geeignet wäre.
Dies ist kein normaler Text, so dass wiederkehrende Muster wahrscheinlich nicht so häufig vorkommen wie im Quellcode oder in reinem Englisch.

Außerdem müsste ein dynamischer Huffman-Code zusammen mit den codierten Daten übergeben werden, was bei sehr kurzen Eingabestrings den Kompressionsgewinn stark schädigen würde.

Ich könnte falsch liegen. In diesem Fall würde ich gerne sehen, dass mich jemand beweist.

Wie auch immer, ich entschied mich für einen anderen Ansatz.

Allgemeines Prinzip

1) Definieren Sie eine Struktur für Ihre URL-Parameter und entfernen Sie den konstanten Teil

zum Beispiel ab:

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

extrakt:

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

wobei , und | als String- und/oder Feldabschlusszeichen fungieren, während boolesche Werte keine benötigen.

2) Definieren Sie eine statische Neuaufteilung von Symbolen basierend auf der erwarteten durchschnittlichen Eingabe und leiten Sie einen statischen Huffman-Code ab

Da das Übertragen einer dynamischen Tabelle mehr Platz als Ihre ursprüngliche Zeichenfolge beansprucht, denke ich, die einzige Möglichkeit, Kompression überhaupt zu erreichen, ist eine statische Huffman-Tabelle.

Sie können jedoch die Struktur Ihrer Daten zu Ihrem Vorteil verwenden, um vernünftige Wahrscheinlichkeiten zu berechnen.

Sie können mit der Aufteilung von Buchstaben in Englisch oder anderen Sprachen beginnen und einen bestimmten Prozentsatz an Zahlen und anderen Interpunktionszeichen einfügen.

Beim Testen mit einer dynamischen Huffman-Codierung sah ich Kompressionsraten von 30 bis 50%.

Dies bedeutet, dass bei einer statischen Tabelle möglicherweise ein Komprimierungsfaktor von 0,6 (Verringerung der Datenlänge um 1/3) zu erwarten ist, nicht viel mehr.

3) Konvertieren Sie diesen binären Huffmann-Code in etwas, das ein URI verarbeiten kann

Die 70 regulären ASCII 7-Bit-Zeichen in dieser Liste

!'()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz

würde Ihnen einen Expansionsfaktor von etwa 30% geben, praktisch nicht besser als eine base64-Codierung.

Eine 30% ige Expansion würde den Gewinn einer statischen Huffman-Komprimierung ruinieren, daher ist dies kaum eine Option!

Da Sie jedoch die Codierungsclient- und -serverseite steuern, können Sie alles verwenden, bei dem es sich nicht um ein reserviertes URI-Zeichen handelt.

Eine interessante Möglichkeit wäre die Vervollständigung des obigen Satzes auf 256 mit beliebigen Unicode-Glyphen, wodurch die Binärdaten mit der gleichen Anzahl URI-konformer Zeichen codiert werden könnten, wodurch ein schmerzhafter und langsamer Haufen langer Ganzzahldivisionen durch einen Blitz ersetzt wird schnelle tabellensuche.

Strukturbeschreibung

Der Codec ist sowohl für die Client- als auch für die Serverseite vorgesehen. Daher müssen Server und Clients eine gemeinsame Datenstrukturdefinition verwenden.

Da sich die Benutzeroberfläche wahrscheinlich weiterentwickelt, ist es ratsam, aus Gründen der Aufwärtskompatibilität eine Versionsnummer zu speichern.

Die Schnittstellendefinition verwendet eine sehr minimalistische Beschreibungssprache wie folgt:

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

Jede unterstützte Sprache hat eine Häufigkeitstabelle für alle verwendeten Buchstaben

ziffern und andere Computersymbole wie -, . oder _ haben unabhängig von den Sprachen eine globale Häufigkeit

trennzeichen (, und |) werden entsprechend der Anzahl der Listen und Felder in der Struktur berechnet.

Alle anderen "fremden" Zeichen werden mit einem bestimmten Code maskiert und als einfaches UTF-8 codiert.

Implementierung

Der bidirektionale Konvertierungspfad lautet wie folgt:

liste der Felder <-> UTF-8-Datenstrom <-> Huffman-Codes <-> URI

Hier ist der Hauptcodec

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

Der zugrunde liegende Huffman-Codec

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

Und das Huffman-Wörterbuch

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

Beispiel

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

ausgabe:

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

In diesem Beispiel wurde die Eingabe in 64 Unicode-Zeichen für eine Eingabelänge von etwa 100 gepackt, was eine Reduzierung um 1/3 ergibt.

Eine äquivalente Zeichenfolge:

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

Wird durch eine dynamische Huffman-Tabelle auf 59 Zeichen komprimiert. Kein großer Unterschied. 

Zweifellos würde das Umordnen von Daten durch Smart Data das reduzieren, aber dann müssten Sie die dynamische Tabelle weitergeben ...

Chinesen zur Rettung?

Nach der Idee von ttepasse könnte man die enorme Anzahl asiatischer Zeichen nutzen, um einen Bereich von 0x4000 (12 Bit) zusammenhängenden Werten zu finden, um 3 Bytes in 2 CJK-Zeichen zu codieren.

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

und zurück:

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

Die vorherige Ausgabe von 64 lateinischen Zeichen

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

würde auf 42 asiatische Zeichen "verkleinern":

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

Wie Sie jedoch sehen können, macht die schiere Masse Ihres durchschnittlichen Ideogramms die Zeichenfolge tatsächlich länger (pixelweise). Selbst wenn die Idee vielversprechend war, ist das Ergebnis eher enttäuschend.

Dünnere Glyphen auswählen

Auf der anderen Seite können Sie versuchen, "dünne" Zeichen als Basis für die URI-Codierung auszuwählen. Zum Beispiel:

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

anstatt

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

Dadurch wird die Länge bei proportionalen Schriftarten einschließlich der Adressleiste des Browsers um die Hälfte verringert.Mein bisher bester Kandidatensatz von 256 "dünnen" Glyphen:.

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


Es ist nicht schwierig und macht Spaß, aber das bedeutet noch mehr Arbeit :).

Der Huffman-Gewinn in Bezug auf Zeichen beträgt etwa 30%.

.



Auf der anderen Seite bewirkt das Auswählen dünner Glyphen tatsächlich mehr, um die Zeichenfolge zu verkleinern.

Alles in allem kann die Kombination von beidem tatsächlich etwas erreichen, obwohl es viel Arbeit für ein bescheidenes Ergebnis ist.

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

So wie Sie es selbst vorgeschlagen haben, würde ich zunächst alle Zeichen loswerden, die keine Informationen enthalten, da sie Teil des "Formats" sind.

Z.B. Schalten Sie "labels = open, ssl, cypher & repository = 275643 & username = ryanbrg & milestones = & with_comment = yes" auf "open, ssl, cyper | 275643 | ryanbrg || yes".

Verwenden Sie dann eine Huffmann-Codierung mit einem festen Wahrscheinlichkeitsvektor (was zu einer festen Zuordnung von Zeichen zu Bitstrings mit variabler Länge führt - wobei die wahrscheinlichsten Zeichen kürzeren Bitstrings und weniger wahrscheinlichen Zeichen längeren Bitstrings zugeordnet werden).

Sie können sogar unterschiedliche Wahrscheinlichkeitsvektoren für die verschiedenen Parameter verwenden. Im Parameter "labels" haben die Alpha-Zeichen beispielsweise eine hohe Wahrscheinlichkeit, im Parameter "Repository" haben die numerischen Zeichen jedoch die höchste Wahrscheinlichkeit. Wenn Sie dies tun, sollten Sie das Trennzeichen "|" in Betracht ziehen. ein Teil des vorhergehenden Parameters.

Und schließlich verwandeln Sie den langen Bitstring (das ist die Verkettung aller Bitstrings, denen die Zeichen zugeordnet wurden) in etwas, das Sie durch base64url-Codierung in eine URL einfügen können.

Wenn Sie mir eine Reihe von repräsentativen Parameterlisten schicken könnten, könnte ich sie durch einen Huffmann-Codierer laufen lassen, um zu sehen, wie gut sie komprimiert werden.

Der Wahrscheinlichkeitsvektor (oder entsprechend das Mapping von Zeichen zu Bitstrings) sollte als konstante Arrays in die Javascript-Funktion codiert werden, die an den Browser gesendet wird.

Natürlich könnten Sie noch weiter gehen und versuchen, eine Liste möglicher Labels mit ihren Wahrscheinlichkeiten zu erhalten. Dann können Sie mit einer Huffmann-Codierung ganze Markierungen Bitstrings zuordnen. Dadurch erhalten Sie eine bessere Komprimierung, aber Sie haben zusätzliche Arbeit für die neuen Beschriftungen (z. B. Zurückgreifen auf die Kodierung einzelner Zeichen) und natürlich das Mapping (das - wie oben erwähnt - ein konstantes Array in der Javascript-Funktion ist ) wird viel größer sein.

17
mschoenert

Ich habe einen listigen Plan! (Und ein Getränk Gin Tonic)

Sie scheinen sich nicht für die Länge des Bytestreams zu interessieren, sondern für die Länge der resultierenden Glyphen, z. welche Zeichenfolge wird dem Benutzer angezeigt.

Browser sind ziemlich gut beim Konvertieren eines IRI in den zugrunde liegenden [URI] [2], während der IRI weiterhin in der Adressleiste angezeigt wird. IRIs verfügen über ein größeres Repertoire an möglichen Zeichen, während der Satz möglicher Zeichen eher begrenzt ist.

Das heißt, Sie können Bigramme Ihrer Zeichen (aa, ab, ac,…, zz & Sonderzeichen) in ein Zeichen des gesamten Unicode-Spektrums kodieren. Angenommen, Sie haben 80 mögliche ASCII - Zeichen: Die Anzahl der möglichen Kombinationen von zwei Zeichen beträgt 6400. Die in Unicodes zugewiesenen Zeichen sind leicht zu finden, z. im han einheitlichen CJK-Spektrum:

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

Ich habe CJK gewählt, weil dies nur (geringfügig) sinnvoll ist, wenn die Zielzeichen in Unicode zugewiesen wurden und Glyphen auf den wichtigsten Browsern und Betriebssystemen zugewiesen wurden. Aus diesem Grund ist der private Verwendungsbereich deaktiviert, und die effizientere Version, die Trigramme verwendet (deren mögliche Kombinationen alle möglichen Codepunkte von Unicodes 1114112 verwenden könnten) ist out.

Um es noch einmal zusammenzufassen: Die darunter liegenden Bytes sind immer noch vorhanden und bei UTF-8-Kodierung noch länger möglich, aber die Zeichenfolge der angezeigten Zeichen und Kopien ist um 50% kürzer.

Ok, Ok, Gründe, warum diese Lösung verrückt ist:

  • IRIs sind nicht perfekt. Viele weniger Tools als moderne Browser haben ihre Probleme.

  • Der Algorithmus benötigt offensichtlich viel mehr Arbeit. Sie benötigen eine Funktion, die die Bigramme auf die Zielzeichen und zurück abbildet. Und es sollte vorzuziehen, arithmetisch zu arbeiten, um große Hashtabellen im Speicher zu vermeiden.

  • Die Zielzeichen sollten überprüft werden, ob sie zugewiesen wurden und ob sie einfache Zeichen sind und keine unicodischen Dinge sind, wie das Kombinieren von Zeichen oder solche, die irgendwo in der Unicode-Normalisierung verloren gegangen sind. Auch wenn der Zielbereich eine durchgehende Reihe von Zeichen mit Glyphen ist.

  • Browser sind manchmal vorsichtig bei IRIs. Aus gutem Grund, angesichts der Angriffe der IDN-Homographen. Sind sie mit all diesen Nicht-ASCII-Zeichen in ihrer Adressleiste in Ordnung?

  • Und das Größte: Menschen sind bekanntermaßen schlecht darin, sich Charaktere in Skripten zu merken, die sie nicht kennen. Sie sind noch schlimmer beim Versuch, diese Zeichen (wieder) einzugeben. Und copy'n'paste kann bei vielen verschiedenen Klicks schief gehen. Es gibt einen Grund, warum URL-Verkürzungen Base64 und sogar kleinere Alphabete verwenden. 

… Wovon ich reden könnte: Das wäre meine Lösung. Verkürzen Sie die Arbeit, indem Sie die Verknüpfungen entweder zum Benutzer verkürzen oder goo.gl oder bit.ly über ihre APIs integrieren.

11
ttepasse

Kleiner Tipp: Sowohl parseInt als auch Number#toString unterstützen Radix-Argumente. Verwenden Sie eine Radix von 36, um Zahlen (oder Indizes in Listen) in URLs zu kodieren.

9
thomasfuchs

Warum nicht Protokollpuffer verwenden?

Protokollpuffer sind ein flexibler, effizienter und automatisierter Mechanismus zur Serialisierung strukturierter Daten - denken Sie an XML, aber kleiner, schneller und einfacher. Sie legen fest, wie Ihre Daten einmal strukturiert werden sollen. Anschließend können Sie mit speziell generiertem Quellcode Ihre strukturierten Daten in verschiedene Datenströme und in verschiedene Sprachen schreiben und aus ihnen lesen. Sie können sogar Ihre Datenstruktur aktualisieren, ohne die bereitgestellten Programme zu beschädigen, die mit dem "alten" Format kompiliert werden.

ProtoBuf.js konvertiert Objekte in Protokollpuffernachrichten und umgekehrt.

Das folgende Objekt wird konvertiert in: CgFhCgFiCgFjEgFkEgFlEgFmGgFnGgFoGgFpIgNqZ2I=

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

Beispiel

Das folgende Beispiel wird mit requir.js erstellt. Probieren Sie es mit jsfiddle aus.

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

Das Problem hat zwei Hauptaspekte: Kodierung und Komprimierung. 

Die Komprimierung für allgemeine Zwecke scheint auf kleinen Zeichenketten nicht gut zu funktionieren. Da Browser keine APIs zum Komprimieren von Strings bereitstellen, müssen Sie auch die Quelle laden, die sehr umfangreich sein kann.

Viele Zeichen können mit einer effizienten Kodierung gespeichert werden. Ich habe eine Bibliothek namens μ geschrieben, um den Codierungs- und Decodierungsteil zu bearbeiten. Die Idee ist, so viele Informationen wie möglich über die Struktur und Domäne der URL-Parameter als Spezifikation anzugeben. Diese Angabe kann dann verwendet werden, um die Kodierung und Dekodierung voranzutreiben. Zum Beispiel kann boolean mit nur einem Bit codiert werden, Integer kann in eine andere Basis (64) konvertiert werden, wodurch die Anzahl der erforderlichen Zeichen reduziert wird. Objektschlüssel müssen nicht codiert werden, da sie aus der Spezifikation abgeleitet werden können Log2(numberOfAllowedValues) -Bits.

4
Anantha Kumaran

Update: Ich habe ein NPM-Paket mit einigen weiteren Optimierungen veröffentlicht, siehe https://www.npmjs.com/package/@yaska-eu/jsurl2

Einige weitere Tipps:

  • Base64-Kodierungen mit a..zA..Z0..9+/= und nicht kodierte URI-Zeichen sind a..zA..Z0..9-_.~. Base64-Ergebnisse müssen also nur +/= für -_. eintauschen, und es werden keine URIs erweitert.
  • Sie könnten ein Array von Schlüsselnamen behalten, so dass Objekte dargestellt werden können, wobei das erste Zeichen der Versatz im Array ist, z. {foo:3,bar:{g:'hi'}} wird zu a3,b{c'hi'}, wenn ein Schlüsselarray angegeben wird ['foo','bar','g']

Interessante Bibliotheken:

  • JSUrl codiert JSON spezifisch, sodass es ohne Änderungen in eine URL eingefügt werden kann, obwohl mehr Zeichen als in der RFC angegeben verwendet werden. {"name":"John Doe","age":42,"children":["Mary","Bill"]} wird zu ~(name~'John*20Doe~age~42~children~(~'Mary~'Bill)) und mit einem Schlüsselwörterverzeichnis ['name','age','children'], das ~(0~'John*20Doe~1~42~2~(~'Mary~'Bill)) sein könnte, also von 101 Bytes URI-codiert bis 38 .
    • Geringer Platzbedarf, schnelle, vernünftige Komprimierung.
  • lz-string verwendet einen LZW-basierten Algorithmus zum Komprimieren von Zeichenfolgen in UTF16 zum Speichern in localStorage. Es hat auch eine compressToEncodedURIComponent()-Funktion, um eine URI-sichere Ausgabe zu erzeugen .
    • Noch nur wenige KB Code, ziemlich schnell, gute/gute Komprimierung.

Grundsätzlich würde ich empfehlen, eine dieser beiden Bibliotheken auszuwählen und das Problem als gelöst zu betrachten.

3
w00t

Vielleicht finden Sie einen URL-Shortener mit einer Jsonp-API. Auf diese Weise könnten Sie alle URLs automatisch sehr kurz machen.

http://yourls.org/ hat sogar Unterstützung von Jsonp.

2
Jeena

Es sieht so aus, als hätten die Github-APIs für viele Dinge numerische IDs (wie Repos und Benutzer haben sie, aber Labels haben sie nicht) unter den Deckblättern. Es ist möglich, diese Nummern anstelle von Namen zu verwenden, wenn dies von Vorteil ist. Sie müssen dann herausfinden, wie Sie diejenigen am besten in etwas codieren, das in einer Abfragezeichenfolge überleben wird, z. so etwas wie base64 (url).

Beispielsweise hat Ihr Hoodie.js-Repository die ID 4780572.

Wenn wir das in ein Big-Endian-unsigned int (so viele Bytes wie nötig) packen, erhalten wir \x00H\xf2\x1c.

Wir werden nur die führende Null werfen, wir können das später immer wiederherstellen, jetzt haben wir H\xf2\x1c.

Verschlüsseln Sie als URL-sicheres base64, und Sie haben SPIc (werfen Sie alle möglichen Auffüllungen).

Der Wechsel von hoodiehq/hoodie.js zu SPIc scheint ein guter Gewinn zu sein!

Im Allgemeinen können Sie, wenn Sie bereit sind, Zeit zu investieren, versuchen, eine Reihe von Redudanzen in Ihren Abfragezeichenfolgen zu nutzen. Andere Ideen beziehen sich auf das Zusammenfassen der beiden booleschen Parameter in ein einzelnes Zeichen, möglicherweise zusammen mit einem anderen Status (z. B. welche Felder enthalten sind). Wenn Sie die base64-Codierung verwenden (die aufgrund der URL-sicheren Version hier die beste Option zu sein scheint - ich habe mir base85 angeschaut, aber es gibt eine Reihe von Zeichen, die in einer URL nicht überleben), erhalten Sie 6 Bits Entropie pro Charakter ... Sie können damit viel tun.

Zur Bemerkung von Thomas Fuchs hinzufügen: Ja, wenn es eine Art inhärenter, unveränderlicher Reihenfolge in einigen der Dinge gibt, die Sie kodieren, dann würde dies natürlich auch helfen. Dies scheint jedoch sowohl für die Labels als auch für die Meilensteine ​​schwierig zu sein.

2
djc

Warum nicht einen Link-Shortener von Drittanbietern verwenden?

(Ich gehe davon aus, dass Sie kein Problem mit URI-Längengrenzen haben, da Sie bereits erwähnt haben, dass dies eine vorhandene Anwendung ist.)

Es sieht so aus, als ob Sie ein Greasemonkey -Skript oder ähnliches schreiben. Vielleicht haben Sie Zugriff auf GM_xmlhttpRequest () , wodurch die Verwendung eines Link-Shortener von Drittanbietern ermöglicht wird.

Andernfalls müssten Sie XMLHttpRequest () verwenden und Ihren eigenen Link-Verkürzungsdienst auf demselben Server hosten, um ein Überschreiten der same-Origin-Richtlinie zu vermeiden. Eine schnelle Online-Suche für das Hosting eigener Kürzel lieferte auf GitHub eine list von 7 free/open source PHP -Link-Shortener-Skripts und one more , obwohl diese Frage wahrscheinlich ausgeschlossen ist Ansatz seit "Die Logik der App ist nur im Browser und es gibt kein Backend, in das ich schreiben kann."

Beispielcode, der diese Art von Dingen implementiert, ist im UserScript URL Shortener (für Greasemonkey) zu sehen, das beim Drücken von UMSCHALT + T eine verkürzte Version der URL der aktuellen Seite anzeigt.

Natürlich leiten die Verkürzer die Benutzer auf die Langform-URL um. Dies wäre jedoch bei jeder nicht serverseitigen Lösung ein Problem. Zumindest ein Shortener kann theoretisch Proxy (wie RewriteRule mit [P] von Apache) oder ein <frame> -Tag verwenden.

2
Adam Katz

Vielleicht hilft Ihnen ein einfacher JS-Minifier. Sie müssen es nur bei Serialisierungs- und Deserialisierungspunkten integrieren. Ich denke, es wäre die einfachste Lösung.

1
not-found.404

Kurz

Verwenden Sie ein URL-Verpackungsschema, z. B. mein eigenes, und beginnen Sie nur mit dem Abschnitt params Ihrer URL.

Länger

Wie andere hier gezeigt haben, funktionieren typische Kompressionssysteme nicht für kurze Saiten. Es ist jedoch wichtig zu wissen, dass URLs und Params ein Serialisierungsformat eines Datenmodells sind: ein von Menschen lesbares Textformat mit bestimmten Abschnitten - wir wissen, dass das Schema zuerst ist, der Host direkt danach gefunden wird, der Port jedoch impliziert ist überschrieben werden, etc ...

Mit dem ursprünglichen Datenmodell kann mit einem biteffizienten Serialisierungsschema serialisiert werden. Tatsächlich habe ich selbst eine solche Serialisierung erstellt, die eine Komprimierung von 50% vorsieht: siehe http://blog.alivate.com.au/packed-url/

0
Todd