利用Python轻松实现报表自动化
我跟你说啊,报表这玩意,是真能把人整崩溃。
前两天一个朋友在群里吐槽,说他们老板每天早上 9 点要看到前一天的销售日报,他每天 8 点 40 打开 Excel,vlookup 一顿狂拉,透视表一顿狂点,正算着呢,微信还跳出来一句:“今天报表晚了两分钟,是不是状态不对?”……我当时听完就回他一句:你为啥不用 Python 啊兄弟。
其实所谓“报表自动化”,翻译成人话就是:把你每天重复那套鼠标点来点去的动作,换成一段死心塌地不会离职的 Python 代码。你只要保证机器开着,它比你还积极。
我先给你看个最土的版本哈,就那种“我啥都不懂但想先跑起来”的:
import pandas as pd
from datetime import datetime, timedelta# 昨天的日期字符串,比如 2024-01-01
yesterday = (datetime.today() - timedelta(days=1)).strftime("%Y-%m-%d")
# 1. 读原始数据,可能是数据库导出的 CSV
df = pd.read_csv(f"data/order_{yesterday}.csv", encoding="utf-8")
# 2. 按门店 + 商品汇总一下销售额
report = (
df.groupby(["store_name", "product_name"], as_index=False)["amount"]
.sum()
.rename(columns={"amount": "total_amount"})
)
# 3. 导出到一个新的 Excel 报表
output_file = f"report/sale_report_{yesterday}.xlsx"
report.to_excel(output_file, index=False)
就这几十行,你平时在 Excel 里点半小时的东西,它三秒钟就干完。重点是——你不用每次去改日期,它自己滚动到昨天。你只要保证每天有一份 order_日期.csv 扔到那个 data 目录就行。
有一次我们财务小姐姐找我,说他们每个月结账要合并 20 多个门店的 Excel,手一抖就错一列,我看她桌子上摆了一排咖啡,我说你这不是财务,这是肝硬化预备役。然后我拿她其中一份 Excel 看了下,结构特别规矩:第一行是表头,后面就是数据,我就直接开干了。
import pandas as pd
from pathlib import Pathinput_dir = Path("shops") # 里面一堆 门店_2024-01.xlsx
all_dfs = []
for file in input_dir.glob("*.xlsx"):
# 从文件名里抠出门店名
store_name = file.stem.split("_")[0]
df = pd.read_excel(file)
df["store_name"] = store_name
all_dfs.append(df)
merged = pd.concat(all_dfs, ignore_index=True)
# 来个简单的月度汇总
summary = merged.groupby("store_name", as_index=False)["amount"].sum()
with pd.ExcelWriter("月度汇总.xlsx") as writer:
merged.to_excel(writer, sheet_name="明细", index=False)
summary.to_excel(writer, sheet_name="门店汇总", index=False)
这玩意跑完,她电脑那边“叮”一声,多了个《月度汇总》,她点开一看,两页 sheet,明细齐的,汇总也对,然后看我眼神就变了——那种“你早干嘛去了”的眼神。😅
这里有个小坑我顺便提前说一下,不然你肯定踩:报表不是只有数据,还有“格式”,尤其是那种老板亲自调过颜色、边框、加粗、logo 的模板,动一下都会被质疑 KPI。这个时候就不能简单 to_excel 了,要走一个“套模板”的路线。
大概像这样:
import pandas as pd
from openpyxl import load_workbooktemplate_file = "模板.xlsx"
output_file = "日报_成品.xlsx"
# 1. 生成数据
df = pd.read_csv("data/today.csv")
# 2. 先用模板创建一份新文件
wb = load_workbook(template_file)
wb.save(output_file) # 相当于复制了一份
# 3. 再往这个新文件里写数据到指定 sheet、指定起始单元格
with pd.ExcelWriter(output_file, engine="openpyxl", mode="a", if_sheet_exists="overlay") as writer:
df.to_excel(writer, sheet_name="数据明细", startrow=3, startcol=1, index=False)
上面这个思路就是:模板归模板,数据归数据,先把壳子弄好,再往里灌数据,logo、颜色、合并单元格全都在模板里,不用你管。而且财务小姐姐最爱的“货币格式”、“千分位”,也全都继承下来。
有同学可能会问:那图表呢?我想要自动生成折线图、柱状图啥的。图表其实有两种懒人打法:
一种是图表也画在模板里,只要数据区域不变,Python 只要把数据刷进去,Excel 自己就刷新图表,这种最省事。
另一种是真正用 openpyxl 或 xlsxwriter 在代码里创建图表,大概像这样:
import pandas as pd
import xlsxwriterdf = pd.read_csv("data/daily_sale.csv")
with pd.ExcelWriter("带图表的报表.xlsx", engine="xlsxwriter") as writer:
df.to_excel(writer, sheet_name="数据", index=False)
workbook = writer.book
worksheet = writer.sheets["数据"]
chart = workbook.add_chart({"type": "column"})
# 这里的 row/col 是0基的,所以 A2:C10 要自己换算一下
chart.add_series({
"name": "销量",
"categories": ["数据", 1, 0, len(df), 0], # 日期
"values": ["数据", 1, 1, len(df), 1], # 销量
})
worksheet.insert_chart("E2", chart)
不过老实说啊,大部分时候我都用第一种“模板自带图表”的偷懒方法,写 chart 的坐标太费脑细胞了。
说完生成报表,还有一个灵魂拷问:谁去点开报表发邮件?如果这一步还是你自己干,那就不叫自动化,最多叫半自动。
最简单的思路,就是 Python 顺手把报表丢邮件里发给老板。那天我给另一个小伙伴搞这个,他服务器是 Linux 的,我们就直接上 smtplib,也不搞花里胡哨的 HTML,先能用再说:
import smtplib
from email.message import EmailMessage
from pathlib import Pathdefsend_report(receiver, file_path):
msg = EmailMessage()
msg["Subject"] = "每日销售日报"
msg["From"] = "[email protected]"
msg["To"] = receiver
msg.set_content("附件是今天的销售报表,那个你先看,有问题再艾特我~")
file_path = Path(file_path)
data = file_path.read_bytes()
msg.add_attachment(data, maintype="application", subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=file_path.name)
with smtplib.SMTP("smtp.demo.com", 25) as server:
server.login("[email protected]", "your_password_here")
server.send_message(msg)
然后在生成报表之后一句:
send_report("[email protected]", output_file)
你就从“手动发件人”变成了“邮件的最上面那行发件人名字”,体验完全不一样。
当然,真正的自动化的最后一块拼图,是定时跑。这个我当时是这么干的:Windows 的同学就搞个 bat 脚本:
@echo off
cd /d D:\report_project
python run_report.py
然后丢到“任务计划程序”里,说人话就是设置一下:“每天早上 8:30 跑一次”。Linux 上就 crontab 一行:
30 8 * * * /usr/bin/python3 /home/app/run_report.py >> /home/app/report.log 2>&1
从此每天 8:31,你的老板邮箱里就自动多出一封邮件,你还在路上挤地铁,系统已经帮你上班了。
有人问我,那如果报表逻辑变复杂了怎么办,比如要从 MySQL、接口、Excel 各种地方拉数据,搞一堆 join、过滤、异常值处理,会不会很乱?其实 pandas 就是给你干这个的,和你在数据库里写 SQL 一个味,只是换了个语法皮肤。你看我之前写数据库、消息队列那几篇,都是一个套路:先把底层原理搞明白,再在上面堆业务逻辑,别一上来就被“工具”牵着鼻子走。
还有一点特别现实的:你自动化了报表之后,线上出问题排查起来也要有点日志,不然哪天老板说“今天咋没有报表”,你啥都看不到,只能怀疑人生。这个跟我之前排查 SpringBoot 默认配置坑和 Feign 超时那个问题一样,日志打得清楚,人不会太慌。
你可以在脚本里加点最简单的日志,比如:
import logginglogging.basicConfig(
filename="report.log",
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
logging.info("开始生成日报")
# ... 中间出错的地方可以 logging.exception("生成日报失败")
logging.info("日报生成完成,保存到 %s", output_file)
这样哪天真挂了,你至少知道是读数据挂了,还是写 Excel 挂了,还是发邮件那块又被某个奇怪的附件名卡住了。就跟抓 TCP 包排 1024 那个诡异 bug 一样,有痕迹就好说话,不然真的是“盲人摸象”。
行了我先说到这儿,等会儿还得去帮人看个“为什么我用 Python 发邮件老板收不到”的问题,十有八九又是被当垃圾邮件扔了……你先把你们家那堆 Excel 拿出来,挑一份最简单的,照着上面这个思路撸一版脚本,跑通了再慢慢加料,不要一上来就想搞个“数据中台级别”的东西,容易劝退自己。