Python 数据处理进阶(二十四)把脚本变成工具:argparse、logging 与定时任务

写在前面

这是第二批的最后一篇。前 23 篇把「怎么处理数据」讲完了,这一篇讲怎么让脚本被别人用起来。

写给自己用的脚本,改参数、print 调试、手动运行都没问题;但只要交给同事、或者要每周自动跑,就必须补三件事:参数 (不改代码就能换输入输出)、日志 (出问题能查)、定时(不用人盯着)。

这篇把这三件事做成一个完整的「报表生成器」,你抄走就能用。

先确认示例数据在手边:

python 复制代码
import os
import mkdata

if not os.path.exists("data/sales.csv"):
    mkdata.main()
print("示例数据就绪:", os.path.exists("data/sales.csv"))

这段会检查第 1 篇生成的 data/ 目录在不在,不在就自动补一次;已经有数据时只会打印一行 示例数据就绪: True,不影响后面的结果。

一、argparse:把参数从代码里挪出来

最朴素的写法是「改代码里的路径」,麻烦且容易改错。argparse 让你在命令行传参,还能自动生成 --help。

python 复制代码
import argparse

def build_parser():
    parser = argparse.ArgumentParser(
        prog="report.py", description="从销售明细生成 Excel 报表"
    )
    parser.add_argument("input", help="输入 CSV 路径")
    parser.add_argument("--top", type=int, default=5, help="每个维度取前 N 名")
    parser.add_argument("--out", default="output/report.xlsx", help="输出 xlsx 路径")
    parser.add_argument("--verbose", action="store_true", help="输出调试日志")
    return parser

parser = build_parser()
# 文章里演示用显式参数,实际运行时直接 python report.py data/sales.csv
args = parser.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()
text 复制代码
解析结果:
  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)给默认值,命令行里少写几个也能跑;
  • type=int 让 argparse 自动转换并校验,传 abc 会直接报错,不用自己判断;
  • action="store_true" 是布尔开关,--verbose 出现即为 True。

二、logging:print 是调试,logging 是工程

print 的问题:没法分级、没法关、没法带时间戳和行号。logging 四行配置就解决。

python 复制代码
import logging
import os

os.makedirs("output", exist_ok=True)

logger = logging.getLogger("report")
logger.setLevel(logging.INFO)
logger.handlers.clear()          # 避免重复添加 handler(Jupyter 里尤其容易)

fmt = logging.Formatter("%(asctime)s | %(levelname)-7s | %(message)s", "%Y-%m-%d %H:%M:%S")

console = logging.StreamHandler()
console.setFormatter(fmt)
logger.addHandler(console)

file_handler = logging.FileHandler("output/report.log", encoding="utf-8")
file_handler.setFormatter(fmt)
logger.addHandler(file_handler)

logger.info("开始生成报表")
logger.warning("示例数据只有 16 行,仅供演示")
try:
    1 / 0
except ZeroDivisionError:
    logger.exception("计算失败,已捕获异常并继续")   # exception 会带上 traceback
logger.info("流程结束,日志已写入 output/report.log")

with open("output/report.log", encoding="utf-8") as f:
    print("---- 日志文件内容 ----")
    print(f.read())
text 复制代码
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 写进日志------这是排查线上问题最值钱的一行。

三、合起来:一个能交付的报表生成器

把参数、日志、数据处理、导出串起来,每个环节都留日志,出错不崩。

python 复制代码
import argparse
import logging
import os

import pandas as pd

def setup_logger(verbose=False):
    logger = logging.getLogger("report")
    logger.setLevel(logging.DEBUG if verbose else logging.INFO)
    logger.handlers.clear()
    fmt = logging.Formatter("%(asctime)s | %(levelname)-7s | %(message)s", "%H:%M:%S")
    sh = logging.StreamHandler()
    sh.setFormatter(fmt)
    logger.addHandler(sh)
    return logger

def build_report(path, top=5, out="output/report.xlsx", logger=None):
    logger = logger or setup_logger()
    logger.info("读取数据:%s", path)
    df = pd.read_csv(path, encoding="utf-8-sig")
    df["金额"] = df["金额"].fillna((df["数量"] * df["单价"]).round(2))
    clean = df[df["金额"] > 0].drop_duplicates()
    logger.info("原始 %d 行,清洗后 %d 行", len(df), len(clean))

    by_city = (clean.groupby("城市", dropna=False)["金额"].sum()
               .sort_values(ascending=False).head(top).round(2).reset_index())
    by_product = (clean.groupby("产品")["金额"].sum()
                  .sort_values(ascending=False).head(top).round(2).reset_index())

    os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
    with pd.ExcelWriter(out, engine="openpyxl") as writer:
        by_city.to_excel(writer, sheet_name="城市Top", index=False)
        by_product.to_excel(writer, sheet_name="产品Top", index=False)
    logger.info("已写出:%s", out)
    return out, by_city, by_product

parser = argparse.ArgumentParser(prog="report.py", description="生成销售报表")
parser.add_argument("input")
parser.add_argument("--top", type=int, default=3)
parser.add_argument("--out", default="output/report.xlsx")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args(["data/sales.csv", "--top", "3", "--verbose"])

log = setup_logger(args.verbose)
try:
    out, city, product = build_report(args.input, args.top, args.out, log)
except FileNotFoundError:
    log.error("输入文件不存在:%s", args.input)
    raise SystemExit(1)

print("城市 Top:")
print(city.to_string(index=False))
print("产品 Top:")
print(product.to_string(index=False))
text 复制代码
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 / macOS crontab 0 8 * * 1 cd /path/to/project && python3 report.py data/sales.csv >> cron.log 2>&1
任意 GitHub Actions 定时 workflow,schedule: - cron: "0 0 * * 1"
服务器 系统服务(systemd / supervisor) 适合常驻任务,不适合「跑一次就退出」

三条工程习惯,比工具本身重要:

  1. 幂等 :同一天跑两次,结果应该一样(文件名带日期、if_exists="replace");
  2. 日志落盘:定时任务没人看着,日志是唯一的现场;
  3. 失败要能知道:最简单的做法是失败时发一封邮件 / 一条 webhook,别等下周才发现没跑。

五、第二批 12 篇回顾

# 主题 一句话
13 多层索引 set_index 造、loc/xs 取、unstack 转
14 向量化 列运算比 for 循环快上千倍
15 多种数据源 JSON、网页表格、数据库各有各的 read_*
16 内存优化 降级 + category,55 MB 压到 4 MB
17 自动周报 groupby → 多 sheet → openpyxl 格式
18 批量处理 glob → 循环读加来源列 → concat
19 Pandas 与 Excel Pandas 算,Excel 呈现和协作
20 数据质量 10 项校验做成函数,挂到交付流程
21 相关性与分组 corr / crosstab / groupby / 热力图
22 用户行为分析 留存、复购、月活,都是分组建透视
23 Seaborn 统计图:误差棒、箱线、热力、pairplot
24 工具化 argparse + logging + 定时(本篇)

三批方向,按需选:

  • 往深了学 :polars(更快的新一代 DataFrame)、duckdb(用 SQL 查 Parquet/CSV,本地分析神器);
  • 往宽了学 :SQL(GROUP BY 和 groupby 是同一套思维)、统计学基础、scikit-learn;
  • 往工程走:把脚本包成 API(FastAPI)、上调度(Airflow / Dagster)、加测试(pytest + 样例数据)。

常见问题

Q1:argparse 在 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:。

Q3:python report.py 报找不到 data/sales.csv。

相对路径是相对于「你运行命令时所在的目录」。定时任务里尤其容易踩:crontab 里先 cd 到项目目录,或者干脆在代码里用绝对路径。

小结

工具化的三件套:argparse 管输入、logging 管过程、定时管触发。判断一个脚本是否「可交付」,就看三点:别人能不能不改代码就用、出错时能不能查到原因、能不能无人值守地跑。做到这三点,你就从「会写脚本」跨到了「会做工具」。

第二批到此结束。数据这条路很长,但你已经把最常用的 80% 走完了------剩下的,遇到问题再查,比从头看教程快得多。

相关推荐
shelly聊外贸3 小时前
WhatsApp 聊天记录如何导出为 CSV?使用 Python 进行数据分析
开发语言·python·数据分析
I Am a robert girl3 小时前
从 FlowAct-R2 拆解“会说话的数字人“:流式多模态参考与主动式 Agent 规划
python·数字人·多模态大模型·扩散transformer·流式生成·agent规划
梯度下降者3 小时前
Python使用统计建模解决业务预测问题的常见操作步骤【教程】
python·可解释性·数据清洗·模型选择·业务预测
Frag0ut4 小时前
Notepad++ 零基础入门:从记事本到代码编辑器的平滑过渡(2026最新版)
python·编辑器·html·json·notepad++·markdown·记事本
深蓝AI5 小时前
Webhook 重投别再执行两次:Python + SQLite 收件箱实测
python
Eider19985 小时前
Python基础知识之并发相关-2
开发语言·前端·python
第九序言T5 小时前
Python 数据处理进阶(二十二)用 Pandas 做一次完整的用户行为分析
开发语言·python·数据分析·pandas
云和数据.ChenGuang5 小时前
再论深拷贝和浅拷贝
开发语言·人工智能·python·1024程序员节
第九序言T5 小时前
Python 数据处理进阶(二十一)相关性与分组对比:数据里藏着什么关系
大数据·人工智能·python·数据分析·pandas