HOWTO · Python
Python でファイルにバイトを書き込む
このチュートリアルでは、Python でバイナリファイルにバイトを書き込む方法を示します。
このページの内容
Pythonでファイルにバイトを書き込むには、open() を wb モードで呼び出し、bytes-like オブジェクトをファイルの write() メソッドに渡します。with ブロックを使うと、ファイルは自動的に閉じられます。既存の内容に追記する場合は、代わりに ab を使います。
各コード例は一時ディレクトリにファイルを作成します。with ブロックが終わると、Pythonがそのディレクトリを削除します。ファイルを残すには、一時パスを保存先のパスに置き換えてください。
Pythonでファイルにバイトを書き込む方法
wb の w は、ファイルを書き込み用に開くことを示します。ファイルがなければ作成し、既存のファイルは開いた時点で切り詰めます。b はバイナリモードを選択するため、Pythonはテキストのエンコードや改行の変換をせずにバイトを書き込みます。
次の例では、データを書き込んでから読み戻します。
このセクションのコードと表示されるラベルや出力は英語のままです。
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
path = Path(directory) / "example.bin"
data = b"This is some binary data."
with open(path, "wb") as file:
written = file.write(data)
with open(path, "rb") as file:
saved = file.read()
print(written)
print(saved == data)
25
True
通常のバッファ付きファイルでは、write() は書き込んだバイト数を返します。このプログラムはその数を表示し、保存したデータと元のデータを比較します。読み取りは、書き込み用のブロックがファイルを閉じてPythonのバッファをフラッシュした後に行われます。.bin 拡張子はファイル名の慣例であり、バイナリモードはモード引数によって決まります。
ファイルがまだ存在しない場合に限って作成したいときは、xb を使います。wb とは異なり、保存先がすでに存在すると上書きせず、エラーになります。どちらのモードも、存在しない親ディレクトリは作成しません。
複数のバイト列をファイルに書き込む
連続するチャンクを書き込む間は、ファイルを開いたままにします。各 write() は現在の位置から書き込みを続けます。ループ内でファイルを wb モードで開くと、それまでに書き込んだチャンクが破棄されます。
このセクションのコードと表示されるラベルや出力は英語のままです。
from pathlib import Path
from tempfile import TemporaryDirectory
chunks = [b"First line.\n", b"Second line.\n", b"Third line.\n"]
with TemporaryDirectory() as directory:
path = Path(directory) / "chunks.bin"
with open(path, "wb") as file:
for chunk in chunks:
file.write(chunk)
各チャンクには、bytes、bytearray、適切なバッファビューなどの bytes-like データを使います。バイト列のイテラブルを file.writelines(chunks) に渡すこともできます。名前とは異なり、writelines() は区切り文字や改行を追加しません。必要な場合はチャンクに含めてください。ループを使えば、ペイロード全体をメモリ上で結合せず、到着したチャンクを順に処理できます。
bytearray をファイルに書き込む
bytes オブジェクトは変更できません。一方、bytearray では、保存前に個々のバイトを変更したり、列を拡張したりできます。どちらもバイナリファイルに直接書き込めます。
このセクションのコードと表示されるラベルや出力は英語のままです。
from pathlib import Path
from tempfile import TemporaryDirectory
payload = bytearray(b"Line 1\n")
payload.extend(b"Line 2\n")
payload[0] = ord("l")
with TemporaryDirectory() as directory:
path = Path(directory) / "bytearray.bin"
with open(path, "wb") as file:
file.write(payload)
ここでは、extend() で別の列を追加し、インデックスによる代入で先頭のバイトを変更しています。書き込みのために bytes(payload) へ変換する必要はありません。ほかの場所で不変の値が必要な場合に変換してください。後から bytearray を変更しても、ファイルの内容は自動では更新されません。
BytesIO のデータをファイルに書き込む
io モジュールの BytesIO は、メモリ上のバイナリストリームを提供します。getbuffer() メソッドは、内容を別の bytes オブジェクトにコピーせず、基になるバッファ全体のビューを返します。
このセクションのコードと表示されるラベルや出力は英語のままです。
from io import BytesIO
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
path = Path(directory) / "buffer.bin"
with BytesIO() as buffer:
buffer.write(b"Header\n")
buffer.write(b"Payload\n")
with buffer.getbuffer() as view:
with open(path, "wb") as file:
file.write(view)
このビューは、ストリームの現在位置にかかわらず、バッファ内のすべてのデータを対象にします。ビューが存在する間は、BytesIO オブジェクトのサイズ変更やクローズはできません。入れ子になったコンテキストマネージャーを使うと、ストリームを閉じる前にビューが解放されます。代わりに bytes 値が必要な場合は buffer.getvalue() を使います。buffer.read() でコピーする場合、先頭から読み始めるには、先に buffer.seek(0) を呼び出してください。
バイトの追記と位置を指定した書き込み
既存の内容を切り詰めずに末尾へバイトを追加するには、ab で開きます。このモードでは、保存先が存在しない場合も作成されます。既存ファイルの特定のオフセットにあるバイトを上書きするには、r+b を使い、ファイルオブジェクトの seek() メソッドで位置を指定します。
このセクションのコードと表示されるラベルや出力は英語のままです。
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
path = Path(directory) / "records.bin"
with open(path, "wb") as file:
file.write(b"ABCDEF")
with open(path, "ab") as file:
file.write(b"GH")
with open(path, "r+b") as file:
file.seek(2)
file.write(b"xy")
seek(2) は、先頭から数えてバイトオフセット2に移動します。次の書き込みでは、データを挿入したり残りの末尾を削除したりせず、2バイトを置き換えます。短いデータで置き換えた後に末尾のバイトを削除する必要がある場合は、明示的に truncate() を使います。
r+b は既存ファイルを必要とし、開いたときにその内容を保持します。w+b は読み書きできますが、最初にファイルを切り詰めます。位置を指定して更新するときに追記モードを使うのは避けてください。書き込みは末尾に向けられるため、シークによってその動作を確実に変えることはできません。末尾を越えてシークしただけではファイルは拡張されません。その後に書き込めば拡張されることがありますが、空き領域の扱いはファイルシステムによって異なります。
バッファリングと大きなデータの書き込み
open() で開いた通常のバイナリファイルは、デフォルトでバッファリングされます。buffering=8192 のように明示するとバッファサイズを指定できますが、書き込みが速くなる保証はなく、各OS書き込みのサイズが必ず8192バイトになるわけでもありません。
大きなデータを生成する場合は、全体を1つの大きなオブジェクトにまとめるのではなく、再利用できるチャンクに分けて書き込みます。
このセクションのコードと表示されるラベルや出力は英語のままです。
from pathlib import Path
from tempfile import TemporaryDirectory
chunk = b"record\n" * 1024
with TemporaryDirectory() as directory:
path = Path(directory) / "large.bin"
with open(path, "wb", buffering=8192) as file:
for _ in range(100):
file.write(chunk)
この例では、任意のバッファ設定を指定し、1つのチャンクを再利用しています。バッファサイズは実際の処理を測定して選んでください。どの状況にも最適なサイズがあるわけではありません。ファイルを閉じると、Pythonのバッファがフラッシュされます。先に flush() を呼ぶと、バッファ内のデータが基になるストリームに渡されますが、それだけで物理ストレージへの永続化が保証されるわけではありません。
バイトの書き込みに関するよくある質問
書き込んだバイトを読み戻すには? ファイルを rb で開き、file.read() を呼び出します。戻り値は bytes です。大きなファイルでは、すべてを一度に読み込まず、一定サイズのチャンクに分けて読み取ります。
バイナリモードでテキストを書き込めますか? 先にエンコードしてください。たとえば text.encode("utf-8") や bytes(text, "utf-8") を使います。テキストに戻す側では、対応するエンコードを使う必要があります。テキストだけを保存する場合は、encoding="utf-8" を指定してテキストモードで開くほうが分かりやすいことがあります。
write() が TypeError を報告するのはなぜですか? バイナリストリームには bytes-like データが必要です。str、整数、リストをそのまま渡しても要件を満たしません。テキストはエンコードするか、整数のバイト値の列を bytes(values) で変換してください。各整数は0から255までの範囲でなければなりません。
空のペイロードを書き込むとどうなりますか? b"" を書き込んでもバイトは追加されませんが、既存ファイルを wb で開いた時点ですでに切り詰められています。新しいペイロードが空の場合も含め、追記時に既存データを保持するには ab を使います。
保存先が作成されなかったのはなぜですか? 親ディレクトリが存在することと、プロセスに書き込み権限があることを確認してください。相対パスはプロセスの現在の作業ディレクトリを基準に解決されます。アプリケーションコードでは該当する OSError を処理してください。ファイルを開けた後でも、書き込みやクローズが失敗することがあります。