tutorial - scraping python 3
¿Cómo convertir un entero a la cadena más corta segura para url en Python? (14)
Base64 toma 4 bytes / caracteres para codificar 3 bytes y solo puede codificar múltiplos de 3 bytes (y agrega relleno de lo contrario).
Entonces, representar 4 bytes (su int promedio) en Base64 tomaría 8 bytes. La codificación de los mismos 4 bytes en hexadecimal también tomaría 8 bytes. Entonces no ganarías nada por una sola int.
Quiero la forma más corta posible de representar un número entero en una URL. Por ejemplo, 11234 se puede acortar a ''2be2'' usando hexadecimal. Como base64 usa una codificación de 64 caracteres, debería ser posible representar un entero en base64 utilizando incluso menos caracteres que hexadecimal. El problema es que no puedo encontrar la manera más limpia de convertir un entero a base64 (y viceversa) usando Python.
El módulo base64 tiene métodos para tratar con cadenas de bytes, así que tal vez una solución sea convertir un número entero en su representación binaria como una cadena de Python ... pero tampoco estoy seguro de cómo hacer eso.
Esta respuesta es similar en espíritu a la de Douglas Leeder, con los siguientes cambios:
- No usa Base64 real, por lo que no hay personajes de relleno
En lugar de convertir el número primero en una cadena de bytes (base 256), lo convierte directamente en la base 64, que tiene la ventaja de permitirle representar números negativos utilizando un carácter de signo.
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))
''$_____''
Algunas notas secundarias:
- Usted podría ( marginalmente ) aumentar la legibilidad humana de los números de la base 64 al poner string.digits primero en el alfabeto (y hacer que el carácter del signo ''-''); Elegí el orden que hice basado en el urlsafe_b64encode de Python.
- Si está codificando muchos números negativos, puede aumentar la eficiencia mediante el uso de un bit de signo o el complemento de uno / dos en lugar de un carácter de signo.
- Debería poder adaptar fácilmente este código a diferentes bases cambiando el alfabeto, ya sea para restringirlo a caracteres alfanuméricos o para agregar caracteres adicionales "URL-safe".
- En la mayoría de los casos, recomendaría no usar una representación distinta de la base 10 en URI: agrega complejidad y dificulta la depuración sin ahorros significativos en comparación con la sobrecarga de HTTP, a menos que busque algo TinyURL-esque.
Estoy trabajando en hacer un paquete de pip para esto.
Te recomiendo que uses mi base.py https://github.com/kamijoutouma/bases.py que fue inspirado por 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
consulte https://github.com/kamijoutouma/bases.py#known-basesalphabets para saber qué bases son utilizables
Para su caso
Te recomiendo que uses cualquiera de las bases 32, 58 o 64
Advertencia de base 64: además de que existen varios estándares diferentes, el relleno no se agrega actualmente y las longitudes de línea no se rastrean. ¡No se recomienda su uso con API que esperan cadenas base-64 formales!
Lo mismo ocurre con la base 66 que actualmente no es compatible con bases.js y bases.py, pero podría ser en el future
Lo fácil es convertir la cadena de bytes a base64 segura para la web:
import base64
output = base64.urlsafe_b64encode(s)
El truco es el primer paso: convertir el entero en una cadena de bytes.
Si tus enteros son pequeños, es mejor que los codifiques hexadecimales - ver saua
De lo contrario (versión recursiva hacky):
def convertIntToByteString(i):
if i == 0:
return ""
else:
return convertIntToByteString(i >> 8) + chr(i & 255)
Mantengo una pequeña biblioteca llamada zbase62: http://pypi.python.org/pypi/zbase62
Con él puedes convertir desde un objeto Python 2 str a una cadena codificada en base 62 y viceversa:
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''
Sin embargo, aún necesita convertir de entero a str. Esto viene incorporado a 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''
Para convertir de int a bytes y viceversa en Python 2, hasta donde yo sé no hay una forma conveniente y estándar. Creo que tal vez debería copiar alguna implementación, como esta: https://github.com/warner/foolscap/blob/46e3a041167950fa93e48f65dcf106a576ed110e/foolscap/banana.py#L41 en zbase62 para su comodidad.
Me gustaría ir al ''encode entero como una cadena binaria, luego base64 codifica ese'' método que sugieres, y lo haría usando struct:
>>> import struct, base64
>>> base64.b64encode(struct.pack(''l'', 47))
''LwAAAA==''
>>> struct.unpack(''l'', base64.b64decode(_))
(47,)
Edite de nuevo: para quitar los 0 adicionales en números que son demasiado pequeños para necesitar una precisión completa de 32 bits, intente esto:
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,)
Necesitaba un número entero con signo, así que terminé yendo con:
import struct, base64
def b64encode_integer(i):
return base64.urlsafe_b64encode(struct.pack(''i'', i)).rstrip(''=/n'')
Ejemplo:
>>> b64encode_integer(1)
''AQAAAA''
>>> b64encode_integer(-1)
''_____w''
>>> b64encode_integer(256)
''AAEAAA''
No quiere codificación base64, quiere representar un número base 10 en la base numeral X.
Si quiere que su número base 10 esté representado en las 26 letras disponibles, puede usar: http://en.wikipedia.org/wiki/Hexavigesimal . (Puede extender ese ejemplo para una base mucho más grande usando todos los caracteres de URL legales)
Al menos deberías poder obtener la base 38 (26 letras, 10 números, +, _)
Para codificar n
:
data = ''''
while n > 0:
data = chr(n & 255) + data
n = n >> 8
encoded = base64.urlsafe_b64encode(data).rstrip(''='')
Para decodificar s
:
data = base64.urlsafe_b64decode(s + ''==='')
decoded = 0
while len(data) > 0:
decoded = (decoded << 8) | ord(data[0])
data = data[1:]
Con el mismo espíritu que otros para una codificación "óptima", puede usar 73 caracteres de acuerdo con RFC 1738 (en realidad 74 si cuenta "+" como utilizable):
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_`/"!$''()*,-."
encoded = ''''
while n > 0:
n, r = divmod(n, len(alphabet))
encoded = alphabet[r] + encoded
y la decodificación:
decoded = 0
while len(s) > 0:
decoded = decoded * len(alphabet) + alphabet.find(s[0])
s = s[1:]
Probablemente no desee una codificación base64 real para esto: agregará relleno, etc., lo que posiblemente resultará en cadenas más grandes que las que tendría el hexadecimal para números pequeños. Si no hay necesidad de interoperar con otra cosa, solo use su propia codificación. P.ej. aquí hay una función que codificará para cualquier base (obsérvese que los dígitos se almacenan realmente menos importantes primero para evitar llamadas inversas adicionales):
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
Esto tiene la ventaja de que puede usar cualquier base que desee, simplemente agregando caracteres apropiados a la cadena base del codificador.
Tenga en cuenta que las ganancias para bases más grandes no van a ser tan grandes sin embargo. la base 64 solo reducirá el tamaño a 2 / 3rds de la base 16 (6 bits / char en lugar de 4). Cada duplicación solo agrega un bit más por personaje. A menos que tenga una necesidad real de compactar cosas, solo usar hexadecimal probablemente sea la opción más simple y rápida.
Si está buscando una manera de acortar la representación de enteros utilizando base64, creo que debe buscar en otra parte. Cuando codifica algo con base64 no se acorta, de hecho se hace más largo.
Por ejemplo, 11234 codificado con base64 produciría MTEyMzQ =
Al usar base64, ha pasado por alto el hecho de que no está convirtiendo solo los dígitos (0-9) en una codificación de 64 caracteres. Está convirtiendo 3 bytes en 4 bytes, por lo que tiene la garantía de que la cadena codificada en base64 sería un 33,33% más larga.
Todas las respuestas dadas con respecto a Base64 son soluciones muy razonables. Pero son técnicamente incorrectos. Para convertir un número entero en la cadena segura de URL más corta posible, lo que desea es la base 66 (hay 66 caracteres URL seguros ).
Ese código se ve así:
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]
Aquí hay una implementación completa con el paquete instalable de fuente y listo para usar:
un poco hacky, pero funciona:
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'')
puede reemplazar la llamada a .encode (''base64'') con algo en el módulo base64, como urlsafe_b64encode ()
Python puro, sin dependencias, sin codificación de cadenas de bytes, etc., simplemente convirtiendo una base 10 int en base 64 int con los caracteres RFC 4648 correctos:
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)