HOWTO · Python

用 Python 向文件写入字节

本教程演示了如何在 Python 中向二进制文件写入字节。

本页内容

在 Python 中向文件写入字节时,使用 open() 并以 wb 模式打开文件,然后将类字节对象传给文件的 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 或合适的缓冲区视图。也可以将字节序列的可迭代对象传给 file.writelines(chunks)。虽然名称中有“lines”,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 个字节偏移处。接下来的写入会替换两个字节,不会插入数据,也不会删除后续内容。如果替换内容较短且需要删除末尾多余字节,请显式调用 truncate()。

r+b 要求文件已存在,并且打开时会保留原有内容。w+b 支持读写,但会先清空文件。定位更新时不要使用追加模式:写入会被定向到文件末尾,依靠定位来覆盖这一行为并不具备可移植性。仅将位置移到文件末尾之后不会扩展文件;后续写入可以扩展文件,而间隙的存储细节取决于文件系统。

缓冲与大数据写入

通过 open() 打开的常规二进制文件默认启用缓冲。显式设置 buffering=8192 之类的参数可以请求缓冲区大小,但这不保证写入更快,也不表示每次操作系统写入都恰好包含 8192 字节。

对于生成的大量数据,可以重复写入可复用的数据块,而不是先构造一个很大的合并对象。代码块中的英文字符串以及显示的标签或输出均保留英文:

本节中的代码以及显示的标签或输出仍为英文。

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)

此示例展示了可选的缓冲设置,并重复使用同一个数据块。应根据实际工作负载测量并选择缓冲区大小;不存在普遍适用的最佳大小。关闭文件时会刷新 Python 缓冲区。提前调用 flush() 会将缓冲数据传给底层流,但这本身并不保证数据已持久化到物理存储设备。

关于写入字节的常见问题

如何读回字节? 使用 rb 打开文件并调用 file.read()。返回值是 bytes。对于大文件,应分块读取固定大小的数据,而不是一次性全部载入。

可以在二进制模式下写入文本吗? 可以,但要先编码,例如使用 text.encode("utf-8") 或 bytes(text, "utf-8")。读取方必须使用对应的编码才能还原文本。如果只需要存储文本,使用文本模式并设置 encoding="utf-8" 可能更清楚。

为什么 write() 会报告 TypeError? 二进制流要求输入类字节数据。直接传入 str、整数或列表都不符合要求。请先编码文本,或使用 bytes(values) 转换由整数表示的字节值序列;每个整数都必须在 0 到 255 之间。

空数据载荷会怎样? 写入 b"" 不会写入任何字节,但如果以 wb 打开已有文件,文件在写入前就已经被清空。追加时使用 ab 可以保留已有数据,即使新数据载荷为空也一样。

为什么没有创建目标文件? 请检查父目录是否存在,以及当前进程是否有写入权限。相对路径是相对于进程的当前工作目录解析的。在应用代码中处理相关的 OSError;即使文件已成功打开,写入或关闭时仍可能失败。