web-dev-qa-db-fra.com

Comment convertir un entier en la chaîne la plus courte sans danger pour les URL en Python?

Je veux la manière la plus courte possible de représenter un entier dans une URL. Par exemple, 11234 peut être raccourci à "2be2" en utilisant hexadécimal. Étant donné que l'utilisation de base64 est un codage de 64 caractères, il devrait être possible de représenter un entier dans base64 en utilisant encore moins de caractères que hexadécimal. Le problème est que je ne peux pas trouver la manière la plus propre de convertir un entier en base64 (et vice-versa) en utilisant Python.

Le module base64 a des méthodes pour traiter les bytestrings - donc peut-être une solution serait de convertir un entier en sa représentation binaire comme une chaîne Python ... mais je ne sais pas comment faire cela Soit.

63
Simon Willison

Cette réponse est similaire dans son esprit à celle de Douglas Leeder, avec les modifications suivantes:

  • Il n'utilise pas de Base64 réel, donc il n'y a pas de caractères de remplissage
  • Au lieu de convertir d'abord le nombre en une chaîne d'octets (base 256), il le convertit directement en base 64, ce qui a l'avantage de vous permettre de représenter des nombres négatifs en utilisant un caractère de signe.

    import string
    ALPHABET = string.ascii_uppercase + string.ascii_lowercase + \
               string.digits + '-_'
    ALPHABET_REVERSE = dict((c, i) for (i, c) in enumerate(ALPHABET))
    BASE = len(ALPHABET)
    SIGN_CHARACTER = '$'
    
    def num_encode(n):
        if n < 0:
            return SIGN_CHARACTER + num_encode(-n)
        s = []
        while True:
            n, r = divmod(n, BASE)
            s.append(ALPHABET[r])
            if n == 0: break
        return ''.join(reversed(s))
    
    def num_decode(s):
        if s[0] == SIGN_CHARACTER:
            return -num_decode(s[1:])
        n = 0
        for c in s:
            n = n * BASE + ALPHABET_REVERSE[c]
        return n
    

    >>> num_encode(0)
    'A'
    >>> num_encode(64)
    'BA'
    >>> num_encode(-(64**5-1))
    '$_____'

Quelques notes annexes:

  • Vous pouvez (marginalement) augmenter la lisibilité humaine des nombres en base 64 en plaçant string.digits en premier dans l'alphabet (et en faisant le signe "-"); J'ai choisi l'ordre que j'ai fait basé sur urlsafe_b64encode de Python.
  • Si vous encodez un grand nombre de nombres négatifs, vous pouvez augmenter l'efficacité en utilisant un bit de signe ou un complément à un/deux au lieu d'un caractère de signe.
  • Vous devriez être en mesure d'adapter facilement ce code à différentes bases en changeant l'alphabet, soit pour le restreindre à des caractères alphanumériques uniquement, soit pour ajouter des caractères "URL-safe" supplémentaires.
  • Je recommanderais contre d'utiliser une représentation autre que la base 10 dans les URI dans la plupart des cas - cela ajoute de la complexité et rend le débogage plus difficile sans économies significatives par rapport à la surcharge de HTTP - sauf si vous optez pour quelque chose TinyURL- esque.
60
Miles

Toutes les réponses données concernant Base64 sont des solutions très raisonnables. Mais ils sont techniquement incorrects. Pour convertir un nombre entier en chaîne de sécurité URL la plus courte possible, ce que vous voulez est la base 66 (il y a 66 caractères de sécurité URL ).

Ce code ressemble à ceci:

from io import StringIO
import urllib

BASE66_ALPHABET = u"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.~"
BASE = len(BASE66_ALPHABET)

def hexahexacontadecimal_encode_int(n):
    if n == 0:
        return BASE66_ALPHABET[0].encode('ascii')

    r = StringIO()
    while n:
        n, t = divmod(n, BASE)
        r.write(BASE66_ALPHABET[t])
    return r.getvalue().encode('ascii')[::-1]

Voici une implémentation complète d'un schéma comme celui-ci, prêt à l'emploi en tant que package installable pip:

https://github.com/aljungberg/hhc

18

Vous ne voulez probablement pas de véritable encodage base64 pour cela - cela ajoutera du rembourrage, etc., ce qui pourrait même entraîner des chaînes plus grandes que hex ne le ferait pour les petits nombres. S'il n'est pas nécessaire d'interagir avec autre chose, utilisez simplement votre propre encodage. Par exemple. voici une fonction qui encodera dans n'importe quelle base (notez que les chiffres sont en fait stockés les moins significatifs en premier pour éviter les appels reverse () supplémentaires:

def make_encoder(baseString):
    size = len(baseString)
    d = dict((ch, i) for (i, ch) in enumerate(baseString)) # Map from char -> value
    if len(d) != size:
        raise Exception("Duplicate characters in encoding string")

    def encode(x):
        if x==0: return baseString[0]  # Only needed if don't want '' for 0
        l=[]
        while x>0:
            l.append(baseString[x % size])
            x //= size
        return ''.join(l)

    def decode(s):
        return sum(d[ch] * size**i for (i,ch) in enumerate(s))

    return encode, decode

# Base 64 version:
encode,decode = make_encoder("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")

assert decode(encode(435346456456)) == 435346456456

Cela a l'avantage que vous pouvez utiliser la base de votre choix, simplement en ajoutant des caractères appropriés à la chaîne de base de l'encodeur.

Notez que les gains pour les bases plus importantes ne seront pas aussi importants cependant. la base 64 ne réduira la taille qu'à 2/3 de la base 16 (6 bits/caractère au lieu de 4). Chaque doublement n'ajoute qu'un bit de plus par caractère. À moins que vous n'ayez vraiment besoin de compacter les choses, l'utilisation de hexagone sera probablement l'option la plus simple et la plus rapide.

14
Brian

Pour coder n:

data = ''
while n > 0:
    data = chr(n & 255) + data
    n = n >> 8
encoded = base64.urlsafe_b64encode(data).rstrip('=')

Pour décoder s:

data = base64.urlsafe_b64decode(s + '===')
decoded = 0
while len(data) > 0:
    decoded = (decoded << 8) | ord(data[0])
    data = data[1:]

Dans le même esprit que les autres pour certains encodages "optimaux", vous pouvez utiliser 7 caractères selon la RFC 1738 (en fait 74 si vous comptez "+" comme utilisable):

alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_`\"!$'()*,-."
encoded = ''
while n > 0:
    n, r = divmod(n, len(alphabet))
    encoded = alphabet[r] + encoded

et le décodage:

decoded = 0
while len(s) > 0:
    decoded = decoded * len(alphabet) + alphabet.find(s[0])
    s = s[1:]
9
kmkaplan

Le plus simple est de convertir la chaîne d'octets en base64 Web sécurisée:

import base64
output = base64.urlsafe_b64encode(s)

Le bit délicat est la première étape - convertir l'entier en une chaîne d'octets.

Si vos entiers sont petits, vous feriez mieux de les encoder en hexadécimal - voir saua

Sinon (version récursive hacky):

def convertIntToByteString(i):
    if i == 0:
        return ""
    else:
        return convertIntToByteString(i >> 8) + chr(i & 255)
8
Douglas Leeder

Vous ne voulez pas d'encodage en base64, vous voulez représenter un chiffre de base 10 en base numérique X.

Si vous voulez que votre chiffre de base 10 soit représenté dans les 26 lettres disponibles, vous pouvez utiliser: http://en.wikipedia.org/wiki/Hexavigesimal . (Vous pouvez étendre cet exemple pour une base beaucoup plus grande en utilisant tous les caractères d'URL autorisés)

Vous devriez au moins pouvoir obtenir la base 38 (26 lettres, 10 chiffres, +, _)

7
Øystein E. Krog

Base64 prend 4 octets/caractères pour encoder 3 octets et ne peut encoder que des multiples de 3 octets (et ajoute un remplissage sinon).

Donc, représenter 4 octets (votre entier moyen) en Base64 prendrait 8 octets. Encoder les mêmes 4 octets en hexadécimal prendrait également 8 octets. Vous ne gagneriez donc rien pour un seul int.

4
Joachim Sauer

Je gère une petite bibliothèque nommée zbase62: http://pypi.python.org/pypi/zbase62

Avec lui, vous pouvez convertir un objet Python 2 str en une chaîne codée en base 62 et vice versa:

Python 2.7.1+ (r271:86832, Apr 11 2011, 18:13:53) 
[GCC 4.5.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> d = os.urandom(32)
>>> d
'C$\x8f\xf9\x92NV\x97\x13H\xc7F\x0c\x0f\x8d9}\xf5.u\xeeOr\xc2V\x92f\x1b=:\xc3\xbc'
>>> from zbase62 import zbase62
>>> encoded = zbase62.b2a(d)
>>> encoded
'Fv8kTvGhIrJvqQ2oTojUGlaVIxFE1b6BCLpH8JfYNRs'
>>> zbase62.a2b(encoded)
'C$\x8f\xf9\x92NV\x97\x13H\xc7F\x0c\x0f\x8d9}\xf5.u\xeeOr\xc2V\x92f\x1b=:\xc3\xbc'

Cependant, vous devez toujours convertir un entier en str. Cela vient intégré à Python 3:

Python 3.2 (r32:88445, Mar 25 2011, 19:56:22)
[GCC 4.5.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> d = os.urandom(32)
>>> d
b'\xe4\x0b\x94|\xb6o\x08\xe9oR\x1f\xaa\xa8\xe8qS3\x86\x82\t\x15\xf2"\x1dL%?\xda\xcc3\xe3\xba'
>>> int.from_bytes(d, 'big')
103147789615402524662804907510279354159900773934860106838120923694590497907642
>>> x= _ 
>>> x.to_bytes(32, 'big')
b'\xe4\x0b\x94|\xb6o\x08\xe9oR\x1f\xaa\xa8\xe8qS3\x86\x82\t\x15\xf2"\x1dL%?\xda\xcc3\xe3\xba'

Pour convertir de int en octets et vice versa en Python 2, il n'y a pas de méthode standard pratique pour autant que je sache. Je suppose que je devrais peut-être copier une implémentation, comme celle-ci: https://github.com/warner/foolscap/blob/46e3a041167950fa93e48f65dcf106a576ed110e/foolscap/banana.py#L41 dans zbase62 pour votre commodité.

3
Zooko

un peu hacky, mais ça marche:

def b64num(num_to_encode):
  h = hex(num_to_encode)[2:]     # hex(n) returns 0xhh, strip off the 0x
  h = len(h) & 1 and '0'+h or h  # if odd number of digits, prepend '0' which hex codec requires
  return h.decode('hex').encode('base64') 

vous pouvez remplacer l'appel à .encode ('base64') par quelque chose dans le module base64, tel que urlsafe_b64encode ()

3
ʞɔıu

Si vous cherchez un moyen de raccourcir la représentation entière en utilisant base64, je pense que vous devez chercher ailleurs. Lorsque vous encodez quelque chose avec base64, il ne raccourcit pas, en fait il s'allonge.

Par exemple. 11234 codé avec base64 donnerait MTEyMzQ =

Lorsque vous utilisez base64, vous avez ignoré le fait que vous ne convertissez pas uniquement les chiffres (0-9) en un codage de 64 caractères. Vous convertissez 3 octets en 4 octets, vous êtes donc assuré que votre chaîne encodée en base64 serait 33,33% plus longue.

2
Sergey Golovchenko

J'avais besoin d'un entier signé, alors j'ai fini par choisir:

import struct, base64

def b64encode_integer(i):
   return base64.urlsafe_b64encode(struct.pack('i', i)).rstrip('=\n')

Exemple:

>>> b64encode_integer(1)
'AQAAAA'
>>> b64encode_integer(-1)
'_____w'
>>> b64encode_integer(256)
'AAEAAA'
2
toothygoose

Je travaille sur la création d'un package pip pour cela.

Je vous recommande d'utiliser mon bases.py https://github.com/kamijoutouma/bases.py qui a été inspiré par bases.js

from bases import Bases
bases = Bases()

bases.toBase16(200)                // => 'c8'
bases.toBase(200, 16)              // => 'c8'
bases.toBase62(99999)              // => 'q0T'
bases.toBase(200, 62)              // => 'q0T'
bases.toAlphabet(300, 'aAbBcC')    // => 'Abba'

bases.fromBase16('c8')               // => 200
bases.fromBase('c8', 16)             // => 200
bases.fromBase62('q0T')              // => 99999
bases.fromBase('q0T', 62)            // => 99999
bases.fromAlphabet('Abba', 'aAbBcC') // => 300

se référer à https://github.com/kamijoutouma/bases.py#known-basesalphabets pour quelles bases sont utilisables

Pour votre cas

Je vous recommande d'utiliser soit la base 32, 58 ou 64

Avertissement Base-64: en plus de plusieurs normes différentes, le remplissage n'est pas actuellement ajouté et les longueurs de ligne ne sont pas suivies. Non recommandé pour une utilisation avec des API qui attendent des chaînes formelles en base 64!

Il en va de même pour la base 66 qui n'est actuellement pas prise en charge par bases.js et bases.py, mais cela pourrait dans le futur

2
Belldandu

Je choisirais la méthode 'encoder un entier sous forme de chaîne binaire, puis encoder en base64', et je le ferais en utilisant struct:

>>> import struct, base64
>>> base64.b64encode(struct.pack('l', 47))
'LwAAAA=='
>>> struct.unpack('l', base64.b64decode(_))
(47,)

Modifier à nouveau: pour supprimer les 0 supplémentaires sur les nombres trop petits pour avoir besoin d'une précision complète de 32 bits, essayez ceci:

def pad(str, l=4):
    while len(str) < l:
        str = '\x00' + str
    return str

>>> base64.b64encode(struct.pack('!l', 47).replace('\x00', ''))
'Lw=='
>>> struct.unpack('!l', pad(base64.b64decode('Lw==')))
(47,)
1
Jorenko

Pure python, pas de dépendances, pas d'encodage de chaînes d'octets, etc., juste transformer une base 10 int en base 64 int avec les bons caractères RFC 4648:

def tetrasexagesimal(number):
    out=""
    while number>=0:
        if number == 0:
            out = 'A' + out
            break
        digit = number % 64
        out = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[digit] + out
        number /= 64 # //= 64 for py3 (thank spanishgum!)
        if number == 0:
            break
    return out

tetrasexagesimal(1)
1
J.J