Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Декодирование Base64 в Python: полное руководство

Где-то в вашем коде только что приземлилась строка букв, которая совсем не похожа на текст: длинный ряд букв от A до Z, пара цифр, изредка + или /, может быть - или _, и, пожалуй, один-два знака =, припаркованных в самом конце. За этой строкой могут прятаться данные от JWT, который отверг ваш шлюз, картинка, спрятанная внутри страницы HTML, файл, который кто-то прислал вам вложением .b64, или блок сертификата в тикете, побывавшем уже на трёх службах поддержки. Ваша задача - вернуть исходные байты ровно в том виде, в каком они были. И Python в прекрасном настроении для этой работы, потому что весь набор инструментов уже несколько десятилетий поставляется в стандартной библиотеке: одна строка import base64 - и вы готовы на любой платформе, не устанавливая и не настраивая ничего.

Пока вы устраиваетесь поудобнее, небольшой повтор, потому что всем это нужно раз в год: Base64 переписывает каждые три байта данных четырьмя символами, взятыми из алфавита в 64 символа, а когда последняя группа из трёх байтов неполная, заполнение = добирает её до конца, чтобы вывод всегда складывался в четвёрки. Вот и весь трюк. Это не сжатие и не секретность, а лишь способ дать бинарным данным выжить в каналах, которые принимают только текст. Главная страница этого сайта разбирает формат во всех деталях, включая алфавит и арифметику заполнения, поэтому потратим силы там, где боль на самом деле: на Python-стороне декодирования и на том, чтобы результат оставался честным.

Три факта определяют всё, что дальше, и их стоит запомнить, прежде чем читать хотя бы ещё одну строку. Во-первых, у декодера два настроения: вежливое, прощающее поведение по умолчанию, которое молча выбрасывает всё, чего не распознаёт, и строгий режим, который такой ввод отклоняет сразу и безапелляционно. Во-вторых, результат декодирования всегда объект bytes, никогда не строка, и момент, когда из него хочется получить настоящий текст, - это решение, которое нужно принять осознанно. В-третьих, существуют два почти одинаковых на вид алфавита: стандартный и безопасный для URL, и перепутать их - любимый способ потерять данные вообще без единой ошибки. Это руководство проведёт вас мимо всех трёх подвохов, так что в следующий раз, когда в терминале окажется стена каракулей, вы сможете улыбаться, а не щуриться.

Полное меню декодирования

Откройте модуль base64, и вы увидите два поколения интерфейсов, сидящих бок о бок. Современный, в центре которого b64decode, превращает байтоподобные объекты (и обычные ASCII-строки) обратно в байты, и говорит на обоих диалектах Base64, определённых в RFC 4648. Устаревший старше и нацелен на файлы: он работает с файловыми объектами, знает только стандартный алфавит и был построен вокруг перенесённых строк длиной 76 символов, которые RFC 2045 - почтовый стандарт MIME 1996 года - требовал от закодированного вывода. Устаревшие имена вы встретите в изрядном количестве кода, который ходит по свету уже давно, так что вот декодирующая сторона меню целиком:

Функция Что делает Примечания
base64.b64decode(s, altchars=None, validate=False) рабочая лошадка: Base64-глыба обратно в сырые байты принимает байты или ASCII-строку, всегда возвращает байты
base64.standard_b64decode(s) та же работа, но запертая на стандартном алфавите удобен, когда вы точно знаете диалект
base64.urlsafe_b64decode(s) читает URL-безопасный алфавит с - и _ именно она читает JWT
base64.decodebytes(s) декодирует одну или несколько перенесённых строк Base64 добавлен в Python 3.1, MIME-дружелюбный путь, снисходительный
base64.decode(input, output) переправляет Base64-файл в сырой файл потоком устаревшая, читает построчно, снисходительная
base64.b32decode(s, casefold=False) декодирует меньшего Base32-родственника casefold принимает вход в нижнем регистре
base64.b16decode(s, casefold=False) декодирует Base16, то есть обычный шестнадцатеричный код до шести раз быстрее в Python 3.14
binascii.a2b_base64(s, strict_mode=False) функция C-уровня, которая делает настоящую работу прямой доступ к строгости, с strict_mode начиная с Python 3.11

Всё, что ниже, построено на первой строке. Один факт стоит знать, прежде чем идти глубже: в официальной документации модуль живёт в разделе «Интернетная обработка данных», прямо рядом с binascii, и это размещение не случайность. b64decode - тонкая обёртка: она переводит алфавит (когда вы передаёте altchars), а тяжёлую работу отдаёт на откуп функции C-уровня binascii.a2b_base64. Именно поэтому функция быстрая, и именно поэтому её сообщения об ошибках чёткие, без сентиментозности, с тем самым привкусом C.

Рабочая лошадка: b64decode

Вот весь контракт, он достаточно короткий, чтобы держать его в голове. Функция принимает байтоподобный объект или ASCII-строку, необязательную замену пары символов в алфавите и флаг проверки. Она отдаёт объект bytes. При неудаче она возбуждает binascii.Error, который является подклассом ValueError, на случай, если вам когда-нибудь понадобится поймать целое семейство исключений разом:

import base64
data = base64.b64decode("Zm9vYmFy")
print(data)
# b'foobar'
print(type(data))
# <class 'bytes'>

Последняя из этих строк - единственно важная во всей статье. Результат - байты, а не строка, и Python держит вас за руку ровно настолько, насколько надо: печать объекта показывает вам представление b'...', а попытка приклеить его к строке рождает TypeError. В момент, когда вам нужен настоящий текст, решение принимаете вы, а секция про кодировки ниже рассказывает, когда это решение лёгкое, а когда - ловушка.

Необязательный аргумент altchars подменяет + и / стандартного алфавита на другую пару символов. Это ровно та ручка, которая даёт URL-безопасный диалект, и именно так urlsafe_b64decode построена поверх b64decode. Сами вы к altchars почти никогда не потянетесь, но приятно знать, что механизм на месте. Во всех остальных случаях функция просто делает работу, быстро, в C.

По умолчанию снисходительный, по требованию строгий

По умолчанию b64decode - вежливое существо, умеющее забывать. Любой символ, которого нет в 64-символьном алфавите (и нет в вашем altchars), молча выбрасывается до начала декодирования, а декодируется то, что выживет. Без предупреждений, без уведомлений, без возвращаемого значения для проверки, - просто результат. У этой снисходительности есть благородный предок: раздел 6.8 RFC 2045 говорит декодерам, что «все переводы строк и любые другие символы, не найденные в Таблице 1, должны игнорироваться», потому что SMTP исторически переносил длинные строки и сыпал по пути случайными символами. Данные, прошедшие через почтовый клиент, чат-приложение или копию из PDF, часто декодируются вообще без какой-либо подготовки, и это настоящая суперсила.

Это же добродушие - ещё и причина, по которой декодер по умолчанию бесполезен как валидатор. Раздел 12 RFC 4648 прямо описывает риск: игнорирование символов вне алфавита вместо отклонения всего кодирования открывает скрытый канал, которым можно воспользоваться, чтобы утекать информацию, и оно способно сломать проверки равенства строк, потому что два разных входа могут декодироваться в одни и те же байты. Для всего, что вы не кодировали сами, передавайте validate=True и относитесь к исключению как к ответу. Вот отчёт об ущербе: каждая строка воспроизводится на любом современном Python:

Что подаётся на вход Снисходительный (по умолчанию) validate=True
Zm9vYmFy (чистые данные) b'foobar' b'foobar'
Zm9v\r\nYmFy (перевод строки посередине) b'foobar' binascii.Error
Zm9v YmFy (лишние пробелы) b'foobar' binascii.Error
Zm9v!YmFy (случайный восклицательный знак) b'foobar' binascii.Error
junkZm9vYmFy (слово перед данными) b'\x8e\xe9\xe4foobar' b'\x8e\xe9\xe4foobar'
Zm9v=YmFy (знак заполнения посередине) b'foobar' binascii.Error
=Zm9v (заполнение в самом начале) b'foo' binascii.Error
==== (четыре знака заполнения, без данных) b'' binascii.Error
(пустой вход) b'' b''

Посмотрите, как снисходительная колонка молча делает своё дело. Строка, которая удивляет людей первой, - та, где слово стоит впереди: все четыре буквы слова junk как раз находятся в алфавите Base64, так что этот «мусор» декодируется в три настоящих байта и приклеивается к вашим данным с совершенно спокойным видом. Строгий режим в этой строке не спаситель, потому что вход действительно является валидным Base64; прямо и безапелляционно отклоняются остальные строки, и у этих отказов ровно одна форма: binascii.Error с одним из нескольких запоминающихся сообщений:

  • Incorrect padding - длина после отбрасывания не кратна четырём, либо последняя группа слишком короткая. Строка вроде Zm9vYmE без единого знака заполнения попадает сюда.
  • Invalid base64-encoded string: number of data characters (N) cannot be 1 more than a multiple of 4 - в данных на один символ меньше, чем нужно для следующей группы. Это классический отпечаток усечённых или скопированных вставкой данных.
  • Only base64 data is allowed - символ вне алфавита выжил и дожил до строгого режима, и даже одного перевода строки для этого достаточно.
  • Excess padding not allowed - знаки заполнения посередине строки, либо их больше, чем допускает последняя группа.
  • Leading padding not allowed - строка начинается с =.
  • И одно из другого семейства: ValueError: string argument should contain only ASCII characters - его вы получите, если передадите строку с не-ASCII буквами. Строки принимаются, но только ASCII.

За кулисами validate=True вообще не является отдельным путём кода. Модуль передаёт этот флаг в binascii.a2b_base64 в виде её параметра strict_mode - строгой проверки, добавленной в binascii в Python 3.11. Это даёт вам прямой рычаг, когда нужна строгость без прохождения через base64-слой:

import binascii
line = b"Zm9vYmFy"
print(binascii.a2b_base64(line, strict_mode=True))
# b'foobar'

Одна причуда, которую стоит уяснить, прежде чем безоглядо доверять строгому режиму: он отклоняет даже один единственный перевод строки в конце, так что MIME-обёрнутый блок - это работа для снисходительного пути или для decodebytes, а не для validate=True. Оставляйте строгий путь для данных, которые, как вы ожидаете, идеально чистые, - например, для только что отчеканенного токена прямо из вашего собственного кода.

base64url: алфавит, который помещается в URL

В стандартном алфавите есть два символа, которых URL терпеть не могут. Знак + любой декодер форм читает как пробел, а знак / зарезервирован для разделителей путей. Раздел 5 RFC 4648 определяет родственный диалект, где + становится -, а / - _, а заполнение отбрасывается всякий раз, когда длина данных известна из контекста. В RFC у варианта даже есть своё имя, base64url, и там настойчиво требуют, чтобы его не называли просто «base64». Чаще всего вы встретите его внутри JSON Web Tokens, где каждая часть токена - это base64url без заполнения, но он встречается и в OAuth-токенах, и в параметрах курсоров API.

Python поставляет для него специальную функцию, urlsafe_b64decode. Она переводит дефисы и подчёркивания обратно в плюсы и слэши, а потом декодирует, но заполнять заново за вас не будет. Вход без заполнения - обычный случай для JWT, поэтому арифметическая строка идёт первой, и она та же самая, что под капотом используют библиотеки вроде PyJWT:

import base64
segment = "Zm9vYmE"
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'

Выражение "=" * (-len(segment) % 4) выглядит как трюк, но это и есть вся работа: оно порождает ноль, один или два знака заполнения и никогда не три, так что уже заполненная строка проходит через него нетронутой. Именно отрицательный остаток по модулю делает это рабочим для строк любой длины, и это единственная строка Base64-арифметики, которую каждый Python-разработчик в итоге набирает хотя бы один раз.

Теперь опасная путаница, потому что два алфавита похожи достаточно, чтобы спутать. Пропустите base64url-строку через стандартный декодер, и дефисы с подчёркиваниями просто не входят в стандартный алфавит, так что снисходительный декодер проглотит их и декодирует то, что осталось. Для одних данных это изувеченный поток байтов; для других - вовсе ничего:

import base64
tricky = base64.urlsafe_b64encode(b"\xfb\xff\xfe")
print(tricky)
# b'-__-'
print(base64.standard_b64decode(tricky))
# b'' - каждый символ был молча выброшен
print(base64.urlsafe_b64decode(tricky))
# b'\xfb\xff\xfe'

Обратное направление снисходительно, и именно от этого путаница остаётся незамеченной: urlsafe_b64decode сначала переводит свой алфавит, а потом декодирует снисходительно, так что с удовольствием принимает строку стандартного алфавита с + и /. Урок не в том, чтобы импровизировать. Урок в том, чтобы выбрать одну функцию на диалект и держаться её - так же, как с иностранной валютой: тратьте иены там, где иены в ходу, а не в чужом обменном пункте.

На выходе байты: разговор о кодировках

Вот предложение, которое разрешает половину вопросов о кодировках, с которыми люди приходят к Base64: b64decode декодирует байты, он не декодирует текст. Аргумента про кодировку нет, конвертации нет, и ничего во входе не говорит Python, что эти байты должны означать. Смысл - то, что вы должны подставить из контекста, и этот контекст почти всегда что-то одно из трёх: заголовок, который это заявляет, контракт API, который это заявляет, или магическое число, прячущееся в самих байтах.

import base64
raw = base64.b64decode("w6l0w6k=")
print(raw)
# b'\xc3\xa9t\xc3\xa9'
print(raw.decode("utf-8"))
# été

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

import base64
raw = base64.b64decode("/w==")
try:
  raw.decode("utf-8")
except UnicodeDecodeError as caught:
  print(caught)
# 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte

Три практических правила не дают этой секции превратиться в хоррор-шоу. Первое: когда декодированные данные - это JSON, вам вообще не нужно декодировать вручную, потому что json.loads принимает байты напрямую ещё с Python 3.6 и сам распознаёт UTF-8, UTF-16 и UTF-32. Второе: бинарные данные - это не текст, поэтому «распознанная» кодировка для PNG - это удачное предположение, а не факт; проверяйте байты, а не метку. Третье: если отправитель сказал вам кодировку, верьте отправителю, потому что заголовок content-type или документ API перевешивают любой детектор, каждый раз без исключения.

Где декодированный Base64 встречается в Python-коде

Спустя некоторое время вы начинаете узнавать формы. Вот полевой гид по местам, где декодированный Base64 объявляется в Python-приложении, и однострочный рецепт для каждого. Следующие секции подробно разбирают самые распространённые из них:

Где вы его находите Что это Как читать
JWT части: заголовок, данные и подпись (RFC 7519) разрезать по точке, urlsafe_b64decode с поправкой на заполнение
Заголовок Authorization учётные данные HTTP Basic, user:pass (RFC 7617) отбросить префикс Basic, декодировать, разрезать на первом двоеточии
data: URI встроенные медиа в HTML или CSS (RFC 2397) отрезать на первой запятой, декодировать остальное
Вложение письма тело с Content-Transfer-Encoding: base64 (RFC 2045) get_payload(decode=True) для части сообщения
Значение заголовка письма кодируемое слово =?charset?b?...?= (RFC 2047) дайте пакету email декодировать его за вас
PEM-файл ключ или сертификат в PEM-броне (RFC 7468) отбросить строки брони, декодировать тело в DER
Поле JSON API бинарные данные, пронесённые в виде строки декодировать, а затем обращаться с результатом как с байтами, а не с текстом
Колонка TEXT или переменная окружения бинарные данные или JSON, сохранённые в чисто текстовом месте декодировать, а затем разобрать или записать, с той кодировкой, о которой вы договорились

Чтение JSON Web Token

JWT - это три base64url-куска, склеенных точками: заголовок, данные и подпись. Первые два - обычный JSON, так что заглянуть в каждый из них - одна строка, с поправкой на заполнение из секции выше:

import base64
import json
def read_part(segment):
  padded = segment + "=" * (-len(segment) % 4)
  return base64.urlsafe_b64decode(padded)
token = ("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
         "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
         "8Rmup2hf8jZvoBgoCRqRWlBFNtvUYmA0eR7YKellPMs")
head, body, _signature = token.split(".")
print(json.loads(read_part(head)))
# {'alg': 'HS256', 'typ': 'JWT'}
print(json.loads(read_part(body)))
# {'sub': '1234567890', 'name': 'John Doe'}

Замечание о границах, потому что это важно: инспекция токена таким способом - это инструмент отладки, а не механизм аутентификации. То, что данные читаются, не значит, что они подлинные; злоумышленник может подделать первые два сегмента, так и не узнав вашего секрета. Для настоящей проверки отдайте токен PyJWT (pip install pyjwt), который проверяет подпись и отказывается декодировать без явного списка алгоритмов:

import jwt
# Ключ короче 32 байт заслуживает InsecureKeyLengthWarning от PyJWT (PyJWT 2.11+), справедливое ворчание для демо-ключа.
decoded = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
print(decoded)
# {'sub': '1234567890', 'name': 'John Doe'}

С неверным ключом вы получите исключение вместо словаря, и это ровно то поведение, которое нужно в производственном коде. А если токен пришёл с просроченной меткой времени, PyJWT возмутится и по этому поводу, так что вам не нужно самим помнить имена утверждений (claims).

Открываем data URI

Data URI встраивает медиа непосредственно в HTML или CSS, чтобы браузер не отправил второй запрос: data:, тип медиа, слово base64, запятая и закодированные байты. Разрез - на первой запятой, точка, и всё, что после неё, - обычные данные стандартного алфавита:

import base64
uri = ("data:image/png;base64,"
       "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ"
       "AAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==")
mime, payload = uri.split(",", 1)
data = base64.b64decode(payload)
print(mime)
# data:image/png;base64
print(data[:8])
# b'\x89PNG\r\n\x1a\n'

Восьмбайтная PNG-подпись в начале результата - дешёвая и весёлая проверка того, что вы декодировали именно то, что нужно. Два подвоха заслуживают упоминания. Если URI пришёл со скрапнутой страницы или из сообщения в чате, сначала отбросьте HTML-сущности и лишние пробельные символы, потому что снисходительный декодер простит много мусора и отдаст вам повреждённую картинку вместо ошибки. А если вы декодируете недоверенный ввод партиями, передавайте validate=True: data URI, не прошедший строгую проверку, - это data URI, который никогда не был сформирован как надо, и писать его на диск наугад вы не хотите.

Вскрываем заголовок Authorization

Basic auth (RFC 7617) - самая старая схема в HTTP, и до сих пор она держит на себе удивительное количество API-интеграций, вебхуков и CI-конвейеров. Клиент отправляет свои учётные данные в виде user:pass, закодированные в base64, под словом Basic:

import base64
header = "Basic amFuZTpwYTpzcw=="
decoded = base64.b64decode(header[len("Basic "):]).decode("utf-8")
user, _, password = decoded.partition(":")
print(user, password)
# jane pa:ss

Обратите внимание на partition, потому что это та деталь, которая спасёт вас потом: пароль может содержать двоеточия, идентификатор пользователя - нет, и разделителем является только первое двоеточие. Одно честное замечание, потому что сам RFC говорит об этом прямо: base64 - это не шифрование. RFC 4648 утверждает, что базовое кодирование «визуально скрывает иначе легко узнаваемую информацию, например пароли, но не предоставляет никакой вычислительной конфиденциальности». Заголовок Basic может декодировать любой, кто видит трафик, так что относитесь к нему как к удобству для соединений под защитой TLS, а не как к границе безопасности. Когда заголовок отправляете вы, requests соберёт его за вас через auth=("jane", "pa:ss"), и этим стоит пользоваться всякий раз, когда библиотека уже есть в вашем стеке.

Электронная почта, самый первый заказчик

Base64 был стандартизирован в 1993 году ровно для одной задачи: дать бинарным данным выжить в электронной почте. RFC 2045, стандарт MIME, определил кодирование тела Content-Transfer-Encoding: base64, и по сей день это способ по умолчанию, которым вложения путешествуют по интернету. Пакет email в Python делает всю работу за вас: он разбирает заголовки, декодирует кодируемые слова =?utf-8?b?...?=, которые RFC 2047 прячет в полях заголовков, и декодирует тела из base64, когда вы об этом просите:

import email
from email import policy
raw = (b"Subject: =?utf-8?b?w6l0w6k=?=\r\n"
       b"From: sender@example.com\r\n"
       b"To: reader@example.com\r\n"
       b"Content-Transfer-Encoding: base64\r\n"
       b"\r\n"
       b"w6l0w6kgbWFpbA==\r\n")
msg = email.message_from_bytes(raw, policy=policy.default)
print(msg["Subject"])
# été
print(msg.get_payload(decode=True))
# b'\xc3\xa9t\xc3\xa9 mail'

Вызов get_payload(decode=True) читает заголовок Content-Transfer-Encoding и декодирует тело из base64 за вас, попутно расправляясь со строками длиной 76 символов. Аргумент policy=policy.default выбирает современный интерфейс, доступный начиная с Python 3.6, когда новый API почты на основе политик перестал быть пробным, - и вы сразу получаете декодированные значения заголовков; устаревший парсер по-прежнему работает, но кодируемые слова приходится декодировать вручную. Спускаетесь к decodebytes только тогда, когда разбираете голый фрагмент, а не целое сообщение, - например, блок, который кто-то вставил в тикет. Для многочастных сообщений итерируйте через iter_attachments() и дайте каждой части ту же однострочную обработку.

PEM-броня и пакет cryptography

PEM-файл - это строка заголовка, немного перенесённого Base64 и строка подвала, и ничего больше. Броня - чисто украшение; Base64 - вся история, потому что он декодируется в сырую структуру DER, лежащую под ней. Пакет cryptography (pip install cryptography) может загрузить результат напрямую, и именно поэтому он - стандартный инструмент для всего, что связано с сертификатами и ключами:

import base64
from cryptography import x509
pem = b"""-----BEGIN CERTIFICATE-----
MIIBGzCBwaADAgECAgEBMAoGCCqGSM49BAMCMBcxFTATBgNVBAMMDGV4YW1wbGUu
dGVzdDAeFw0yNjA4MjkxNzIxMzZaFw0yNjA4MzAxNzIxMzZaMBcxFTATBgNVBAMM
DGV4YW1wbGUudGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPvNHjdF4b1n
SkBDT6UWtG2k8ICe45eL3kSkVfuhriev1uO9PBLMP50HWnrLbCXtl3lhWaVibctl
QbWRG4xqGLcwCgYIKoZIzj0EAwIDSQAwRgIhAKdFm5GLecg2fF7qUhSmKGtgNFaL
qVyKtDXK07N6GZd/AiEAtRXemnYqDMz77o9+VpM/NsNEwDi0yaVB+tKGLbdKJb0=
-----END CERTIFICATE-----
"""
body = b"".join(pem.splitlines()[1:-1])
der = base64.b64decode(body)
cert = x509.load_der_x509_certificate(der)
print(cert.subject.rfc4514_string())
# CN=example.test

В большинстве производственного кода вы никогда не делаете «снять броню и декодировать» вручную: load_pem_x509_certificate принимает закованные в броню байты и сам обрабатывает шаг Base64 под капотом. Ручной путь оправдывает себя, когда байты DER уже у вас в руках (колонка в базе данных, файл конфигурации, байтовый буфер из протокола), или когда блок пришёл, обёрнутый в строку, и вы хотите увидеть, что внутри, прежде чем ему доверять. Ключи работают так же: с другой стороны того же декодирования поджидает load_der_private_key.

Файлы, магические числа и привычка к .b64

Декодирование - лишь половина работы; байтам обычно нужен файл. Паттерн: прочитать, декодировать, проверить, записать. Проверка важна, потому что битые данные в противном случае приведут к неверному файлу, о котором вы узнаете только через недели:

import base64
import binascii
with open("payload.b64", "rb") as handle:
  encoded = handle.read()
try:
  data = base64.b64decode(encoded, validate=True)
except binascii.Error:
  data = base64.b64decode(encoded)
with open("payload.bin", "wb") as out:
  out.write(data)

Для быстрых разовых конверсий устаревшая функция «файл в файл» делает весь путь за один вызов, перенесённые строки и всё прочее:

import base64
with open("photo.b64", "rb") as src, open("photo.png", "wb") as dst:
  base64.decode(src, dst)

Что вы вообще только что декодировали? Первые байты почти любого распространённого формата - фиксированная подпись, и поскольку Base64 детерминирован, закодированная подпись тоже фиксирована. Увидеть один из этих префиксов - всё равно что узнать номерной знак вдалеке:

Base64 начинается с Скорее всего, это
iVBORw0KGgo изображение PNG
/9j/ изображение JPEG
R0lGODlh изображение GIF
JVBERi0 документ PDF
UEsDBA== архив ZIP
UklGRg== контейнер RIFF (WAV, WEBP, AVI)
LS0tLS1CRUdJTg== блок в ASCII-броне («-----BEGIN ...»)

И пока файл записывается, сделайте подсчёт размеров, потому что именно это число удивляет людей, когда диск наполняется: кодирование раздувает данные примерно на треть, так что файл в 300 KB путешествует как около 400 KB текста Base64, а файл, который вы декодируете обратно, - того самого маленького, исходного размера. Ваш диск, и ваша память, если вы читаете весь файл разом, должны закладывать этот разрыв в свой бюджет.

Базы данных, файлы конфигурации и переменные окружения

Base64 - любимое средство пронести бинарные данные (или JSON) через хранилище, которое принимает только текст: колонка TEXT, значение в файле .ini, переменная окружения в конвейере развёртывания. Рецепт декодирования тот же, что для файлов, только без диска:

import base64
import json
stored = "eyJyb2xlIjogImFkbWluIiwicHJvamVjdCI6Im15c2l0ZSJ9"
payload = json.loads(base64.b64decode(stored))
print(payload)
# {'role': 'admin', 'project': 'mysite'}

Две заметки для этого угла дома. Когда сохранённое значение - это JSON, пропустите промежуточный шаг .decode("utf-8") и позвольте json.loads принять байты напрямую: он делает это ещё с Python 3.6. И одно честное предупреждение, потому что здесь живёт самая дорогая путаница во всей статье: Base64 в переменной окружения или файле конфигурации - это щит от человека, который бросит взгляд на файл, а не от того, кто его прочитает. Если значение по-настоящему чувствительное, сначала зашифруйте его (пакет cryptography поставляется с Fernet ровно для этого), и только потом кодируйте шифртекст в Base64, если ваше хранилище требует текста.

Когда данные приходят кусками

В стандартной библиотеке нет инкрементального декодера Base64: нет пары «обновление-и-завершение», так что потоковым данным потребуется немного вашей собственной бухгалтерии. Арифметика проста и строга одновременно. Четыре закодированных символа дают три байта, так что декодировать можно только целые группы по четыре символа, а остаток нужно переносить в следующий фрагмент:

import base64
def chunked_decode(chunks):
  out = []
  leftover = b""
  for chunk in chunks:
    buffer = leftover + chunk
    whole = len(buffer) // 4 * 4
    if whole:
      out.append(base64.b64decode(buffer[:whole]))
    leftover = buffer[whole:]
  if leftover:
    out.append(base64.b64decode(leftover + b"=" * (-len(leftover) % 4)))
  return b"".join(out)

Подайте в него буфер сокета, файл, читаемый кусками по 64 KB, или генератор строк с отстриженными переводами строк, - и выход будет идентичен декодированию всего объёма разом. Если ваш вход гарантированно чистый и без переносов, сохраняйте строгость, декодируя каждую целую группу с validate=True, и помните, что последнему остатку может потребоваться поправка на заполнение: именно поэтому вспомогательная функция добавляет его перед последним декодированием. Это та же самая логика шва, которую кодировщики используют с другой стороны, только с четырьмя символами вместо трёх байтов.

Из командной строки

Модуль base64 удваивается в крошечную утилиту командной строки, и это удобно, когда данные сидят в вашем терминале, а не в коде. Кодирование - поведение по умолчанию; декодирует -d (или его близнец -u):

echo -n "hello world" | python3 -m base64
aGVsbG8gd29ybGQ=
echo -n "aGVsbG8gd29ybGQ=" | python3 -m base64 -d
hello world

Когда вы не называете файл, он читает из stdin, либо из того файла, который вы назовёте, а под капотом - устаревший интерфейс «файл в файл», так что вывод приходит перенесённым на 76 символов, с завершающим переводом строки в каждой строке. Для вставки данных в сессию с повышенной строгостью однострочная версия декодера - приятная привычка:

import base64
import sys
print(base64.b64decode(sys.stdin.read(), validate=True))

Девять способов обжечься

Каждый баг декодирования Base64 в Python - это один из них. Держите этот список там, где вы найдёте его в панике, потому что он поглотил больше чьих-то послеобеденных часов, чем любой другой документ, который вам предстоит прочесть в этом году. Три первых идут с кодом, потому что их легче запомнить, когда ты уже видел последствия:

Пропущенное заполнение. Самый частый сбой из всех: обычно потому, что часть JWT или значение из API пришло без своих знаков заполнения:

import base64
import binascii
segment = "Zm9vYmE"
try:
  base64.urlsafe_b64decode(segment)
except binascii.Error as caught:
  print(caught)
# Incorrect padding
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'

Усечённая строка. Когда в ошибке говорится, что количество символов данных «не может быть на 1 больше, чем кратное 4», данные были обрублены в пути, либо копирование и вставка уронили символ в конце. Никакое количество заполнения не починит строку, длина которой равна одному по модулю четырёх: данных просто нет, и честный ответ - попросить данные ещё раз.

Тихий мусор. Снисходительный режим декодирует всё, что выжило, а обычные английские слова полны букв алфавита Base64, так что случайное слово перед данными становится настоящими байтами, приклеенными к вашим:

import base64
print(base64.b64decode("junkZm9vYmFy"))
# b'\x8e\xe9\xe4foobar' - три байта чистого вымысла, а затем истина

Остальные шесть не требуют никакого кода:

  • Вы декодировали base64url-строку стандартным декодером. Дефисы и подчёркивания не входят в стандартный алфавит, так что они исчезли молча, и данные вышли изувеченными, либо вовсе пустыми. Используйте urlsafe_b64decode с поправкой на заполнение.
  • Вы забыли, что результат - байты. Приклеивание к строке рождает TypeError, а проталкивание в JSON-ответ сериализует представление b'...'. Вызывайте .decode(encoding) на границе, сознательно, с той кодировкой, которую вы имеете в виду на самом деле.
  • Вы передали не-ASCII строку. Декодер принимает строки, но только ASCII; всё остальное - ValueError. Если ваши данные вышли из текстового файла, прочитанного с неверной кодировкой, чините чтение, а не декодирование.
  • Вы декодировали дважды. Данные уже были декодированы выше по потоку, либо это был Base64 из Base64, и второй проход превратил ваш пароль в шесть байтов, которые ни один человек уже никогда не прочитает.
  • Вы применили строгий режим к перенесённым данным. Одного перевода строки достаточно, чтобы validate=True кинул исключение, так что MIME-блоки и тела PEM принадлежат снисходительным инструментам, а не строгому.
  • Вы поверили знаку заполнения посередине. В снисходительном режиме = в любом месте строки молча выбрасывается, так что повреждённые данные со сдвинутым заполнением могут декодироваться в «правильный» ответ. Замечает это только строгий режим, и замечает он, отказывая.

Если ваша работа - быть сторожем, вот небольшая вспомогательная функция, которая ставит два настроения работать вместе: сначала строгость, потом поправка на заполнение, и громкий сбой, когда ничего не помогает:

import base64
import binascii
def safe_decode(text):
  candidate = text.strip()
  try:
    return base64.b64decode(candidate, validate=True)
  except binascii.Error:
    padded = candidate + "=" * (-len(candidate) % 4)
    return base64.b64decode(padded, validate=True)
print(safe_decode("Zm9vYmE"))
# b'fooba'
print(safe_decode("Zm9vYmFy"))
# b'foobar'

Заметьте, что вспомогательная функция по-прежнему доверяет тому алфавиту, которому ей велено доверять. Если ваш вход может оказаться base64url, подавайте его в urlsafe_b64decode вместо неё. Проверка - это контракт, и в контракте написано, в каком диалекте находятся данные.

Три десятилетия тихого модуля

Модуль находится в стандартной библиотеке уже четверть века, и большую часть времени он сидел неподвижно. Когда он всё-таки двигался, движения были небольшими, но реальными, и они объясняют несколько историй «у меня работает», которые носятся по старым форумам:

  • 1995 - Джек Янсен переписал base64.py, передав настоящую работу модулю C-уровня binascii. Комментарий до сих пор лежит в файле, и передача по-прежнему настоящая и сегодня.
  • 2003, поставлен в Python 2.4 - Барри Варшав добавил полную поддержку RFC 3548: семейства b16, b32 и b64, плюс варианты standard_* и urlsafe_*, которыми вы пользуетесь сегодня.
  • Python 3.1 - encodestring и decodestring объявлены устаревшими в пользу encodebytes и decodebytes - имён, которые прижились.
  • Python 3.3 - функции декодирования начали принимать ASCII-строки, завершив эпоху, когда каждое декодирование начиналось с байтового литерала.
  • Python 3.4 - где угодно принимают любой байтоподобный объект (включая memoryview), а Base85-родственники a85 и b85 вступили в модуль.
  • Python 3.9 - давно устаревшие encodestring и decodestring наконец удалены. Старым туториалам, которые их вызывают, требуется переименование в одно слово.
  • Python 3.10 - b32hexencode и b32hexdecode прибыли с расширенным шестнадцатеричным алфавитом - тем, что держит закодированные данные сортируемыми в лексикографическом порядке.
  • Python 3.11 - у binascii.a2b_base64 появилась strict_mode, и именно на ней под капотом ездит validate=True.
  • Python 3.13 - z85encode и z85decode привнесли диалект Z85 от ZeroMQ в стандартную библиотеку, а древний модуль uu был удалён по PEP 594 с ядовитой заметкой, что вместо него следует пользоваться base64.
  • Python 3.14 - b16decode стал до шести раз быстрее: его проверка теперь работает на bytes.translate вместо регулярного выражения, а модуль вообще больше не импортирует re. Время его импорта тоже попало в список улучшенных модулей.

Ни одно из этих изменений не меняет того, что делают функции, - а это тихая роскошь модуля столь почтенного возраста: код, который декодировал Base64 в 2005 году, декодирует его и в 2026-м, на той же строке, с тем же результатом.

Удовольствия из полей

Серьёзная работа сделана, так что вот мелкие удовольствия, которые модуль прячет в своих полях:

  • Документация самого модуля уже более десяти лет гоняет одно и то же демо: на входе b'data to be encoded', на выходе b'ZGF0YSB0byBiZSBlbmNvZGVk'. Если вы читали страницу base64 в каком-либо выпуске Python за последние двадцать лет, вы уже встречали эту пару.
  • Слово junk - вполне валидная Base64-строка. Все четыре буквы входят в алфавит, поэтому случайное слово в начале данных становится тремя байтами чистого вымысла, а не ошибкой, и именно за это снисходительный режим получил своё прозвище.
  • urlsafe_b64decode случайно двуязычна. Она сначала переводит свой алфавит, а потом декодирует снисходительно, так что читает и строки стандартного алфавита с + и /. Одна функция, два диалекта, ноль жалоб.
  • Сообщения об ошибках - стабильный минилексикон, который не шелохнулся со времён C-реализации: Неверное заполнение, Допускаются только данные Base64, Избыточное заполнение не допускается, Заполнение в начале не допускается. Выучите их, и вы сможете разбираться с битыми данными, не запуская ни одной строки кода.
  • Пустая строка - единственный вход, на который не следует вообще никакой реакции: b'' на входе, b'' на выходе, в обоих настроениях. Ничего на входе, ничего на выходе, тревоги нет.
  • Docstring модуля до сих пор называет RFC 3548, издание спецификации 2003 года. RFC 4648 - действующий стандарт с 2006 года, и модуль следует ему безукоризненно, не утруждая себя обновлением этого предложения.
  • В Python 2 на стороне декодирования не было типовой стены: обычная str на входе, обычная str на выходе. Байтовая реформа 2007 года в разработке Python 3 это изменила, и именно на старые туториалы по Python 2 до сих пор смотрит большинство тредов «почему моё декодирование сломано».

Итак, вот вся философия в четырёх правилах. Передавайте validate=True для всего, что вы не кодировали сами, и относитесь к исключению как к настоящему ответу, а не как к предложению. Знайте, какой диалект у вас в руках: стандартный, base64url или MIME-перенесённый, потому что декодер вам не скажет; он лишь будет догадываться, выбрасывая всё, что не подходит. Относитесь к результату как к байтам, пока не докажете, что это текст, а потом спросите, кто владел кодировкой. И помните, что самая дружелюбная черта этой функции - готовность декодировать вещи, которые не совсем Base64, - это та же самая черта, которая делает её опасной, так что решайте при каждом вызове, сколько доверия заработал вход.

Если когда-нибудь понадобится пойти в обратную сторону, накрутив свежие байты обратно на ту дружелюбную ленту букв ради токена, вложения или встроенной картинки, вся история b64encode подробно разобрана в связанной статье про кодирование Base64 в конце этой страницы. Два направления - зеркальные отражения, но у каждого свой набор сюрпризов, и этот вы теперь знаете наизусть. Удачного декодирования.

Последнее обновление: 2026-09-08

Связанная статья: Кодирование Base64 в Python: полное руководство