
销售日报需要汇总各渠道的订单数、销售额和客单价。渠道超过十个以后,手工把数据从各平台抄进同一张 Excel 就成了明显负担:耗时、易错、且无法追溯数据来源。以 11 个渠道、日均 42 分钟手工汇总估算,每月仅此一项就占用约 15 个小时。本文用一个可落地的 Python 脚本,把这份日报改成每天定时自动生成。流程拆成取数、归一化、算指标、写 Excel、调度五步,代码可直接复用,数据源与代理配置集中管理,渠道字段变动时只改对应函数。先说清日报的结构:左侧各渠道一行,右侧是当日订单数、销售额、客单价,末尾合计并附环比。格式稳定是自动化的前提,若格式频繁变动,脚本反而增加维护成本。先把事情拆开一份"日报"其实就三件事。第一,数据从哪儿来。第二,拿回来怎么算。第三,算完怎么落盘成 Excel 并发出去。这三步我分开写,后面渠道改字段或者老板要加指标,都不用动全身。有人会问,为什么不直接用 pandas 一口气读库写表完事。因为我们的情况是混合数据源,没法一刀切。公司自有订单系统在公司内网,直接连库拉就行。另外两个渠道的销量要走平台开放接口,而这个接口按出口 IP 限流,办公室那条固定 IP 请求两轮就被封半小时。这两类数据源的取数逻辑完全不同,硬塞一个函数里只会越写越乱。也有人推荐直接用 Excel 的 Power Query。试过,图形化界面点起来爽,但渠道一多,每个查询的刷新顺序、依赖关系全藏在界面背后,出了错没法 diff,也没法进版本库。脚本丑是丑,但每一行都在眼前,新人接手看代码就行,不用我坐在旁边讲半小时。抓数据,并且要能重试网络这种事你控制不了,别指望一次就通。接口超时、平台抖一下、代理偶尔抽风,都是常态。我用了 tenacity 做重试,最多试三次,失败之间退避等待,避免一拥而上把接口打挂。报表自动化为什么绕不开代理先把背景讲透。日报要拉十几个渠道,其中有几个是淘宝、抖音这类开放平台,它们的订单接口都带反爬。办公室那条固定出口 IP,请求两轮就被限流半小时。我一开始加了 sleep 退避,没用,平台认的是 IP 不是频率。后来接了亿牛云的动态转发代理(隧道代理)。它背后是一个很大的 IP 池,只配一个固定入口,每次转发自动换出口 IP,对目标平台来说每次请求都来自不同地方,而且是匿名的,拉数据跟正常访问没两样。我不用自己维护 IP 池,某个 IP 被封也不会全盘停摆。日报一天几十兆流量,动态转发按月付,成本比自己租服务器搭池子低很多,这也是我没自己养 IP 的原因。代理的鉴权是用户名加密码,格式是 http://用户名:密码@入口域名:端口。用户名里通常带订单号和地区参数,具体以后台接入串为准,别照抄网上的例子。动态转发一般要求把调用方出口 IP 加进白名单,否则拒绝连接。我第一次配完连不上,查了半天是忘绑白名单,这种坑踩一次就记住了。
from dataclasses import dataclass
from datetime import date
from typing import Optional
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
@dataclass
class YiniuProxy:
"""动态转发代理配置,集中管理鉴权与入口。"""
order_id: str
secret: str
host: str = "dynamic.ynyun-proxy.com"
port: int = 8000
region: str = "cn"
@property
def tunnel(self) -> str:
# 用户名里带订单号与地区是代理的鉴权约定,按后台接入串填
user = f"{self.order_id}-region-{self.region}"
return f"http://{user}:{self.secret}@{self.host}:{self.port}"
PROXY = YiniuProxy(order_id="YOUR_ORDER", secret="YOUR_SECRET").tunnel
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
def fetch_channel_orders(channel: str, day: date) -> list[dict]:
"""拉取单个渠道某天的订单原始数据。
Args:
channel: 渠道标识,例如 "taobao" 或 "douyin"
day: 统计日期
Returns:
平台返回的原始订单字典列表
Raises:
httpx.HTTPError: 三次重试后仍失败
"""
resp = httpx.get(
f"https://api.example.com/{channel}/orders",
params={"date": day.isoformat()},
headers={"User-Agent": "Mozilla/5.0 (compatible; daily-report/1.0)"},
proxies={"http://": PROXY, "https://": PROXY},
timeout=15,
)
resp.raise_for_status()
return resp.json()["data"]抓取我用的是 httpx 而不是 requests,看中的就是它的异步能力。渠道一多,十几个请求串行太慢,改成 asyncio 并发之后,隧道代理给每个请求分配不同出口 IP,天然适合并发跑,日报生成时间从一分钟压到了十秒出头。有个坑得提醒:平台的开放接口大多校验 User-Agent,不带就直接返回 403,第一次跑不通我查了半小时才定位到。还有一件事要提前想:十几个渠道里只要有一个挂了,整份日报就废了吗?我的做法是每个渠道单独 try,失败的记进日志并跳过,最后如果全部失败才整体告警。这样某个平台凌晨维护,不影响其他渠道的日报照常出来。
def fetch_all(channels: list[str], day: date) -> list[SalesRecord]:
records: list[SalesRecord] = []
for ch in channels:
try:
raw = fetch_channel_orders(ch, day)
records.extend(normalize(raw, ch, day))
except Exception as exc: # 单渠道失败不应拖垮整份日报
logging.error("渠道 %s 拉取失败: %s", ch, exc)
return records用 dataclass 把一行日报钉死拿到原始数据先别急着算,先定义清楚"一行日报长什么样"。我习惯用 dataclass 把字段和类型钉死,后面无论是存库还是出 Excel,都围绕这个结构转。类型不对解释器当场报错,比跑起来才发现某列是 None 强太多。比起直接用 dict,dataclass 还有个好处:你能把客单价这种派生字段写成 property,调用方拿到的永远是算好的值,不用到处重复那段除法。
@dataclass
class SalesRecord:
channel: str
order_date: date
order_count: int
total_amount: float
customer_count: int
@property
def avg_order_value(self) -> float:
if self.customer_count == 0:
return 0.0
return self.total_amount / self.customer_count不同渠道返回的字段名天差地别,有的叫 order_cnt,有的叫 pay_count,金额字段有的带单位后缀。这一步是纯体力活,但集中在一个归一化函数里,以后改起来不头疼。
def normalize(raw: list[dict], channel: str, day: date) -> list[SalesRecord]:
"""把平台原始订单转成统一的 SalesRecord。"""
records: list[SalesRecord] = []
for item in raw:
records.append(
SalesRecord(
channel=channel,
order_date=day,
order_count=int(item["order_cnt"]),
total_amount=float(item["pay_amount"]),
customer_count=int(item["buyer_cnt"]),
)
)
return records算指标,逻辑摊开在眼前总销售额求和,客单价用总额除以人数,环比拿今天和昨天的差额除昨天。数据量小的时候我更偏向手写而不是上 pandas,逻辑摊开在眼前,新人也能一眼看懂。等哪天单渠道一天几万单,再考虑换 pandas 也不迟。除以昨天那段一定要先判断昨天是不是零,否则新渠道上线第一天就会抛除零异常,我踩过这个坑。
落盘成 Excel出文件用 openpyxl。样式最容易写成一坨,我把它单独抽成一个函数,只负责写数据和设置表头加粗、列宽自适应。这样哪天要把日报改成横向排版,只动这个函数。金额这类字段,openpyxl 默认按浮点存,Excel 里会显示成一长串小数或者科学计数法。我在写之前用 round 保留两位小数,需要千分位的话再套一个 f"{x:,.2f}"。老板看报表不爱看 12345.6789,他要看 12,345.68。这种细节代码不会替你想,得你自己定。日报我按日期命名存本地,顺手也传一份到共享盘,三个月攒下九十多个文件,哪天要拉趋势直接读回来,连数据库都不用碰。
上线前先测一遍脚本写完别直接丢服务器上。我本地用 pytest 写了三个用例:一个造几条假 SalesRecord 验 aggregate 的环比计算,一个验 normalize 在字段缺失时不崩,一个验 write_report 产出的文件能被 openpyxl 重新打开。跑通再上线,比凌晨被告警叫起来改 bug 舒服得多。这三个用例加起来不到四十行,但每次改归一化逻辑之前跑一遍,心里有底。
from datetime import date
import pytest
from openpyxl import load_workbook
def test_aggregate_mom():
today = [SalesRecord("self", date(2026, 8, 1), 10, 1000.0, 8)]
prev = [SalesRecord("self", date(2026, 7, 31), 10, 500.0, 8)]
result = aggregate(today, prev)
assert result["mom"] == 100.0
def test_normalize_missing_field():
raw = [{"order_cnt": "3", "pay_amount": "300", "buyer_cnt": "2"}]
assert len(normalize(raw, "self", date(2026, 8, 1))) == 1
def test_write_report_readable(tmp_path):
recs = [SalesRecord("self", date(2026, 8, 1), 3, 300.0, 2)]
p = tmp_path / "r.xlsx"
write_report(str(p), recs, aggregate(recs))
assert load_workbook(p)["销售日报"]["A1"].value == "渠道"串起来,并留好日志main 里就四步:拉数据、归一化、算指标、写文件。再加一层 logging,哪天报表没出来,能回看是卡在抓取还是卡在写盘。我真实遇到过一次:某渠道接口凌晨升级,返回的字段改了名,归一化函数直接抛错。多亏有日志,我早上看了一眼就知道是哪一个渠道,十分钟修完补跑,没耽误早会。光写日志不够,失败了得有人知道。我在 main 外层包了一层 try,任何渠道连续失败就发一条钉钉告警到运维群。第一次收到告警是某个渠道证书过期,我比平台客服早发现,挺有成就感的。代理侧也不是永远稳,有回代理入口做了维护,日志里一排连接拒绝,我看了一眼就知道不是我代码的问题,等它恢复就行,没白折腾去查自己的脚本。
import logging
logging.basicConfig(filename="report.log", level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")
def main() -> None:
day = date.today()
all_records: list[SalesRecord] = fetch_all(["taobao", "douyin", "self"], day)
if not all_records:
logging.error("全部渠道拉取失败,终止生成")
return
summary = aggregate(all_records)
write_report(f"sales_{day.isoformat()}.xlsx", all_records, summary)
logging.info("日报已生成:%s", summary)定时跑我用 schedule 库,比 crontab 好懂,部署在 Windows 上也零配置。要是跑在 Linux 服务器上,我更推荐直接交给 crontab,不用常驻一个进程占着内存。
import schedule
import time
schedule.every().day.at("07:00").do(main)
while True:
schedule.run_pending()
time.sleep(30)发出去那步我接了公司 SMTP,日报作为附件发到指定邮箱。你们团队用钉钉或者企业微信也行,把写文件换成调对应的机器人接口即可,改动很小。三个月下来的体会脚本上线三个月,日报没漏过一天。我估摸每天省下至少半小时,更值钱的是不再有抄错行的低级事故,老板追问某个数字时我也能从日志里翻出来源。以前那种"这个数字好像不太对但又说不清哪不对"的心虚感,没了。这脚本第一版其实很丑,所有逻辑堆在一个文件里,连配置文件都没有。它能跑,我就先让它跑了三个月。等真的稳定了,才把渠道列表、代理配置挪进一个 config 文件。我建议大家都这么干:先让能用的版本上线,再谈优雅。追求一步到位容易卡在抽象设计上,最后啥也没上线。也不是所有报表都该自动化。那种只出一次的季度复盘、字段随时在变的临时分析,写脚本的维护成本可能比手工还高。判断标准很简单:这份报表接下来三个月还会不会以同样的格式出现。会,就值得自动化;不会,老老实实复制粘贴。维护成本真实存在,渠道字段平均两个月变一次,每次十分钟,对比每天省下的半小时,这笔账怎么算都划算。如果团队不止你一个人要看,把脚本和 config 一起丢进 git,谁改了什么一目了然,比把 Excel 传来传去强。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。