StringIO 和 BytesIO
StringIO 和 BytesIO 把内存中的数据包装成“像文件一样”的对象:前者读写 str,后者读写 bytes。因此,原本接收文件对象的代码,也可以处理内存数据,而不必真的创建临时文件。
from io import StringIO
stream = StringIO()
stream.write("Python")
stream.write(" IO")
print(stream.getvalue()) # Python IO
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 根据数据类型选择
StringIO或BytesIO; - 像操作文件一样读写、定位和截断内存流;
- 理解文件指针、
getvalue()和关闭行为; - 在文本、字节和真实文件对象之间正确转换;
- 使用内存流完成测试、格式拼接和数据转换。
什么是内存流
普通文件对象把数据保存在磁盘中,内存流则把数据保存在当前 Python 进程的内存中。两者都提供 read()、write()、seek()、tell() 等文件接口。
| 对象 | 保存的数据 | 读写类型 | 是否访问磁盘 |
|---|---|---|---|
| 文本文件 | 磁盘字节 | str | 是 |
| 二进制文件 | 磁盘字节 | bytes | 是 |
StringIO | 内存文本 | str | 否 |
BytesIO | 内存字节 | bytes | 否 |
内存流适合以下场景:
- 测试一个需要文件对象的函数;
- 暂存程序生成的文本或二进制数据;
- 捕获本来要写入文件的输出;
- 在写入真实文件、网络请求或压缩模块之前组装数据。
内存流不能替代持久化存储。程序结束后,其中的数据就会消失;数据很大时,它也可能占用大量内存。
使用 StringIO 读写文本
创建并写入
StringIO() 创建一个空文本流,write() 从当前文件指针处写入字符串,并返回写入的字符数:
from io import StringIO
stream = StringIO()
count = stream.write("第一行\n")
stream.write("第二行\n")
print(count) # 4
print(stream.getvalue()) # 返回全部文本
和文本文件一样,write() 不会自动添加换行符。需要格式化输出时,可以让 print() 写入流:
from io import StringIO
stream = StringIO()
print("姓名", "成绩", sep=",", file=stream)
print("小林", 95, sep=",", file=stream)
csv_text = stream.getvalue()
print(csv_text)
使用初始文本
创建对象时可以直接传入初始字符串。此时文件指针位于开头,可以立即读取:
from io import StringIO
stream = StringIO("Python\nJava\nGo\n")
print(stream.readline().rstrip()) # Python
print(stream.readline().rstrip()) # Java
还可以像遍历文本文件一样逐行处理:
from io import StringIO
stream = StringIO("10\n20\n30\n")
total = sum(int(line) for line in stream)
print(total) # 60
文件指针
内存流也有文件指针。读写完成后,指针会移动;tell() 返回当前位置,seek() 负责移动位置。
from io import StringIO
stream = StringIO()
stream.write("hello")
print(stream.tell()) # 5,指针在末尾
print(stream.read()) # 空字符串,因为后面没有内容
stream.seek(0)
print(stream.read()) # hello
这是初学时最常见的问题:写完后直接 read() 得不到刚写入的数据。解决方法是先调用 seek(0) 回到开头,或者直接使用不受当前指针影响的 getvalue()。
从指定位置读写
seek(position) 可以移动到指定位置,之后的写入会从那里开始,并可能覆盖已有内容:
from io import StringIO
stream = StringIO("Hello Python")
stream.seek(6)
stream.write("World")
print(stream.getvalue()) # Hello Worldn
上例只覆盖了 5 个字符,原来末尾的 n 仍然存在。若要丢弃当前位置之后的内容,可以调用 truncate():
from io import StringIO
stream = StringIO("Hello Python")
stream.seek(6)
stream.write("World")
stream.truncate()
print(stream.getvalue()) # Hello World
truncate(size=None) 用来截断流;省略 size 时,以当前指针位置作为截断点。它不会移动当前文件指针。
getvalue() 与 read() 的区别
| 方法 | 返回内容 | 是否移动指针 |
|---|---|---|
read() | 从当前位置读取到末尾 | 是 |
read(size) | 从当前位置最多读取指定长度 | 是 |
getvalue() | 返回缓冲区的全部内容 | 否 |
如果后续逻辑仍依赖当前指针,使用 getvalue() 获取完整结果通常更直接。
使用 BytesIO 读写字节
BytesIO 的接口与 StringIO 类似,但只接受和返回 bytes。它适合图片、压缩数据、协议报文等二进制内容。
from io import BytesIO
stream = BytesIO()
stream.write(b"\x50\x59")
stream.write(bytes([84, 72, 79, 78]))
print(stream.getvalue()) # b'PYTHON'
也可以使用初始字节创建流:
from io import BytesIO
stream = BytesIO(b"abcdef")
print(stream.read(3)) # b'abc'
print(stream.tell()) # 3
stream.seek(0)
print(stream.read()) # b'abcdef'
BytesIO 的位置按字节计算,所以很适合需要随机定位的二进制格式。
文本与字节之间的转换
StringIO 不能写入 bytes,BytesIO 也不能写入 str:
from io import BytesIO, StringIO
text_stream = StringIO()
binary_stream = BytesIO()
# text_stream.write(b"hello") # TypeError
# binary_stream.write("hello") # TypeError
文本变为字节需要编码,字节变为文本需要解码:
text = "你好,Python"
data = text.encode("utf-8")
restored = data.decode("utf-8")
print(data)
print(restored)
不要依靠默认编码;只要数据格式可控,就明确指定 UTF-8 或协议要求的编码。
用 TextIOWrapper 包装二进制流
某些接口提供二进制流,但调用方希望使用文本接口。可以用 TextIOWrapper 增加编码和换行转换层:
from io import BytesIO, TextIOWrapper
buffer = BytesIO()
with TextIOWrapper(buffer, encoding="utf-8") as text_stream:
text_stream.write("你好")
text_stream.flush()
data = buffer.getvalue()
print(data) # b'\xe4\xbd\xa0\xe5\xa5\xbd'
flush() 确保文本包装层的缓冲数据已经写入底层 BytesIO。退出 with 后,TextIOWrapper 默认也会关闭底层流,所以应在退出前取得结果。如果还要继续使用底层流,可以先调用 detach(),但分离后的包装对象不能再使用。
关闭内存流
内存流也是上下文管理器,可以使用 with 自动关闭:
from io import StringIO
with StringIO() as stream:
stream.write("temporary data")
result = stream.getvalue()
print(result)
print(stream.closed) # True
关闭后再调用 read()、write() 或 getvalue() 都会抛出 ValueError。因此,需要保留的数据应在离开 with 之前取出。
短小函数也可以显式创建内存流并返回 getvalue() 的结果;对象不再被引用后会由 Python 回收。对于生命周期较长或由多层代码传递的流,使用 with 更清晰。
常见应用
测试接收文件对象的函数
让业务函数接收“文件对象”而不是固定路径,测试时就能传入 StringIO:
from io import StringIO
from typing import TextIO
def count_nonempty_lines(source: TextIO) -> int:
return sum(1 for line in source if line.strip())
fake_file = StringIO("Python\n\nGo\n")
print(count_nonempty_lines(fake_file)) # 2
生产代码可以传入真实文件,测试代码则不需要创建和清理临时文件:
with open("languages.txt", encoding="utf-8") as file:
count = count_nonempty_lines(file)
这种设计通常称为面向文件接口编程。
捕获标准输出
contextlib.redirect_stdout() 可以把 print() 的输出临时重定向到 StringIO:
from contextlib import redirect_stdout
from io import StringIO
output = StringIO()
with redirect_stdout(output):
print("任务开始")
print("任务完成")
print(output.getvalue())
它适合测试命令行输出。多线程程序中重定向全局标准输出可能影响其他线程,需要谨慎使用。
在内存中生成二进制结果
许多处理图片、压缩包或网络上传的库都接受二进制文件对象。下面使用标准库在内存中创建 gzip 数据:
import gzip
from io import BytesIO
buffer = BytesIO()
with gzip.GzipFile(fileobj=buffer, mode="wb") as gzip_file:
gzip_file.write("需要压缩的文本".encode("utf-8"))
compressed_data = buffer.getvalue()
print(len(compressed_data))
关闭 GzipFile 不会关闭由调用方传入的 buffer,因此之后仍可读取压缩结果。不同库对底层流的关闭规则可能不同,应查看对应 API 文档。
性能与限制
- 内存流省去了临时磁盘文件,但数据仍要占用进程内存;大文件应使用真实文件或分块流式处理。
- 频繁拼接少量字符串时,列表加
"".join(parts)往往更简单;需要文件接口或持续格式化写入时再选择StringIO。 StringIO保存的是 Python 文本,不负责字符编码;需要最终字节格式时必须编码。- 内存流通常不应由多个线程同时修改。需要并发访问时,应在外部同步或为每个任务创建独立对象。
常见错误
写完后直接读取
写入会让指针停在末尾。使用 seek(0) 后再读,或使用 getvalue() 获取全部内容。
混用 str 和 bytes
根据数据类型选择流。跨越文本与二进制边界时,明确调用 encode() 或 decode()。
关闭后才获取结果
getvalue() 不能读取已关闭的流。使用 with 时,应在代码块内部保存结果。
把内存流当作持久化文件
内存流没有文件路径,程序退出后数据不会保留。需要长期保存时,最终仍要写入文件、数据库或远程存储。
小结
StringIO在内存中读写str,BytesIO在内存中读写bytes;- 两者提供与文件对象相似的
read()、write()、seek()、tell()和truncate(); read()从当前指针读取并移动指针,getvalue()返回全部内容且不移动指针;- 文本与字节之间通过明确编码的
encode()和decode()转换; - 内存流适合测试、捕获输出和组装中小型数据,不适合存储超大数据或长期持久化。