Python:文件处理、序列化、时间处理和路径管理有哪些高频坑

Python 写脚本时,真正反复出问题的往往不是 iffor,而是这些看起来像“边角料”的内容:

  • 文件到底从哪读、往哪写
  • JSON 明明能打印,为什么一落盘就报错
  • 时间明明长得差不多,为什么一比较就不对
  • 路径在自己电脑上能跑,换个目录就找不到文件

这几块单独看都不复杂,但只要进到真实脚本里,很快就会变成下面这些实际问题:

  • 读取配置文件时路径错了
  • 导出 JSON 时中文变成乱码
  • 时间字符串排序看着对,实际顺序错了
  • Windows 和 macOS 上的路径拼接写法混在一起
  • 临时打开的文件没关,后面写入和重命名都出问题

这一篇不按零散知识点讲,而是直接围绕一个实际脚本展开:做一个日报归档脚本

这个脚本要完成四件事:

  1. 读取一个目录下的日报 JSON 文件
  2. 按日期汇总成归档文件
  3. 过滤掉格式错误的数据
  4. 输出一份归档结果和错误清单

用这个场景把四块高频内容串起来:

  1. 文件处理怎么写才不乱
  2. JSON 序列化和反序列化怎么做才稳
  3. 时间字符串怎么转成真正可比较的时间对象
  4. 路径为什么应该尽量交给 pathlib

一、这篇文章要解决什么问题

读完这一篇,应该能独立完成这些动作:

  • pathlib 找到需要处理的文件
  • 安全地读取和写入文本文件
  • 把 JSON 字符串转成 Python 对象,再写回文件
  • 正确处理日期时间,而不是只靠字符串硬比
  • 识别几类最常见的文件和路径错误
  • 给这类脚本补最小测试和排错输出

如果这些动作能独立做出来,Python 脚本就不再只是“能跑一下”,而是开始具备稳定处理文件和数据的能力。

二、先看这篇文章要完成的实际场景

假设现在有一个目录 daily_reports/,里面每天都会生成日报文件:

1
2
3
4
daily_reports/
├── report_2018-06-01.json
├── report_2018-06-02.json
└── report_2018-06-03.json

单个文件内容类似这样:

1
2
3
4
5
6
7
8
{
"date": "2018-06-01 09:30:00",
"author": "Tom",
"tasks": [
"修复登录接口超时",
"补回归脚本"
]
}

现在要做一个归档脚本,输出:

  • 一个合并后的 archive.json
  • 一个记录坏数据的 errors.log

理想结果类似这样:

1
2
3
4
5
读取文件数量: 3
成功归档数量: 2
错误数量: 1
归档文件: archive/archive_2018-06.json
错误日志: archive/errors.log

这个场景已经足够覆盖:

  • 文件读取
  • JSON 解析
  • 时间解析
  • 路径组织
  • 错误记录

三、先写一个最小可运行版本

先不要一上来就处理整个目录,先把最小版本跑起来。

新建 archive_reports.py

1
2
3
4
5
6
7
8
9
10
from pathlib import Path
import json


file_path = Path("daily_reports/report_2018-06-01.json")
content = file_path.read_text(encoding="utf-8")
data = json.loads(content)

print(data["author"])
print(data["tasks"])

执行:

1
python archive_reports.py

输出:

1
2
Tom
['修复登录接口超时', '补回归脚本']

这个最小版本里已经有四个关键动作:

  • Path 表示文件路径
  • read_text() 读取文本
  • json.loads() 解析 JSON
  • 从字典和列表里拿到真正需要的数据

后面只是把它从“读取一个文件”扩成“处理一批文件”。

四、文件处理真正高频的坑是什么

1. 直接用字符串拼路径,后面最容易乱

错误写法:

1
2
file_path = "daily_reports/" + "report_2018-06-01.json"
print(file_path)

输出:

1
daily_reports/report_2018-06-01.json

这看起来没问题,但一旦路径层级变深,或者需要处理不同系统的分隔符,代码会越来越乱。

实际写法:

1
2
3
4
5
from pathlib import Path

base_dir = Path("daily_reports")
file_path = base_dir / "report_2018-06-01.json"
print(file_path)

输出:

1
daily_reports/report_2018-06-01.json

这两种写法打印结果可能一样,但第二种更稳,因为路径本身还是路径对象,不只是字符串拼接结果。

2. 文件存在性先判断,不要等报错再猜

1
2
3
4
5
from pathlib import Path

file_path = Path("daily_reports/report_2018-06-01.json")
print(file_path.exists())
print(file_path.is_file())

可能输出:

1
2
True
True

如果路径错了,至少能先定位是:

  • 文件根本不存在
  • 还是路径指向了目录

3. 一个实际错误:相对路径看起来对,但运行目录错了

错误写法:

1
2
3
4
from pathlib import Path

file_path = Path("daily_reports/report_2018-06-01.json")
print(file_path.read_text(encoding="utf-8"))

可能报错:

1
FileNotFoundError: [Errno 2] No such file or directory: 'daily_reports/report_2018-06-01.json'

这时不一定是文件不存在,也可能是当前运行目录不对。

实际排查动作:

1
2
3
from pathlib import Path

print(Path.cwd())

如果当前目录和项目目录不一致,相对路径就会失效。

更稳的写法是尽量从脚本所在目录出发:

1
2
3
4
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
file_path = BASE_DIR / "daily_reports" / "report_2018-06-01.json"

五、JSON 序列化和反序列化到底容易错在哪

1. loadsdumps 先分清

最常见的两个动作是:

  • json.loads():把 JSON 字符串变成 Python 对象
  • json.dumps():把 Python 对象变成 JSON 字符串

例如:

1
2
3
4
5
6
7
import json

content = '{"name": "Tom", "count": 2}'
data = json.loads(content)

print(data)
print(type(data))

输出:

1
2
{'name': 'Tom', 'count': 2}
<class 'dict'>

2. 一个实际错误:把字典再拿去 loads

错误写法:

1
2
3
4
import json

data = {"name": "Tom"}
json.loads(data)

报错:

1
TypeError: the JSON object must be str, bytes or bytearray, not dict

原因很直接:

  • loads() 需要的是字符串
  • 传进去的却已经是字典

修复写法:

1
2
3
4
5
import json

data = {"name": "Tom"}
text = json.dumps(data, ensure_ascii=False)
print(text)

输出:

1
{"name": "Tom"}

3. 中文输出为什么会变成转义字符

如果直接这样写:

1
2
3
4
import json

data = {"task": "修复登录接口超时"}
print(json.dumps(data))

输出可能是:

1
{"task": "\u4fee\u590d\u767b\u5f55\u63a5\u53e3\u8d85\u65f6"}

这不是数据错了,而是默认会做 ASCII 转义。

实际写法:

1
2
3
4
import json

data = {"task": "修复登录接口超时"}
print(json.dumps(data, ensure_ascii=False))

输出:

1
{"task": "修复登录接口超时"}

4. 文件写回时顺手补格式化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pathlib import Path
import json

data = {
"task": "修复登录接口超时",
"count": 2,
}

output_path = Path("archive/output.json")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)

这类脚本不是只给程序看,后面通常还要给人查,所以加上 indent=2 很有价值。

六、时间处理为什么不能只靠字符串

1. 时间字符串看着像时间,不等于真能比较

例如:

1
2
3
4
time1 = "2018-06-01 09:30:00"
time2 = "2018-06-02 08:00:00"

print(time1 < time2)

输出:

1
True

这个例子碰巧没问题,因为格式足够规整。

但一旦格式不统一,例如:

1
2
3
4
time1 = "2018-6-1 9:30:00"
time2 = "2018-06-02 08:00:00"

print(time1 < time2)

这种比较就不可靠了。

2. 实际写法:先转成 datetime

1
2
3
4
5
6
7
from datetime import datetime

time_text = "2018-06-01 09:30:00"
dt = datetime.strptime(time_text, "%Y-%m-%d %H:%M:%S")

print(dt)
print(type(dt))

输出:

1
2
2018-06-01 09:30:00
<class 'datetime.datetime'>

3. 一个实际错误:格式字符串写错

错误写法:

1
2
3
4
from datetime import datetime

time_text = "2018-06-01 09:30:00"
datetime.strptime(time_text, "%Y/%m/%d %H:%M:%S")

报错:

1
ValueError: time data '2018-06-01 09:30:00' does not match format '%Y/%m/%d %H:%M:%S'

修复写法:

1
2
3
4
5
from datetime import datetime

time_text = "2018-06-01 09:30:00"
dt = datetime.strptime(time_text, "%Y-%m-%d %H:%M:%S")
print(dt.strftime("%Y-%m"))

输出:

1
2018-06

这一步在归档脚本里特别重要,因为归档路径通常就是按月份或日期组织的。

七、把四块内容真正串起来

现在把文件处理、JSON、时间和路径合到一起。

archive_reports.py

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
from pathlib import Path
from datetime import datetime
import json


BASE_DIR = Path(__file__).resolve().parent
REPORT_DIR = BASE_DIR / "daily_reports"
ARCHIVE_DIR = BASE_DIR / "archive"


def load_report(file_path: Path) -> dict:
content = file_path.read_text(encoding="utf-8")
data = json.loads(content)

report_time = datetime.strptime(data["date"], "%Y-%m-%d %H:%M:%S")

return {
"date": report_time,
"author": data["author"],
"tasks": data["tasks"],
"source_file": file_path.name,
}


def archive_reports():
reports = []

for file_path in REPORT_DIR.glob("report_*.json"):
report = load_report(file_path)
reports.append(report)

reports.sort(key=lambda item: item["date"])

output = []
for report in reports:
output.append({
"date": report["date"].strftime("%Y-%m-%d %H:%M:%S"),
"author": report["author"],
"tasks": report["tasks"],
"source_file": report["source_file"],
})

ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
archive_path = ARCHIVE_DIR / "archive_2018-06.json"
archive_path.write_text(
json.dumps(output, ensure_ascii=False, indent=2),
encoding="utf-8",
)

print(f"读取文件数量: {len(reports)}")
print(f"归档文件: {archive_path}")


if __name__ == "__main__":
archive_reports()

执行:

1
python archive_reports.py

可能输出:

1
2
读取文件数量: 3
归档文件: /path/to/project/archive/archive_2018-06.json

八、把坏数据也处理进去

真实脚本里最麻烦的不是顺利路径,而是文件里混入坏数据。

例如某个文件内容可能是:

1
2
3
4
5
{
"date": "2018/06/03 09:30:00",
"author": "Lucy",
"tasks": ["核对报告格式"]
}

这里时间格式和预期不一致,就会在 strptime() 时报错。

所以更稳的写法是把错误收集下来,而不是让整个脚本直接中断。

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
from pathlib import Path
from datetime import datetime
import json


def load_report(file_path: Path) -> dict:
content = file_path.read_text(encoding="utf-8")
data = json.loads(content)
report_time = datetime.strptime(data["date"], "%Y-%m-%d %H:%M:%S")

return {
"date": report_time,
"author": data["author"],
"tasks": data["tasks"],
"source_file": file_path.name,
}


def archive_reports(report_dir: Path, archive_dir: Path):
reports = []
errors = []

for file_path in report_dir.glob("report_*.json"):
try:
report = load_report(file_path)
except Exception as exc:
errors.append(f"{file_path.name}: {exc}")
continue

reports.append(report)

reports.sort(key=lambda item: item["date"])

output = []
for report in reports:
output.append({
"date": report["date"].strftime("%Y-%m-%d %H:%M:%S"),
"author": report["author"],
"tasks": report["tasks"],
"source_file": report["source_file"],
})

archive_dir.mkdir(parents=True, exist_ok=True)

archive_path = archive_dir / "archive_2018-06.json"
archive_path.write_text(
json.dumps(output, ensure_ascii=False, indent=2),
encoding="utf-8",
)

error_path = archive_dir / "errors.log"
error_path.write_text("\n".join(errors), encoding="utf-8")

print(f"读取文件数量: {len(list(report_dir.glob('report_*.json')))}")
print(f"成功归档数量: {len(reports)}")
print(f"错误数量: {len(errors)}")
print(f"归档文件: {archive_path}")
print(f"错误日志: {error_path}")

这时候脚本才真正开始像一个可交付的小工具。

九、完整版本放在一起

下面给出一个可以直接运行的完整版本。

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
from pathlib import Path
from datetime import datetime
import json


BASE_DIR = Path(__file__).resolve().parent
REPORT_DIR = BASE_DIR / "daily_reports"
ARCHIVE_DIR = BASE_DIR / "archive"


def load_report(file_path: Path) -> dict:
content = file_path.read_text(encoding="utf-8")
data = json.loads(content)
report_time = datetime.strptime(data["date"], "%Y-%m-%d %H:%M:%S")

return {
"date": report_time,
"author": data["author"],
"tasks": data["tasks"],
"source_file": file_path.name,
}


def archive_reports(report_dir: Path, archive_dir: Path) -> None:
reports = []
errors = []

for file_path in report_dir.glob("report_*.json"):
try:
report = load_report(file_path)
except Exception as exc:
errors.append(f"{file_path.name}: {exc}")
continue

reports.append(report)

reports.sort(key=lambda item: item["date"])

output = []
for report in reports:
output.append({
"date": report["date"].strftime("%Y-%m-%d %H:%M:%S"),
"author": report["author"],
"tasks": report["tasks"],
"source_file": report["source_file"],
})

archive_dir.mkdir(parents=True, exist_ok=True)

archive_path = archive_dir / "archive_2018-06.json"
archive_path.write_text(
json.dumps(output, ensure_ascii=False, indent=2),
encoding="utf-8",
)

error_path = archive_dir / "errors.log"
error_path.write_text("\n".join(errors), encoding="utf-8")

print(f"读取文件数量: {len(list(report_dir.glob('report_*.json')))}")
print(f"成功归档数量: {len(reports)}")
print(f"错误数量: {len(errors)}")
print(f"归档文件: {archive_path}")
print(f"错误日志: {error_path}")


if __name__ == "__main__":
archive_reports(REPORT_DIR, ARCHIVE_DIR)

十、怎么测试这一层知识是不是真的掌握了

这一层不能只看懂,要自己改过。

可以直接做这些动作:

  1. 把归档文件名从按月改成按天
  2. 增加一个字段 project
  3. tasks 为空的日报单独记录告警
  4. errors.log 改成 JSON 结构
  5. 只归档某一时间范围内的日报

如果这些改动都能独立完成,说明文件、JSON、时间、路径这四块已经开始真正会用了。

十一、一个实际排错场景

这类脚本最常见的实际问题不是 JSON 完全读不出来,而是“有的文件能处理,有的文件一到生产环境就报错”。

例如日志里看到:

1
report_2018-06-03.json: time data '2018/06/03 09:30:00' does not match format '%Y-%m-%d %H:%M:%S'

这时排查顺序应该很直接:

  1. 先打开出错文件,看原始时间字符串
  2. 再看 strptime() 的格式串
  3. 判断是数据格式不统一,还是代码只支持一种格式
  4. 最后决定是先清洗数据,还是让代码兼容多种格式

如果一看到报错就去改路径或改 JSON 解析,方向通常就偏了。因为这里的根因很清楚:时间格式不匹配。

十二、一个实际练习

可以直接把这一篇变成一个完整练习。

练习目标:做一个“测试报告归档脚本”。

要求:

  1. reports/ 目录读取多个 JSON 报告
  2. 每个报告至少包含 case_namestatusrun_at
  3. 过滤掉缺字段或时间格式不对的报告
  4. 按日期排序后输出到一个归档文件
  5. 单独输出错误日志
  6. 全程使用 pathlib

如果这个练习能独立做完,说明这一篇最核心的四块能力已经开始真正串起来了。

十三、这篇文章学完以后,下一步应该补什么

如果这一篇已经能跟着做完,下一步最适合继续补的是:

  1. Python 项目从脚本到工程,目录和职责怎么继续拆
  2. pytest 的 fixture、mock 和测试数据怎么组织
  3. 数据处理脚本和接口工具如何避免越写越乱

因为到这一步,已经不仅仅是在写单个脚本,而是在处理真正会落盘、会归档、会出错的数据文件了。接下来更容易卡住的,是脚本越来越多以后如何保持结构清楚。

十四、结语

文件处理、序列化、时间处理和路径管理看起来像几块分散的基础知识,但在实际脚本里,它们经常一起出现。

只要脚本涉及:

  • 读文件
  • 写文件
  • 存 JSON
  • 处理时间
  • 组织目录

这四块就一定会一起上场。

所以这类内容最好不要分散记忆,而应该放进一个实际功能里一起练。只要“读文件 -> 解析 JSON -> 处理时间 -> 写回归档”这条链路能独立完成,Python 脚本能力就会稳很多。