小白python入门 - 39. 采集流水线小项目
2026/7/27 6:47:10 网站建设 项目流程

1. 本课定位:是什么、为何重要

从 33 到 38,你分别学会了 HTTP、稳健客户端、并发选型、协程、HTML 解析、接口优先。但它们若一直是散落的 demo,仍然不像「能交付的东西」。工作里老板要的是:输入源列表,输出库里可查的数据,并且跑第二遍不能乱套。

这一课做迷你采集流水线:配置源→拉 JSON→抽字段→SQLite 去重更新→汇总打印。明确不做 Selenium 和分布式。学完你应能讲清模块怎么拆、如何验收、如何和前几课能力一一对应。

33~38收成一条可演示流水线:

配置源 → 拉取 JSON → 抽取字段 → 写入 SQLite(去重/更新)→ 汇总打印

已学能力用在本项目
Day33/34 HTTP + requestsfetch
Day35/36 并发(可选扩展)多源可改 gather/线程池
Day37 解析/清洗直觉从 dict 取字段、校验
Day38 接口优先不爬整页 HTML
Day24~32 数据库习惯参数绑定、upsert、commit

为何重要:真实工作里「能跑通的端到端」比零散知识点更有说服力;也练习模块边界与验收标准。

本课不做:Selenium、分布式爬虫、复杂调度系统。


2. 流水线总览图

先给一张总图:SOURCES 进来,经 fetch、parse、store 进 SQLite,第二遍 upsert 不增行,最后 COUNT 验收。

有了图,后面每一节都是在填某个方框的细节,不会迷路。

SOURCES 列表 | v +-------+ +-------+ +-------+ | fetch | --> | parse | --> | store | --> SQLite items +-------+ +-------+ +-------+ ^ | | 第二遍同源 | +--------- upsert 不增行 -----+ | v COUNT / 打印 rows 验收
阶段输入输出
fetchurldict(JSON)
parsedictname, stars
storesource, name, stars表中 1 行(插入或更新)
report连接COUNT + 明细

3. 本质:各段职责与失败

流水线本质是分段:每段输入输出是什么、失败时怎么办。错误设计每次 INSERT 新行,跑两遍变四行;正确设计 UNIQUE+UPSERT,仍两行只刷新。

把「修改前/后」记牢,验收时才知道 COUNT 该看什么。

步骤职责失败时
fetchHTTP GET,timeout、UA网络错 / 4xx5xx → 抛错或重试
parsefull_namestargazers_count缺字段 → 降级空串/0 或跳过
store写入,同源更新SQL 错 → 事务回滚(本示例简单 commit)
run编排多源;第二遍验证去重打印 COUNT 对照预期

第一遍抓取后:N 个源 → N 行。
第二遍同源再抓:仍是 N 行(ON CONFLICT DO UPDATE),不是 2N 行。

修改前(错误设计:每次 INSERT 新行):跑两遍变成 4 行重复源。
修改后(UNIQUE + UPSERT):仍 2 行,字段刷新。


4. 约束、坑与合规

公开 API 有限流,密钥不能进仓,唯一键要设计对,不要把 HTML 爬虫硬塞进本课目标。

合规清单与安全课精神一致:timeout、重试、UA、不采隐私。技术正确包含合规正确。

约束/坑说明
公开 API 限流控制频率;设合理 timeout
Token 不进仓库需要鉴权时用环境变量
唯一键设计sourceUNIQUE 才能稳定去重
把 HTML 爬虫硬塞进本项目偏离「接口优先」教学目标
raise_for_status错误页被当数据
star 写死断言真实数据会变,断言会误伤

合规清单(项目级):

  1. 优先官方/公开 API
  2. timeout + 有限重试
  3. 不把密钥写进 Git
  4. 不采未授权隐私
  5. 遵守对方 rate limit
  6. User-Agent 标明自己的教学客户端

5. 表设计详解

items 表字段为何这样设:source 逻辑名唯一,name/stars 业务字段,fetched_at 记录新鲜度。

为什么不用 URL 当唯一键?URL 易变、带 query。逻辑源名更稳、更好读。

CREATETABLEitems(idINTEGERPRIMARYKEYAUTOINCREMENT,sourceTEXTNOTNULLUNIQUE,nameTEXTNOTNULL,starsINTEGERNOTNULL,fetched_atREALNOTNULL);
含义备注
id自增主键内部用
source逻辑源 idgithub-cpython业务唯一
name仓库全名python/cpython
starsstar 数会变,upsert 更新
fetched_at抓取时间戳time.time()

为什么 source 用逻辑名而不是 URL?
URL 可能带 query、换域名;逻辑名更稳定,也好读。


6. 能力分组(代码怎么切)

init_db、fetch、store、main 各干什么,对应测试与阅读友好。UPSERT SQL 在说什么,用表解释插入与更新两种情况。

函数短、SQL 参数化,承接 Day28/32 习惯,避免一个 200 行脚本揉成一团。

函数输入输出注意
init_db连接建表IF NOT EXISTS
fetchurldicttimeout + UA + raise_for_status
storesource, name, stars写入/更新参数绑定 + UPSERT
main源列表打印过程与 COUNT可第二遍

保持函数短——一个函数只做一件事,便于测fetch/store

6.1 UPSERT 在说什么
INSERTINTOitems(source,name,stars,fetched_at)VALUES(?,?,?,?)ONCONFLICT(source)DOUPDATESETname=excluded.name,stars=excluded.stars,fetched_at=excluded.fetched_at
情况行为
source 不存在插入新行
source 已存在更新 name/stars/fetched_at

7. 数据源设计

SOURCES 用(逻辑名, URL)元组列表,扩展第三源只加一行。

验收 COUNT 与源数量绑定,改源列表时预期也要改——这是可验收的关键。

SOURCES=[("github-cpython","https://api.github.com/repos/python/cpython"),("github-requests","https://api.github.com/repos/psf/requests"),]
字段含义
元组第一项逻辑 source
元组第二项真实请求 URL

扩展第三个源:再加一行元组即可,验收 COUNT 变 3。


8. 综合实践

一键脚本跑 GitHub 两仓库,第二遍 upsert,看 COUNT=2。star 数会变,正常。

请真实运行。这是阶段收官的主证据:你能端到端交付,而不只是会单课语法。

mkdir-p~/python-lab/src/day39cd~/python-lab/src/day39 pipinstallrequests

Windows 推荐 Cygwin 或 WSL。

cat>pipeline_demo.py<<'EOF' # Day39: mini pipeline API + sqlite import sqlite3 import time from pathlib import Path import requests DIR = Path(__file__).resolve().parent DB = DIR / "pipeline.db" SOURCES = [ ("github-cpython", "https://api.github.com/repos/python/cpython"), ("github-requests", "https://api.github.com/repos/psf/requests"), ] def init_db(conn): conn.execute( """ CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, name TEXT NOT NULL, stars INTEGER NOT NULL, fetched_at REAL NOT NULL, UNIQUE(source) ) """ ) conn.commit() def fetch(url: str) -> dict: r = requests.get( url, timeout=20, headers={"User-Agent": "python-lab-day39/1.0", "Accept": "application/json"}, ) r.raise_for_status() return r.json() def store(conn, source: str, name: str, stars: int): conn.execute( """ INSERT INTO items (source, name, stars, fetched_at) VALUES (?, ?, ?, ?) ON CONFLICT(source) DO UPDATE SET name=excluded.name, stars=excluded.stars, fetched_at=excluded.fetched_at """, (source, name, stars, time.time()), ) conn.commit() def main(): if DB.exists(): DB.unlink() with sqlite3.connect(DB) as conn: init_db(conn) for source, url in SOURCES: data = fetch(url) name = data.get("full_name") or data.get("name") or "" stars = int(data.get("stargazers_count") or 0) print(f"fetched {source}: {name} stars={stars}") store(conn, source, name, stars) print("--- second pass (upsert same sources) ---") for source, url in SOURCES: data = fetch(url) name = data.get("full_name") or data.get("name") or "" stars = int(data.get("stargazers_count") or 0) store(conn, source, name, stars) n = conn.execute("SELECT COUNT(*) FROM items").fetchone()[0] print("--- count ---") print(n) print("--- rows ---") for row in conn.execute( "SELECT id, source, name, stars FROM items ORDER BY id" ): print(row) if __name__ == "__main__": main() EOFpython3 pipeline_demo.py

实测输出(star 数会变):

fetched github-cpython: python/cpython stars=73867 fetched github-requests: psf/requests stars=54147 --- second pass (upsert same sources) --- --- count --- 2 --- rows --- (1, 'github-cpython', 'python/cpython', 73867) (2, 'github-requests', 'psf/requests', 54147)

第二遍前:已有 2 行。
第二遍后:仍是 2 行,字段被刷新——不是插入重复源。


9. 验收清单(务必过一遍)

用清单当「作业评分表」:跑通、COUNT、去重、参数化与 UA、合规、错误可见。

全部勾上,本课才算完成,而不是脚本能 print 就行。

  • 一行命令跑通
  • COUNT(*) == 源数量(本例 2)
  • 第二遍不产生重复source
  • 全程参数化 SQL、有 timeout 与 UA
  • 合规:公开 API + 控制频率
  • 失败时(可手动改错 URL)能看到异常而不是静默脏数据

10. 可选扩展(作业方向)

异步、线程池、重试、Redis、配置文件、日志——指向 35/36/30 等课。

强调:先串行正确,再并发。顺序反了会放大错误,排障更痛苦。

扩展做法对应课
异步拉取httpx+asyncio.gatherDay36
线程池拉取ThreadPoolExecutorDay35
失败重试for + sleep + timeoutDay34
Redis 去重/缓存记 source 或缓存 JSONDay30
配置文件sources.yaml工程化
日志logging 打到文件运维习惯

扩展时先保证串行正确,再加并发——并发会放大错误。


11. 错误处理怎么加(示例思路)

给 fetch 加重试循环的示例思路,让短暂网络抖动不至于整条挂掉。

修改前一次失败就崩,修改后可恢复仍失败再抛——工程上更常见。

deffetch(url:str)->dict:last_err=Noneforattemptinrange(1,4):try:r=requests.get(url,timeout=20,headers={...})r.raise_for_status()returnr.json()exceptrequests.RequestExceptionase:last_err=e time.sleep(0.5*attempt)raiselast_err

修改前:一次网络抖就整条流水线挂。
修改后:短暂故障可恢复;仍失败再抛出。


12. 常见问答

为何第二遍还请求、COUNT 不是 2 怎么办、403、能否改爬 HTML——集中答疑。

帮助你独立排障,而不是一出错就怀疑整课概念。

Q:为什么第二遍还要请求网络?
A:演示 upsert 与「刷新 stars」;生产可按 TTL 决定是否重抓。

Q:COUNT 不是 2?
A:检查是否删库失败、是否改过 SOURCES、是否旧 DB 未删。

Q:403/rate limit?
A:降频、加 Token、换时段;不要死循环猛打。

Q:能否改成爬 HTML?
A:能,但本课教学目标是接口流水线;HTML 请回到 Day37 思路。


13. 和「脚本随便写写」的差别

对照随便写与本课结构:分离模块、upsert、验收、密钥。

这是从「练手」到「像项目」的差别列表,写简历项目时也能用这套说法。

随便写本课结构
请求和 SQL 揉在一起fetch/store 分离
每次插入新行唯一键 + upsert
无验收COUNT + 第二遍
密钥写死环境变量(扩展)

14. 阶段收官对照

一张表回顾 33~39 你带走的能力,形成阶段闭环。

网络与异步不是终点,而是能取数、能叠等待、能入库的底座。

你带走的能力
33HTTP 五件套
34稳健 requests 客户端
35并发选型
36asyncio + httpx
37HTML 解析入库
38接口优先
39端到端流水线

15. 模块边界:什么样叫「好拆」

好拆与坏拆对照,强调测试友好:fetch 可 mock,不必事事连外网。

为以后写更大项目留接口意识。

不好
fetch只负责 HTTPfetch里又写 SQL 又 print 排版
store只负责写入全局到处sqlite3.connect
main只编排一个 200 行函数从头写到尾

测试友好:可以对fetch做 mock,不必真连外网(进阶)。


16. 验收脚本片段(人工也可)

用 assert 锁住行数与源集合,但不要断言 star 具体数字。

把「顺眼」升级成「可自动检查」的一小步。

n=conn.execute("SELECT COUNT(*) FROM items").fetchone()[0]assertn==len(SOURCES)sources={row[0]forrowinconn.execute("SELECT source FROM items")}assertsources=={sfors,_inSOURCES}

修改前:只看 print 是否「顺眼」。
修改后:用断言锁住「行数与源集合」。

注意:不要断言 star 的具体数字。


17. 并发改造草图(可选,接 Day35/36)

线程池与 async 改造直觉,并警告 SQLite 多线程写连接不安全。

再次强调先串行正确再并发——和 35/36 课的选型、限流呼应。

线程池版直觉:

# 伪代码withThreadPoolExecutor(max_workers=4)asex:futs={ex.submit(fetch,url):sourceforsource,urlinSOURCES}forfutinas_completed(futs):source=futs[fut]data=fut.result()store(conn,source,...)

注意:SQLite 多线程写同一连接不安全;应「主线程统一 store」,或每任务短连接并小心锁。

async 版直觉:gather多个 fetch,回到主协程再 store。

先串行正确,再并发——顺序不要反。


18. 故障演练建议

错 URL、断网、第二遍、加源——主动演练,建立预期。

故障演练是工程习惯,不是可选闲聊。

演练操作期望
错 URL改成不存在的仓库非 200,进程报错退出
断网拔网/防火墙超时或连接错误
第二遍连续跑两轮COUNT 不变
加源SOURCES 加一项COUNT+1

19. 自我检查清单

打勾:图、UPSERT、COUNT、timeout/UA/绑定、扩展与陷阱。

全过则本阶段实践目标达成。

  • 能画出 fetch → parse → store
  • 会 UPSERT 去重
  • 会用 COUNT 验收
  • timeout/UA/参数绑定齐全
  • 知道可选扩展与并发陷阱

20. 字段字典(本项目)

逻辑名、JSON 来源、SQLite 列对照,外加解析两行示例。

换数据源时先填这种字典,再写代码,少返工。

逻辑名JSON 来源SQLite 列
源标识自定 SOURCES[0]source
仓库名full_name / namename
Starstargazers_countstars
抓取时间time.time()fetched_at

解析示例:

name=data.get("full_name")ordata.get("name")or""stars=int(data.get("stargazers_count")or0)

21. 运行结果怎么读(对照实测)

逐段解读 fetched 行、second pass、count、rows;COUNT 为 4 时如何排查。

会读输出,才会判断实验成功还是环境/代码问题。

fetched github-cpython: python/cpython stars=73867
片段含义
fetched …fetch+parse 成功
stars=数字当前公开 star,会变
second pass再次 upsert
count 2去重成功
rows 两行与 SOURCES 一一对应

若 count 为 4:说明唯一约束没生效或跑了两次建库逻辑异常——检查UNIQUE(source)与是否每次unlinkDB。


22. 提交作业前检查(给学员)

交作业应贴完整输出、说明 SOURCES 与第二遍 COUNT、扩展另说、禁止贴 Token。

这是课堂规范,也是职业沟通的缩影。

  1. 贴运行完整终端输出
  2. 说明 SOURCES 列表
  3. 说明第二遍 COUNT
  4. 若做了扩展(重试/异步),单独说明
  5. 不要贴 Token

总结

流水线四段:拉、解析、存、汇总;接口优先;阶段收官。可重复可验收,比「写出过一次请求」更重要。

你可以继续走向 Web 服务(对外提供 API)或数据分析(消费已入库数据)。

  • 流水线 = 拉 → 解析 → 存 → 汇总。
  • 接口优先降低解析成本;SQLite 承接持久化能力。
  • 「网络与异步」阶段收官:HTTP → 客户端 → 并发 → 解析/接口 → 项目。
  • 可重复、可验收,比「写出过一次请求」更重要。

小练笔

自测含 upsert、验收、安全与可选加源实践。先做后看答案。

可选实践请真加第三源并验证 COUNT。

题 1

为什么第二遍 count 仍是 2?

题 2

源改成 3 个仓库且均成功时,验收 count 应是?

题 3(可选)

fetch增加失败重试 2 次(思路即可)。

题 4

判断:本流水线必须以 Selenium 打开 GitHub 页面才能取 star。

题 5

store里用?占位的主要安全收益是什么?

题 6

source列为什么建议 UNIQUE?

题 7

raise_for_status放在fetch里而不是忽略状态码,好处是?

题 8

判断:应用assert stars == 73867作为长期自动化测试。

题 9(可选实践)

增加第三个公开仓库源,确认 COUNT 为 3,且第二遍仍为 3。

题 10

列出流水线四个阶段名称(中文或英文)。


小练笔参考答案

先独立完成。意思对即可。

与 UPSERT、合规、参数绑定冲突的理解需修正。

题 1

UNIQUE(source)+ON CONFLICT DO UPDATE,同源更新不新增行。

题 2

3

题 3

for attempt in range(3): try/except包裹requests.get,失败 sleep 再试。

题 4

(公开 REST API 即可)。

题 5

降低 SQL 注入风险,参数与语句分离。

题 6

保证同一逻辑源只有一行,支撑 upsert 去重。

题 7

尽早发现 4xx/5xx,避免把错误响应当业务 JSON。

题 8

(star 会变;应断言类型/存在性或允许范围)。

题 9

以你运行为准。

题 10

拉取(fetch)、解析(parse)、存储(store)、汇总/验收(report)。(合理即可)

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询