如何将整数转换为 Python 中最短的 url 安全字符串?

Posted

技术标签:

【中文标题】如何将整数转换为 Python 中最短的 url 安全字符串?【英文标题】:How to convert an integer to the shortest url-safe string in Python? 【发布时间】:2010-10-08 08:53:06 【问题描述】:

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

base64 模块具有处理字节串的方法 - 所以也许一种解决方案是将整数转换为其二进制表示形式作为 Python 字符串......但我也不知道该怎么做。

【问题讨论】:

Simon:请看 Øystein krog 的回答。您想使用整数数据的“base 64”表示,而不是 base64 模块,该模块旨在编码任意二进制数据并且不压缩数字的文本表示。见en.wikipedia.org/wiki/Base_64) 我希望可以在部分工作中重用现有的 base64 模块,但遗憾的是,情况似乎并非如此。感谢大家的所有出色回复。 对于任何感兴趣的人,我最终滚动了自己的代码来执行此操作:djangosnippets.org/snippets/1431 在阅读了 Ricardo 关于 Øystein Krog 答案的评论(没有任何代码)之后,我在底部写了一些非常基本的 Python,获得 0 票:P 【参考方案1】:

这个答案在精神上与道格拉斯·利德的相似,但有以下变化:

它不使用实际的 Base64,所以没有填充字符

它不是先将数字转换为字节字符串(以 256 为基数),而是将其直接转换为以 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 选择了我做的顺序。 如果要对大量负数进行编码,则可以通过使用符号位或 1/2 的补码代替符号字符来提高效率。 您应该能够通过更改字母表轻松地将此代码调整为不同的基数,将其限制为仅包含字母数字字符或添加其他“URL 安全”字符。 我建议反对在大多数情况下在 URI 中使用基数 10 以外的表示形式 - 与 HTTP 的开销相比,它增加了复杂性并使得调试更加困难,但没有显着节省 - 除非您要TinyURL 风格的东西。

【讨论】:

投票赞成考虑负数。但是符号的一个字节不是有点贵吗? 是的,这是我在第二个笔记中提到的;但如果这不是问题,使用符号字符的实现是最简单的;) 我要使用的初始位置是“恢复您的帐户”样式的 URL,其中包括用户 ID、时间戳和 sha1 哈希 - 理想情况下应该少于 80 个字符以确保它们可以安全地通过电子邮件发送,没有文字包装搞砸了。 这是非常好的代码,但根据 Alex Martelli (***.com/questions/931092/reverse-a-string-in-python/…) 的说法,s[::-1] 是一种更快的反转字符串的方法 @hwiechers: s 实际上不是一个字符串,它是一个列表,所以我还是要加入它;我可以使用''.join(s[::-1])''.join(s)[::-1],但它们只是稍微快一些——远低于telliot99 用于反转字符串的微基准测试中看到的数量级。【参考方案2】:

所有关于 Base64 的答案都是非常合理的解决方案。但它们在技术上是不正确的。要将整数转换为可能的最短 URL 安全字符串,您需要的是 base 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

【讨论】:

~ 在 RFC 1738 中被认为是不安全的:其他字符是不安全的,因为已知网关和其他传输代理有时会修改此类字符。这些字符是“”、“”、“|”、“\”、“^”、“~”、“[”、“]”和“`”。 — 发现于tantek.pbworks.com/w/page/24308279/NewBase64 这很有趣。不过,关于 URI 的 RFC 3986 较新,并且似乎部分过时了 RFC 1738。在更实际的说明中,~ 一直在 URL 中使用。例如。想想example.com/~user/,一个可以追溯到早期网络时代的经典 URL。 jkorpela.fi/tilde.html 说明了在 URL 中不使用波浪号的几个原因,主要集中在可读性上。但是 base64 并不真正应该是人类可读的。我个人认为出于“兼容性”原因的人为限制是无稽之谈。例如,在搜索 Google 时,Firefox 不会转义 !\"'()*-.&lt;&gt;[\\]^_`|~+,而 Chrome 只允许 "*-.&lt;&gt;_~,然后是非 ASCII/UTF-8 字符:¡¢£¤¥¦§¨©ª«¬ 都以明文形式发送,不需要百分比编码. 是的,我认为不管有没有波浪号,编码的长数字无论如何都不是特别“可读”。关于"*-.&lt;&gt;_~ 的好点。需要更多研究以确保所有浏览器都可以使用这些。【参考方案3】:

您可能不想要真正的 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/3(6 位/字符而不是 4)。每次加倍只会为每个字符增加一位。除非你真的需要压缩东西,否则只使用十六进制可能是最简单和最快的选择。

【讨论】:

【参考方案4】:

编码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:]

【讨论】:

我将my answer 的答案改编为How to make unique short URL with Python? 问题。【参考方案5】:

简单的一点是将字节字符串转换为网络安全的 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)

【讨论】:

【参考方案6】:

您不想使用 base64 编码,而是想以数字基 X 表示以 10 为基数的数字。

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

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

【讨论】:

你是对的,但是他仍然可以通过使用数字、小写、大写和-_来使用base 64。【参考方案7】:

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

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

【讨论】:

@saua:您忘记了每个数字仅编码约 3.3 位,而 base64 的每个字符编码 6,因此表示 base64 中的整数(而不是基数 10)将产生大约一半长度的字符串. @Mike 我讨论了十六进制(base-16)编码与 base64 的长度,并且由于填充,4 字节数据的长度相同。当然,对于更长的字符串,这会发生变化,但问题是明确地关于编码 int。 @saua:但你不一定有一个需要 4 个完整字节的 int。十进制1仍然可以是B64 1,那么十进制64可以是B64 10。【参考方案8】:

有点笨拙,但它有效:

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

您可以将 .encode('base64') 调用替换为 base64 模块中的某些内容,例如 urlsafe_b64encode()

【讨论】:

我用 12345 尝试过。它给了我:'MDk=\n' 这似乎已将 5 位整数转换为长度为 5 的字符串。我可以想到更简单的方法来实现这一点:-) = 和 \n 是可以去掉的填充【参考方案9】:

我维护了一个名为 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'

但是,您仍然需要将整数转换为字符串。这是 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 中为您提供方便。

【讨论】:

【参考方案10】:

如果您正在寻找一种方法来缩短使用 base64 的整数表示,我认为您需要寻找其他地方。当您使用 base64 对某些内容进行编码时,它不会变短,实际上它会变长。

例如使用 base64 编码的 11234 将产生 MTEyMzQ=

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

【讨论】:

第一步是将整数转换为字节串。 如果您将十进制数的字符串表示形式编码为基数 64,则您是正确的,但如果您想将数字本身编码为基数 64,则不是。每个十进制数字编码约 3.3 位信息,而base 64的每个字符编码6位信息。因此,base64 数字会更短。 “base 64”可能意味着两种不同的东西:“Base64 编码”和以 base 64 表示的数字。"\x01".encode("base64") =&gt; 'AQ==',而以 base 64 表示的 1 只是 1。【参考方案11】:

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

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'

【讨论】:

【参考方案12】:

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

我建议你使用我的 bases.py https://github.com/kamijoutouma/bases.py,它的灵感来自于 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

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

根据你的情况

我建议您使用基数 32、58 或 64

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

base 66 也是如此,目前 bases.js 和 bases.py 都不支持,但它可能在 future

【讨论】:

【参考方案13】:

我会使用你建议的“将整数编码为二进制字符串,然后使用 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,)

【讨论】:

@Jorenko:这远不是最有效的。 base 64 中的 47 可以用单个字符表示(因为 47 小于 64。)【参考方案14】:

纯python,无依赖,无字节串编码等,只需使用正确的 RFC 4648 字符将 base 10 int 转换为 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)

【讨论】:

python3:将number /= 64更改为number //= 64【参考方案15】:

正如在 cmets 中提到的,您可以使用 URL 中未转义的 73 个字符对数据进行编码。 我发现有两个地方使用了这种 Base73 URL 编码:

https://git.nolog.cz/NoLog.cz/f.bain/src/branch/master/static/script.js 基于 JS 的 URL 缩短器 https://gist.github.com/LoneFry/3792021 在 php

但实际上您可能会使用更多字符,例如/[]:; 等。这些字符仅在您执行 encodeURIComponent 时才会转义,即您需要通过 get 参数传递数据。

所以实际上您最多可以使用 82 个字符。完整的字母表是!$&amp;'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz~。我按它们的代码对所有符号进行了排序,因此当 Base82URL 数字被排序为纯字符串时,它们保持相同的顺序。

我在 Chrome 和 Firefox 中进行了测试,它们运行良好,但可能会让普通用户感到困惑。但我将此类 ID 用于没有人看到它们的内部 API 调用。

32位无符号整数可能有最大值2^32=4294967296 编码为 Base82 后,需要 6 个字符:$0~]mx

我没有 Python 代码,但这里有一个 JS 代码,它生成一个随机 id(int32 无符号)并将其编码到 Base82URL:

        /**
         * Convert uint32 number to Base82 url safe
         * @param int number
         * @returns string
         */
        function toBase82Url(number) 
            // all chars that are not escaped in url
            let keys = "!$&'()*+,-./0123456789:;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]_abcdefghijklmnopqrstuvwxyz~"
            let radix = keys.length
            let encoded = []
            do 
                let index = number% radix
                encoded.unshift(keys.charAt(index))
                number = Math.trunc(number / radix)
             while (number !== 0)
            return encoded .join("")
        

        function generateToken() 
            let buf = new Uint32Array(1);
            window.crypto.getRandomValues(buf)
            var randomInt = buf[0]
            return toBase82Url(randomInt)
        

【讨论】:

以上是关于如何将整数转换为 Python 中最短的 url 安全字符串?的主要内容,如果未能解决你的问题,请参考以下文章

列表中最短的列表(Haskell)

为啥有的URL长,有的短?

Javascript reduce() 查找字符串中最短的单词

通过 URL 将用户所在的 URL 提交到另一个进程的最简洁、最短的 Javascript 是啥?

char是所有类型中最短的 char多为8位,

从NSString获取一个char并转换为int