Python: компактно и обратимо кодировать большое целое число как base64 или base16, имеющее переменную или фиксированную длину

Я хочу компактно закодировать большое целое число без знака или со знаком, имеющее произвольное количество битов, в представление base64, base32 или base16 (шестнадцатеричное). Вывод в конечном итоге будет использоваться как строка, которая будет использоваться в качестве имени файла, но это не относится к делу. Я использую последнюю версию Python 3.

Это работает, но далеко не компактно:

>>> import base64, sys
>>> i: int = 2**62 - 3  # Can be signed or unsigned.
>>> b64: bytes =  base64.b64encode(str(i).encode()) # Not a compact encoding.
>>> len(b64), sys.getsizeof(b64)
(28, 61)

Существует предыдущий вопрос , теперь закрытые, ответы на которые строго касаются неэффективных представлений. Еще раз обратите внимание, что в этом упражнении мы не хотим использовать какие-либо строки или излишне длинные последовательности байтов. Таким образом, этот вопрос не является дубликатом этого вопроса.


person Acumenus    schedule 11.01.2019    source источник
comment
Какова цель кодирования base64 здесь? Какие данные хранятся, кодируете ли вы, чтобы упростить передачу, каким клиентам нужно снова прочитать эти данные?   -  person Martijn Pieters    schedule 11.01.2019
comment
Я прошу контекст, потому что вполне вероятно, что есть лучшие варианты. base64 не является общепринятой кодировкой для отдельных целочисленных значений.   -  person Martijn Pieters    schedule 11.01.2019
comment
@ A-B-B: если цель состоит в том, чтобы компактно встроить значение в строку, то доступно больше вариантов. Можно ли использовать base85? Какие ограничения вы накладываете на значение по символам?   -  person Martijn Pieters    schedule 11.01.2019
comment
Следующий вопрос: на какую файловую систему и ОС вы ориентируетесь? Некоторые файловые системы нечувствительны к регистру (даже если они могут сохранять регистр), но base64 чувствителен к регистру. Вы можете тривиально создать пары строк base64, которые будут отображаться в один и тот же файл в такой файловой системе.   -  person Martijn Pieters    schedule 11.01.2019
comment
@А-В-В; base85 небезопасен для имен файлов, так что нет, на данный момент это не лучший ответ.   -  person Martijn Pieters    schedule 11.01.2019
comment
цель состоит в том, чтобы эффективно кодировать int как строку и использовать как можно больше встроенных функций и не ограничивать ее размером строки. лучший способ сделать это: а) преобразовать int.to_bytes, а затем преобразовать байты в строку. выбор безопасных версий base64 позволит встраивать в имена файлов и URL-адреса.   -  person Erik Aronesty    schedule 15.01.2019


Ответы (2)


Этот ответ частично мотивирован разрозненными комментариями Эрика А., например, для этого ответа. Целое число сначала компактно преобразуется в байты, после чего байты кодируются в переменную base.

from typing import Callable, Optional
import base64

class IntBaseEncoder:
    """Reversibly encode an unsigned or signed integer into a customizable encoding of a variable or fixed length."""
    # Ref: https://stackoverflow.com/a/54152763/
    def __init__(self, encoding: str, *, bits: Optional[int] = None, signed: bool = False):
        """
        :param encoder: Name of encoding from base64 module, e.g. b64, urlsafe_b64, b32, b16, etc.
        :param bits: Max bit length of int which is to be encoded. If specified, the encoding is of a fixed length,
        otherwise of a variable length.
        :param signed: If True, integers are considered signed, otherwise unsigned.
        """
        self._decoder: Callable[[bytes], bytes] = getattr(base64, f'{encoding}decode')
        self._encoder: Callable[[bytes], bytes] = getattr(base64, f'{encoding}encode')
        self.signed: bool = signed
        self.bytes_length: Optional[int] = bits and self._bytes_length(2 ** bits - 1)

    def _bytes_length(self, i: int) -> int:
        return (i.bit_length() + 7 + self.signed) // 8

    def encode(self, i: int) -> bytes:
        length = self.bytes_length or self._bytes_length(i)
        i_bytes = i.to_bytes(length, byteorder='big', signed=self.signed)
        return self._encoder(i_bytes)

    def decode(self, b64: bytes) -> int:
        i_bytes = self._decoder(b64)
        return int.from_bytes(i_bytes, byteorder='big', signed=self.signed)

# Tests:
import unittest

class TestIntBaseEncoder(unittest.TestCase):

    ENCODINGS = ('b85', 'b64', 'urlsafe_b64', 'b32', 'b16')

    def test_unsigned_with_variable_length(self):
        for encoding in self.ENCODINGS:
            encoder = IntBaseEncoder(encoding)
            previous_length = 0
            for i in range(1234):
                encoded = encoder.encode(i)
                self.assertGreaterEqual(len(encoded), previous_length)
                self.assertEqual(i, encoder.decode(encoded))

    def test_signed_with_variable_length(self):
        for encoding in self.ENCODINGS:
            encoder = IntBaseEncoder(encoding, signed=True)
            previous_length = 0
            for i in range(-1234, 1234):
                encoded = encoder.encode(i)
                self.assertGreaterEqual(len(encoded), previous_length)
                self.assertEqual(i, encoder.decode(encoded))

    def test_unsigned_with_fixed_length(self):
        for encoding in self.ENCODINGS:
            for maxint in range(257):
                encoder = IntBaseEncoder(encoding, bits=maxint.bit_length())
                maxlen = len(encoder.encode(maxint))
                for i in range(maxint + 1):
                    encoded = encoder.encode(i)
                    self.assertEqual(len(encoded), maxlen)
                    self.assertEqual(i, encoder.decode(encoded))

    def test_signed_with_fixed_length(self):
        for encoding in self.ENCODINGS:
            for maxint in range(257):
                encoder = IntBaseEncoder(encoding, bits=maxint.bit_length(), signed=True)
                maxlen = len(encoder.encode(maxint))
                for i in range(-maxint, maxint + 1):
                    encoded = encoder.encode(i)
                    self.assertEqual(len(encoded), maxlen)
                    self.assertEqual(i, encoder.decode(encoded))

if __name__ == '__main__':
    unittest.main()

При использовании вывода в качестве имени файла инициализация кодировщика с кодировкой 'urlsafe_b64'< /a> или даже 'b16' — более безопасный выбор.

Примеры использования:

# Variable length encoding
>>> encoder = IntBaseEncoder('urlsafe_b64')
>>> encoder.encode(12345)
b'MDk='
>>> encoder.decode(_)
12345

# Fixed length encoding
>>> encoder = IntBaseEncoder('b16', bits=32)
>>> encoder.encode(12345)
b'00003039'
>>> encoder.encode(123456789)
b'075BCD15'
>>> encoder.decode(_)
123456789

# Signed
encoder = IntBaseEncoder('b32', signed=True)
encoder.encode(-12345)
b'Z7DQ===='
encoder.decode(_)
-12345
person Acumenus    schedule 11.01.2019

Следующий фрагмент из этого ответа должен соответствовать вашим потребностям, с тем преимуществом, что он не имеет зависимостей:

def v2r(n, base): # value to representation
    """
    Convert a positive integer to its string representation in a custom base.
    
    :param n: the numeric value to be represented by the custom base
    :param base: the custom base defined as a string of characters, used as symbols of the base
    :returns: the string representation of natural number n in the custom base
    """
    if n == 0: return base[0]
    b = len(base)
    digits = ''
    while n > 0:
        digits = base[n % b] + digits
        n  = n // b
    return digits

Он не выполняет непосредственно типичное преобразование base64 (хотя его можно использовать для его получения), но результат аналогичен, поскольку он возвращает представление большого целого числа (только положительное, но вы можете легко преодолеть это ограничение) в пользовательском формате. числовая база длины, состоящая из пользовательских символов.

Некоторые примеры лучше любого слова показывают его простое и универсальное использование:

# base64 filename-safe characters
# perform a base64 conversion if applied to 4 bytes chunks
>>> v2r(123456789,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_')
'4pDLq'

# hexadecimal base
>>> v2r(123456789,'0123456789ABCDEF')
'75BCD15'
>>> v2r(255,'0123456789ABCDEF')
'FF'

# custom base of 62 filename-safe characters
>>> v2r(123456789,'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
'8m0Kx'

# custom base of 36 filename-safe lowercase characters for case insensitive file systems
>>> v2r(123456789,'0123456789abcdefghijklmnopqrstuvwxyz')
'21i3v9'

# binary conversion
>>> v2r(123456789,'01')
'111010110111100110100010101'
>>> v2r(255,'01')
'11111111'
person mmj    schedule 07.03.2021
comment
Вам может понадобиться особый случай для n == 0, чтобы вы не получили пустой вывод. - person Mark Ransom; 09.03.2021
comment
@MarkRansom Хороший вопрос, я обновил ответ. - person mmj; 09.03.2021