本篇是《Python 从入门到初级》第 18 / 20 篇。示例使用 Python 3.12 兼容语法,完整目录结构与数据见配套源码包。
能运行的脚本,还不一定是好用的工具。使用者不应该每次打开代码改输入路径;调度系统也需要知道成功还是失败。本篇为订单模块增加命令行参数、日志和退出码。

1. 一个小工具应该有什么接口
本例接收 --input、--output,提供 --dry-run 预览和 --verbose 调试日志。默认输入使用脚本旁的教学 CSV,输出必须由使用者明确指定。
正常统计结果以 JSON 写到标准输出,运行日志写到标准错误。这样别人可以接收 JSON,而不必从“开始处理……”这样的文字里提取数据。
2. 完整入口:report_cli.py
保留 ops_tools 包,在示例根目录运行:
import argparse
import csv
import json
import logging
from pathlib import Path
from ops_tools.orders import read_orders, save_json, summarize
def main():
base = Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description="统计订单 CSV,金额单位为整数分")
parser.add_argument("--input", type=Path, default=base / "data" / "orders.csv")
parser.add_argument("--output", type=Path, required=True, help="新的 JSON 文件;存在时拒绝覆盖")
parser.add_argument("--dry-run", action="store_true", help="仅计算和展示,不创建输出文件")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
format="%(levelname)s %(message)s")
try:
logging.debug("输入路径:%s", args.input.resolve())
orders = read_orders(args.input)
summary = summarize(orders)
if args.dry_run:
logging.info("预览完成,没有创建输出文件")
else:
save_json(summary, args.output)
logging.info("报告已保存:%s", args.output.resolve())
print(json.dumps(summary, ensure_ascii=False, indent=2))
except (OSError, ValueError, csv.Error) as exc:
logging.error("处理失败:%s", exc)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
argparse 会解析参数、生成帮助,并检查必需选项。logging 让程序按 DEBUG、INFO、ERROR 等级输出记录;这些级别是日志的分类,不自动等同于监控告警。
机制见 argparse 文档 与 logging 文档。
3. 先看帮助,再预览
python report_cli.py --help
python report_cli.py --output output/first-report.json --dry-run
预期显示总订单 5、已支付 3、金额 38300 分,提示完成预览,但没有创建输出文件。dry-run 仍会读取和验证输入,因此缺少文件或数据非法时仍可能失败。
--output 即使预览时也必需,这是本工具明确的参数规则。换一个工具,规则可能不同,所以先读帮助而不是猜选项。
4. 实际生成,再故意重复一次
python report_cli.py --output output/first-report.json
第一次预期成功创建文件。原样再执行一次,预期因文件已存在而失败。选择一个新的输出文件名可以继续生成,不需要把保护已有结果的行为当成程序错误。
输入与输出的相对路径按当前工作目录解释;只有默认输入明确基于脚本目录。脚本不会把你提供的所有相对路径都自动改成它自己的目录。
5. 退出码是给调用者的结果
| 退出码 | 本工具约定 |
|---|---|
0 |
统计和要求的输出完成 |
1 |
文件或数据处理失败 |
2 |
命令参数不合法,由 argparse 报告 |
Linux/macOS 可在执行后立即用 echo $? 查看;PowerShell 使用 $LASTEXITCODE。它们都是外层 Shell 提供的状态,不能把不同 Shell 的写法随意混用。
Jenkins 普通 sh 步骤会把非零退出码视为失败;如果外层把状态忽略,正确输出错误的脚本也可能被误报为成功。
6. 日志应该记录什么
记录阶段、输入输出位置、数量和失败原因,通常比打印完整业务数据更合适。示例的 verbose 会显示输入路径,但不会逐行打印全部订单。
DEBUG 不意味着应该输出密码、令牌或完整敏感数据。正式使用时还要考虑日志轮转、集中收集和使用权限,这些并不由 basicConfig 自动完成。
7. 练习与参考结果
不提供 output,预期参数错误;提供不存在的 input,预期处理失败;使用新 output,预期成功;给原始 CSV 加重复订单,预期被模块拒绝。
尝试把标准输出保存成另一个文件,并确认其中仍是可解析 JSON。不要把这个重定向文件与 --output 指向同一文件,因为外层 Shell 可能先创建它,触发工具的拒绝覆盖规则。