Python での Base64 デコード:完全ガイド
あなたのコードのどこかで、まったくテキストらしく見えない文字列がちょうど着地しました:A から Z までの長い連なり、いくつかの数字、ときどき現れる + や /、もしかすると - や _、そして末尾に 1 つか 2 つ停まっている =。その文字列の裏に潜むのは、あなたのゲートウェイが拒否した JWT のペイロードかもしれない。HTML のページの中に隠された画像かもしれない。誰かが .b64 の添付ファイルとしてメールで送ってきたファイルかもしれない。3 つのヘルプデスクを渡り歩いたチケットの中にある証明書ブロックかもしれない。あなたの仕事は、元のバイトを、その通りそのまま手渡すことです。そして Python はこの仕事に絶好調です:ツールボックスは数十年にわたって標準ライブラリに同梱されてきたので、import base64 の 1 行で、どのプラットフォームでも準備完了。インストールも設定も、何もいりません。
腰を据える間に簡単な復習を:誰でも年に 1 度は必要になります。Base64 はデータ 3 バイトごとを、64 文字のアルファベットから選んだ 4 文字に書き換えます。最後の 3 バイト組が揃わないときは、= パディングが組を埋め、出力はいつも 4 つごとのまとまりで来ます。これが全部のトリックです。圧縮でも秘密でもなく、テキストしか受け入れないチャネルでバイナリを生き残らせる手段にすぎません。このサイトのホームページではアルファベットからパディングの計算まで、形式を徹底的に説明しているので、ここでは本当につらい場所でエネルギーを使います:デコードの Python 側と、結果を正直に保つことです。
ここからのすべてを形づくる 3 つの事実があります。もう 1 行先を読む前に覚える価値があります。第一に、デコーダーには 2 つの気分があります:認識できないものは黙って捨ててしまう、丁寧で寛大なデフォルトモードと、そうした入力を真っ向から拒否する厳格モード。第二に、デコードの結果は常に bytes オブジェクトで、決して文字列ではありません。そこから本当の文字を取り出したい瞬間は、あなたが意図的に下すべき判断です。第三に、ほぼ同じに見える 2 つのアルファベットがあります:標準のものと URL 安全なもの。この 2 つを混同するのは、エラーの 1 つも出さずにデータを失うお気に入りの方法です。このガイドは 3 つすべてを案内します。次に謎文字の壁がターミナルに着地したとき、目を細める代わりに微笑んでいられるように。
デコード機能のフルメニュー
base64 モジュールを開くと、2 世代のインターフェースが並んで座っています。モダンなほうは b64decode を中心にしていて、バイト風オブジェクト(そしてプレーンな ASCII 文字列)をバイトに戻し、RFC 4648 で定義された 2 つの Base64 方言の両方を話します。レガシーなほうはより古く、ファイル志向です:ファイルオブジェクト上で動いて、知っているのは標準アルファベットだけで、1996 年の MIME メール標準である RFC 2045 がエンコードされた出力に要求した 76 文字の折り返し行を中心に作られました。かなり前からあるコードにはレガシーの名前がまだまだ残っているので、メニューのデコード側を完全な形で紹介します:
| 関数 | 何をするか | 備考 |
|---|---|---|
base64.b64decode(s, altchars=None, validate=False) |
主力関数:Base64 の塊を生のバイトに戻す | バイトまたは ASCII 文字列を受け取り、常にバイトを返す |
base64.standard_b64decode(s) |
同じ仕事、ただし標準アルファベットに固定 | 方言が確実に分かるときに便利 |
base64.urlsafe_b64decode(s) |
- と _ を使う URL 安全アルファベットを読む |
JWT を読むのがこれ |
base64.decodebytes(s) |
折り返された 1 行以上の Base64 をデコード | Python 3.1 で追加、MIME に優しい経路、寛大 |
base64.decode(input, output) |
Base64 ファイルを生のファイルへストリーム | レガシー、行単位で読む、寛大 |
base64.b32decode(s, casefold=False) |
ひと回り小さい兄弟の Base32 をデコード | casefold で小文字の受け入れが可能 |
base64.b16decode(s, casefold=False) |
Base16 をデコード、これはそのまま 16 進数 | Python 3.14 では最大 6 倍速 |
binascii.a2b_base64(s, strict_mode=False) |
本業をこなす C レベルの関数 | 厳格さへの直接ハンドル、strict_mode は Python 3.11 から |
ここから先はすべて 1 行目の上に積まれています。深掘りする前に知っておく価値のある事実が 1 つ:公式ドキュメントでは、このモジュールは「インターネットデータ処理」セクションの下に置かれていて、binascii のすぐ隣です。その配置は偶然ではありません。b64decode は薄いラッパーで、アルファベットを翻訳する(altchars を渡した場合)、そして重労働は C レベルの binascii.a2b_base64 に任せます。この関数が速いのはそのためで、エラーメッセージが C 特有の、飾りのない切り詰めた風味を持つのもそのためです。
主力関数:b64decode
契約全体はこれだけです。頭に入るほど短いので。関数は、バイト風オブジェクトまたは ASCII 文字列、任意の 2 文字アルファベット交換、そして検証フラグを受け取ります。返すのは bytes オブジェクト。失敗時は binascii.Error を送出しますが、これは ValueError のサブクラスで、例外の一族をまとめて捕捉したいときに役立ちます:
import base64
data = base64.b64decode("Zm9vYmFy")
print(data)
# b'foobar'
print(type(data))
# <class 'bytes'>
その最後の 1 行が、この記事でたった 1 番目の重要な行です。結果はバイトであり、文字列ではありません。そして Python はちょうどいいところで手を離します:オブジェクトを表示すると b'...' の表記が見え、文字列とくっつけようとすると TypeError が起きます。実際の文字が必要になる瞬間、判断はあなたにあります。下の文字コードのセクションでは、その判断が簡単な場合と罠になる場合を扱います。
任意の altchars 引数は、標準アルファベットの + と / を別の文字のペアに差し替えます。まさにそれで URL 安全方言が生まれるノブで、urlsafe_b64decode が b64decode の上に組み立てられている理由もこれです。自分で altchars に手を伸ばすことはめったにありませんが、機構がそこにあると知っておくのは良いことです。それ以外のすべてにおいて、この関数は仕事をするだけです。速く、C で。
デフォルトは寛大、要求すれば厳格
デフォルトでは、b64decode は丁寧な忘れ者です。64 文字アルファベットにない文字(あなたの altchars にもないもの)は、デコードが始まる前に静かに捨てられ、生き残ったものだけがデコードされます。警告も通知も、確認すべき戻り値もありません。あるのは結果だけです。その寛容さには高貴な祖先がいます:RFC 2045 の 6.8 節は、デコーダーに対して「Table 1 にない改行やその他の文字はすべて無視されなければならない」と伝えています。SMTP は歴史的に長い行を折り返し、その過程で余分な文字をまき散らしてきたからです。メールクライアント、チャットアプリ、PDF のコピーを渡り歩いてきたペイロードは、ほとんど準備なしでデコードできることが多く、それは本物の超能力です。
同じ親切さが、デフォルトのデコーダーを検証器としては無役にしている理由でもあります。RFC 4648 の 12 節はリスクを明言しています:エンコード全体を拒否する代わりにアルファベット外の文字を無視すると、情報の漏洩に使える隠れチャネルが開き、2 つの異なる入力が同じバイトにデコードされるため、文字列の等価チェックも壊れます。自分でエンコードしていないものには、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 |
====(パッド 4 つ、データなし) |
b'' |
binascii.Error |
(空の入力) |
b'' |
b'' |
寛大モードの列が静かに仕事をしているのを見てください。最初に人々を驚かせるのは、先頭に単語がある行です:junk の 4 文字はすべてたまたま Base64 アルファベットに座っているので、「ゴミ」は 3 つの実際のバイトとしてデコードされ、真顔でペイロードにくっついてきます。厳格モードはこの行では救世主ではありません。入力は本当に有効な Base64 だからです。真っ向から拒否されるのは他の行で、その拒否はちょうど 1 つの形しか持ちません:覚えやすいメッセージの 1 つを担った binascii.Error です:
Incorrect padding- 破棄後の長さが 4 の倍数でない、または最後の組が短すぎる場合。Zm9vYmEのようにパッドが 1 つもない文字列はここで落ちます。Invalid base64-encoded string: number of data characters (N) cannot be 1 more than a multiple of 4- 入力がちょうど 1 文字足りない、次の組まで。これは切断されたペイロード、あるいはコピー&ペーストされたペイロードの古典的な指紋です。Only base64 data is allowed- アルファベット外の文字が厳格モードまで生き残った場合です。改行 1 つでも 1 つとして数えられます。Excess padding not allowed- 文字列の真ん中にパッドがある、または最後の組が許す数よりパッドが多い場合。Leading padding not allowed- 文字列が=で始まる場合。- 別の一族から 1 つ:
ValueError: string argument should contain only ASCII characters。ASCII 以外の文字を含む文字列を渡すとこれになります。文字列は受け取れますが、ASCII のものだけです。
舞台裏では、validate=True はまったく別のコードパスではありません。モジュールはこのフラグを binascii.a2b_base64 の strict_mode パラメータとして転送します。これは Python 3.11 で binascii に追加された厳格チェックです。base64 レイヤーを経由せずに厳格さがほしいとき、これが直接ハンドルになります:
import binascii
line = b"Zm9vYmFy"
print(binascii.a2b_base64(line, strict_mode=True))
# b'foobar'
厳格モードを盲目的に信頼する前に押さえておくべきクセが 1 つ:末尾の改行が 1 つあるだけでも拒否します。だから MIME 折り返しのブロックは、寛大モードか decodebytes の仕事であって、validate=True の仕事ではありません。厳格モードは、自分のコードからちょうど出てきたばかりの新札トークンのように、完璧にきれいなデータだけに留めておきましょう。
base64url: URL に収まるアルファベット
標準アルファベットには、URL が嫌う 2 つの文字があります。+ 記号はどのフォームデコーダーでもスペースとして読まれ、/ 記号はパスの区切り文字として予約されています。RFC 4648 の 5 節は兄弟の方言を定義しています:+ が - に、/ が _ になり、データ長が文脈から分かるときはパディングを落とします。RFC はこの変形に正式な名前 base64url まで与え、単に「base64」と呼ぶべきではないと強く言っています。最も頻繁に出会うのは JSON Web Token の中で、トークンの各部分がすべてパディングなしの base64url になっています。また OAuth トークンや API のカーソルパラメータにも顔を出します。
Python には専用の関数 urlsafe_b64decode が同梱されています。ダッシュとアンダースコアをプラスとスラッシュに戻してからデコードしますが、パッドを戻してはくれません。パディングなしの入力は JWT では普通なので、先に算術の 1 行が来ます。PyJWT のようなライブラリが内部で使っているのと同じものです:
import base64
segment = "Zm9vYmE"
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'
式 "=" * (-len(segment) % 4) はトリックのように見えますが、これが全部の仕事です:0 つ、1 つ、2 つのパッドを生み、決して 3 つは生まないので、すでにパディングされた文字列はそのまま素通しになります。負の剰余が、あらゆる長さの文字列で機能する秘密で、これはすべての Python 開発者が少なくとも 1 度は打つことになる Base64 算術の 1 行です。
では危険な混同の話です。2 つのアルファベットは紛らわしいほど似ているので。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 はまずアルファベットを翻訳してから寛大にデコードするので、+ と / を含む標準アルファベットの文字列も喜んで受け入れます。教訓は、即興をしないことです。方言ごとに 1 つの関数を選んで、それにこだわることが大切で、それは外国通貨の使い方に似ています:円が通用する場所で円を遣い、間違った両替所では遣わない、ということです。
出力はバイト:文字コードの会話
ここには、人々が Base64 に持ち込む文字コードの質問の半分を解決する一文があります:b64decode がデコードするのはバイトであり、テキストではありません。文字コードの引数もなく、変換もない。入力に関する何ひとつが、Python にそのバイトが何を意味すべきか教えていません。意味は、あなたが文脈から供給しなければならないものです。その文脈は、ほぼ常に 3 つのうちの 1 つです:そう書いてあるヘッダー、そう定めた 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
このセクションをホラーショーにしないための 3 つの経験則があります。1 つ目:デコードされたデータが JSON の場合、手動でデコードする必要はまったくない。json.loads は Python 3.6 からバイトを直接受け入れており、UTF-8、UTF-16、UTF-32 を自力で検出します。2 つ目:バイナリはテキストではないので、PNG に対して「検出された」文字コードは事実ではなく幸運な推測にすぎません。ラベルではなくバイトを確認してください。3 つ目:送信者が文字コードを教えてくれたら、送信者を信じなさい。content-type ヘッダーや API ドキュメントは、どんな検出器よりも常に上位だからです。
デコードされた Base64 が Python コードに現れる場所
しばらくすると、形を見分けられるようになります。デコードされた Base64 が Python アプリケーションのどこで顔を出すのか、そしてそれぞれへの 1 行レシピ。それがこのフィールドガイドです。続くセクションでは、最もよくあるものにフルな扱いをします:
| どこで見つかるか | 何か | 読み方 |
|---|---|---|
| JWT | ヘッダー、ペイロード、署名の 3 部分(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 ファイル | アーマーをまとった鍵または証明書(RFC 7468) | アーマーの行を落として、ボディを DER にデコード |
| JSON API のフィールド | 文字列として紛れ込ませられたバイナリ | デコードしてから、結果をテキストではなくバイトとして扱う |
| TEXT カラムや環境変数 | テキスト専用場所に保管されたバイナリまたは JSON | デコードしてから、取り決めておいた文字コードで解析または書き出す |
JSON Web Token を読む
JWT はドットで結ばれた 3 つの base64url 部分です:ヘッダー、ペイロード、そして署名。最初の 2 つはプレーンな JSON なので、上のセクションのパディング修正を使えば、中をのぞき込むのはそれぞれ 1 行で済みます:
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'}
範囲についての注意です。ここは重要だから:このようにトークンを検査するのはデバッグ用のツールであり、認証機構ではありません。ペイロードが読めるのは、それが本物であるという意味ではありません。攻撃者はあなたの秘密を知らずに、最初の 2 セグメントを偽造できます。本物の検証には、トークンを PyJWT(pip install pyjwt)に任せましょう。署名をチェックし、明示的なアルゴリズムリストなしではデコードを拒否します:
import jwt
# 32 バイト未満のキーは PyJWT の InsecureKeyLengthWarning を受け取ります(PyJWT 2.11+)、デモキーに対する公平な文句です。
decoded = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
print(decoded)
# {'sub': '1234567890', 'name': 'John Doe'}
鍵が間違っている場合、辞書の代わりに例外が来ます。これは本番コードでまさに欲しい挙動です。そしてトークンに失効したタイムスタンプが付いてきた場合も、PyJWT はそれに対する例外も送出するので、クレームの名前を自分で覚える必要はありません。
Data URI を開く
Data URI は、ブラウザが 2 番目のリクエストを発火しないように、メディアを 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'
結果の先頭にある 8 バイトの PNG シグネチャは、正しいものをデコードできたことを確認するための安くて確かなチェックです。触れるべき落とし穴が 2 つあります。URI がスクレイピングしたページやチャットメッセージから来た場合は、まず HTML エンティティとよたらしい空白を取り除いてください。寛大モードのデコーダーは多くのゴミを許容して、エラーの代わりに壊れた画像を手渡すだけだからです。そして信頼できない入力をまとめてデコードするなら、validate=True を渡してください:厳格な検証に失敗する data URI は、最初から正しく形成されていなかった data URI です。勘でそれをディスクに書き込みたいとは思いません。
Authorization ヘッダーを解く
Basic 認証(RFC 7617)は HTTP で最も古いスキームで、今なお驚くほど多くの API 統合、Web フック、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 に注意してください。後であなたを救ってくれる細部だからです:パスワードはコロンを含み得ますが、ユーザー ID は含めず、区切りとなるのは最初のコロンだけです。正直な注意を 1 つ。RFC 自体がこれについてぶっきらぼうに言っているからです:Base64 は暗号化ではありません。RFC 4648 は、Base64 エンコーディングは「パスワードのように、それ自体で簡単に識別できる情報を視覚的に隠すだけで、計算上の秘匿性を一切提供しない」と言います。Basic ヘッダーはトラフィックを見る誰でもデコードできるので、セキュリティの境界ではなく、TLS 保護された接続のための便利機能として扱ってください。あなたがヘッダーを送る側なら、requests が auth=("jane", "pa:ss") で代わりに作ってくれます。このライブラリがすでにスタックに入っているなら、使う価値があります。
メール、最初の顧客
Base64 が 1993 年に標準化されたのは、たった 1 つの仕事のためです:バイナリをメールで生き残らせること。MIME 標準である RFC 2045 は Content-Transfer-Encoding: base64 のボディエンコーディングを定義し、それは今も添付ファイルがインターネットを渡るデフォルトの手段です。Python の email パッケージは全部やってくれます:ヘッダーを解析し、RFC 2047 がヘッダーフィールドに隠した =?utf-8?b?...?= のエンコードワードをデコードし、頼めばボディを 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 引数は、新しいポリシーベースの email API が暫定でなくなった Python 3.6 以降のモダンなインターフェースを選択し、そのままデコードされたヘッダーの値が得られます。レガシーなパーサーもまだ動きますが、エンコードワードを手動でデコードすることになります。メッセージ全体ではなく、誰かがチケットに貼り付けたブロックのような素の断片を解析するときだけ、decodebytes に降りていきます。MIME 複合メッセージの場合は、iter_attachments() で繰り返し、各部分に同じ 1 行の扱いをします。
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)
素早く 1 回だけの変換には、レガシーのファイル間関数が折り返し行ごと、旅全体を 1 回の呼び出しでこなします:
import base64
with open("photo.b64", "rb") as src, open("photo.png", "wb") as dst:
base64.decode(src, dst)
ところで、さっきデコードしたのは何でしたか?ほぼすべての一般的な形式の先頭バイトは固定のシグネチャであり、Base64 は決定的なので、エンコードされたシグネチャも固定です。これらのプレフィックスの 1 つを見るのは、遠くからナンバープレートを見分けるようなものです:
| Base64 の先頭 | 正体(たぶん) |
|---|---|
iVBORw0KGgo |
PNG イメージ |
/9j/ |
JPEG イメージ |
R0lGODlh |
GIF イメージ |
JVBERi0 |
PDF ドキュメント |
UEsDBA== |
ZIP アーカイブ |
UklGRg== |
RIFF コンテナ(WAV、WEBP、AVI) |
LS0tLS1CRUdJTg== |
ASCII アーマー付きブロック(「-----BEGIN ...」) |
ファイルを書き込む間にサイズ計算もしておきましょう。ディスクが埋まったとき人々を驚かせるのはこの数字だからです:エンコードはデータを約 3 分の 1 膨らませるので、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'}
この隅について 2 つの注意を。保存された値が JSON の場合、中間の .decode("utf-8") ステップをスキップして、json.loads にバイトを直接受け取らせましょう。Python 3.6 からそうしてきたからです。そして正直な警告を 1 つ。この記事全体で最も高価な誤解が住んでいる場所だからです:環境変数や設定ファイルにある Base64 は、ファイルを一瞥する人間への盾であって、それを読む人間へのものではありません。値が本当に機密なら、まず暗号化しましょう(cryptography パッケージはまさにこのために Fernet を同梱しています)。そして、ストレージがテキストを要求するなら、そのとき初めて暗号文を Base64 にしてください。
ペイロードが断片として到着したとき
標準ライブラリには増分 Base64 デコーダーはありません:update と finish のペアもないので、ストリーミングデータには自分で少し帳簿管理が必要です。算術は簡単で、同時に厳格でもあります。エンコードされた 4 文字が 3 バイトを作るので、デコードできるのは完全な 4 文字組だけで、余った分は次のチャンクに持ち越さなければなりません:
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 でデコードして厳格さを保ち、最後の残りにはパディング修正が必要になり得ることを覚えておいてください。だからこそヘルパーは最後のデコード前にそれを付け加えています。これはエンコーダーが向こう側で使っているのと同じ継ぎ目の論理で、3 バイトではなく 4 文字になっているだけです。
コマンドラインから
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))
やけどを負う 9 通りの方法
Python の Base64 デコードのバグは、すべてこれらの中にあります。このリストは、パニックのときに見つけられる場所に貼っておいてください。あなたが今年読む他のどんな単一のドキュメントよりも、多くの午後を捕まえてきたからです。最初の 3 つはコード付きです。壊れた跡を見れば記憶に残りやすいからです:
パディングの欠落。 最も一般的なクラッシュです。たいていは 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'
切断された文字列。 エラーがデータ文字数が「4 の倍数より 1 つ多いことはできない」と言っているなら、ペイロードは伝送中に切断されたか、コピー&ペーストで末尾の 1 文字が落ちたということです。長さが 4 で割って 1 余る文字列は、どんなにパディングしても直りません。データは単にそこにないだけです。正直な答えは、ペイロードをもう一度頼むことです。
静かなゴミ。 寛大モードは生き残ったものを何でもデコードし、普通の英語の単語は Base64 アルファベットの文字で満ちているので、ペイロードの前に迷い込んだ単語は、あなたのデータにくっついた本物のバイトになります:
import base64
print(base64.b64decode("junkZm9vYmFy"))
# b'\x8e\xe9\xe4foobar' - 純粋なフィクションの 3 バイト、そして真実
残りの 6 つにはコードすら不要です:
- base64url の文字列を標準デコーダーでデコードした。 ダッシュとアンダースコアは標準アルファベットにないので、黙って消え、ペイロードは壊れたまま、あるいは空のまま出てきます。
urlsafe_b64decodeをパディング修正付きで使ってください。 - 結果がバイトだということを忘れた。 文字列にくっつけると
TypeErrorが起き、JSON レスポンスに突っ込むとb'...'の表記が直列化されます。境界で、意図的に、本当に意味するエンコーディングで.decode(encoding)を呼びましょう。 - ASCII 以外の文字列を渡した。 デコーダーは文字列を受け取りますが、ASCII のものだけです。それ以外は
ValueErrorになります。ペイロードが誤ったエンコーディングで読んだテキストファイルから出たものなら、デコードではなく読み取りを直してください。 - 2 回デコードした。 データは上流ですでにデコード済みだったか、Base64 の Base64 だったかで、2 回目の試行があなたのパスワードを、もう誰も読み返さない 6 バイトに変えてしまいました。
- 折り返されたデータに厳格モードを使った。
validate=Trueを発火させるのに改行 1 つで十分なので、MIME ブロックや PEM のボディは寛大側のツールのものであって、厳格側のものではありません。 - 真ん中のパッドを信じた。 寛大モードでは文字列のどこにあろうと
=は黙って破棄されるので、パッドの位置がずれた壊れたペイロードは「正しい」答えにデコードできてしまいます。気づくのは厳格モードだけで、しかも気づき方は拒否です。
あなたの仕事が門番であるなら、2 つの気分を一緒に働かせる小さなヘルパーがこちらです:先に厳格、次にパディング修正、どちらもダメなら大きな音で失敗:
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 に流してください。検証は契約であり、その契約はデータがどの方言かを言っています。
静かなモジュールの 30 年
このモジュールは標準ライブラリに四半世紀入り、そのほとんどを座りっぱなしで過ごしました。動いたときは、動きは小さかったけれど実在し、古いフォーラムを漂う「うちは動くんだけど」の話をいくつか説明してくれます:
- 1995 - Jack Jansen が
base64.pyを書き直し、本業を C レベルのbinasciiモジュールに委託しました。コメントはファイルに今も残っており、その委託は今日でも真です。 - 2003、Python 2.4 に同梱 - Barry Warsaw が 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がようやく削除されました。それらを呼び出す古いチュートリアルには、1 語の名前変更が必要です。 - Python 3.10 -
b32hexencodeとb32hexdecodeが拡張 16 進アルファベットとともに到着しました。エンコードされたデータを辞書順でソートし続けられるのがこのアルファベットです。 - Python 3.11 -
binascii.a2b_base64がstrict_modeを獲得しました。validate=Trueが内部で乗っているのはこれです。 - Python 3.13 -
z85encodeとz85decodeが ZeroMQ の Z85 方言を標準ライブラリに持ち込み、大昔のuuモジュールは PEP 594 の下で削除され、代わりにbase64を使うよう指摘するコメントが付いていました。 - Python 3.14 -
b16decodeが最大 6 倍速くなりました:検証は正規表現ではなくbytes.translateで走るようになり、モジュールはもはやreを一切インポートしなくなりました。そのインポート時間も、改善されたモジュールのリストに載っています。
このどれもの関数の挙動を変えません。これがこれほど古いモジュールの静かな贅沢です:2005 年に Base64 をデコードしたコードは、2026 年でも同じ行で、同じ結果でデコードしています。
余白からの喜び
真面目な仕事は終わったので、ここからはモジュールが余白に隠した小さな喜びたちです:
- モジュール自身のドキュメントは 10 年以上、同じデモを回し続けています:
b'data to be encoded'が入って、b'ZGF0YSB0byBiZSBlbmNvZGVk'が出てくる。ここ 20 年のどの Python リリースの base64 ページを読んだことがあっても、このペアにはすでに会っているはずです。 - 単語
junkは完全に有効な Base64 文字列です。4 文字すべてがアルファベットに入っているため、ペイロードの先頭に迷い込んだ単語はエラーではなく 3 バイトのフィクションになり、寛大モードがその愛称を稼いでいるのもそのためです。 urlsafe_b64decodeは偶然にバイリンガルです。まずアルファベットを翻訳してから寛大にデコードするので、+と/を含む標準アルファベットの文字列も読むことができます。関数 1 つ、方言 2 つ、文句ゼロ。- エラーメッセージは、C 実装の日以来動いていない安定したミニ辞書です:
Incorrect padding、Only base64 data is allowed、Excess padding not allowed、Leading padding not allowed。これらを覚えれば、コードを 1 行も実行せずに壊れたペイロードを切り分けられます。 - 空の文字列は、反応がまったく得られない唯一の入力です:
b''が入ってb''が出て、2 つの気分のどちらも同じ。何も入って、何も出ず、警報もなし。 - モジュールの ドクストリング はまだ 2003 年版仕様の RFC 3548 名を掲げています。現行の標準は 2006 年から RFC 4648 ですが、モジュールはそれに忠実に従いながら、その文を更新する手間を惜しんでいます。
- Python 2 のデコード側には型の壁がなかった:プレーンな
strが入って、プレーンなstrが出てくる。Python 3 開発中の 2007 年のバイト大刷新がそれを代え、「なぜ私のデコードが壊れるんだ」スレッドのほとんどが、今も古い Python 2 チュートリアルを指しています。
そこで、哲学全体を 4 つの規則にまとめます。自分でエンコードしていないものには validate=True を渡し、例外を提案ではなく本物の答えとして扱ってください。あなたが手にしているのがどの方言かを知っておいてください:標準、base64url、MIME 折り返しのどれか。デコーダーが教えてくれることはなく、合わないものを落とすことで推測するだけだからです。結果を、それがテキストだと証明できるまでバイトとして扱い、それから文字コードの所有者を問うてください。そして覚えておいてください。この関数の最も親しみやすい特徴、すなわち Base64 としては少しずれているものでもデコードしようとする意志は、それを危険にしているのと同一の特徴です。だから毎回、その呼び出しごとに、入力がどれだけの信頼を稼いでいるかを決定してください。
ある時点で逆向きに行きたくなったら、トークン、添付ファイル、インライン画像のために新しいバイトをその親しみやすい文字のリボンに再び包むなら、b64encode の物語全体は、このページの下の関連する Base64 エンコード記事で詳しく扱われています。2 つの方向は鏡像ですが、それぞれに独自のサプライズのセットがあります。そしてあなたはもう、この方のことを暗記しています。良いデコードを。
最終更新: 2026-10-09