1. 本课定位:是什么、为何重要
从 33 到 38,你分别学会了 HTTP、稳健客户端、并发选型、协程、HTML 解析、接口优先。但它们若一直是散落的 demo,仍然不像「能交付的东西」。工作里老板要的是:输入源列表,输出库里可查的数据,并且跑第二遍不能乱套。
这一课做迷你采集流水线:配置源→拉 JSON→抽字段→SQLite 去重更新→汇总打印。明确不做 Selenium 和分布式。学完你应能讲清模块怎么拆、如何验收、如何和前几课能力一一对应。
把 33~38 收成一条可演示流水线:
配置源 → 拉取 JSON → 抽取字段 → 写入 SQLite(去重/更新)→ 汇总打印
| 已学能力 | 用在本项目 |
|---|---|
| Day33/34 HTTP + requests | fetch |
| Day35/36 并发(可选扩展) | 多源可改 gather/线程池 |
| Day37 解析/清洗直觉 | 从 dict 取字段、校验 |
| Day38 接口优先 | 不爬整页 HTML |
| Day24~32 数据库习惯 | 参数绑定、upsert、commit |
为何重要: 真实工作里「能跑通的端到端」比零散知识点更有说服力;也练习模块边界与验收标准。
本课不做: Selenium、分布式爬虫、复杂调度系统。
2. 流水线总览图
先给一张总图:SOURCES 进来,经 fetch、parse、store 进 SQLite,第二遍 upsert 不增行,最后 COUNT 验收。
有了图,后面每一节都是在填某个方框的细节,不会迷路。
text
SOURCES 列表
|
v
+-------+ +-------+ +-------+
| fetch | --> | parse | --> | store | --> SQLite items
+-------+ +-------+ +-------+
^ |
| 第二遍同源 |
+--------- upsert 不增行 -----+
|
v
COUNT / 打印 rows 验收
| 阶段 | 输入 | 输出 |
|---|---|---|
| fetch | url | dict(JSON) |
| parse | dict | name, stars |
| store | source, name, stars | 表中 1 行(插入或更新) |
| report | 连接 | COUNT + 明细 |
3. 本质:各段职责与失败
流水线本质是分段:每段输入输出是什么、失败时怎么办。错误设计每次 INSERT 新行,跑两遍变四行;正确设计 UNIQUE+UPSERT,仍两行只刷新。
把「修改前/后」记牢,验收时才知道 COUNT 该看什么。
| 步骤 | 职责 | 失败时 |
|---|---|---|
| fetch | HTTP GET,timeout、UA | 网络错 / 4xx5xx → 抛错或重试 |
| parse | 取 full_name、stargazers_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 不进仓库 | 需要鉴权时用环境变量 |
| 唯一键设计 | source UNIQUE 才能稳定去重 |
| 把 HTML 爬虫硬塞进本项目 | 偏离「接口优先」教学目标 |
无 raise_for_status |
错误页被当数据 |
| star 写死断言 | 真实数据会变,断言会误伤 |
合规清单(项目级):
- 优先官方/公开 API
- timeout + 有限重试
- 不把密钥写进 Git
- 不采未授权隐私
- 遵守对方 rate limit
- User-Agent 标明自己的教学客户端
5. 表设计详解
items 表字段为何这样设:source 逻辑名唯一,name/stars 业务字段,fetched_at 记录新鲜度。
为什么不用 URL 当唯一键?URL 易变、带 query。逻辑源名更稳、更好读。
sql
CREATE TABLE items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
stars INTEGER NOT NULL,
fetched_at REAL NOT NULL
);
| 列 | 含义 | 备注 |
|---|---|---|
| id | 自增主键 | 内部用 |
| source | 逻辑源 id | 如 github-cpython,业务唯一 |
| name | 仓库全名 | 如 python/cpython |
| stars | star 数 | 会变,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 |
fetch |
url | dict | timeout + UA + raise_for_status |
store |
source, name, stars | 写入/更新 | 参数绑定 + UPSERT |
main |
源列表 | 打印过程与 COUNT | 可第二遍 |
保持函数短------一个函数只做一件事,便于测 fetch/store。
6.1 UPSERT 在说什么
sql
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 不存在 | 插入新行 |
| source 已存在 | 更新 name/stars/fetched_at |
7. 数据源设计
SOURCES 用(逻辑名, URL)元组列表,扩展第三源只加一行。
验收 COUNT 与源数量绑定,改源列表时预期也要改------这是可验收的关键。
python
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 数会变,正常。
请真实运行。这是阶段收官的主证据:你能端到端交付,而不只是会单课语法。
bash
mkdir -p ~/python-lab/src/day39
cd ~/python-lab/src/day39
pip install requests
Windows 推荐 Cygwin 或 WSL。
bash
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()
EOF
python3 pipeline_demo.py
实测输出(star 数会变):
text
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.gather |
Day36 |
| 线程池拉取 | ThreadPoolExecutor |
Day35 |
| 失败重试 | for + sleep + timeout | Day34 |
| Redis 去重/缓存 | 记 source 或缓存 JSON | Day30 |
| 配置文件 | sources.yaml | 工程化 |
| 日志 | logging 打到文件 | 运维习惯 |
扩展时先保证串行正确,再加并发------并发会放大错误。
11. 错误处理怎么加(示例思路)
给 fetch 加重试循环的示例思路,让短暂网络抖动不至于整条挂掉。
修改前一次失败就崩,修改后可恢复仍失败再抛------工程上更常见。
python
def fetch(url: str) -> dict:
last_err = None
for attempt in range(1, 4):
try:
r = requests.get(url, timeout=20, headers={...})
r.raise_for_status()
return r.json()
except requests.RequestException as e:
last_err = e
time.sleep(0.5 * attempt)
raise last_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 你带走的能力,形成阶段闭环。
网络与异步不是终点,而是能取数、能叠等待、能入库的底座。
| 课 | 你带走的能力 |
|---|---|
| 33 | HTTP 五件套 |
| 34 | 稳健 requests 客户端 |
| 35 | 并发选型 |
| 36 | asyncio + httpx |
| 37 | HTML 解析入库 |
| 38 | 接口优先 |
| 39 | 端到端流水线 |
15. 模块边界:什么样叫「好拆」
好拆与坏拆对照,强调测试友好:fetch 可 mock,不必事事连外网。
为以后写更大项目留接口意识。
| 好 | 不好 |
|---|---|
fetch 只负责 HTTP |
fetch 里又写 SQL 又 print 排版 |
store 只负责写入 |
全局到处 sqlite3.connect |
main 只编排 |
一个 200 行函数从头写到尾 |
测试友好: 可以对 fetch 做 mock,不必真连外网(进阶)。
16. 验收脚本片段(人工也可)
用 assert 锁住行数与源集合,但不要断言 star 具体数字。
把「顺眼」升级成「可自动检查」的一小步。
python
n = conn.execute("SELECT COUNT(*) FROM items").fetchone()[0]
assert n == len(SOURCES)
sources = {
row[0]
for row in conn.execute("SELECT source FROM items")
}
assert sources == {s for s, _ in SOURCES}
修改前: 只看 print 是否「顺眼」。
修改后: 用断言锁住「行数与源集合」。
注意:不要断言 star 的具体数字。
17. 并发改造草图(可选,接 Day35/36)
线程池与 async 改造直觉,并警告 SQLite 多线程写连接不安全。
再次强调先串行正确再并发------和 35/36 课的选型、限流呼应。
线程池版直觉:
python
# 伪代码
with ThreadPoolExecutor(max_workers=4) as ex:
futs = {ex.submit(fetch, url): source for source, url in SOURCES}
for fut in as_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 列 |
|---|---|---|
| 源标识 | 自定 SOURCES0 | source |
| 仓库名 | full_name / name | name |
| Star | stargazers_count | stars |
| 抓取时间 | time.time() | fetched_at |
解析示例:
python
name = data.get("full_name") or data.get("name") or ""
stars = int(data.get("stargazers_count") or 0)
21. 运行结果怎么读(对照实测)
逐段解读 fetched 行、second pass、count、rows;COUNT 为 4 时如何排查。
会读输出,才会判断实验成功还是环境/代码问题。
text
fetched github-cpython: python/cpython stars=73867
| 片段 | 含义 |
|---|---|
| fetched ... | fetch+parse 成功 |
| stars=数字 | 当前公开 star,会变 |
| second pass | 再次 upsert |
| count 2 | 去重成功 |
| rows 两行 | 与 SOURCES 一一对应 |
若 count 为 4:说明唯一约束没生效或跑了两次建库逻辑异常------检查 UNIQUE(source) 与是否每次 unlink DB。
22. 提交作业前检查(给学员)
交作业应贴完整输出、说明 SOURCES 与第二遍 COUNT、扩展另说、禁止贴 Token。
这是课堂规范,也是职业沟通的缩影。
- 贴运行完整终端输出
- 说明 SOURCES 列表
- 说明第二遍 COUNT
- 若做了扩展(重试/异步),单独说明
- 不要贴 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)。(合理即可)