文件读写操作

程序早晚要和文件系统打交道——读配置、写日志、导入 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 Path

p = Path("data") / "input.txt"
print(p) # data/input.txt(跨平台自动用正确分隔符)

# 常用方法
p.exists() # 是否存在
p.is_file()
p.is_dir()
p.parent # 父目录
p.name # input.txt
p.stem # input
p.suffix # .txt
p.with_suffix(".json") # data/input.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 上是 GBK

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 csv

# 读
with 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 json

# 写
data = {"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): # 1MB 一次
process(chunk)

处理 GB 级文件也不成问题。

十三、临时文件

1
2
3
4
5
6
7
8
9
10
11
import tempfile

# 临时文件(会自动删除)
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", encoding="utf-8", delete=True) as f:
f.write("temp content")
print(f.name) # /tmp/tmpXXXXXX.txt

# 临时目录
with tempfile.TemporaryDirectory() as d:
print(d)
# 退出块时目录被清空

十四、文件锁与并发

多进程/多线程同时写一个文件会出问题。简单方案:

1
2
3
4
5
6
7
import fcntl        # Unix
# import msvcrt # Windows

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 Path
import shutil

p = Path("old.txt")
p.unlink(missing_ok=True) # 删除文件(missing_ok 3.8+)

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
# log_analyze.py
"""分析 Web 访问日志,统计 IP 请求次数。"""

from pathlib import Path
from collections import Counter
import csv
import json


def 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)

# JSON
(out_dir / "report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2),
encoding="utf-8",
)

# Top IP CSV
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)

十七、常见陷阱

  1. 忘记指定 encoding:Windows 上 GBK 与 UTF-8 混用是乱码首因。
  2. 忘用 with:文件对象没关闭,Linux 上有文件描述符泄漏。
  3. 写 CSV 忘 newline="":Windows 上多空行。
  4. open("w") 覆盖了重要文件:写模式会清空。想追加用 "a"
  5. Path("a") + Path("b")/ 才是拼接运算符,不是 +
  6. 一次 read() 读大文件:OOM。用迭代。
  7. 相对路径乱:脚本从不同目录启动时相对路径变。用 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 背后的原理,以及怎么自己写一个。