欢迎来到 Python 学习计划的第 63 天!🎉
昨天我们学习了 文件读取 (open() 与 read()),掌握了如何安全地获取外部数据。今天,我们将学习如何将数据安全、正确地写入文件。
写入操作比读取更危险,因为它可能覆盖现有数据或导致编码乱码。掌握写入模式、编码处理和原子写入技巧,是工程化开发的基本功!
一、文件写入模式详解
1. 核心写入模式
模式 | 说明 | 文件不存在 | 文件存在 | 指针位置 |
|---|
"w"
| 文本写入 | 创建 | 覆盖清空 | 开头 |
"a"
| 文本追加 | 创建 | 保留内容,末尾追加 | 末尾 |
"x"
| 独占创建 | 创建 | 报错 (FileExistsError) | 开头 |
"wb"
| 二进制写入 | 创建 | 覆盖清空 | 开头 |
# ✅ 覆盖写入(配置文件更新)with open("config.json", "w", encoding="utf-8") as f: f.write("{\"key\": \"value\"}")# ✅ 追加写入(日志记录)with open("app.log", "a", encoding="utf-8") as f: f.write("2024-01-01 12:00:00 INFO Start\n")# ✅ 安全创建(防止意外覆盖)try: with open("important.txt", "x", encoding="utf-8") as f: f.write("重要数据")except FileExistsError: print("文件已存在,避免覆盖!")
💡 最佳实践:默认使用 "w" 模式时务必小心,重要数据建议先用 "x" 检查或使用临时文件。二、写入方法对比
1. write() - 写入字符串
with open("data.txt", "w", encoding="utf-8") as f: f.write("第一行\n") # 需要手动添加换行符 f.write("第二行\n")
2. writelines() - 写入字符串列表
lines = ["第一行\n", "第二行\n", "第三行\n"]with open("data.txt", "w", encoding="utf-8") as f: f.writelines(lines) # 不会自动添加换行符
3. 方法对比表
方法 | 参数类型 | 换行符 | 适用场景 |
|---|
write()
| str
| 需手动添加 | 少量数据,精确控制 |
writelines()
| list[str]
| 需手动添加 | 批量写入行数据 |
print()
| 任意 | 自动添加 | 快速调试(重定向) |
# 使用 print 写入文件(较少用,但可行)with open("output.txt", "w", encoding="utf-8") as f: print("Hello", "World", file=f) # 自动添加换行
三、编码问题与处理(核心)
1. 为什么会出现乱码?
- 写入编码 ≠ 读取编码。
- 不同操作系统的默认编码不同(Windows 通常是
gbk,Linux/Mac 通常是 utf-8)。
2. 最佳实践:统一使用 UTF-8
# ❌ 风险:依赖系统默认编码with open("data.txt", "w") as f: f.write("中文内容")# ✅ 安全:显式指定 UTF-8with open("data.txt", "w", encoding="utf-8") as f: f.write("中文内容")
3. 编码错误处理策略
errors 参数控制遇到无法编码字符时的行为。
策略 | 说明 | 示例 |
|---|
"strict"
| 抛出异常(默认) | encoding="utf-8", errors="strict"
|
"ignore"
| 忽略错误字符 | errors="ignore"
|
"replace"
| 用 ? 替换错误字符 | errors="replace"
|
"xmlcharrefreplace"
| 用 XML 字符引用替换 | errors="xmlcharrefreplace"
|
# 处理可能包含特殊字符的数据try: with open("output.txt", "w", encoding="utf-8", errors="strict") as f: f.write("正常内容 \udcff") # 无效 Unicodeexcept UnicodeEncodeError as e: print(f"编码错误:{e}")
四、换行符处理
1. 跨平台差异
- Windows:
\r\n - Linux/Mac:
\n - Old Mac:
\r
2. Python 的自动转换
默认情况下,Python 在文本模式下会自动转换换行符。
# Windows 上写入 \n,实际存储为 \r\nwith open("test.txt", "w") as f: f.write("Line1\nLine2")# 禁用自动转换(二进制模式或 newline="")with open("test.txt", "w", newline="") as f: f.write("Line1\nLine2") # 保持原样
💡 建议:处理 CSV 或需要精确控制换行符的文件时,使用 newline=""。
五、OOP 实战应用(结合知识库)
结合 异常处理(参考 [File 89-92](92-异常链 (raise ... from ...).md))、类设计(参考 [File 67](67-实例方法与 self 的本质.md)、[File 66](66-类属性 vs 实例属性的区别.md))和 类型提示(参考 [File 85](85-Python 3.10+ 新语法:联合类型.md)),构建健壮的文件写入类。
1. 安全文件写入器
from pathlib import Pathfrom typing import Unionclass FileWriteError(Exception): """文件写入错误""" passclass SafeFileWriter: """安全文件写入器,支持原子写入""" def __init__(self, filepath: Union[str, Path], encoding: str = "utf-8"): self.filepath = Path(filepath) self.encoding = encoding def write(self, content: str, mode: str = "w") -> None: """写入内容""" try: # 确保父目录存在 self.filepath.parent.mkdir(parents=True, exist_ok=True) with open(self.filepath, mode, encoding=self.encoding) as f: f.write(content) except OSError as e: # 异常链:保留原始错误信息 raise FileWriteError(f"写入失败:{self.filepath}") from e def write_lines(self, lines: list[str], append: bool = False) -> None: """写入多行""" mode = "a" if append else "w" content = "\n".join(lines) self.write(content, mode=mode) def write_safe(self, content: str) -> None: """原子写入:先写临时文件,再重命名""" temp_path = self.filepath.with_suffix(self.filepath.suffix + ".tmp") try: # 1. 写入临时文件 with open(temp_path, "w", encoding=self.encoding) as f: f.write(content) # 2. 原子替换(Windows 可能需要额外处理) temp_path.replace(self.filepath) except Exception as e: # 清理临时文件 if temp_path.exists(): temp_path.unlink() raise FileWriteError("原子写入失败") from e finally: # 确保临时文件被清理 if temp_path.exists(): try: temp_path.unlink() except: pass# 使用writer = SafeFileWriter("data/config.json")writer.write('{"key": "value"}')writer.write_safe("重要数据,防止损坏")
2. 日志记录器
import datetimefrom typing import Optionalclass Logger: """简单日志记录器""" def __init__(self, filepath: str, encoding: str = "utf-8"): self.filepath = filepath self.encoding = encoding def log(self, message: str, level: str = "INFO") -> None: """记录日志""" timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_line = f"[{timestamp}] [{level}] {message}\n" try: # 使用追加模式 with open(self.filepath, "a", encoding=self.encoding) as f: f.write(log_line) except OSError as e: # 日志写入失败不应影响主程序 print(f"日志写入失败:{e}") def clear(self) -> None: """清空日志""" with open(self.filepath, "w", encoding=self.encoding) as f: pass# 使用logger = Logger("app.log")logger.log("系统启动")logger.log("发生错误", level="ERROR")
3. 配置保存器
import jsonfrom typing import Anyclass ConfigSaver: """配置文件保存器""" def __init__(self, filepath: str): self.filepath = filepath self.default_config: dict[str, Any] = { "version": "1.0", "debug": False } def save(self, config: dict[str, Any]) -> None: """保存配置为 JSON""" try: with open(self.filepath, "w", encoding="utf-8") as f: # indent 美化输出 json.dump(config, f, indent=2, ensure_ascii=False) except TypeError as e: raise ValueError("配置数据无法序列化") from e except OSError as e: raise IOError(f"无法写入配置文件:{self.filepath}") from e def save_backup(self, config: dict[str, Any]) -> None: """保存配置并创建备份""" import shutil from pathlib import Path path = Path(self.filepath) if path.exists(): backup_path = path.with_suffix(path.suffix + ".bak") shutil.copy2(path, backup_path) self.save(config)# 使用saver = ConfigSaver("config.json")saver.save({"theme": "dark", "language": "zh"})
六、常见误区与注意事项
1. 忘记关闭文件
# ❌ 危险:数据可能丢失f = open("data.txt", "w")f.write("content")# 如果程序崩溃,内容可能未写入磁盘# ✅ 安全:使用 withwith open("data.txt", "w") as f: f.write("content")
2. 覆盖重要文件
# ❌ 危险:直接覆盖with open("database.db", "w") as f: f.write(new_data)# ✅ 安全:先备份或原子写入import shutilshutil.copy("database.db", "database.db.bak")# 然后写入
3. 编码不一致
# ❌ 写入 UTF-8,读取 GBKwith open("data.txt", "w", encoding="utf-8") as f: f.write("中文")with open("data.txt", "r", encoding="gbk") as f: # 乱码! print(f.read())
4. 路径问题
# ❌ 硬编码路径open("C:\\Users\\data\\file.txt", "w")# ✅ 使用 pathlibfrom pathlib import Pathpath = Path("data") / "file.txt"path.parent.mkdir(exist_ok=True)with open(path, "w") as f: f.write("content")
七、总结
知识点 | 说明 |
|---|
写入模式 | "w" 覆盖,"a" 追加,"x" 独占创建
|
写入方法 | write() 写字符串,writelines() 写列表
|
编码 | 始终显式指定 encoding="utf-8" |
换行符 | 文本模式自动转换,newline="" 禁用 |
安全写入 | 使用 with,重要数据用临时文件 + 重命名 |
异常处理 | 捕获 OSError, UnicodeEncodeError |
核心要点
- 始终指定编码,避免跨平台乱码。
- 慎用
"w" 模式,防止意外覆盖重要数据。 - 使用
with 语句,确保文件正确关闭。 - 重要数据原子写入,先写临时文件再替换。
📌 明日预告:上下文管理器 with 语句的深度解析
明天我们将进入 异常与文件模块最后一天!
- 主题:上下文管理器
with 语句的深度解析 - 核心问题:
with 语句的底层原理是什么?__enter__ 和 __exit__ 方法的作用?- 如何自定义上下文管理器?
contextlib 模块有哪些实用工具?- 除了文件,
with 还能管理什么资源?
💡 提前思考:
with open(...) as f: 背后执行了什么代码?- 如果
with 块中发生异常,__exit__ 会收到什么信息? - 如何用类实现一个自定义的计时器上下文管理器?
掌握文件写入,是数据持久化的关键!明天深入理解 with 的魔法!继续加油!🚀