如何在Python中将整数转换为最短的url-safe字符串?

时间:2009-02-18 15:25:26

标签: python url base64

我想要一种在URL中表示整数的最短方法。例如,11234可以使用十六进制缩短为“2be2”。由于base64使用的是64字符编码,因此应该可以使用比十六进制更少的字符来表示base64中的整数。问题是我无法弄清楚使用Python将整数转换为base64(以及再返回)的最简洁方法。

base64模块有处理字节串的方法 - 所以也许一个解决方案是将整数转换为二进制表示形式的Python字符串...但我不知道如何做到这一点。

14 个答案:

答案 0 :(得分:60)

这个答案与Douglas Leeder的精神相似,但有以下变化:

  • 它不使用实际的Base64,因此没有填充字符
  • 不是将数字首先转换为字节字符串(基数256),而是将其直接转换为base 64,这样可以让您使用符号字符表示负数。

    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))
    '$_____'

一些附注:

  • 你可以(略微)通过将string.digits放在字母表中的第一个(并使符号字符' - ')来增加base-64数字的人类可读性;我根据Python的urlsafe_b64encode选择了我的订单。
  • 如果您编码了很多负数,可以使用符号位或一个/两个补码而不是符号字符来提高效率。
  • 您应该可以通过更改字母表轻松地将此代码调整到不同的基础,或者将其限制为仅使用字母数字字符或添加其他“URL安全”字符。
  • 在大多数情况下,我建议在URI中使用基本10以外的表示 - 它增加了复杂性,并且与HTTP的开销相比,调试更加困难而且没有显着的节省 - 除非你想要TinyURL-esque。

答案 1 :(得分:18)

关于Base64的所有答案都是非常合理的解决方案。但他们在技术上是不正确的。要将整数转换为最短的URL安全字符串,您想要的是基数66(有66 URL safe characters)。

该代码看起来像这样:

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]

这是一个像这样的计划的完整实现,随时可以作为一个pip可安装包:

https://github.com/aljungberg/hhc

答案 2 :(得分:14)

你可能不需要真正的base64编码 - 它会添加填充等,甚至可能导致比小数字更大的字符串。如果不需要与其他任何东西进行互操作,只需使用您自己的编码即可。例如。这是一个将编码到任何基数的函数(注意数字实际上是先存储最不重要的,以避免额外的reverse()调用:

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

这样做的好处是你可以使用你想要的任何基础,只需添加适当的 字符到编码器的基本字符串。

请注意,较大基数的收益不会那么大。 base 64只会将大小减小到base 16的2 / 3rds(6位/ char而不是4位)。每次加倍只会为每个字符增加一位。除非你真的需要压缩东西,否则只使用十六进制可能是最简单和最快的选择。

答案 3 :(得分:9)

编码n

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

解码s

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

与一些“最佳”编码的精神相同,您可以根据RFC 1738使用 73 字符(如果您将“+”视为可用,则实际为74):

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

和解码:

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

答案 4 :(得分:8)

简单的位是将字节字符串转换为web-safe base64:

import base64
output = base64.urlsafe_b64encode(s)

棘手的位是第一步 - 将整数转换为字节串。

如果您的整数很小,那么最好使用十六进制编码 - 请参阅saua

否则(hacky递归版):

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

答案 5 :(得分:7)

您不希望使用base64编码,您希望在数字基数X中表示基数为10的数字。

如果您希望以26个字母表示您的基数10数字,您可以使用:http://en.wikipedia.org/wiki/Hexavigesimal。 (您可以使用所有合法的网址字符扩展该示例以获得更大的基础)

你应该至少能得到38(26个字母,10个数字,+,_)

答案 6 :(得分:4)

Base64需要4个字节/字符来编码3个字节,并且只能编码3个字节的倍数(否则会添加填充)。

因此,在Base64中表示4个字节(平均int)将占用8个字节。以十六进制编码相同的4个字节也需要8个字节。所以你不会为一个int获得任何东西。

答案 7 :(得分:3)

有点hacky,但它确实有效:

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

您可以使用base64模块中的内容替换对.encode('base64')的调用,例如urlsafe_b64encode()

答案 8 :(得分:3)

我维护了一个名为zbase62的小库:http://pypi.python.org/pypi/zbase62

有了它,你可以从Python 2 str对象转换为base-62编码的字符串,反之亦然:

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'

但是,您仍需要将整数转换为str。这内置于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'

要在Python 2中从int转换为字节,反之亦然,据我所知,没有一种方便的标准方法。我想也许我应该将一些实现,例如这个:https://github.com/warner/foolscap/blob/46e3a041167950fa93e48f65dcf106a576ed110e/foolscap/banana.py#L41复制到zbase62中,以方便您使用。

答案 9 :(得分:2)

如果您正在寻找使用base64 缩短整数表示的方法,我认为您需要查看其他地方。使用base64编码内容时,它不会变短,实际上它会变长。

E.g。用base64编码的11234将产生MTEyMzQ =

使用base64时,您忽略了一个事实,即您不是只将数字(0-9)转换为64个字符的编码。您正在将3个字节转换为4个字节,因此您可以保证base64编码的字符串长33.33%。

答案 10 :(得分:2)

我需要一个有符号整数,所以我最终选择了:

import struct, base64

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

示例:

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

答案 11 :(得分:2)

我正在为此制作一个pip包。

我建议您使用受bases.js启发的bases.py https://github.com/kamijoutouma/bases.py

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

参考https://github.com/kamijoutouma/bases.py#known-basesalphabets 什么基地可用

对于你的情况

我建议你使用32,58或64

Base-64警告:除了有几种不同的标准外,目前还没有添加填充,也没有跟踪线路长度。不建议与期望正式base-64字符串的API一起使用!

同样适用于base 66,bases.js和bases.py目前都不支持,但它可能在future

答案 12 :(得分:1)

我将'编码整数作为二进制字符串,然后使用base64编码'你建议的方法,我会使用struct:

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

再次编辑: 要删除太小而不需要完全32位精度的数字上的额外0,请尝试:

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

答案 13 :(得分:1)

纯python,没有依赖,没有字节串的编码等,只是将一个基数10 int转换为带有正确RFC 4648字符的base 64 int:

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)