Python 网页数据采集指南:合规、稳定、可维护
Python 很适合采集公开网页数据,但生产级采集不是“发请求、解析 HTML”这么简单。真实项目需要权限检查、礼貌访问、稳定选择器、重试逻辑、数据校验和监控。
这篇文章关注合规、稳定、可维护的数据采集。它不讲绕过访问控制、规避反爬、破解验证码,或采集登录、付费墙、明确禁止访问后的数据。如果网站提供 API、数据导出、Feed 或合作通道,应优先使用官方方式。
参考文档:
先做权限检查
写代码前先回答这些问题:
- 目标数据是否公开,且无需登录即可查看?
- 网站是否提供 API、站点地图、RSS 或导出?
robots.txt是否允许抓取目标路径?- 服务条款是否限制自动访问或数据再利用?
- 数据是否可能包含个人信息、版权内容或受监管记录?
- 你的爬虫是否能标识自己并提供联系方式?
如果答案不清楚,不要先写爬虫。应先获得许可,或使用官方数据源。
选择合适工具
| 页面类型 | 推荐方式 | 原因 |
|---|---|---|
| 静态 HTML | httpx 或 requests + BeautifulSoup/parsel |
简单、快速、开销低 |
| 大量相似页面 | Scrapy | 调度、重试、管道、导出更完整 |
| JavaScript 渲染页面 | Playwright | 可以执行客户端渲染 |
| API 驱动页面 | 在允许的情况下调用公开 API | 比解析 HTML 更稳定高效 |
| 一次性数据清洗 | Pandas + 已保存 HTML/CSV | 避免重复请求 |
原则是:用能稳定、礼貌完成任务的最简单工具。
一个小型静态页面采集器
静态页面可以从明确超时、清晰 User-Agent 和严格解析开始。
from __future__ import annotations
import time
from dataclasses import dataclass
from urllib.parse import urljoin
import httpx
from bs4 import BeautifulSoup
BASE_URL = "https://example.com/articles/"
@dataclass
class Article:
title: str
url: str
summary: str
def fetch_html(url: str) -> str:
headers = {
"User-Agent": "ExampleResearchBot/1.0 (+https://example.com/contact)"
}
with httpx.Client(timeout=15, follow_redirects=True, headers=headers) as client:
response = client.get(url)
response.raise_for_status()
return response.text
def parse_articles(html: str) -> list[Article]:
soup = BeautifulSoup(html, "html.parser")
articles: list[Article] = []
for card in soup.select("article.card"):
title_el = card.select_one("h2 a")
summary_el = card.select_one(".summary")
if not title_el:
continue
title = title_el.get_text(" ", strip=True)
url = urljoin(BASE_URL, title_el.get("href", ""))
summary = summary_el.get_text(" ", strip=True) if summary_el else ""
articles.append(Article(title=title, url=url, summary=summary))
return articles
def main() -> None:
html = fetch_html(BASE_URL)
for article in parse_articles(html):
print(article)
time.sleep(2)
if __name__ == "__main__":
main()
选择器要容易测试。选择器失效时,应少返回数据并记录问题,不要静默写入错误数据。
用 Playwright 处理动态页面
当目标内容由 JavaScript 生成,且没有允许使用的 API 或 Feed 时,再考虑 Playwright。
from playwright.sync_api import sync_playwright
def collect_titles(url: str) -> list[str]:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, wait_until="networkidle", timeout=30_000)
titles = [
item.inner_text().strip()
for item in page.locator("article h2").all()
]
browser.close()
return titles
Playwright 比普通 HTTP 请求重得多。只有页面确实需要渲染时才用它。如果同样数据能从 HTML 或允许访问的公开 API 得到,就不要启动浏览器。
用 Scrapy 扩展规模
当页面很多、需要重试、导出和数据管道时,Scrapy 更合适。
import scrapy
class ArticleSpider(scrapy.Spider):
name = "articles"
allowed_domains = ["example.com"]
start_urls = ["https://example.com/articles/"]
custom_settings = {
"ROBOTSTXT_OBEY": True,
"CONCURRENT_REQUESTS_PER_DOMAIN": 2,
"DOWNLOAD_DELAY": 2,
"AUTOTHROTTLE_ENABLED": True,
"AUTOTHROTTLE_TARGET_CONCURRENCY": 1.0,
"USER_AGENT": "ExampleResearchBot/1.0 (+https://example.com/contact)",
"FEEDS": {
"articles.jsonl": {"format": "jsonlines", "encoding": "utf8"}
},
}
def parse(self, response):
for card in response.css("article.card"):
title = card.css("h2 a::text").get()
href = card.css("h2 a::attr(href)").get()
if title and href:
yield {
"title": title.strip(),
"url": response.urljoin(href),
"source_url": response.url,
}
next_url = response.css("a.next::attr(href)").get()
if next_url:
yield response.follow(next_url, callback=self.parse)
关键不只是 Spider。ROBOTSTXT_OBEY、域名并发、下载间隔和 AutoThrottle 可以让访问流量更可预测。
数据质量管道
原始采集数据很少能直接使用。应尽早做校验和规范化。
from itemadapter import ItemAdapter
class CleanArticlePipeline:
def process_item(self, item, spider):
adapter = ItemAdapter(item)
title = (adapter.get("title") or "").strip()
url = (adapter.get("url") or "").strip()
if not title or not url:
raise ValueError("Missing title or URL")
adapter["title"] = " ".join(title.split())
adapter["url"] = url
return item
生产环境中,校验失败的数据应单独记录,这样选择器变更能被及时发现。
礼貌采集规则
礼貌采集能减少站点压力,也更容易长期运行:
- 遵守
robots.txt和网站条款。 - 默认使用较低并发。
- 设置访问间隔和 AutoThrottle。
- 开发阶段缓存响应,避免反复请求。
- 避免重复下载未变化页面。
- 连续错误时停止,不要激进重试。
- 使用清晰的 User-Agent 标识爬虫。
- 提供联系页面或邮箱。
如果网站阻止或挑战爬虫,应把它当作边界。没有许可时,不应围绕限制继续构建。
增量采集
周期性任务不要每次全量抓取。
可以使用:
- 站点地图或 Feed。
- 上次看到的 URL 或时间戳。
- 站点支持时使用 ETag 和 Last-Modified。
- 内容哈希检测变化。
- 持久化的已访问 URL 存储。
import hashlib
def content_hash(text: str) -> str:
normalized = " ".join(text.split())
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
增量采集可以节省带宽、降低站点压力,也让数据变化更容易审计。
监控指标
生产爬虫应该报告:
- 请求页面数。
- 提取记录数。
- 空页面数。
- HTTP 状态码分布。
- 重试次数。
- 解析失败次数。
- 重复率。
- 任务耗时。
如果提取记录突然下降,通常说明页面结构变了,或爬虫触达了限制。
常见错误
选择器太脆弱
.container > div:nth-child(3) > span 这类选择器很容易失效。优先使用语义标签、稳定属性或页面内嵌的结构化数据。
忽略字符编码
一定要测试多语言数据。编码错误可能悄悄破坏标题、地址和姓名。
混合采集和业务逻辑
采集、解析、校验、存储应分层。这样某一层变化时,不需要改动全部代码。
跑得太快
高并发会带来封禁、不完整数据和运维风险。一个慢但稳定的爬虫,比一个快但经常失败的爬虫更有价值。
没有可复现输入
保存代表性 HTML 样本用于解析器测试。网站结构变化后,可以对比新旧标记。
最终检查清单
重复运行爬虫前确认:
- 你被允许采集目标数据。
- 已检查
robots.txt和网站条款。 - 爬虫有明确身份标识。
- 已配置限速和访问间隔。
- 解析器测试覆盖代表性页面。
- 数据校验能捕获空记录和格式错误。
- 增量采集避免不必要请求。
- 日志和指标能暴露失败。
- 个人或敏感数据按适用规则处理。
可靠的数据采集主要靠工程纪律:只采集允许采集的数据,温和请求,验证所有结果,并在任务运行后持续观察。
本站提供浏览器本地工具,免注册即可试用 →