跳到主要内容

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() 转换;
  • 内存流适合测试、捕获输出和组装中小型数据,不适合存储超大数据或长期持久化。