Кодирование Base64 в Python: полное руководство
Вот и обратная сторона медали. У вас на руках данные - файл, пара «пользователь и пароль», бинарная глыба, абзац Unicode - и где-то ниже по потоку им предстоит пройти через канал, который принимает только буквы. В этом и есть вся работа Base64: переписать три байта данных четырьмя символами из 64-символьного алфавита, добрать последнюю группу =, чтобы всё выходило четвёрками, и отдать буквы. Главная страница этого сайта полностью объясняет формат, алфавит включительно, так что ограничимся одним вздохом и потратим остальное время на то, что Python делает с ним на самом деле.
Экономике положено одно честное предложение перед стартом, потому что это число всплывает в каждом разговоре о теме: плата за эту текстовую безопасность - размер. Base64 расширяет ваши данные примерно на треть: четыре символа на каждые три входных байта, так что мегабайт бинарных данных становится мегабайтом и третью букв. Для токена или значения конфигурации это не проблема; для видеофайла - повод задуматься о вариантах.
И хорошая новость: ответ Python на всё это - один импорт и одна функция. base64.b64encode сидит в стандартной библиотеке уже несколько десятилетий, не требует установки и под капотом работает на скорости C. Остальная часть этого руководства - длинный хвост, который делает однострочник полезным в реальном мире: правило «только байты», которое останавливает половину всех багов TypeError, URL-безопасный алфавит, инструменты MIME-переноса строк и протоколы - JWT, HTTP-заголовки, рукопожатия WebSocket, электронная почта, PEM, data URL, - внутри которых Base64 тихо делает свою работу.
Знакомьтесь: b64encode
Контракт умещается в четыре предложения. Первое: вход - байтоподобный объект: bytes, bytearray, memoryview, - а обычная строка отклоняется. Второе: выход - объект bytes, никогда не str. Третье: выход всегда дополняется заполнением до кратного четырём числа символов, так что даже один входной байт даёт Zg==. Четвёртое: выход - одна единственная строка, никогда не переносится, каким бы большим ни был вход. Всё остальное в этой статье - комментарии к этим четырём предложениям:
import base64
encoded = base64.b64encode(b"foobar")
print(encoded)
# b'Zm9vYmFy'
print(len(encoded))
# 8
Чтобы получить настоящую строку - для URL, заголовка или поля JSON, - декодируйте результат как ASCII. Алфавит гарантирует, что ничего другого в нём не может быть, что делает этот шаг безопасным и дешёвым:
import base64
text = base64.b64encode(b"foobar").decode("ascii")
print(text)
# Zm9vYmFy
В сигнатуре есть ещё один аргумент, altchars, и он подменяет + и / стандартного алфавита на другую пару символов. Это ровно та ручка, которая стоит за URL-безопасным вариантом, так что держите эту мысль в уме - вы встретите её через несколько секций, когда будем говорить о токенах и строках запросов.
Стена типов: str - это не bytes
Первая специфически Python-овская стена в этой статье - система типов, и стоит научиться её чувствовать. b64encode отклоняет строки одним из наименее снисходительных сообщений об ошибках в языке:
import base64
try:
base64.b64encode("hello")
except TypeError as caught:
print(caught)
# a bytes-like object is required, not 'str'
Исправление - самая важная привычка во всём этом руководстве: сначала превращайте текст в байты, и выбирайте кодировку осознанно, а не на надежде:
import base64
print(base64.b64encode("été".encode("utf-8")))
# b'w6l0w6k='
print(base64.b64encode("été".encode("utf-16")))
# b'//7pAHQA6QA='
Одни и те же символы, два разных байтовых массива, два разных Base64-выхода. Выбор кодировки - это решение, а не деталь. UTF-8 - кодировка по умолчанию для всего, что пересечёт провод, базу данных или API. UTF-16 появляется, когда вы говорите с Windows-API, и приносит с собой метку порядка байтов в начале, которую вы, возможно, не захотите кодировать: от неё можно избавиться, используя utf-16-le или обрезав её через lstrip("\ufeff"). Latin-1 до сих пор прячется в старых европейских файлах, где один символ - ровно один байт, и сам вопрос даже не возникает. Ментальная модель, которую стоит держать: кодировщик никогда не смотрит на ваш текст; он видит только биты. В момент, когда байты пересекают стену, вопрос кодировки закрыт - и именно поэтому декодирующая сторона потом должна спросить, кто владел той кодировкой.
base64url: поменяй две буквы, сбрось заполнение
В стандартном алфавите прячутся два символа, которых URL и файловые системы терпеть не могут. Знак + любой декодер форм молча читает как пробел, а знак / - разделитель путей, так что данные стандартного алфавита в строке запроса или имени файла - бомба замедленного действия. Раздел 5 RFC 4648 определяет исправление: вариант, где + становится -, а / - _, где заполнение отбрасывается всякий раз, когда длина данных известна из контекста, и который RFC требует называть base64url, а не просто «base64». Вы встретите его в JSON Web Tokens, OAuth-токенах и параметрах курсоров API, другими словами, в большей части современного веба.
Python поставляет и специальную функцию, и ручку altchars из первой секции, и они дают идентичный результат:
import base64
data = b"\xfb\xff\xfe"
print(base64.b64encode(data))
# b'+//+'
print(base64.urlsafe_b64encode(data))
# b'-__-'
print(base64.b64encode(data, altchars=b"-_"))
# b'-__-'
В токенах и строках запросов заполнение обычно уходит тоже, потому что завершающий = потребовал бы процентного кодирования, а кое-какие промежуточные узлы изувечили бы его всё равно:
import base64
padded = base64.urlsafe_b64encode(b"fooba")
print(padded)
# b'Zm9vYmE='
print(padded.rstrip(b"="))
# b'Zm9vYmE'
Срежьте, отправьте, и получатель достроит заполнение обратно трюком с модулем, "=" * (-len(s) % 4), который порождает ровно столько знаков заполнения, сколько требует длина. Правило большого пальца: если данные будут жить в URL, имени файла или JWT, используйте urlsafe-вариант и сбросьте заполнение; если они будут жить в теле письма или текстовом файле, стандартный алфавит с его заполнением и есть норма.
Когда читателю нужны строки: MIME и правило 76 символов
Одна бесконечная строка от b64encode идеальна для полей JSON, заголовков и URL, но у почты есть своё мнение. RFC 2045, стандарт MIME, требует, чтобы вывод Base64 был разбит на строки длиной не более 76 символов, и устаревшие инструменты Python строились ровно под это. encodebytes, добавленный в Python 3.1, делает перенос для байтового объекта:
import base64
wrapped = base64.encodebytes(b"x" * 100)
for line in wrapped.splitlines():
print(len(line), line[:12])
# 76 eHh4eHh4eHh4
# 60 eHh4eHh4eHh4
Механика немного забавная. Модуль кодирует кусками по 57 байтов - константа MAXBINSIZE, - потому что 57 байтов дают ровно 76 символов, а в современном CPython каждая перенесённая строка заканчивается простым переводом строки. RFC 2045 просил CRLF, но LF-вывод Python принимается каждым декодером в экосистеме, включая самого Python. Устаревшая функция «файл в файл» encode делает тот же перенос прямо из одного файлового дескриптора в другой, что делает её аккуратным инструментом для больших файлов, которые вы не хотите держать в памяти дважды.
Коротко, какой инструмент когда: b64encode для всего, что попадает в поле JSON, URL, заголовок или колонку базы данных; encodebytes для тел писем и PEM-подобной брони; устаревший encode, когда вы перекачиваете большой файл и хотите получить перенос даром. Выбрать не тот - классический баг, потому что одного лишнего перевода строки внутри поля JSON достаточно, чтобы строгий декодер на другом конце кинул исключение.
Расширенная семья
Модуль base64 на самом деле - модуль base-N: в нём живёт всё семейство RFC 4648 плюс пара родственников из других углов мира вычислений. Большинство из них - однострочные замены под тот же контракт «байты на входе, байты на выходе»:
| Функции | Алфавит | Когда вы с ним встречаетесь |
|---|---|---|
b16encode / b16decode |
0-9A-F |
«Base16» - это просто шестнадцатеричный код; самый быстрый круговой путь в модуле, хорош для хэшей и UUID |
b32encode / b32decode |
A-Z2-7 |
ключи лицензий и коды активации; без 0, O, 1 и I, так что переживает пересказ вслух |
b32hexencode / b32hexdecode |
0-9A-V |
Base32 с шестнадцатеричным алфавитом, добавлен в Python 3.10; держит закодированные данные сортируемыми в лексикографическом порядке |
a85encode / a85decode |
85 печатаемых символов | ASCII85 из PostScript и PDF, потомок утилиты Unix btoa; в модуле с Python 3.4 |
b85encode / b85decode |
85 печатаемых символов | формат Base85, который используют бинарные диффы git и Mercurial; тоже с Python 3.4 |
z85encode / z85decode |
85 печатаемых символов | Z85 от ZeroMQ, добавлен в Python 3.13; группирует данные в пакеты по четыре байта |
Ни один из них не меняет правил, которые вы уже выучили: байты на входе, байты на выходе, алфавит на выбор и соответствующая функция декодирования, ждущая на другом конце. На практике вы потянетесь к b16, когда значение должно быть читаемым человеком, к b32, когда значение будут вводить или произносить вручную, а к 85-символьным родственникам - только когда велит спецификация. Для всего остального пара Base64 из начала этой статьи - правильный инструмент, и на ней строится всё остальное.
Картинки в странице: data URL
Самый заметный Base64 в вебе - это data: URI: медиа, встроенные непосредственно в HTML или CSS, чтобы браузер не выстрелил вторым запросом. Формат: data:, тип медиа, слово base64, запятая и закодированные байты. Собрать его из файла на диске - три строки:
import base64
with open("logo.png", "rb") as handle:
encoded = base64.b64encode(handle.read()).decode("ascii")
uri = "data:image/png;base64," + encoded
print(uri[:40])
# data:image/png;base64,iVBORw0KGgoAAAAN...
Две предосторожности, обе дешёвые в соблюдении. Первая: браузер с удовольствием отрендерит data URI и с удовольствием держит мегабайты их в документе: для всего, что больше нескольких килобайт, обычный запрос картинки с правильным заголовком кэша выигрывает по каждой метрике, которая имеет значение. Вторая: тип медиа после двоеточия - обещание. Если байты - это JPEG, URI говорит image/jpeg, потому что кое-какие инструменты проверяют эту пару, а кое-какие рендереры просто отказываются гадать. Шаг .decode("ascii") тоже не украшение: без него вы склеиваете объект bytes со строкой и собираете TypeError, - стена типов делает очередной обход.
Токены, которые можно раздавать: JWT
JSON Web Token - это три base64url-куска, склеенных точками: заголовок, данные и подпись. Если вы выпускаете настоящие токены, не собирайте куски вручную. Поставьте PyJWT (pip install pyjwt) и позвольте ему собрать base64url-части, заполнение и подпись одним вызовом:
import jwt
# Ключ короче 32 байт заслуживает InsecureKeyLengthWarning от PyJWT, справедливое ворчание для демо-ключа.
token = jwt.encode(
{"sub": "1234567890", "name": "John Doe"},
"super-secret-key",
algorithm="HS256"
)
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...
print(type(token))
# <class 'str'>
Под капотом PyJWT делает ровно то, что описано в этой статье: сериализует в JSON, прогоняет через urlsafe-кодировщик и срезает заполнение, согласно определению JWS в RFC 7515. Если когда-нибудь понадобится собрать кусок вручную, для тестового фикстура или сессии отладки, рецепт - та же арифметика повсюду:
import base64
import json
payload = json.dumps({"sub": "1234567890"}).encode("ascii")
part = base64.urlsafe_b64encode(payload).rstrip(b"=")
print(part)
# eyJzdWIiOiAiMTIzNDU2Nzg5MCJ9
Одно замечание о направлении доверия: собирать токен - лёгкая половина. Получатель должен проверить подпись, прежде чем доверять хотя бы одному утверждению, и PyJWT 2.x не декодирует токен без явного списка algorithms, и это к лучшему, потому что ошибка «любой алгоритм» - одна из самых дорогих строк кода аутентификации, когда-либо написанных.
HTTP: Basic auth и рукопожатие WebSocket
Два HTTP-момента живут или гибнут благодаря Base64. Первый - самая старая схема аутентификации в протоколе: Basic auth (RFC 7617), где клиент отправляет user:pass, закодированный в base64, под словом Basic:
import base64
credentials = base64.b64encode(b"jane:pa:ss").decode("ascii")
header = "Basic " + credentials
print(header)
# Basic amFuZTpwYTpzcw==
Если requests уже есть в вашем стеке, он соберёт этот заголовок за вас через auth=("jane", "pa:ss"), и этим стоит пользоваться, потому что это держит детали кодирования вне вашего кода. И, пока вы в этом, будьте честны в отношении того, что происходит: RFC 7617 говорит прямо, что схема «не является безопасным методом аутентификации пользователя и ни каким образом не защищает сущность, которая передаётся в открытом виде». Учётные данные восстанавливаются одной строкой кода любым, кто видит трафик, так что это - удобство для соединений под защитой TLS, а не граница безопасности.
Второй момент - рукопожатие WebSocket (RFC 6455), где сервер доказывает, что прочитал случайный ключ клиента, отвечая Base64 от SHA-1-хэша ключа, склеенного с магическим GUID:
import base64
import hashlib
key = "dGhlIHNhbXBsZSBub25jZQ=="
magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
accept = base64.b64encode(
hashlib.sha1((key + magic).encode("ascii")).digest()
).decode("ascii")
print(accept)
# s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Вывод - точное значение из разобранного примера в самом RFC, что прекрасный способ проверить реализацию с нуля. В производстве библиотека websockets делает этот шаг за вас на обоих концах; вы делаете его вручную только тогда, когда пишете крошечный тестовый сервер, доказывающий ваше понимание.
Электронная почта: самый первый заказчик
Base64 был стандартизирован в 1993 году для одной работы, и этой работой была электронная почта: дать бинарным данным выжить в чисто текстовом мире SMTP, согласно Content-Transfer-Encoding: base64 из RFC 2045. Пакет email в Python собирает сообщение, выбирает кодирование и переносит тело по стандартной длине строки, и вам не приходится писать ни одной строки Base64:
import email.mime.multipart
import email.mime.application
msg = email.mime.multipart.MIMEMultipart()
msg["Subject"] = "binary payload"
part = email.mime.application.MIMEApplication(
b"\x00\x01\x02", _subtype="octet-stream"
)
msg.attach(part)
text = msg.as_string()
print(text)
# ...
# Content-Transfer-Encoding: base64
#
# AAEC
# --...
Часть MIMEApplication - интересная: она переносит байты в правильные строки по 76 символов и ставит заголовок transfer-encoding, что ровно поведение encodebytes, которое мы видели выше, применённое фреймворком. Если вы собираете голый фрагмент, а не целое сообщение, email.encoders.encode_base64(obj) делает одиночное кодирование-с-переносом прямо на объекте сообщения. А когда сообщение прибывает на другом конце, - декодирующая сторона истории, от get_payload(decode=True) до декодирования заголовков, - разобрана в связанной статье про декодирование.
PEM-броня для ключей и сертификатов
PEM-файлы - сертификаты, закрытые ключи и CRL, которые открываются строкой -----BEGIN ...-----, - это ничего, кроме брони вокруг Base64: строка с меткой, перенесённый Base64, закрывающая метка. Броню легко разглядеть насквозь, потому что тело - это просто тот самый перенесённый вывод, с которым вы уже встретились:
import base64
der = b"\x30\x03\x02\x01\x05" # крошечный DER-кусочек для примера
body = base64.encodebytes(der).decode("ascii")
armor = ("-----BEGIN CERTIFICATE-----\n"
+ body
+ "-----END CERTIFICATE-----")
print(armor)
# -----BEGIN CERTIFICATE-----
# MAMCAQU=
# -----END CERTIFICATE-----
Срежьте две строки с метками, склейте остальное, и b64decode отдаст вам байты DER обратно. В производстве вы почти никогда не делаете это вручную: пакет cryptography (pip install cryptography) генерирует броню через public_bytes и разбирает её через load_pem_x509_certificate и компанию, делая шаг Base64 за вас под капотом. Ручной путь оправдывает себя в конкретный момент, когда сырые байты DER уже у вас в руках - колонка в базе данных, файл конфигурации, буфер из протокола, - а спецификация перед вами говорит: «PEM, пожалуйста».
Отправляем файлы: загрузка, скачивание и привычка к .b64
Самый старый сценарий в интернете - бинарный файл, которому приходится пересекать канал, переносящий только текст: FTP, который кривит окончания строк, форма, которая отказывается принимать загрузку, чат-окно, которое съедает бинарные данные. Рецепт: прочитать, закодировать, отправить, и пусть другой конец декодирует, а интересная половина - два средних шага:
import base64
with open("photo.png", "rb") as src:
data = src.read()
wrapped = base64.encodebytes(data)
with open("photo.b64", "wb") as dst:
dst.write(wrapped)
print(len(wrapped), "bytes on disk for", len(data), "in the photo")
# примерно на треть больше исходного
Две заметки. Расширение .b64 - общинное соглашение, а не стандарт, так что принимающая сторона должна знать это соглашение тоже, - поэтому JSON-API обычно оборачивают данные в именованное поле вроде "image_base64" и пишут об этом в документации. А перенесённые строки по 76 символов от encodebytes - формат выбора для файла на диске, потому что они безупречно копируются и вставляются через любой текстовый инструмент, которым владеют люди, - от почтовых клиентов до PDF-читалок. Обратное направление, чтение такого файла обратно в байты, - один вызов в связанной статье про декодирование; здесь вы только отправитель, и работа отправителя - быть последовательным.
Вопрос хранения: файлы конфигурации, переменные окружения и базы данных
Разработчики любят прятать Base64 в местах, которые принимают только текст: файл .env, параметр в .ini, колонка TEXT. Шаг кодирования тривиален, и самая распространённая форма - JSON внутри Base64:
import base64
import json
config = {"api_user": "svc-bot", "api_pass": "hunter2-not-really"}
packed = base64.b64encode(
json.dumps(config).encode("utf-8")
).decode("ascii")
print(packed)
# eyJhcGlfdXNlciI6ICJzdmMtYm90IiwgImFwaV9wYXNzIjogImh1bnRlcjItbm90LXJlYWxseSJ9
А теперь предупреждение, потому что здесь живёт самая дорогая путаница во всей этой статье. Base64 - это не обфускация, которая держится, и это не шифрование. Раздел 12 RFC 4648 говорит так же просто, как только может говорить RFC: базовое кодирование «визуально скрывает иначе легко узнаваемую информацию, например пароли, но не предоставляет никакой вычислительной конфиденциальности». Файл .env с Base64-секретами защищает вас от человека, который бросит на него взгляд, а не от человека, который его прочитает, и один запуск base64 -d позже «секрет» лежит в его терминале простым текстом. Если данные по-настоящему чувствительные, сначала зашифруйте их - пакет cryptography поставляется с Fernet ровно для этого, - и только потом кодируйте шифртекст в Base64, если ваше хранилище требует текста.
Миллион байтов спустя: большие данные и фрагментация
b64encode - функция на скорости C: на обычном ноутбуке она обрабатывает мегабайт примерно за миллисекунду, - но это не потоковая функция. В стандартной библиотеке нет ни одной пары «обновление-и-завершение», так что кодировать данные больше, чем вы хотите держать в памяти, значит делать арифметику границ самому. Три входных байта дают четыре выходных символа, так что любая граница фрагмента должна падать на шов по три байта:
import base64
def encode_chunks(chunks):
out = []
leftover = b""
for chunk in chunks:
buffer = leftover + chunk
whole = len(buffer) // 3 * 3
if whole:
out.append(base64.b64encode(buffer[:whole]))
leftover = buffer[whole:]
if leftover:
out.append(base64.b64encode(leftover))
return b"".join(out)
with open("video.mp4", "rb") as handle:
encoded = encode_chunks(iter(lambda: handle.read(65536), b""))
Вывод побайтово идентичен кодированию целого файла одним вызовом, потому что шов по три байта - единственное место, где группировка может быть нарушена. Заполнение появляется ровно один раз, на последнем фрагменте, и именно этого ожидает строгий декодер на другом конце. Строка iter(lambda: handle.read(65536), b"") - стандартная идиома чтения файла кусками фиксированного размера, а переменная leftover - весь алгоритм. Декодирующая сторона держит шов по четыре символа вместо трёх байтов, так что две статьи делят арифметику между собой, а не повторяют её.
Где кодировщики ошибаются
На стороне кодирования ловушек меньше, чем на стороне декодирования, потому что когда буквы производите вы, сбиться не из чего. Тем не менее, эти баги всплывают каждую неделю, и у каждого есть двухминутное исправление, если распознать его вовремя:
- Подача строки кодировщику.
TypeErrorиз секции про стену типов. Чините у источника через.encode("utf-8"), и подумайте, какую именно кодировку вы имеете в виду, прежде чем набрать её. - Забыли, что выход - байты.
b64encodeвозвращает байты;str- это то, что попадает в URL или поле JSON, так что шаг.decode("ascii")- часть рецепта, а не мысль, пришедшая потом. - Ручная замена для URL-безопасности.
str.replace("+", "-").replace("/", "_")работает, но это две буквы долгового обслуживания там, гдеurlsafe_b64encode- один вызов. Хуже того, недоделанная замена - плюсы исправлены, слэши забыты, - даёт алфавит, который не соответствует никакой спецификации. - Заполнение оставлено в URL. Завершающий
=в строке запроса процентно кодируется одним инструментом и срезается другим, и арифметика заполнения у получателя ломается самым запутанным образом. Срезайте: длина говорит декодеру всё, что нужно. - Перенос там, где он не нужен. Переносы строк от
encodebytes- правильные для почты и PEM, и отравленные для поля JSON или URL. Одного лишнего перевода строки достаточно, чтобы строгий декодер на другом конце кинул исключение про ваши данные, а не про ваше форматирование. - Двойное кодирование. Данные выше по потоку уже были Base64 - поле, пришедшее предзакодированным из другого API, файл, получивший обработку
.b64дважды, - и второй проход даёт строку, которая декодируется обратно в первое кодирование. Сделайте круг один раз, проверьте магические байты - и остановитесь. - Доверие к Base64 с секретами. Предупреждение из секции про хранилище, повторённое, потому что оно стоит реальных денег: если в модели угроз есть любой, кто читает файл, вам нужен шифр, а не алфавит.
Чейнджлог, который реально можно прочесть
Возраст модуля виден в тихих, датированных улучшениях, а не в революциях. Короткая версия, в том порядке, в котором куски приземлились, с точки зрения кодировщика:
| Версия | Что произошло |
|---|---|
| Python 2.4 (2004) | поставлена полная поддержка RFC 3548 от Барри Варшав: семейства b16, b32 и b64, плюс варианты standard_* и urlsafe_*, которыми пользуются сегодня |
| Python 3.1 (2009) | encodebytes прибывает, а encodestring объявлен устаревшим, - переименование, о котором до сих пор спотыкаются старые туториалы |
| Python 3.4 (2014) | каждый кодировщик принимает любой байтоподобный объект, а a85encode и b85encode вступают в модуль |
| Python 3.6 (2016) | binascii.b2a_base64 получает переключатель newline, и именно он позволяет b64encode оставаться одной бесконечной строкой |
| Python 3.9 (2020) | устаревшие имена encodestring и decodestring наконец удалены |
| Python 3.10 (2021) | b32hexencode и b32hexdecode, сортируемые шестнадцатеричные родственники |
| Python 3.13 (2024) | z85encode и z85decode, алфавит ZeroMQ, вступают в семью |
| Python 3.14 (2025) | быстрее импорт по всей стандартной библиотеке, включая base64, и b16decode, ставший до шести раз быстрее, потому что его проверка теперь работает на bytes.translate вместо регулярного выражения |
Сквозная нить, если она вам нужна: модуль был переписан в 1995 году, чтобы делегировать свою работу модулю C-уровня binascii, и эта делегация по-прежнему настоящая и сегодня. Первое изменение байтовой эпохи, коммит 2007 года в ходе разработки Python 3, который заставил всё использовать байты везде, - вот откуда взялась стена типов в этой статье, и именно поэтому современный кодировщик принимает байты и отдаёт байты, а всё остальное - обёртки вокруг этого единственного контракта.
О чём модуль вам не расскажет
Серьёзная работа сделана, так что вот мелкие удовольствия на стороне кодирования:
- Собственный пример документации уже более десяти лет гоняет одно и то же демо: на входе
b'data to be encoded', на выходеb'ZGF0YSB0byBiZSBlbmNvZGVk'. Вы уже встречали эту пару, хотите вы того или нет. - C-функция под
b64encodeдобавляет свой завершающий перевод строки с комментарием «добавить перевод строки в порядке вежливости». Целая культура - в одной строке исходников. - Слово
passwordкодируется вcGFzc3dvcmQ=, поэтому Base64 в логе выглядит как секрет для сканера и находится в одной команде от того, чтобы им стать для читателя. b64encodeникогда не переносит. Никогда. Гигабайт входа даёт одну строку в 1,3 гигабайта, и функция даже не моргнёт. Если вам были нужны строки, вам нужно было попроситьencodebytes.- Docstring модуля до сих пор называет RFC 3548, издание спецификации 2003 года. RFC 4648 перенял эстафету в 2006 году; docstring просто этого не заметил.
- В Python 2 не было вообще никакой стены типов:
b64encodeс удовольствием принималstrи возвращал его. Байтовая реформа 2007 года это прекратила, и именно на старые туториалы по Python 2 до сих пор смотрит большинство тредов «почему мой encode падает». z85encode, самый новый член семьи (Python 3.13), - самый придирчивый из всех: ZeroMQ группирует данные в пакеты по четыре байта, поэтому спецификация требует, чтобы закодированный вывод был кратен пяти символам, - и в документации бремя заполнения перекладывается на вас: вход должен приходить кратным четырём байтам (кодировщик не заполнит его за вас; 3-байтовый вход даёт 4-символьный пакет, который ни один ZeroMQ-пир не примет).
Итак, философия кодировщика в трёх правилах. Сначала решите, что за байты, потом - как их кодировать, потому что стена типов - это место, где рождается большинство багов Base64 в Python. Выбирайте алфавит под канал, а не под данные: стандартный с заполнением для почты и файлов, base64url без заполнения для URL и токенов, и никогда не импровизируйте третий вариант на клавиатуре. И держите вывод в той форме, которую ожидает его читатель: одна строка для JSON и заголовков, строки по 76 символов для MIME и PEM, потому что декодер на другом конце предъявит вам счёт.
Когда эти буквы прибывают на другом конце, веселье по-настоящему начинается: пропавшее заполнение, молчаливые выбросы, данные, которые не совсем Base64, и декодер с двумя настроениями, между которыми нужно маневрировать. Всё это подробно разобрано в связанной статье про декодирование Base64 в конце этой страницы, и два руководства хорошо читаются парой. Удачного кодирования.
Последнее обновление: 2026-09-08
Связанная статья: Декодирование Base64 в Python: полное руководство