4. 数据存储与去重
这是什么:数据解析出来了,只 print 一下等于白抓——程序一结束就没了。这一章解决「抓到的数据怎么留、别重复抓」两个问题:三种落盘格式(CSV / JSON / SQLite)加一个去重集合。
为什么重要:爬虫是长期跑的活儿,今天抓一页、明天抓全站,格式选错返工、URL 不去重就死循环。把存储和去重做成固定套路,后面的框架学习才不慌。
如何用:按数据用途三选一:给人看的表格用 CSV、程序之间传递用 JSON、要查询要汇总用 SQLite;然后给爬虫装上「记性」(去重)和「教养」(限速)。
CSV:给 Excel 和 Pandas 看
CSV 就是用逗号分列的纯文本,Excel、Pandas 都能直接打开:
import csv
rows = [
["名言", "作者", "标签"], # 表头
["“The world as we have created it…”", "Albert Einstein", "change,deep-thoughts"],
["“It is our choices…”", "J.K. Rowling", "abilities,choices"],
]
# utf-8-sig:带 BOM 头,Excel 双击打开中文不乱码;newline="" 是 csv 模块的铁规矩
with open("quotes.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerows(rows)
| 参数 | 不写的后果 |
|---|---|
newline="" |
Windows 下每行之间多一个空行 |
encoding="utf-8-sig" |
Excel 打开中文变乱码(记事本正常,Excel 不认) |
JSON:给程序看的格式
下一章开始会大量遇到 JSON 接口,存取也用它——和 Python 的字典/列表无缝互转:
import json
data = [{"text": "It is our choices…", "author": "J.K. Rowling", "tags": ["choices"]}]
with open("quotes.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2) # 中文原样、带缩进可读
with open("quotes.json", encoding="utf-8") as f:
same = json.load(f) # 读回来还是列表套字典
print(same[0]["author"])
ensure_ascii=False 别忘——否则中文全变 \uXXXX 转义。
SQLite:要查询、要汇总的场合
数据量大、要筛选统计时,直接进数据库。Python 标准库自带 SQLite(零安装,详见 SQLite 教程):
import sqlite3
conn = sqlite3.connect("quotes.db")
conn.execute(
"CREATE TABLE IF NOT EXISTS quote ("
" id INTEGER PRIMARY KEY, text TEXT, author TEXT, tags TEXT)"
)
rows = [
("“The world as we have created it…”", "Albert Einstein", "change"),
("“It is our choices…”", "J.K. Rowling", "choices"),
]
conn.executemany("INSERT INTO quote (text, author, tags) VALUES (?, ?, ?)", rows)
conn.commit()
for author, text in conn.execute("SELECT author, text FROM quote WHERE author LIKE ?", ("J%",)):
print(author, text)
conn.close()
选型一句话:CSV 给人看,JSON 给程序传,SQLite 给要查询的数据住——三者都比 print 强一亿倍。
去重:给爬虫装上「记性」
爬虫顺链接爬,同一个 URL 会反复遇到(首页链接到列表页、列表页又链回首页),不去重就是死循环。用一个 set 记住爬过谁:
import requests
from bs4 import BeautifulSoup
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0"}
seen = set() # 已抓过的 URL
queue = ["https://quotes.toscrape.com/"] # 待抓队列
while queue:
url = queue.pop(0) # 取队首
if url in seen: # 抓过就跳过
continue
seen.add(url)
print(f"抓取 {url}(已抓 {len(seen)} 页)")
r = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(r.text, "html.parser")
for a in soup.select("li.next a"): # 找「下一页」链接
full = requests.compat.urljoin(url, a["href"]) # 相对地址 → 绝对地址
if full not in seen:
queue.append(full)
你会看到:程序自己从第 1 页顺着「下一页」爬到第 10 页然后自己停下——队列 + 去重,就是爬虫「爬满全站」的全部秘密。urljoin 负责把 /page/2/ 这种相对地址拼成完整 URL。
礼貌爬取:管住爬虫的手
全站抓取对服务器是有压力的,两行代码做个有教养的爬虫:
import random
import time
time.sleep(random.uniform(1, 3)) # 每次请求之间随机停 1~3 秒
| 规矩 | 怎么做 |
|---|---|
| 请求有间隔 | 每次抓完 time.sleep(1~3),别一秒几十连发 |
| 避开业务高峰 | 大批量采集放深夜,别在人家峰值凑热闹 |
| 遇 429 立刻退 | 降速翻倍,甚至当天别再爬 |
| 声明身份 | User-Agent 里可以带上邮箱/项目名,方便站长联系你 |
常见翻车现场
| 症状 | 原因 | 修复 |
|---|---|---|
| Excel 打开 CSV 乱码 | 编码没带 BOM | encoding="utf-8-sig" |
| CSV 隔行有空行 | 少了 newline="" |
open(..., newline="") |
JSON 打开是 \uXXXX |
默认转义中文 | ensure_ascii=False |
| 爬虫停不下来 | URL 没去重,队列越转越多 | set 记录 + 判断(本章套路) |
| 相对链接拼错 | 直接拿 /page/2/ 当完整 URL 请求 |
urljoin(当前页, href) |
动手改
- 给上面的全站爬虫加存储:每页解析出的名言(用第 3 章的代码)写进
quotes_all.csv - 把
queue.pop(0)换成queue.pop()再跑——变成了什么遍历顺序?(提示:深度优先) - 在爬虫里加
time.sleep(random.uniform(1, 3)),感受一下「有教养」和「没教养」的速度差