小白python入门 - 39. 采集流水线小项目

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_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 不进仓库 需要鉴权时用环境变量
唯一键设计 source UNIQUE 才能稳定去重
把 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。逻辑源名更稳、更好读。

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。

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

  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)。(合理即可)

相关推荐
Wang's Blog1 小时前
Go-Zero 项目开发22:用户群聊功能的实现与完善
开发语言·golang
a1117762 小时前
坦克大战3D Three.js 3D (开源项目)
开发语言·javascript·3d
郭老二2 小时前
【Python】常用模块:xmlrpc
python
名字还没想好☜2 小时前
Python itertools 实战:用 groupby、chain、islice 优雅处理大数据流
linux·windows·python·迭代器·itertools
威联通网络存储3 小时前
TS-h2490FU在面板制造Array段AOI缺陷画廊中的并联
python·制造
Wang's Blog3 小时前
Go-Zero项目开发24: 基于Bitmap实现群聊消息已读未读
开发语言·后端·golang
接针4 小时前
UV 常用命令
python·uv
无垠的广袤5 小时前
【工业树莓派 CM0 Dev Board】扩展板设计
linux·python·嵌入式硬件·pcb设计·模块化·传感器
Yolanda_20226 小时前
在vscode终端中可使用pip,但是cmd终端中找不到命令 pip问题的解决
vscode·python·conda·pip