Python(三十四) 文件与 IO 模块完整教程StringIO 与 BytesIO:内存中的"文件"
想象你有一个笔记本:
| | |
|---|
| open() | 输出(Output) |
| open() | 输入(Input) |
| close() | |
| | |
| | |
IO = Input(输入)+ Output(输出),就是程序和"外部世界"交换数据的过程。
学完本教程后,你将能够:
用 StringIO / BytesIO 在内存中模拟文件操作用 os 和 pathlib 创建、删除、遍历文件和目录用 json 和 pickle 保存和恢复 Python 数据
open("文件名", "模式") 的第二个参数决定了你能对文件做什么。
| | | | |
|---|
"r" | | 报错FileNotFoundError | | |
"r+" | | 报错 | | |
"w" | | 自动创建新文件 | 清空原内容 | |
"w+" | | 自动创建新文件 | 清空原内容 | |
"a" | | 自动创建新文件 | | |
"a+" | | 自动创建新文件 | | |
"x" | | 自动创建新文件 | 报错FileExistsError | |
在以上模式后加 b,就变成了二进制模式,用于处理图片、视频、音频等非文本文件:
原内容: ABCDEFG → 追加后: ABCDEFG123原内容: ABCDEFG → 写入 123 → 文件变成: 123DEFGwithopen("test.txt", "r", encoding="utf-8") as f:# 示例2:写入模式(会自动创建文件,也会清空已有内容!)withopen("output.txt", "w", encoding="utf-8") as f: f.write("Hello, Python!\n")withopen("output.txt", "a", encoding="utf-8") as f:withopen("photo.jpg", "rb") as f:print(f"图片大小:{len(image_data)} 字节")新手易踩坑:
坑 1:用 "w" 打开已有文件 → 内容会被清空! 只想修改请用 "r+" 或 "a"坑 2:用 "r" 打开不存在的文件 → 直接报错! 不确定文件是否存在时,先检查或用 try/except坑 3:读写文本忘了指定 encoding="utf-8" → Windows 上可能乱码!
使用 "w" 模式创建一个名为 hello.txt 的文件,写入三行自我介绍,然后用 "r" 模式读出来打印。试试分别用 "w" 和 "a" 写入,观察区别。
buffering=-1, # 缓冲策略(一般不用改) encoding=None, # 编码格式(文本模式专用) closefd=True, # 是否关闭底层文件描述符日常最常用的写法只需要前三个参数:
f = open("data.txt", "r", encoding="utf-8")为什么必须用 with?
# 不用 with 的写法(不推荐!容易忘记关闭)f = open("data.txt", "r", encoding="utf-8")# 如果 read() 中间出错,close() 根本不会执行 → 资源泄漏withopen("data.txt", "r", encoding="utf-8") as f:# 出了 with 块,文件自动关闭,即使中间出错也会关with 语句的好处:像请了个"保镖",无论代码正常执行还是中途出错,"保镖"都会帮你把文件关上。
withopen("poem.txt", "r", encoding="utf-8") as f:print("=== read() 全部内容 ===")withopen("poem.txt", "r", encoding="utf-8") as f:print("=== readlines() 按行 ===")print(line, end="") # line 已含 \n,所以 end=""withopen("poem.txt", "r", encoding="utf-8") as f:print(line.strip()) # strip() 去掉首尾空白和换行withopen("poem.txt", "r", encoding="utf-8") as f: first_line = f.readline()print(f"第一行:{first_line.strip()}")withopen("静夜思.txt", "w", encoding="utf-8") as f:# f.writelines(["行1\n", "行2\n"]) # 写入字符串列表withopen("静夜思.txt", "r", encoding="utf-8") as f:withopen("test.txt", "w") as f: # 没指定 encoding!withopen("test.txt", "w", encoding="utf-8") as f:withopen("test.txt", "r", encoding="utf-8") as f:encodings_to_try = ["utf-8", "gbk", "gb2312", "latin-1"]for enc in encodings_to_try:withopen("unknown.txt", "r", encoding=enc) as f:print(f"使用 {enc} 编码读取成功:")print(f.read()[:100]) # 只打前100个字符except (UnicodeDecodeError, FileNotFoundError):print(f"使用 {enc} 编码失败,尝试下一个...")新手易踩坑:
坑 1:读取大文件用 read() → 内存爆炸! 大文件请用 for line in f 逐行读坑 2:写完文件忘了 f.close() → 数据可能没真正写入磁盘! 用 with 自动关闭坑 3:Windows 上写中文不给 encoding="utf-8" → 默认 GBK,跨平台乱码!坑 4:read() 读到的是字符串,readlines() 读到的是列表,类型不一样!
用 with 语句写一首你喜欢的诗到 my_poem.txt,然后用三种不同的读取方式(read()、readlines()、逐行遍历)分别读出来并打印。观察它们返回的数据类型有什么不同。
四、StringIO 与 BytesIO:内存中的"文件"生活类比:磁盘文件就像写在纸上的笔记(持久保存,读起来慢),StringIO 就像在脑海里默念(存在内存中,读写极快,但关机就没了)。
StringIO:数据存内存,速度快,程序关了就没,用于临时处理字符串BytesIO:同 StringIO,但处理的是二进制数据(bytes) | |
|---|
| 接口测试 | 要测一个接受"文件对象"的函数,但不想真的创建磁盘文件 |
| 临时拼接数据 | |
| 图片处理 | 用 PIL/Pillow 处理图片后,不存硬盘直接传到网上 |
| 单元测试 | |
print("=== 获取 StringIO 中的内容 ===")print(sio.getvalue()) # 获取全部已写入的字符串sio.seek(0) # 把指针移到开头,否则读不到内容!sio.close() # 用完关闭(虽然程序结束时也会自动释放)# === 从已有字符串创建 StringIO ===text = "Hello\nWorld\nPython"sio2 = StringIO(text) # 直接传入初始内容print(sio2.read()) # 可以直接读关键点:seek(0) 把"光标"移到开头,相当于翻书翻回第一页。写入后不 seek 就读不到东西,因为光标在末尾。
bio.write("你好".encode("utf-8")) # 把字符串编码成 bytes 再写入bio.write(b" World") # 直接写入 bytes(注意前缀 b)print(f"解码后:{data.decode('utf-8')}")# === 处理图片(模拟:下载后不存盘直接处理) ===# 假设 image_bytes 是从网络下载的图片数据image_bytes = b'\x89PNG\r\n...'# 模拟的 PNG 二进制数据bio2 = BytesIO(image_bytes) # 从二进制数据创建 BytesIO4.5 StringIO vs BytesIO vs 磁盘文件 对比新手易踩坑:
坑 1:写完 StringIO 不 seek(0) 就直接 read() → 读到空字符串! 因为光标在末尾坑 2:把 str 直接写入 BytesIO → 报错! 需要先 .encode('utf-8') 转成 bytes坑 3:忘记 close() → 内存不会被立即回收(虽然最终会被垃圾回收,但好习惯是主动关闭)
创建一个 StringIO 对象,写入三句话,然后用 seek(0) 回到开头,逐行读取并打印。再试试不 seek 直接读,观察结果。用 BytesIO 做一个相同的练习(需要把字符串 encode 成 bytes)。
os 模块提供了操作系统级别的文件/目录操作函数。
os.rename("old_name.txt", "new_name.txt")if os.path.exists("temp.txt"):print(os.path.exists("data.txt")) # 是否存在print(os.path.isfile("data.txt")) # 是否存在且是文件print(os.path.isdir("my_folder")) # 是否存在且是目录size = os.path.getsize("data.txt")# 递归创建多层目录(推荐!父目录不存在自动创建)os.makedirs("a/b/c/d", exist_ok=True) # exist_ok=True:已存在不报错items = os.listdir(".") # "." 表示当前目录for root, dirs, files in os.walk("."):print(os.path.join(root, file))print(f"当前工作目录:{os.getcwd()}")# os.chdir("another_folder")5.2 pathlib 模块:现代新秀(Python 3.4+ 推荐)pathlib 用面向对象的方式处理路径,代码更直观、更易读。
p = Path("data") / "subfolder" / "file.txt"# 用 / 拼接路径!print(p) # data/subfolder/file.txt (Linux) 或 data\subfolder\file.txt (Windows)print(p.is_file()) # 是否是文件print(p.is_dir()) # 是否是目录print(p.name) # "file.txt"(文件名)print(p.stem) # "file"(不含后缀)print(p.suffix) # ".txt"(后缀)print(p.parent) # data/subfolder(父目录)print(p.stat().st_size) # 文件大小(字节)# Path("hello.txt").write_text("你好,Pathlib!", encoding="utf-8")# content = Path("hello.txt").read_text(encoding="utf-8")# 创建目录(exist_ok=True 表示已存在不报错)Path("my_project/data").mkdir(parents=True, exist_ok=True)for item in Path(".").iterdir():print(f" [文件] {item.name}")print(f" [目录] {item.name}")for txt_file in Path(".").glob("*.txt"):print("\n所有 .py 文件(递归):")for py_file in Path(".").rglob("*.py"):5.3 os 操作函数 vs pathlib 对照表 | | |
|---|
| os.path.exists("a.txt") | Path("a.txt").exists() |
| os.path.isfile("a.txt") | Path("a.txt").is_file() |
| os.path.isdir("d") | Path("d").is_dir() |
| os.makedirs("a/b/c") | Path("a/b/c").mkdir(parents=True) |
| os.listdir(".") | Path(".").iterdir() |
| os.path.join("a", "b") | Path("a") / "b" |
| os.path.basename(p) | Path(p).name |
| os.path.dirname(p) | Path(p).parent |
建议:新代码尽量用 pathlib,它更简洁、跨平台、面向对象。老代码中可能常见 os.path,需要能读懂。
新手易踩坑:
坑 1:os.rmdir() 删除非空目录 → 报错! 请用 shutil.rmtree() 删除非空目录坑 2:os.mkdir() 创建嵌套目录 → 报错! 改用 os.makedirs() 或 Path.mkdir(parents=True)坑 3:os.listdir() 只返回名字,不包含完整路径 → 要用 os.path.join(dir, name) 拼出完整路径
用 pathlib 在当前目录下创建 practice/data/logs 三层目录(要求一行代码完成)。列出当前目录下所有 .md 文件(用 glob)。递归列出当前目录下所有 .py 文件(用 rglob)。
| | |
|---|
| \ | C:\Users\小明\Documents\file.txt |
| / | /home/小明/Documents/file.txt |
如果代码里硬编码了 \,到了 Linux 上就跑不通了。
path = os.path.join("folder", "subfolder", "file.txt")# Windows: folder\subfolder\file.txt# Linux: folder/subfolder/file.txtdir_part = os.path.dirname("/home/user/file.txt") # /home/userbase_part = os.path.basename("/home/user/file.txt") # file.txtname, ext = os.path.splitext("document.pdf") # ('document', '.pdf')print(f"目录部分:{dir_part}")print(f"文件名:{base_part}")print(f"主名:{name},后缀:{ext}")abs_path = os.path.abspath(".") # 当前目录的绝对路径print(f"当前目录绝对路径:{abs_path}")normalized = os.path.normpath("a/b/../c/./d")print(f"规范化后:{normalized}") # a\c\d (Windows) 或 a/c/d (Linux)print(os.path.exists(path)) # 是否存在print(os.path.isfile(path)) # 是否文件print(os.path.isdir(path)) # 是否目录print(os.path.isabs(path)) # 是否绝对路径home = Path.home() # 用户主目录cwd = Path.cwd() # 当前工作目录data = Path("data") / "subfolder" / "file.txt"# 优雅拼接p = Path("C:/Users/小明/Documents/report.pdf")print(f"文件名:{p.name}") # report.pdfprint(f"不含后缀:{p.stem}") # reportprint(f"后缀:{p.suffix}") # .pdfprint(f"后缀列表:{p.suffixes}") # ['.pdf']print(f"父目录:{p.parent}") # C:\Users\小明\Documentsprint(f"所有父目录:{list(p.parents)}") # 从近到远的所有父级print(f"绝对路径:{p.absolute()}") # 转绝对路径print(f"解析符号链接:{p.resolve()}") # 绝对路径 + 解析所有符号链接print(f"转 string:{str(p)}") # 转为普通字符串target = Path("data.txt")print(target.exists()) # 存在?print(target.is_file()) # 是文件?print(target.is_dir()) # 是目录?# === 读取和写入(pathlib 超便捷功能) ===# content = Path("config.json").read_text(encoding="utf-8")# Path("output.txt").write_text("新内容", encoding="utf-8")# data = Path("image.png").read_bytes()# Path("copy.png").write_bytes(data)# path = "data\images\photo.jpg" # Windows only!path = Path("data") / "images" / "photo.jpg"# path = Path("data", "images", "photo.jpg")input_file = Path("data") / "raw" / "input.csv"output_dir = Path("output") / "results"output_dir.mkdir(parents=True, exist_ok=True) # 确保目录存在output_file = output_dir / "processed.csv"新手易踩坑:
坑 1:Windows 路径写 "C:\Users\小明" → \U 被当成 Unicode 转义!用 "C:\\Users\\小明" 或原始字符串 r"C:\Users\小明" 或直接用 /坑 2:用 + 拼路径 → "data" + "/" + "file.txt" 在 Windows 上是 data/file.txt,可能能跑但不规范,用 Path 或 os.path.join坑 3:用 Path 创建的对象,传给某些老库可能需要先 str(p) 转成字符串
用 pathlib 输出当前文件的:文件名、后缀、父目录、绝对路径。在代码中构建一个跨平台路径 "projects/my_app/data/cache",然后用 mkdir(parents=True, exist_ok=True) 创建它。
生活类比:
序列化(Serialize):把乐高城堡拆成一块块积木,装进盒子里(方便存储和运输)反序列化(Deserialize):从盒子里拿出积木,按图纸重新拼成城堡编程中:
序列化:把 Python 对象(字典、列表、自定义类)→ 转成可以存文件或网络传输的格式反序列化:从文件或网络收到的数据 → 还原成 Python 对象pickle 可以把几乎任何 Python 对象变成字节串,只适合 Python 程序之间交换数据。
withopen("student.pkl", "wb") as f: # 注意:必须用 wb 二进制模式!withopen("student.pkl", "rb") as f: # 注意:必须用 rb 二进制模式! loaded_data = pickle.load(f)# {'name': '小明', 'age': 18, 'scores': [85, 90, 78], 'is_student': True}print(type(loaded_data)) # <class 'dict'># === 序列化为内存中的 bytes(不发文件) ===bytes_data = pickle.dumps(data)print(f"字节长度:{len(bytes_data)}")restored_data = pickle.loads(bytes_data)print(restored_data["name"]) # 小明def__init__(self, name, age):returnf"Student(name='{self.name}', age={self.age})"withopen("student_obj.pkl", "wb") as f:withopen("student_obj.pkl", "rb") as f:print(s2) # Student(name='小红', age=20)print(f"姓名:{s2.name},年龄:{s2.age}")json(JavaScript Object Notation)是最通用的数据交换格式,几乎所有编程语言都支持。
# === 序列化(Python → JSON 字符串) ===withopen("student.json", "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2)# ensure_ascii=False:中文正常显示,不转成 \uXXXXjson_str = json.dumps(data, ensure_ascii=False, indent=2)# === 反序列化(JSON → Python) ===withopen("student.json", "r", encoding="utf-8") as f:print(loaded["name"]) # 小明json_text = '{"name": "小红", "age": 20}'parsed = json.loads(json_text)print(parsed["name"]) # 小红 | | |
|---|
| 数据格式 | | |
| 跨语言 | | |
| 支持类型 | | 仅 dict、list、str、int、float、bool、None |
| 安全性 | | |
| 速度 | | |
| 使用场景 | | |
from datetime import datetime data = {"time": datetime.now()}# 错误:Object of type datetime is not JSON serializableclassDateTimeEncoder(json.JSONEncoder):ifisinstance(obj, datetime):return obj.strftime("%Y-%m-%d %H:%M:%S")returnsuper().default(obj)data = {"time": datetime.now()}json_str = json.dumps(data, cls=DateTimeEncoder, ensure_ascii=False)# {"time": "2026-07-01 14:30:00"}# 错误场景 2:序列化 set/tuple 等类型 json.dumps({"tags": {"python", "java"}}) # set 不能直接序列化# 错误:Object of type set is not JSON serializabledata = {"tags": list({"python", "java"})}json_str = json.dumps(data, ensure_ascii=False)print(json_str) # {"tags": ["python", "java"]} json.loads('{name: 小明}') # 键和字符串值必须用双引号!except json.JSONDecodeError as e: | |
|---|
object | dict |
array | list |
string | str |
number | int |
number | float |
true | True |
null | None |
新手易踩坑:
坑 1:用 json.dumps() 序列化含 datetime、set 的对象 → TypeError! 需要自定义 encoder 或转成支持的类型坑 2:pickle 用 "w"(文本模式)打开文件 → 必须用 "wb" / "rb" 二进制模式!坑 3:JSON 中键和字符串用单引号 → JSON 标准要求双引号!{'key': 'value'} 不合法,{"key": "value"} 才合法坑 4:pickle.load() 加载来路不明的 .pkl 文件 → 安全风险! pickle 可以执行任意代码坑 5:JSON 文件中有中文但不用 ensure_ascii=False → 显示成 \uXXXX 乱码!
创建一个包含 "name", "age", "hobbies" 三个字段的字典,分别用 pickle 和 json 保存到文件,再读出来。尝试用 json.dumps() 序列化一个包含 datetime 对象的字典,观察报错,然后参考上文写出修正方案。
f = open("data.txt", "r")# 忘了 f.close()!文件一直处于打开状态withopen("data.txt", "r", encoding="utf-8") as f:path = "data\images\photo.jpg"# \i 被当成转义符!path = Path("data") / "images" / "photo.jpg"path = os.path.join("data", "images", "photo.jpg")withopen("data.txt", "w") as f: # 写入时没指定编码withopen("data.txt", "r", encoding="gbk") as f: # 读取时用了不同编码withopen("data.txt", "w", encoding="utf-8") as f:withopen("data.txt", "r", encoding="utf-8") as f:# 错误写法:本想往文件里加内容,结果把原内容清空了withopen("important.txt", "w") as f: f.write("新增内容") # 原来的内容被清空了!withopen("important.txt", "a", encoding="utf-8") as f: f.write("新增内容") # 追加在末尾,不丢失原内容withopen("huge_file.log", "r") as f: content = f.read() # 一次性读入内存,可能撑爆withopen("huge_file.log", "r", encoding="utf-8") as f: process(line) # 每次只处理一行,内存友好withopen("data.pkl", "w") as f: # 文本模式!会报错withopen("data.pkl", "wb") as f:data = {"created_at": datetime.now(), "tags": {"a", "b"}}json.dumps(data) # TypeError!"created_at": datetime.now().isoformat(), # datetime → 字符串"tags": list({"a", "b"}) # set → listos.mkdir("a/b/c") # FileNotFoundError!os.makedirs("a/b/c", exist_ok=True)Path("a/b/c").mkdir(parents=True, exist_ok=True)错误 9:StringIO 读完不 seek 就再读sio = StringIO("Hello World")print(sio.read()) # Hello World(光标在末尾了)print(sio.read()) # 空字符串!(光标在末尾,没内容了)print(sio.read()) # Hello World# 错误写法:每次循环都 open/close,性能极差withopen("log.txt", "a") as f:withopen("log.txt", "a") as f:
1. 打开文件 → open("文件名", "模式", encoding="utf-8")3. 内存临时文件 → StringIO(文本)、BytesIO(二进制)4. 文件目录操作 → os 模块(传统)或 pathlib 模块(推荐)5. 跨平台路径 → Path("a") / "b" / "c" 代替字符串拼接6. 数据持久化 → json(通用)、pickle(Python 专属)- 读写文本别忘了 encoding="utf-8"- 路径拼接别忘了用 Path 或 os.path.join