跳转至

aggregator —— 并发抓取聚合器(第 3 周小项目)

给一批 URL,并发抓取 JSON,限流、超时、重试,输出汇总(终端表格 / JSON / CSV),并对比"顺序 / 线程池 / asyncio"三种实现的耗时。自带桩服务器(asyncio 写的假 API,可配置延迟与失败率),演示、测试、基准全部打本地,不依赖外网。规格见 plan/week3.md 末节。

运行

uv sync                                                        # 仓库根目录执行一次
uv run aggregator serve-stub --port 8001 --delay 0.05 --fail-rate 0.2   # 终端 1:起桩服务器
uv run aggregator fetch --count 50 --limit 8 --timeout 2 -o summary.json # 终端 2:抓取并导出
uv run aggregator bench --count 30 --limit 10                            # 三种实现对比
uv run pytest projects/aggregator --cov=aggregator --cov-report=term-missing
uv run ruff check projects/aggregator

配置前缀 AGG_(见 .env.example):AGG_BASE_URLAGG_COUNTAGG_LIMITAGG_TIMEOUTAGG_RETRIESAGG_MAX_WORKERSAGG_LOG_LEVEL

结构与你要做的事

文件 状态
settings.py models.py logging_config.py stub_server.py cli.py 已完整,读懂即可(stub_server.py 本身就是一个 asyncio 服务器范例)
client.py TODOmake_async_client / make_sync_client / classify_error / fetch_one / fetch_one_sync(tenacity 重试、错误分类、永不抛异常)
engine.py TODObuild_urls / fetch_all_async(TaskGroup + Semaphore)/ fetch_all_threaded / fetch_all_sequential
report.py TODOpercentile / summarize / results_table / summary_table / write_json / write_csv(原子写)
tests/ 已完整:test_client.pytest_engine.pyMockTransport 离线跑;test_stub_server.pytest_integration.pytest_cli.py 打会话级本地桩服务器

顺序建议:client.pyengine.pyreport.py,每写完一个文件跑对应测试。骨架里被删掉的 import(asynciotenacityThreadPoolExecutorcsvjsonosCounter…)由你自己加回来。

验收

  • uv run pytest projects/aggregator --cov=aggregator 全绿,覆盖率 ≥80%(参考答案 94%)
  • ruff check projects/aggregator 零警告;Pylance 零错误
  • bench --count 30:线程池比顺序快 ≥5 倍;asyncio 版至少快 2 倍
  • fail-rate 0.2 时成功率因重试明显高于 80%;Ctrl+C 能干净退出
  • ≥8 次 Conventional Commits 推送到 cnb.cool

一个真实的性能观察(第 3 周 M16 的活教材)

参考答案在这台 Windows 机器上的 bench --count 30 --limit 10 实测:顺序 ≈ 1600 ms,线程池 ≈ 220 ms(7×),asyncio ≈ 1000 ms(只有 2×)。排查过程记录在这里,因为它比"asyncio 一定最快"这句话更有价值:

  1. 先怀疑桩服务器:标准库 ThreadingHTTPServer 换成 asyncio 服务器(现在的 stub_server.py)——数字几乎没变,排除。
  2. 再用裸 asyncio.open_connection 直连桩服务器:30 个并发 70 ms——asyncio 本身没问题。
  3. 用 httpx AsyncClient 顺序发 5 个请求:每个约 73 ms,而服务器只睡 50 ms——httpx 异步栈在本机每个请求多花约 20 ms 的 CPU,30 个并发时这些开销在事件循环里串行累加,就成了 ~450 ms 起步。
  4. 同步 httpx.Client 的每请求开销小得多,所以线程池版本快。

结论:这是本机 httpx 异步实现的开销问题,不是你的代码写错了;同一份代码在 H20 的 Linux 上重跑,通常 asyncio 与线程池接近。性能要测,不要猜;测出反直觉的结果,先隔离变量再下结论。

参考答案:exercises/solutions/week3/aggregator_solution/src/aggregator/。重置为骨架:uv run python tools/aggregator_skeleton.py;装回答案对照运行:uv run python tools/aggregator_skeleton.py --solution