nodriver 报表采集实战:自动下载、检查文件与合并数据
配置 nodriver 与下载目录,等待 CSV 文件落盘并校验日期、字段和金额,再按业务键合并日报,避免把下载触发当成完成。
报表页面显示“下载已开始”,不代表文件已经完整写入。若立即关闭浏览器或读取下载目录里的一份旧文件,最终汇总可能既不完整也难以发现。
本文用 nodriver 完成下载目录配置、文件完成检查、CSV 解析与日报合并。正文提供两份内嵌 CSV 下载材料,不需要后台账号或另一个演示系统;真实后台的日期筛选和登录可接在取得下载地址之前。
安装与浏览器准备
使用 Python 3.12,并安装系统 Chrome/Chromium:
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install nodriver==0.50.99nodriver 不是 WebDriver 下载器,安装 Python 包不会自动安装 Chrome。本文遵循项目的异步事件循环用法,见 官方仓库。
明确文件与数据两个完成条件
文件层需要确认目标是本批次的新文件、不是 .crdownload 临时文件,并且写入已稳定。数据层还要确认内容确实是 CSV、列名正确、日期属于本次请求、金额与币种能够解释。
文件大小稳定本身不能在所有下载机制下构成严格完成证明。本例再检查完整 CSV 结构;更严格的流程应启用下载事件并结合实际文件检查。不能只等待 download_file 返回,这个方法主要触发页面下载动作。
完整下载与合并示例
创建 download_reports.py:
import asyncio
import base64
import csv
import io
import time
from datetime import date
from decimal import Decimal
from pathlib import Path
import nodriver as uc
REPORTS = [
("2026-10-27", "date,order_id,amount,currency\n2026-10-27,o1,12.50,CNY\n2026-10-27,o2,20.00,CNY\n"),
("2026-10-28", "date,order_id,amount,currency\n2026-10-28,o3,9.90,CNY\n"),
]
async def wait_for_file(path, timeout=30):
deadline = time.monotonic() + timeout
previous, stable = None, 0
while time.monotonic() < deadline:
if path.exists():
stat = path.stat()
current = (stat.st_size, stat.st_mtime_ns)
stable = stable + 1 if current == previous and stat.st_size > 0 else 0
previous = current
if stable >= 3:
return path
await asyncio.sleep(0.3)
raise TimeoutError(f"文件未稳定:{path.name}")
def parse_report(path, expected_date):
raw = path.read_bytes()
if raw.lstrip().startswith(b"<"):
raise ValueError("下载内容可能是 HTML,不接受为报表")
reader = csv.DictReader(io.StringIO(raw.decode("utf-8-sig")))
if reader.fieldnames != ["date", "order_id", "amount", "currency"]:
raise ValueError("CSV 列名或顺序变化")
rows = list(reader)
if not rows:
raise ValueError("报表没有记录,请检查是否为明确空日报")
for row in rows:
if date.fromisoformat(row["date"]).isoformat() != expected_date:
raise ValueError("报表日期与请求不一致")
value = Decimal(row["amount"])
if not value.is_finite() or value < 0 or row["currency"] != "CNY" or not row["order_id"]:
raise ValueError("金额、币种或订单编号不合法")
return rows
async def main():
output = Path("report-downloads")
output.mkdir()
browser = await uc.start(headless=True)
records, seen = [], set()
try:
tab = await browser.get("about:blank")
await tab.set_download_path(str(output.resolve()))
for report_date, text in REPORTS:
filename = f"report-{report_date}.csv"
target = output / filename
encoded = base64.b64encode(text.encode("utf-8")).decode("ascii")
url = "data:text/csv;base64," + encoded
await tab.download_file(url, filename)
await wait_for_file(target)
for row in parse_report(target, report_date):
key = (row["date"], row["order_id"], row["currency"])
if key in seen:
raise ValueError("跨文件出现重复日报记录")
seen.add(key)
records.append({**row, "source_file": filename})
finally:
browser.stop()
with (output / "merged.csv").open("w", encoding="utf-8-sig", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=[
"date", "order_id", "amount", "currency", "source_file"
])
writer.writeheader()
writer.writerows(records)
total = sum((Decimal(row["amount"]) for row in records), Decimal(0))
print({"rows": len(records), "total_cny": str(total)})
uc.loop().run_until_complete(main())运行:
python download_reports.pyreport-downloads 必须是新目录,防止旧文件满足本次等待条件。data URL 是正文中的已知教学材料,未访问真实后台,也不证明任何后台导出都能采用同一触发方式。
为什么不在方法返回后立即读取
浏览器下载触发可能比文件完成早得多。示例轮询目标文件的大小与修改时间,再解析其内容。它不把“目录里多了某个文件”自动当成正确报表,而是等待当前批次的明确文件名。
真实下载可能采用服务端文件名或按 GUID 命名,不能保证传给 download_file 的名字就是最终名称。若使用下载事件,需显式启用 events_enabled,并处理 completed、canceled 等状态;事件完成后仍核对文件实际存在。事件字段与磁盘文件名不是永远一一对应。
怎样核对合并结果
正文材料推导出三条日报,金额共 42.40 CNY。这个数值是输入算术,不是下载实测日志。
merged.csv 保留 source_file,方便定位哪份日报贡献了某条记录。汇总键包含日期、订单 ID 和币种,表示“某日这笔订单的报表记录”;如果你的报表跨天重复列出同一订单状态,应重新定义快照与事件语义,不能把本文键直接当作全局订单去重。
零条报表没有被自动接受,因为本文合同预期有记录。真实业务允许空日报时,应要求明确的日期与空报表状态,不把一次失败下载的空文件当作合法零销售。
接到真实后台时改哪些地方
先在独立 Profile 中完成登录,选择明确起止日期,等待筛选与导出准备。然后根据网站机制选择点击下载控件或读取已生成的文件地址。
需要登录的下载地址可能依赖 Cookie、POST 参数或一次性 token。直接重新 fetch 一个 URL 不一定等价于点击原生导出动作;也可能受跨域策略影响。沿用页面实际下载流程,再使用相同的完成与数据检查。
Excel 文件不能送给 csv.DictReader。先确认文件类型,使用适合的 Excel 解析器,保留工作表、日期和单元格类型,再按统一记录合同合并。
常见问题与修复
文件一直不出现。 检查目录是否在浏览器运行环境中可写,动作是否真正触发,以及是否使用了另一份文件名。不要先延长 CSV 解析超时。
文件有内容却报列名错误。 检查是否下载成登录 HTML、分隔符变化或导出版本变化。回到原始文件判断,不直接重命名列凑合同。
导出日期不对。 核对页面筛选是否更新,以及是否读取旧缓存或旧文件。日期错误应该阻止合并,而不在后处理时改日期掩盖来源。
重复记录很多。 检查文件是否重复下载、日期范围是否重叠,以及报表究竟是快照还是事件。先明确业务键,再选择保留或合并。
浏览器关闭后文件损坏。 等待下载完成和数据检查后再关闭。stop 不应被当作下载 flush 操作。
大小稳定但后续又变化。 当前轮询策略不适合该下载源,改用完成事件与内容合同结合,而不是继续增加一个随意等待时长。
把文件完成、类型验证和业务对账分开,才能让自动下载变成可信的报表作业。
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。