APIのドキュメントを読んでいて、画像はBase64でエンコードして送ってください、と書かれていたことはありませんか。メールの添付ファイルやWebページの中でも、Base64はあちこちで使われています。
名前はよく見かけるのに、何をしているのかはよくわからない。そんな方も多いのではないでしょうか。
Pythonなら、標準ライブラリのbase64を使えばすぐに扱えます。pip installは要りません。
今回は、Base64の仕組みと基本の使い方に加えて、初心者がよく誤解しがちな、Base64は暗号ではないという点まで順番に見ていきます。
Base64は、バイナリを文字だけで表すための変換方法¶
コンピューターの中のデータは、すべて0と1の並び、つまりバイナリです。画像や音声のファイルには、文字として表示できないバイトがたくさん含まれています。
一方で、メールやJSON、URLのように、文字しか通せない場所がたくさんあります。そこへバイナリをそのまま流すと、途中で壊れたり、うまく読み取れなかったりします。
そこで考えられたのがBase64です。どんなバイナリでも、英字と数字、それに数個の記号だけの文字列に置き換えます。
Base64の決まりは、RFC 4648というインターネットの標準文書で定められています。使う文字は次の65種類です。
| 種類 | 文字 | 数 |
|---|---|---|
| 英大文字 | A〜Z | 26 |
| 英小文字 | a〜z | 26 |
| 数字 | 0〜9 | 10 |
| 記号 | + と / | 2 |
| 埋め草 | = | 1(長さ合わせ専用) |
最後の=は、データの長さをそろえるための埋め草で、パディングと呼ばれます。実質的には64種類の文字でデータを表すので、Base64という名前になっています。
まずはエンコードとデコードを試してみよう¶
さっそく使ってみましょう。Base64に変換することをエンコード、元に戻すことをデコードと呼びます。
ここで大事なのは、base64モジュールが扱うのは文字列ではなくバイト列だという点です。日本語の文字列は、先にUTF-8などでバイト列にしてから渡します。
import base64
data = "こんにちは".encode("utf-8")
encoded = base64.b64encode(data)
print(data, len(data))
print(encoded, len(encoded))
decoded = base64.b64decode(encoded)
print(decoded.decode("utf-8"))
手元のPython 3.11で実行すると、次のように表示されました。
b'\xe3\x81\x93\xe3\x82\x93\xe3\x81\xab\xe3\x81\xa1\xe3\x81\xaf' 15
b'44GT44KT44Gr44Gh44Gv' 20
こんにちは
15バイトのデータが、20文字の英数字に変わりました。デコードすれば、ちゃんと元のこんにちはに戻っています。
文字列とバイト列の違いや、UTF-8について自信がない方は、先にこちらを読んでおくと理解が深まります。【関連記事】Pythonの文字コードとは?
結果もバイト列で返ってくる¶
b64encodeの結果も、文字列ではなくバイト列です。先頭にbが付いているのはそのためです。
JSONに入れるなど、普通の文字列として使いたいときは、.decode("ascii")で文字列に直します。Base64の結果は英数字と記号だけなので、asciiで安全に変換できます。
ファイルをまるごとBase64にする¶
画像などのファイルも、考え方は同じです。pathlibのread_bytes()でファイルをバイト列として読み込み、そのまま渡します。
import base64
from pathlib import Path
data = Path("logo.png").read_bytes()
text = base64.b64encode(data).decode("ascii")
print(len(data), len(text))
# 元のファイルに戻す
Path("logo_copy.png").write_bytes(base64.b64decode(text))
1024バイトのテスト用ファイルで試すと、Base64の文字列は1368文字になりました。戻したファイルは、元のファイルと1バイトも違わず一致しています。
サイズが約4/3倍になる理由¶
先ほどの例では、15バイトが20文字になりました。この増え方には、きちんとした理由があります。
Base64は、3バイト、つまり24ビットを1つのまとまりとして扱います。それを6ビットずつ4つに分け、それぞれを64種類の文字の1つに置き換えます。
6ビットで表せるのは、ちょうど0から63までの64通りです。3バイトが4文字になるので、サイズは約4/3倍、およそ33%増えることになります。
実際に、いろいろな長さのデータで確かめてみました。
| 元のバイト数 | Base64の文字数 |
|---|---|
| 1 | 4 |
| 2 | 4 |
| 3 | 4 |
| 30 | 40 |
| 300 | 400 |
| 3000 | 4000 |
1バイトや2バイトでも4文字になっているのは、パディングの=で長さを4の倍数にそろえているからです。たとえばb"P"はUA==、b"Py"はUHk=、b"Python"はUHl0aG9uになりました。
大きなファイルには向かない¶
この増え方は、大きなファイルでは無視できません。10MBの画像なら、Base64にすると13MB以上になります。
私は10年ほどエンジニアとして開発に関わってきましたが、画像をBase64のままデータベースに保存していたせいで、容量と処理速度の両方に悩まされた現場を見たことがあります。大きなファイルは、ファイルとして保存して場所だけを記録するほうが、たいていうまくいきます。
URLで使うときは、urlsafe版を選ぶ¶
標準のBase64には、+と/が含まれます。ところがこの2つは、URLの中では特別な意味を持つ記号です。
そこでRFC 4648では、+を-に、/を_に置き換えたURL用の変種も定めています。Pythonではurlsafe_b64encodeとurlsafe_b64decodeがそれにあたります。
違いがわかりやすいバイト列で比べてみます。
import base64
raw = bytes([0xFB, 0xFF, 0xFE])
print(base64.b64encode(raw))
print(base64.urlsafe_b64encode(raw))
実行結果は次のとおりです。同じデータでも、使われる記号が変わっています。
b'+//+'
b'-__-'
URLのパラメーターやファイル名にBase64を使うなら、urlsafe版を選びましょう。エンコードとデコードで種類をそろえないと、元に戻せなくなる点にも注意してください。
base64モジュールの主な関数¶
ここまで出てきたものを含めて、よく使う関数を表にまとめます。どれも引数と戻り値はバイト列です。
| 関数 | 役割 |
|---|---|
b64encode(s) |
標準のBase64にエンコードする |
b64decode(s) |
標準のBase64からデコードする |
urlsafe_b64encode(s) |
URL用の変種にエンコードする |
urlsafe_b64decode(s) |
URL用の変種からデコードする |
b32encode(s) / b16encode(s) |
32種類、16種類の文字を使う変種 |
迷ったら、b64encodeとb64decodeの2つを覚えておけば十分です。そのほかは、相手の仕様で指定されたときに使えば問題ありません。
Incorrect paddingエラーの直し方¶
Base64を扱っていると、よく出会うエラーがあります。binascii.Error: Incorrect paddingです。
これは、デコードしようとした文字列の長さが4の倍数になっていないときに出ます。コピーの途中で末尾の=が切れてしまった、というのがよくある原因です。
import base64
import binascii
try:
base64.b64decode("abc")
except binascii.Error as e:
print(type(e).__name__, e)
実行すると、Error Incorrect paddingと表示されました。この例では、末尾に=を1つ補って"abc="にすると、正しくデコードできます。
ただし、むやみに=を足してごまかすのはおすすめしません。まずは、データが途中で欠けていないかを疑いましょう。
余計な文字は黙って捨てられる¶
もう1つ知っておきたいのが、b64decodeは初期設定のままだと、Base64に使われない文字を黙って捨ててしまう点です。公式ドキュメントにも、この動きが説明されています。
不正な文字が混ざっていたらエラーにしたいときは、validate=Trueを付けます。外部から受け取ったデータを扱うときは、こちらを使うと安心です。
Base64は暗号ではない¶
ここがいちばん大切なポイントです。Base64でエンコードした文字列は、一見すると意味のわからない文字の並びに見えます。
けれども、Base64には鍵も秘密もありません。誰でもb64decodeを1回呼ぶだけで、元に戻せてしまいます。
たとえば、Webの古いログイン方式であるBasic認証は、ユーザー名とパスワードをコロンでつないでBase64にしたものを送ります。user:passならdXNlcjpwYXNzです。
これを見て、パスワードが隠されていると考えるのは危険です。RFC 7617でも、Basic認証は通信路が暗号化されていないと安全ではないと注意されており、HTTPSと組み合わせるのが前提になっています。
似た言葉が多いので、違いを表で整理しておきます。
| 方式 | 元に戻せるか | 目的 | Pythonの例 |
|---|---|---|---|
| エンコード(Base64) | 誰でも戻せる | データを運びやすい形にする | base64 |
| 暗号化 | 鍵を持つ人だけ戻せる | 中身を秘密にする | cryptographyなどの外部ライブラリ |
| ハッシュ | 戻せない | 改ざんの検出やパスワードの保存 | hashlib |
パスワードを保存するときに使うべきなのは、Base64ではなくハッシュです。その理由は、こちらで詳しく解説しています。【関連記事】Pythonのhashlibとは?
以前、設定ファイルのパスワードがBase64にしてあるから大丈夫、という説明を受けたことがあります。デコードして見せると、その場の空気が一瞬で変わりました。
実務でよく出会う使い方¶
最後に、Base64が実際にどんな場面で使われているかを見ておきましょう。知っておくと、ドキュメントで出会ったときに慌てずに済みます。
1つ目は、Web APIで画像やファイルを送る場面です。JSONは文字しか入れられないので、画像をBase64の文字列にして入れることがよくあります。
JSONの扱い方は、こちらで紹介しています。【関連記事】PythonでJSONを扱う方法
2つ目は、HTMLやCSSの中に小さな画像を直接埋め込む場面です。data:image/png;base64,に続けてBase64の文字列を書くと、画像ファイルを別に用意しなくても表示できます。
3つ目は、先ほど紹介したBasic認証などのHTTPのヘッダーです。requestsでAPIを呼ぶときにも、裏側でBase64が使われていることがあります。【関連記事】Pythonのrequestsとは?
まとめ¶
Base64は、バイナリを英数字と少しの記号だけの文字列に変える仕組みです。Pythonではbase64モジュールのb64encodeとb64decodeで、バイト列を相手に手軽に扱えます。
サイズが約4/3倍に増えること、URLではurlsafe版を使うこと、そして暗号ではないこと。この3つを押さえておけば、実務で困ることはほとんどありません。
次にAPIのドキュメントでBase64という言葉を見かけたら、ぜひ手元で1行エンコードしてみてください。仕組みがわかると、もうおそれる必要はありません。
ここまでお読みいただきありがとうございました。