nodriver: Download, Check, and Merge Reports

Configure browser downloads, wait for fresh files, validate CSV dates and amounts, and merge daily reports with traceable source files.

16Yun Engineering TeamOct 28, 20263 min read

A “download started” message is not a completed report. Closing the browser immediately or reading an old file can create an incomplete yet plausible summary.

This tutorial provides two embedded CSV download materials and connects nodriver download setup, file checks, parsing, and reconciliation. Real dashboard login and date selection belong before obtaining the actual download.

Install the browser prerequisite and package

Use Python 3.12 and an installed Chrome/Chromium:

python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install nodriver==0.50.99

The package is not a browser downloader. The example follows the project's event-loop style. See the official repository.

Define file and data completion

A file must belong to this batch, not be an old file or temporary download. Its content must also match the expected CSV, date range, and money contract.

Stable size alone is not a universal completion guarantee. The example combines a practical stability check with CSV validation; stronger integrations should correlate browser completion events with actual files.

Run the complete workflow

Create 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())

Run:

python download_reports.py

The output directory must be new. The data URLs contain the article's known teaching material, not real dashboard responses or a guarantee that every report uses the same download mechanism.

Wait beyond the trigger

download_file can return before completion. The example waits for the expected file and then validates it.

Real servers or GUID-based downloads may choose a different filename. With download events, explicitly enable them, handle completion and cancellation, and still verify disk availability. Event identity and local naming are not universally interchangeable.

Check the merged data

The supplied materials imply three rows and 42.40 CNY. This is input-derived arithmetic, not a captured run.

Every merged row retains its source file. The demonstrated key means an order's daily report observation. A report repeatedly listing the same order across days may instead require snapshot or event semantics.

The example rejects empty reports because its input contract expects rows. A real legitimate empty day needs explicit date and empty-state evidence rather than acceptance of any empty file.

Adapt to a real dashboard

Establish login in an independent profile, select explicit dates, and wait for export readiness. Use the site's actual download action or generated address.

Authenticated downloads can depend on cookies, POST data, or one-time tokens. A fresh fetch of a URL is not necessarily equivalent to the original action and can encounter cross-origin restrictions.

Excel requires an appropriate parser rather than CSV handling. Retain sheet, date, and cell-type meaning before mapping records into a common schema.

Troubleshoot

No file appears: inspect the browser's writable path, actual trigger, and selected filename.

Unexpected columns: distinguish login HTML, delimiter changes, and export schema changes before renaming fields.

Wrong dates: inspect filter readiness and old files or cached exports. Do not rewrite dates to conceal provenance errors.

Duplicate records: inspect repeated files, overlapping ranges, and snapshot-versus-event meaning.

Closing damages a download: finish file and content checks first; browser stop is not a flush operation.

A stable file later changes: use a source-appropriate completion event and content contract rather than adding arbitrary sleeps.

Trustworthy report automation separates triggering, file completion, format, and business reconciliation.

Need an enterprise proxy plan?

We can tailor architecture to your target domains, concurrency, and reliability goals.