Python 写脚本时,真正反复出问题的往往不是 if 和 for,而是这些看起来像“边角料”的内容:
文件到底从哪读、往哪写
JSON 明明能打印,为什么一落盘就报错
时间明明长得差不多,为什么一比较就不对
路径在自己电脑上能跑,换个目录就找不到文件
这几块单独看都不复杂,但只要进到真实脚本里,很快就会变成下面这些实际问题:
读取配置文件时路径错了
导出 JSON 时中文变成乱码
时间字符串排序看着对,实际顺序错了
Windows 和 macOS 上的路径拼接写法混在一起
临时打开的文件没关,后面写入和重命名都出问题
这一篇不按零散知识点讲,而是直接围绕一个实际脚本展开:做一个日报归档脚本 。
这个脚本要完成四件事:
读取一个目录下的日报 JSON 文件
按日期汇总成归档文件
过滤掉格式错误的数据
输出一份归档结果和错误清单
用这个场景把四块高频内容串起来:
文件处理怎么写才不乱
JSON 序列化和反序列化怎么做才稳
时间字符串怎么转成真正可比较的时间对象
路径为什么应该尽量交给 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 Pathimport jsonfile_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 Pathbase_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 Pathfile_path = Path("daily_reports/report_2018-06-01.json" ) print (file_path.exists())print (file_path.is_file())
可能输出:
如果路径错了,至少能先定位是:
3. 一个实际错误:相对路径看起来对,但运行目录错了 错误写法:
1 2 3 4 from pathlib import Pathfile_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 Pathprint (Path.cwd())
如果当前目录和项目目录不一致,相对路径就会失效。
更稳的写法是尽量从脚本所在目录出发:
1 2 3 4 from pathlib import PathBASE_DIR = Path(__file__).resolve().parent file_path = BASE_DIR / "daily_reports" / "report_2018-06-01.json"
五、JSON 序列化和反序列化到底容易错在哪 1. loads 和 dumps 先分清 最常见的两个动作是:
json.loads():把 JSON 字符串变成 Python 对象
json.dumps():把 Python 对象变成 JSON 字符串
例如:
1 2 3 4 5 6 7 import jsoncontent = '{"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 jsondata = {"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 jsondata = {"name" : "Tom" } text = json.dumps(data, ensure_ascii=False ) print (text)
输出:
3. 中文输出为什么会变成转义字符 如果直接这样写:
1 2 3 4 import jsondata = {"task" : "修复登录接口超时" } print (json.dumps(data))
输出可能是:
1 {"task": "\u4fee\u590d\u767b\u5f55\u63a5\u53e3\u8d85\u65f6"}
这不是数据错了,而是默认会做 ASCII 转义。
实际写法:
1 2 3 4 import jsondata = {"task" : "修复登录接口超时" } print (json.dumps(data, ensure_ascii=False ))
输出:
4. 文件写回时顺手补格式化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pathlib import Pathimport jsondata = { "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 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 datetimetime_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 datetimetime_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 datetimetime_text = "2018-06-01 09:30:00" dt = datetime.strptime(time_text, "%Y-%m-%d %H:%M:%S" ) print (dt.strftime("%Y-%m" ))
输出:
这一步在归档脚本里特别重要,因为归档路径通常就是按月份或日期组织的。
七、把四块内容真正串起来 现在把文件处理、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 Pathfrom datetime import datetimeimport jsonBASE_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 Pathfrom datetime import datetimeimport jsondef 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 Pathfrom datetime import datetimeimport jsonBASE_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)
十、怎么测试这一层知识是不是真的掌握了 这一层不能只看懂,要自己改过。
可以直接做这些动作:
把归档文件名从按月改成按天
增加一个字段 project
对 tasks 为空的日报单独记录告警
把 errors.log 改成 JSON 结构
只归档某一时间范围内的日报
如果这些改动都能独立完成,说明文件、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'
这时排查顺序应该很直接:
先打开出错文件,看原始时间字符串
再看 strptime() 的格式串
判断是数据格式不统一,还是代码只支持一种格式
最后决定是先清洗数据,还是让代码兼容多种格式
如果一看到报错就去改路径或改 JSON 解析,方向通常就偏了。因为这里的根因很清楚:时间格式不匹配。
十二、一个实际练习 可以直接把这一篇变成一个完整练习。
练习目标:做一个“测试报告归档脚本”。
要求:
从 reports/ 目录读取多个 JSON 报告
每个报告至少包含 case_name、status、run_at
过滤掉缺字段或时间格式不对的报告
按日期排序后输出到一个归档文件
单独输出错误日志
全程使用 pathlib
如果这个练习能独立做完,说明这一篇最核心的四块能力已经开始真正串起来了。
十三、这篇文章学完以后,下一步应该补什么 如果这一篇已经能跟着做完,下一步最适合继续补的是:
Python 项目从脚本到工程,目录和职责怎么继续拆
pytest 的 fixture、mock 和测试数据怎么组织
数据处理脚本和接口工具如何避免越写越乱
因为到这一步,已经不仅仅是在写单个脚本,而是在处理真正会落盘、会归档、会出错的数据文件了。接下来更容易卡住的,是脚本越来越多以后如何保持结构清楚。
十四、结语 文件处理、序列化、时间处理和路径管理看起来像几块分散的基础知识,但在实际脚本里,它们经常一起出现。
只要脚本涉及:
这四块就一定会一起上场。
所以这类内容最好不要分散记忆,而应该放进一个实际功能里一起练。只要“读文件 -> 解析 JSON -> 处理时间 -> 写回归档”这条链路能独立完成,Python 脚本能力就会稳很多。