Python 网页数据采集指南:合规、稳定、可维护

编程语言

Python 很适合采集公开网页数据,但生产级采集不是“发请求、解析 HTML”这么简单。真实项目需要权限检查、礼貌访问、稳定选择器、重试逻辑、数据校验和监控。

这篇文章关注合规、稳定、可维护的数据采集。它不讲绕过访问控制、规避反爬、破解验证码,或采集登录、付费墙、明确禁止访问后的数据。如果网站提供 API、数据导出、Feed 或合作通道,应优先使用官方方式。

参考文档:

先做权限检查

写代码前先回答这些问题:

  • 目标数据是否公开,且无需登录即可查看?
  • 网站是否提供 API、站点地图、RSS 或导出?
  • robots.txt 是否允许抓取目标路径?
  • 服务条款是否限制自动访问或数据再利用?
  • 数据是否可能包含个人信息、版权内容或受监管记录?
  • 你的爬虫是否能标识自己并提供联系方式?

如果答案不清楚,不要先写爬虫。应先获得许可,或使用官方数据源。

选择合适工具

页面类型 推荐方式 原因
静态 HTML httpxrequests + 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 和网站条款。
  • 爬虫有明确身份标识。
  • 已配置限速和访问间隔。
  • 解析器测试覆盖代表性页面。
  • 数据校验能捕获空记录和格式错误。
  • 增量采集避免不必要请求。
  • 日志和指标能暴露失败。
  • 个人或敏感数据按适用规则处理。

可靠的数据采集主要靠工程纪律:只采集允许采集的数据,温和请求,验证所有结果,并在任务运行后持续观察。

本站提供浏览器本地工具,免注册即可试用 →

#Python爬虫#反爬虫#Scrapy#Playwright爬虫#2026#编程语言