web-dev-qa-db-ja.com

Pythonで整数を最短のURLセーフ文字列に変換する方法は?

URLで整数を表すための最短の方法が必要です。たとえば、11234は16進数を使用して「2be2」に短縮できます。 base64は64文字のエンコーディングを使用するため、16進数よりも少ない文字を使用してbase64で整数を表すことができるはずです。問題は、Pythonを使用して整数をbase64に変換する(そして再び元に戻す)ための最もクリーンな方法を理解できないことです。

Base64モジュールには、バイト文字列を処理するためのメソッドがあります。そのため、おそらく1つの解決策は、整数をバイナリ表現にPython string ...として変換することです...どちらか。

63
Simon Willison

この回答は、精神的にダグラスリーダーの回答と似ていますが、次の点が異なります。

  • 実際の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セーフ」文字を追加して、アルファベットを変更することで、このコードをさまざまなベースに簡単に適合させることができるはずです。
  • 私はお勧めしますagainstほとんどの場合、URIでベース10以外の表現を使用します—これは複雑さを追加し、HTTPのオーバーヘッドと比較して大幅な節約なしにデバッグを困難にします—エスク。
60
Miles

Base64に関するすべての回答は非常に合理的なソリューションです。しかし、それらは技術的に正しくありません。整数を可能な最短のURLセーフ文字列に変換するには、ベース66が必要です( 66 URLセーフ文字があります )。

そのコードは次のようになります。

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

18

あなたはおそらくこれのために本当のbase64エンコーディングを望まないでしょう-それはパディングなどを追加し、小さな数値に対して16進数よりも大きな文字列をもたらす可能性さえあります。他のものと相互運用する必要がない場合は、独自のエンコーディングを使用してください。例えば。これは、任意の基数にエンコードする関数です(余分な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にのみ縮小します(4ではなく6ビット/文字)。倍加するたびに、文字ごとに1ビットが追加されます。本当にコンパクトにする必要がない限り、16進数を使用するのがおそらく最も簡単で最速のオプションです。

14
Brian

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に従って7文字を使用できます(「+」を使用可能とカウントした場合、実際には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:]
9
kmkaplan

簡単なビットは、バイト文字列をWebセーフのbase64に変換することです。

import base64
output = base64.urlsafe_b64encode(s)

トリッキーなビットは最初のステップです-整数をバイト文字列に変換します。

整数が小さい場合は、16進数でエンコードするほうがよい-参照 saua

それ以外の場合(ハッキーな再帰バージョン):

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

Base64エンコードは必要ありません。10進数を数値ベースXで表します。

使用可能な26文字で表される10を基数とする数値が必要な場合は、 http://en.wikipedia.org/wiki/Hexavigesimal を使用できます。 (すべての正当なURL文字を使用することにより、この例をより大きなベースに拡張できます)

少なくとも38進数(26文字、10桁の数字、+、_)を取得できる必要があります。

7

Base64は3バイトをエンコードするために4バイト/文字を取り、3バイトの倍数のみをエンコードできます(それ以外の場合はパディングを追加します)。

したがって、Base64で4バイト(平均int)を表すには8バイトかかります。同じ4バイトを16進数でエンコードすると、8バイトもかかります。したがって、1つのintに対しては何も得られません。

4
Joachim Sauer

私は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に入れます。

3
Zooko

少しハックですが、うまくいきます:

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')の呼び出しを、urlsafe_b64encode()などのbase64モジュールの何かで置き換えることができます。

3
ʞɔıu

shortenを表現する方法を探している場合は、base64を使用した整数表現で、他の場所を調べる必要があると思います。 base64でエンコードした場合、短くはなりませんが、実際には長くなります。

例えば。 base64でエンコードされた11234はMTEyMzQ =を生成します

Base64を使用する場合、数字(0〜9)だけを64文字エンコードに変換しないという事実を見落としました。 3バイトを4バイトに変換しているので、base64でエンコードされた文字列が33.33%長くなることが保証されます。

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'
2
toothygoose

このための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での使用は推奨されません!

同じことがbases.jsとbases.pyの両方で現在サポートされていないbase 66にも当てはまりますが、 future

2
Belldandu

私は「整数をバイナリ文字列としてエンコードし、次にbase64エンコードする」メソッドを提案し、構造体を使用してそれを行います。

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

純粋な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)
1
J.J