8. 综合练习:全站名言采集器
这是什么:同一个需求做两遍——把 quotes.toscrape.com 全站 10 页、100 条名言(文本、作者、标签)采集存成 CSV。方案 A 用 requests + BeautifulSoup 手写(前四章知识总装),方案 B 用 Scrapy(一个文件 20 行)。做完你会亲手摸到框架的价值。
为什么这么练:只会框架不懂原理,页面一变就束手无策;只会手写不用框架,规模一大全是调度杂活。同一题各写一遍,界限自然清晰。
如何用:先手写版(理解每一步),再 Scrapy 版(体会省了什么),最后对照验收清单自查。
需求清单
| # | 功能 | 用到哪章 |
|---|---|---|
| 1 | 从首页出发,自动翻页爬完 10 页 | 第 4 章 队列 + urljoin |
| 2 | 每页提取名言文本、作者、标签列表 | 第 3 章 选择器 |
| 3 | 带浏览器 UA、超时、失败重试 | 第 2 章 三件套 |
| 4 | URL 去重,不重复请求 | 第 4 章 set |
| 5 | 请求间隔 1~2 秒,礼貌爬取 | 第 4 章 sleep |
| 6 | 结果存 all_quotes.csv(Excel 打开不乱码) |
第 4 章 CSV |
方案 A:手写版(requests + bs4)
存成 spider_manual.py 运行,约 40 行:
import csv
import random
import time
import requests
from bs4 import BeautifulSoup
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0"}
BASE = "https://quotes.toscrape.com/"
def fetch(url, session, retries=3):
for i in range(retries):
try:
r = session.get(url, headers=HEADERS, timeout=10)
r.raise_for_status()
return r.text
except requests.RequestException as e:
print(f"第 {i + 1} 次失败:{e}")
time.sleep(2 ** i)
return None
def parse(html):
soup = BeautifulSoup(html, "html.parser")
rows = []
for quote in soup.select("div.quote"):
rows.append({
"text": quote.select_one("span.text").get_text(strip=True),
"author": quote.select_one("small.author").get_text(strip=True),
"tags": ",".join(a.get_text() for a in quote.select("a.tag")),
})
next_page = soup.select_one("li.next a")
return rows, (next_page["href"] if next_page else None)
def main():
session = requests.Session()
seen = set()
url, all_rows = BASE, []
while url and url not in seen:
seen.add(url)
html = fetch(url, session)
if html is None:
break
rows, next_path = parse(html)
all_rows.extend(rows)
print(f"{url} 抓到 {len(rows)} 条,累计 {len(all_rows)} 条")
time.sleep(random.uniform(1, 2)) # 礼貌间隔
url = requests.compat.urljoin(BASE, next_path) if next_path else None
with open("all_quotes.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=["text", "author", "tags"])
writer.writeheader()
writer.writerows(all_rows)
print(f"完成:{len(all_rows)} 条已写入 all_quotes.csv")
if __name__ == "__main__":
main()
你会看到:10 行抓取日志逐页刷出,最后 完成:100 条已写入——这一份脚本里,前四章每一章都有一块拼图。
方案 B:Scrapy 版
按第 5 章建好项目后,一个 Spider 文件解决:
import scrapy
class AllQuotesSpider(scrapy.Spider):
name = "all_quotes"
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get().strip(),
"author": quote.css("small.author::text").get(),
"tags": ",".join(quote.css("a.tag::text").getall()),
}
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
scrapy crawl all_quotes -O all_quotes_scrapy.csv
同题对比:框架到底替你干了什么
| 活儿 | 手写版位置 | Scrapy 版位置 |
|---|---|---|
| 请求 + 重试 + 超时 | fetch() 手写重试 |
settings RETRY_TIMES,一行 |
| 翻页调度 | while 循环 + urljoin |
yield response.follow(...) |
| URL 去重 | seen 集合 |
框架内置 |
| 礼貌限速 | time.sleep(random...) |
DOWNLOAD_DELAY = 1.0 |
| 存 CSV | csv.DictWriter 8 行 |
命令行 -O xxx.csv |
| 并发 | (手写并发还要线程池) | CONCURRENT_REQUESTS 配置 |
结论不是「手写无用」:手写版让你在页面一变、接口一封时知道每个环节在哪;框架版让你在需要规模时不用重造轮子。懂原理的人用框架,如虎添翼;不懂原理的人用框架,只会在报错里迷路。
验收清单
- [ ]
all_quotes.csv恰好 100 条(含表头 101 行) - [ ] Excel 双击打开中文正常显示(utf-8-sig)
- [ ] 控制台没有任何一次重复 URL 的请求
- [ ] Scrapy 版导出的
all_quotes_scrapy.csv与手写版条数一致 - [ ] 两个版本的 CSV 各自能被 Pandas 一行
pd.read_csv()读成表格
进阶挑战
- 换站实战:把方案 A 改成抓 books.toscrape.com(书名在
h3 a的 title 属性、价格在p.price_color),注意书站没有「下一页」按钮的位置差别 - SQLite 入库:方案 A 的存储段换成第 4 章的 sqlite3,建表 + executemany
- Pandas 分析:读入 CSV 统计哪个作者名言最多、哪个标签出现最频繁(衔接 Pandas 统计与筛选)
- 断点续爬:手写版把
seen集合定期落盘 JSON,程序中断重跑时读回(第 4 章 + JSON 存储)
学完这一栈,你已经能独立完成「原理 → 手写 → 框架」的全链路采集。下一步可以把数据交给 Pandas 分析,或去了解 Playwright 自动化应对 JS 渲染页面 🐸