Создайте две функции, которые могут преобразовывать двоичные данные в строку в кодировке Base64 и наоборот не используя символы "=" padding
Постановка задачи
Задание с сайта codewars.com
Кодировка Base64 позволяет представлять произвольные двоичные данные в виде ASCII-текста.
Ваша задача - предоставить кодировщик и декодер для преобразования в Base64 и из него.
Создайте две функции, которые могут преобразовывать двоичные данные в строку в кодировке Base64 и наоборот:
def to_base_64(data: bytes) -> str: ...
def from_base_64(encoded: str) -> bytes: ...
Хотя во многих реализациях Base64 используются символы "=" padding, ваши функции не должны использовать padding.
Можете ли вы создать свой собственный кодер и декодер, а не использовать реализацию base64 на вашем языке программирования?
Пример (ввод -> вывод):
b"this is a bytestring!" -> "dGhpcyBpcyBhIGJ5dGVzdHJpbmch"
b"\x00" -> "AA"
b"\x00\x01" -> "AAE"),
b"this is a test!" -> "dGhpcyBpcyBhIHRlc3Qh"
b"TVRJek5EVTJOemc1TUNBZyAg" -> "VFZSSmVrNUVWVEpPZW1jMVRVTkJaeUFn"
b"\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb\xe2\x80\x8d\xe2\b\xe2\x80\x8d\xf0\x9f\x91\xa8\xf0\x9f\x8f\xbb" ->"8J+RqPCfj7vigI3inaTvuI/igI3wn5KL4oCN8J+RqPCfj7s"
Моё решение
Создал 2 функции как по заданию
import base64
def to_base_64(data: bytes) -> str:
if data == b'':
return ''
data = data.decode('utf-8')
message_bytes = data.encode('utf-8')
base64_bytes = base64.b64encode(message_bytes)
base64_message = base64_bytes.decode('utf-8')
count_padding = base64_message.count('=')
if count_padding == 0: # Проверка количества padding
return base64_message # Вернуть если нет padding
else:
return base64_message[:-count_padding] # Вернуть без padding - ТАКОЕ ЗАДАНИЕ
# print(to_base_64(data) )
def from_base_64(encoded: str) -> bytes:
if encoded == '':
return b''
if len(encoded) % 4 != 0:
encoded += '=' * (4 - len(encoded) % 4)
base64_bytes = encoded.encode('utf-8')
message_bytes = base64.b64decode(base64_bytes)
message = message_bytes.decode('utf-8')
return message.encode('utf-8')
Решение нашёл - тесты проходят, но получаю некоторые ошибки с сервера:
100 random tests encoding to base64
Log
encodes b"\x8f\x89\x02\x87u\x80\xde\xa1T nG\x83\t\x87m\x94\xa3\xd6n\xa7h\xff\x87\x03\xac\xc6/'=\x14\x1c\xcd\xdf2D\xc2\x9aC\x89\xa0\xea\xfa-T\xe7\xf0\xaa\x0e{\xd2/\xae\x1c\x12\x1dP=\x85x\xc4\x98b\xd0\xbd\xfb\xcc\x1b\x8a\x11 `\x11Mgd\xd6Q\xcd\x8b" to 'j4kCh3WA3qFUIG5HgwmHbZSj1m6naP+HA6zGLyc9FBzN3zJEwppDiaDq+i1U5/CqDnvSL64cEh1QPYV4xJhi0L37zBuKESBgEU1nZNZRzYs'
b"\x8f\x89\x02\x87u\x80\xde\xa1T nG\x83\t\x87m\x94\xa3\xd6n\xa7h\xff\x87\x03\xac\xc6/'=\x14\x1c\xcd\xdf2D\xc2\x9aC\x89\xa0\xea\xfa-T\xe7\xf0\xaa\x0e{\xd2/\xae\x1c\x12\x1dP=\x85x\xc4\x98b\xd0\xbd\xfb\xcc\x1b\x8a\x11 `\x11Mgd\xd6Q\xcd\x8b"
data = data.decode('utf-8')
^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8f in position 0: invalid start byte
100 random tests decoding from base64
Log
decodes ['9', 'Y', '9', 'A', 'r', 'A', '3', 'x', 'n', 'D', 'g', 'R', 'k', '8', 'G', '7', '9', '1', '+', '/', 'R', 'u', 'Z', 'r', 'N', 'B', 'z', 'K'] to b'\xf5\x8f@\xac\r\xf1\x9c8\x11\x93\xc1\xbb\xf7_\xbfF\xe6k4\x1c\xca'
base64_bytes = encoded.encode('utf-8')
^^^^^^^^^^^^^^
AttributeError: 'list' object has no attribute 'encode'
Много раз редактировал своё решение по советам в комментариях, кодировку 'utf-8' сменить или как, и как быть со списком - к строке привести?
Ответы (1 шт):
Ответ
В начало функции from_base_64 вставьте оператор:
encoded = ''.join(encoded)
Он нужен потому что в кате есть тест, когда на вход подаётся не строка, а список строк. Если encoded строка, этот код ничего не поменяет. Если encoded список, он превратит его в строку.
В обоих функциях есть проверки на пустые входные строки. Они не нужны, без них всё работает отлично. Удаляем.
В начале функции to_base_64 байты декодируются в UTF8, затем кодируются обратно. В кате нет тестов, которые показывают ошибку, но вызов to_base_64(bytes([255])) упадёт. Декодирование и последующее кодирование убираем.
Отрезать выравнивание справа можно прямо в байтовой строке.
Последнее декодирование .decode('utf-8') переводит байты в строку. Это не ошибка, но можно обойтись более простым и быстрым декодированием в latin1.
В from_base_64 проверку длины строки и коррекцию можно заменить на один оператор encoded += '=' * (-len(encoded) % 4).
Дальше кодирование в utf-8 не нужно. base64.b64decode умеет работать со строками.
В конце функции опять бесполезное декодирование/кодирование. Удалить.
После этих изменений получим:
def to_base_64(data: bytes) -> str:
return base64.b64encode(data).rstrip(b'=').decode('latin1')
def from_base_64(encoded: str) -> bytes:
encoded = ''.join(encoded)
return base64.b64decode(encoded + '=' * (-len(encoded) % 4))
Не ответ
Интересная оказалась ката, и решить её можно сравнительно просто.
alphabet хранит алфавит – отображение из индексов в символы.
indices наоборот отображает символы в индексы.
encode_chunk и decode_chunk представляют один и тот же алгоритм: сперва из исходных данных собираем 24-битовую строку, затем вырезаем из неё кусочки нужной длины. Обработка неполных данных (менее трёх байт или менее четырёх символов) делается автоматически. Секрет в соответствии:
3 байта ⇆ 4 символа;
2 байта ⇆ 3 символа;
1 байта ⇆ 2 символа.
Символов всегда на единицу больше чем байт.
to_base_64 и from_base_64 снова одинаковы: режем исходные данные на кусочки, вызываем обработку кусочка, склеиваем результаты.
Оператор s = ''.join(s) во from_base_64 нужен потому что в кате есть тест, когда на вход подаётся не строка а список строк. Для строки он ничего не делает (только процессор греет), для списка возвращает единую строку.
Хорошо было бы переписать алгоритм на потоковую обработку, но тогда красота и простота решения будут не так видны.
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
indices = {c: i for i, c in enumerate(alphabet)}
def encode_chunk(b):
bits = sum(bi << (16 - 8 * i) for i, bi in enumerate(b))
return ''.join(
alphabet[(bits >> (18 - 6 * j)) & 0b111111] for j in range(len(b) + 1)
)
def decode_chunk(s):
bits = sum(indices[sj] << (18 - 6 * j) for j, sj in enumerate(s))
return bytes(
(bits >> (16 - 8 * i)) & 0b11111111 for i in range(len(s) - 1)
)
def to_base_64(b):
return ''.join(map(
encode_chunk,
(b[i:i + 3] for i in range(0, len(b), 3))
))
def from_base_64(s):
s = ''.join(s)
return b''.join(map(
decode_chunk,
(s[i:i + 4] for i in range(0, len(s), 4))
))