把脚本变成工具:argparse、logging 与定时任务)
写在前面这是第二批的最后一篇。前 23 篇把「怎么处理数据」讲完了这一篇讲怎么让脚本被别人用起来。写给自己用的脚本改参数、print调试、手动运行都没问题但只要交给同事、或者要每周自动跑就必须补三件事参数不改代码就能换输入输出、日志出问题能查、定时不用人盯着。这篇把这三件事做成一个完整的「报表生成器」你抄走就能用。先确认示例数据在手边importosimportmkdataifnotos.path.exists(data/sales.csv):mkdata.main()print(示例数据就绪,os.path.exists(data/sales.csv))这段会检查第 1 篇生成的data/目录在不在不在就自动补一次已经有数据时只会打印一行示例数据就绪 True不影响后面的结果。一、argparse把参数从代码里挪出来最朴素的写法是「改代码里的路径」麻烦且容易改错。argparse让你在命令行传参还能自动生成--help。importargparsedefbuild_parser():parserargparse.ArgumentParser(progreport.py,description从销售明细生成 Excel 报表)parser.add_argument(input,help输入 CSV 路径)parser.add_argument(--top,typeint,default5,help每个维度取前 N 名)parser.add_argument(--out,defaultoutput/report.xlsx,help输出 xlsx 路径)parser.add_argument(--verbose,actionstore_true,help输出调试日志)returnparser parserbuild_parser()# 文章里演示用显式参数实际运行时直接 python report.py data/sales.csvargsparser.parse_args([data/sales.csv,--top,3,--out,output/report.xlsx])print(解析结果)print( input ,args.input)print( top ,args.top)print( out ,args.out)print( verbose,args.verbose)print(----)print(帮助信息)parser.print_help()解析结果 input data/sales.csv top 3 out output/report.xlsx verbose False ---- 帮助信息 usage: report.py [-h] [--top TOP] [--out OUT] [--verbose] input 从销售明细生成 Excel 报表 positional arguments: input 输入 CSV 路径 options: -h, --help show this help message and exit --top TOP 每个维度取前 N 名 --out OUT 输出 xlsx 路径 --verbose 输出调试日志三个设计要点位置参数input给必需项可选参数--top给默认值命令行里少写几个也能跑typeint让 argparse 自动转换并校验传abc会直接报错不用自己判断actionstore_true是布尔开关--verbose出现即为 True。二、loggingprint 是调试logging 是工程print的问题没法分级、没法关、没法带时间戳和行号。logging四行配置就解决。importloggingimportos os.makedirs(output,exist_okTrue)loggerlogging.getLogger(report)logger.setLevel(logging.INFO)logger.handlers.clear()# 避免重复添加 handlerJupyter 里尤其容易fmtlogging.Formatter(%(asctime)s | %(levelname)-7s | %(message)s,%Y-%m-%d %H:%M:%S)consolelogging.StreamHandler()console.setFormatter(fmt)logger.addHandler(console)file_handlerlogging.FileHandler(output/report.log,encodingutf-8)file_handler.setFormatter(fmt)logger.addHandler(file_handler)logger.info(开始生成报表)logger.warning(示例数据只有 16 行仅供演示)try:1/0exceptZeroDivisionError:logger.exception(计算失败已捕获异常并继续)# exception 会带上 tracebacklogger.info(流程结束日志已写入 output/report.log)withopen(output/report.log,encodingutf-8)asf:print(---- 日志文件内容 ----)print(f.read())2026-10-21 19:42:10 | INFO | 开始生成报表 2026-10-21 19:42:10 | WARNING | 示例数据只有 16 行仅供演示 2026-10-21 19:42:10 | ERROR | 计算失败已捕获异常并继续 Traceback (most recent call last): File article, line 23, in module ZeroDivisionError: division by zero 2026-10-21 19:42:10 | INFO | 流程结束日志已写入 output/report.log ---- 日志文件内容 ---- 2026-10-21 19:42:10 | INFO | 开始生成报表 2026-10-21 19:42:10 | WARNING | 示例数据只有 16 行仅供演示 2026-10-21 19:42:10 | ERROR | 计算失败已捕获异常并继续 Traceback (most recent call last): File article, line 23, in module ZeroDivisionError: division by zero 2026-10-21 19:42:10 | INFO | 流程结束日志已写入 output/report.log四个级别够用DEBUG调试细节、INFO正常进度、WARNING可继续的问题、ERROR出错了。logger.exception()要写在except里它会自动把 traceback 写进日志——这是排查线上问题最值钱的一行。三、合起来一个能交付的报表生成器把参数、日志、数据处理、导出串起来每个环节都留日志出错不崩。importargparseimportloggingimportosimportpandasaspddefsetup_logger(verboseFalse):loggerlogging.getLogger(report)logger.setLevel(logging.DEBUGifverboseelselogging.INFO)logger.handlers.clear()fmtlogging.Formatter(%(asctime)s | %(levelname)-7s | %(message)s,%H:%M:%S)shlogging.StreamHandler()sh.setFormatter(fmt)logger.addHandler(sh)returnloggerdefbuild_report(path,top5,outoutput/report.xlsx,loggerNone):loggerloggerorsetup_logger()logger.info(读取数据%s,path)dfpd.read_csv(path,encodingutf-8-sig)df[金额]df[金额].fillna((df[数量]*df[单价]).round(2))cleandf[df[金额]0].drop_duplicates()logger.info(原始 %d 行清洗后 %d 行,len(df),len(clean))by_city(clean.groupby(城市,dropnaFalse)[金额].sum().sort_values(ascendingFalse).head(top).round(2).reset_index())by_product(clean.groupby(产品)[金额].sum().sort_values(ascendingFalse).head(top).round(2).reset_index())os.makedirs(os.path.dirname(out)or.,exist_okTrue)withpd.ExcelWriter(out,engineopenpyxl)aswriter:by_city.to_excel(writer,sheet_name城市Top,indexFalse)by_product.to_excel(writer,sheet_name产品Top,indexFalse)logger.info(已写出%s,out)returnout,by_city,by_product parserargparse.ArgumentParser(progreport.py,description生成销售报表)parser.add_argument(input)parser.add_argument(--top,typeint,default3)parser.add_argument(--out,defaultoutput/report.xlsx)parser.add_argument(--verbose,actionstore_true)argsparser.parse_args([data/sales.csv,--top,3,--verbose])logsetup_logger(args.verbose)try:out,city,productbuild_report(args.input,args.top,args.out,log)exceptFileNotFoundError:log.error(输入文件不存在%s,args.input)raiseSystemExit(1)print(城市 Top)print(city.to_string(indexFalse))print(产品 Top)print(product.to_string(indexFalse))19:42:10 | INFO | 读取数据data/sales.csv 19:42:10 | INFO | 原始 16 行清洗后 12 行 19:42:10 | INFO | 已写出output/report.xlsx 城市 Top 城市 金额 杭州 3404.1 北京 1425.9 上海 772.5 产品 Top 产品 金额 D-显示器 4193.0 C-键盘 1157.0 A-笔记本 548.9图 1脚本工具化的五段流水线——参数、日志、处理、导出、定时。前四段写完就是「能交付」第五段让它自己跑起来。注意raise SystemExit(1)工具化脚本的退出码很重要定时任务和 CI 靠它判断成功失败崩了要返回非 0。四、定时运行让它自己跑起来系统方式最小配置Windows任务计划程序触发器选「每周一 8:00」操作填python.exe参数填脚本全路径Linux / macOScrontab0 8 * * 1 cd /path/to/project python3 report.py data/sales.csv cron.log 21任意GitHub Actions定时 workflowschedule: - cron: 0 0 * * 1服务器系统服务systemd / supervisor适合常驻任务不适合「跑一次就退出」三条工程习惯比工具本身重要幂等同一天跑两次结果应该一样文件名带日期、if_existsreplace日志落盘定时任务没人看着日志是唯一的现场失败要能知道最简单的做法是失败时发一封邮件 / 一条 webhook别等下周才发现没跑。五、第二批 12 篇回顾#主题一句话13多层索引set_index造、loc/xs取、unstack转14向量化列运算比 for 循环快上千倍15多种数据源JSON、网页表格、数据库各有各的 read_*16内存优化降级 category55 MB 压到 4 MB17自动周报groupby → 多 sheet → openpyxl 格式18批量处理glob → 循环读加来源列 → concat19Pandas 与 ExcelPandas 算Excel 呈现和协作20数据质量10 项校验做成函数挂到交付流程21相关性与分组corr / crosstab / groupby / 热力图22用户行为分析留存、复购、月活都是分组建透视23Seaborn统计图误差棒、箱线、热力、pairplot24工具化argparse logging 定时本篇三批方向按需选往深了学polars更快的新一代 DataFrame、duckdb用 SQL 查 Parquet/CSV本地分析神器往宽了学SQLGROUP BY和groupby是同一套思维、统计学基础、scikit-learn往工程走把脚本包成 APIFastAPI、上调度Airflow / Dagster、加测试pytest 样例数据。常见问题Q1argparse在 Jupyter 里报SystemExit: 2。Jupyter 传了它不认识的参数比如-f kernel.json。文章里的写法是在代码里显式parse_args([...])不要用parse_args()读sys.argv。Q2日志重复输出两遍。logger.addHandler调了两次比如脚本被 import 两次。加logger.handlers.clear()或者在addHandler前判断if not logger.handlers:。Q3python report.py报找不到data/sales.csv。相对路径是相对于「你运行命令时所在的目录」。定时任务里尤其容易踩crontab 里先cd到项目目录或者干脆在代码里用绝对路径。小结工具化的三件套argparse 管输入、logging 管过程、定时管触发。判断一个脚本是否「可交付」就看三点别人能不能不改代码就用、出错时能不能查到原因、能不能无人值守地跑。做到这三点你就从「会写脚本」跨到了「会做工具」。第二批到此结束。数据这条路很长但你已经把最常用的 80% 走完了——剩下的遇到问题再查比从头看教程快得多。