程序早晚要和文件系统打交道——读配置、写日志、导入 CSV、生成报表。Python 的文件 IO 简单直观,但一些细节(编码、二进制/文本模式、大文件、路径处理)经常让新手踩坑。这一篇讲清楚 open 的正确姿势、pathlib 现代化的路径处理、CSV/JSON 的读写、以及大文件的流式处理。
一、open:打开文件 1 2 3 f = open ("hello.txt" , mode="r" , encoding="utf-8" ) data = f.read() f.close()
但永远推荐用 with :
1 2 3 with open ("hello.txt" , encoding="utf-8" ) as f: data = f.read()
二、open 的关键参数 1 open (file, mode="r" , buffering=-1 , encoding=None , newline=None )
file :路径(字符串或 Path 对象);
mode :模式(下节详解);
encoding :文本模式下的编码,永远显式写 encoding="utf-8" ;
buffering :缓冲,一般不用管;
newline :换行符处理,写 CSV 时经常用到。
三、mode:文件模式
mode
含义
文件不存在
已存在时
r
只读(默认)
报错
从头读
w
只写
创建
清空后重写
a
追加
创建
末尾追加
x
独占创建
创建
报错(防覆盖)
r+
读写
报错
保留内容
b
二进制模式(加在上面)
-
-
t
文本模式(默认,加在上面)
-
-
常用组合 :
"r" / "rt" = 文本读;
"rb" = 二进制读(图片、PDF);
"w" = 文本写(覆盖);
"a" = 文本追加(日志);
"wb" = 二进制写。
四、文本 vs 二进制 文本模式 :
返回 str;
换行会被”翻译”(Windows 的 \r\n 变成 \n);
必须指定 encoding。
二进制模式 :
返回 bytes;
原样读写,不做换行翻译;
不能指定 encoding;
用于图片、PDF、Excel、压缩包等。
五、read / readline / readlines / 遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 with open ("data.txt" , encoding="utf-8" ) as f: text = f.read() with open ("data.txt" , encoding="utf-8" ) as f: line = f.readline() with open ("data.txt" , encoding="utf-8" ) as f: lines = f.readlines() with open ("data.txt" , encoding="utf-8" ) as f: for line in f: print (line.rstrip())
大文件永远用第 4 种 ——文件对象本身是可迭代的,逐行读,内存友好。
六、写文件 1 2 3 4 5 6 with open ("out.txt" , "w" , encoding="utf-8" ) as f: f.write("第一行\n" ) f.write("第二行\n" ) f.writelines(["行 A\n" , "行 B\n" ])
write 不会自动加换行 ,要自己写 \n。
七、追加 1 2 with open ("log.txt" , "a" , encoding="utf-8" ) as f: f.write(f"{msg} \n" )
日志、增量写数据用 "a"。
八、pathlib:现代化的路径处理 别再用 os.path 拼路径 (Windows 反斜杠、字符串拼接易错)。用 pathlib:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from pathlib import Pathp = Path("data" ) / "input.txt" print (p) p.exists() p.is_file() p.is_dir() p.parent p.name p.stem p.suffix p.with_suffix(".json" ) p.parent.mkdir(parents=True , exist_ok=True ) Path("hello.txt" ).write_text("hi 咖飞" , encoding="utf-8" ) Path("hello.txt" ).read_text(encoding="utf-8" ) Path("img.png" ).read_bytes() Path("img.png" ).write_bytes(b"..." ) for f in Path("logs" ).iterdir(): print (f) for f in Path("logs" ).glob("*.log" ): print (f) for f in Path("logs" ).rglob("*.log" ): print (f)
九、编码陷阱 永远显式指定 encoding :
1 2 open ("file.txt" , "r" , encoding="utf-8" ) open ("file.txt" , "r" )
Windows 上不指定 encoding 会用 GBK,读 UTF-8 中文会乱码。
BOM :Windows 记事本存的 UTF-8 有时带 BOM(3 字节头 \xef\xbb\xbf),用 encoding="utf-8-sig" 自动去掉。
判断文件编码 :chardet 库能猜(不 100% 准)。安全做法是要求所有输入都是 UTF-8。
十、CSV 处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import csvwith open ("data.csv" , encoding="utf-8" , newline="" ) as f: reader = csv.reader(f) for row in reader: print (row) with open ("data.csv" , encoding="utf-8" , newline="" ) as f: reader = csv.DictReader(f) for row in reader: print (row["name" ], row["age" ]) with open ("out.csv" , "w" , encoding="utf-8" , newline="" ) as f: writer = csv.writer(f) writer.writerow(["name" , "age" ]) writer.writerow(["咖飞" , 3 ]) writer.writerows([["小王" , 20 ], ["小李" , 25 ]]) with open ("out.csv" , "w" , encoding="utf-8" , newline="" ) as f: writer = csv.DictWriter(f, fieldnames=["name" , "age" ]) writer.writeheader() writer.writerow({"name" : "咖飞" , "age" : 3 })
关键 :写 CSV 时必须 newline="",否则 Windows 上会写出多余的空行。
对于复杂的数据分析,用 pandas.read_csv / to_csv 更强大。
十一、JSON 处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import jsondata = {"name" : "咖飞" , "age" : 3 , "hobbies" : ["读书" , "跑步" ]} with open ("data.json" , "w" , encoding="utf-8" ) as f: json.dump(data, f, ensure_ascii=False , indent=2 ) with open ("data.json" , encoding="utf-8" ) as f: data = json.load(f) s = json.dumps(data, ensure_ascii=False ) d = json.loads(s)
关键参数 :
ensure_ascii=False:让中文正常显示(默认是 True,会转成 \uXXXX);
indent=2:格式化,便于人读;
sort_keys=True:按 key 排序,方便 diff。
复杂的 JSON 讲解见 JSON 数据处理 。
十二、大文件处理 不能一次 f.read()(会 OOM)。用迭代:
1 2 3 4 5 6 7 8 9 with open ("huge.log" , encoding="utf-8" ) as f: for line in f: process(line) with open ("huge.bin" , "rb" ) as f: while chunk := f.read(1024 * 1024 ): process(chunk)
处理 GB 级文件也不成问题。
十三、临时文件 1 2 3 4 5 6 7 8 9 10 11 import tempfilewith tempfile.NamedTemporaryFile(mode="w" , suffix=".txt" , encoding="utf-8" , delete=True ) as f: f.write("temp content" ) print (f.name) with tempfile.TemporaryDirectory() as d: print (d)
十四、文件锁与并发 多进程/多线程同时写一个文件会出问题。简单方案:
1 2 3 4 5 6 7 import fcntl with open ("data.txt" , "a" ) as f: fcntl.flock(f, fcntl.LOCK_EX) f.write("safe line\n" ) fcntl.flock(f, fcntl.LOCK_UN)
跨平台的锁库:filelock(pip install filelock)。
十五、安全删除与移动 1 2 3 4 5 6 7 8 9 10 11 from pathlib import Pathimport shutilp = Path("old.txt" ) p.unlink(missing_ok=True ) Path("dir" ).rmdir() shutil.rmtree("dir" ) shutil.copy("a.txt" , "b.txt" ) shutil.move("a.txt" , "b/a.txt" )
十六、一个完整例子:日志分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 """分析 Web 访问日志,统计 IP 请求次数。""" from pathlib import Pathfrom collections import Counterimport csvimport jsondef parse_line (line: str ) -> dict | None : """ 简单解析日志行: 2025-09-21 10:00:00 GET /path 200 1.2.3.4 """ parts = line.split() if len (parts) < 6 : return None return { "date" : parts[0 ], "time" : parts[1 ], "method" : parts[2 ], "path" : parts[3 ], "status" : int (parts[4 ]), "ip" : parts[5 ], } def analyze (log_path: Path ): ip_counter = Counter() status_counter = Counter() total = 0 with log_path.open (encoding="utf-8" ) as f: for line in f: rec = parse_line(line.strip()) if rec is None : continue total += 1 ip_counter[rec["ip" ]] += 1 status_counter[rec["status" ]] += 1 return { "total" : total, "top_ips" : ip_counter.most_common(10 ), "status" : dict (status_counter), } def save_report (report: dict , out_dir: Path ): out_dir.mkdir(parents=True , exist_ok=True ) (out_dir / "report.json" ).write_text( json.dumps(report, ensure_ascii=False , indent=2 ), encoding="utf-8" , ) with (out_dir / "top_ips.csv" ).open ("w" , encoding="utf-8" , newline="" ) as f: writer = csv.writer(f) writer.writerow(["ip" , "count" ]) writer.writerows(report["top_ips" ]) if __name__ == "__main__" : log_path = Path("access.log" ) if not log_path.exists(): log_path.write_text( "2025-09-21 10:00:00 GET /a 200 1.1.1.1\n" "2025-09-21 10:00:01 GET /b 200 1.1.1.1\n" "2025-09-21 10:00:02 GET /a 404 2.2.2.2\n" "2025-09-21 10:00:03 GET /c 200 1.1.1.1\n" , encoding="utf-8" , ) report = analyze(log_path) save_report(report, Path("out" )) print (report)
十七、常见陷阱
忘记指定 encoding :Windows 上 GBK 与 UTF-8 混用是乱码首因。
忘用 with :文件对象没关闭,Linux 上有文件描述符泄漏。
写 CSV 忘 newline="" :Windows 上多空行。
open("w") 覆盖了重要文件 :写模式会清空。想追加用 "a"。
Path("a") + Path("b") :/ 才是拼接运算符,不是 +。
一次 read() 读大文件 :OOM。用迭代。
相对路径乱 :脚本从不同目录启动时相对路径变。用 Path(__file__).parent 确定脚本自身位置。
十八、小结与延伸阅读
用 with open(..., encoding="utf-8") as f: 是万能模板;
mode:r/w/a/x,加 b 是二进制;
逐行遍历文件对象最省内存;
pathlib 取代 os.path 拼路径;
CSV 写记得 newline="";
JSON 中文用 ensure_ascii=False;
大文件用块读;
临时文件/目录用 tempfile。
延伸阅读:
下一篇 with 语句与上下文管理器 我们讲清楚 with 背后的原理,以及怎么自己写一个。