爬取网页是指编写软件遍历一组 URL,逐一获取页面,并从 HTML 中提取结构化字段。这是将面向人眼构建的页面转化为可查询数据的方式:用于监控的价格数据集、用于研究的文章存档、用于市场分析的商品列表,或用于训练模型的语料库。这些数据大多是公开的,清晰可见,但大规模手动阅读毫无可行性,因此你需要一个爬虫。

本指南展示如何使用 Python 成熟的爬虫框架 Scrapy 爬取网页,同时将每个请求路由到 Crawling API,使页面经过渲染后返回,且请求附带轮换的可信 IP 而非你的数据中心地址。你将构建一个可运行的小型 Scrapy 爬虫,获取搜索结果页面,从每个列表中解析一个字段,并输出整洁的记录。本演练的范围限于公开的商品列表数据,结尾附有关于负责任爬取的简短说明,在将爬虫指向任何真实流量之前值得一读。

你将构建什么

一个单文件的 Scrapy 爬虫,通过 Crawling API 获取搜索结果页面,并为每条搜索结果生成一条结构化记录。我们以 Amazon 搜索作为贯穿整个教程的示例,与原版教程保持一致,并从每个商品卡片中提取两个字段:

  • Title(标题):搜索结果卡片上显示的商品标题文本。
  • URL(链接):从卡片指向该商品详情页的链接。

两个字段使示例保持可读性,且这种模式可以扩展到任意你想添加的选择器。同样的爬虫结构适用于任何你有权爬取的网站:替换起始 URL 和选择器,获取与解析的循环逻辑保持不变。

为何普通请求会被拦截

将裸 Scrapy 请求指向繁忙的商业网站,通常会遇到两个问题。首先,许多页面在浏览器中渲染内容:初始 HTML 是一个薄壳,商品列表只有在页面的 JavaScript 运行后才会出现。直接获取只能得到这个空壳,没有任何可供解析的内容。其次,大型网站会监控自动化流量。来自数据中心 IP 的快速重复请求,如果看起来不像真实浏览器,就会被 CAPTCHA 挑战或直接封锁,通常在你看到第一个商品之前就已发生。

因此,一个真正有效的爬虫需要在同一个请求中具备两点:能够渲染页面的浏览器,以及被网站识别为真实访客的 IP。你可以自己用无头浏览器加上一批轮换住宅代理来实现这一点,但组装这些组件并保持其健康运转就是工作的主要部分。Crawling API 将两者合并为一次调用。你提供一个 URL,它通过可信住宅 IP 获取页面(在你要求时还会在真实浏览器中渲染),然后将完整 HTML 返回给 Scrapy 解析。你的爬虫只与一个端点通信,无需接触任何代理列表。

路由工作原理

你的爬虫不直接请求目标站点,而是请求 https://api.crawlbase.com/?token=YOUR_CRAWLBASE_TOKEN&url=...。API 通过其 IP 池代你获取目标页面,并将响应体流式传回给 Scrapy。从 Scrapy 的角度看,这只是一个普通的 HTTP 响应,因此你已经熟悉的所有选择器和管道仍然正常工作。

前置条件

在编写任何代码之前,需要准备好以下几项。每项都不需要花太长时间。

基础 Python 知识。你应该能够编写和运行 Python 脚本,并用 pip 安装依赖包。如果你刚开始接触爬虫,关于如何用 Python 抓取网站的完整演练涵盖了本教程所假设的基础知识。

Python 3.8 或更高版本。python --version 确认你的版本。如果没有,请从 python.org 或 Anaconda 等发行版安装。

Crawlbase 账号和 token。注册后,打开你的控制台并复制你的 token。Crawlbase 提供两种 token:用于静态 HTML 的普通 token,以及需要渲染的页面所用的 JavaScript token。我们在整个教程中使用占位符 YOUR_CRAWLBASE_TOKEN。请将其视为密码:它负责对你的请求进行身份验证,所以不要将其提交到版本控制中。

项目设置

创建一个隔离环境,使项目依赖不与其他内容冲突,然后安装爬虫所需的两个库。

bash
python --version

python -m venv crawler_env
source crawler_env/bin/activate

pip install scrapy crawlbase

在 Windows 上,使用 crawler_env\Scripts\activate 替代 source 命令来激活环境。两个依赖库各司其职。scrapy 是爬虫框架:它管理请求队列、下载器和解析循环。crawlbase 是 Crawling API 的官方 Python 客户端,其 CrawlingAPI 类提供了 buildURL 辅助方法,能将任意目标 URL 封装为包含 token 的完整 API 请求,无需手动拼接查询字符串。

Scrapy 可以用 scrapy runspider 直接从单个文件运行爬虫,所以本教程无需完整的项目脚手架。创建一个文件来存放爬虫:

bash
touch myspider.py

第 1 步:通过 Crawling API 获取页面

从一个只证明路由是否正常工作的爬虫开始。继承 scrapy.Spider,设置 name,并配置 start_urls。这里的技巧在于起始 URL 不是直接指向目标的:你用 api.buildURL 封装它,让 Scrapy 请求 Crawling API 端点,由 API 代你获取目标页面。

python
import scrapy
from crawlbase import CrawlingAPI

# Replace YOUR_CRAWLBASE_TOKEN with the token from your dashboard
api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

class AmazonSpider(scrapy.Spider):
    name = "amazonspider"

    # Target page to crawl, then route it through the Crawling API
    targets = ["https://www.amazon.com/s?k=cold+brew+coffee+maker"]
    start_urls = [api.buildURL(url, {}) for url in targets]

爬虫还没有 parse 方法,所以它会获取页面后就停止。这是有意为之:在编写任何选择器之前,你需要先确认请求能通过 API 到达目标并以 200 状态返回。在项目目录中运行:

bash
scrapy runspider myspider.py

在日志中你应该能看到一行针对 api.crawlbase.comGET 请求对应的 Crawled (200),其中 url 查询参数携带了你的目标 URL。这个 200 状态正是这一步的全部意义:请求通过 Crawling API 发出,API 通过可信 IP 获取了 Amazon 搜索页面,渲染后的 HTML 返回给了 Scrapy。由于还没有解析器,Scrapy 会在日志中记录默认的 parse 回调未定义,然后关闭爬虫。管道已经通了,现在可以提取数据了。

Crawlbase Crawling API

那个针对高难度商业目标的 Crawled (200) 正是大多数爬虫无法做到的部分。Crawling API 获取你传给 buildURL 的 URL,通过轮换住宅 IP 获取页面,在需要时在真实浏览器中渲染,并将完整的 HTML 交给 Scrapy,让你无需自行运维无头浏览器集群和代理池。先用免费套餐在你自己的目标上试试。

第 2 步:用 CSS 和 XPath 选择器解析字段

现在添加 parse 方法。Scrapy 会为每个获取的页面自动调用它,传入 response,而 response 提供了对 HTML 的 CSS 和 XPath 选择器访问。对搜索页面上的每个商品卡片,你提取标题和链接,并 yield 一个小字典。Scrapy 会收集你 yield 的所有内容作为抓取项目。

python
    def parse(self, response):
        for card in response.css("div[data-component-type='s-search-result']"):
            title = card.css("h2 a span::text").get()
            href = card.css("h2 a::attr(href)").get()
            if not title or not href:
                continue
            yield {
                "title": title.strip(),
                "url": response.urljoin(href),
            }

有几点值得说明。卡片选择器使用稳定的 data-component-type 属性而非脆弱的工具类,这是在任何网站上都应优先选择的耐用锚点。response.css(...).get() 返回第一个匹配项的文本,无匹配时返回 None,因此 if not title or not href 的守卫跳过了不携带这两个字段的赞助位和布局行。response.urljoin(href) 将卡片提供的相对链接转换为绝对 URL。如果你偏好 XPath,同样的两个字段可以写作 card.xpath(".//h2//a//span/text()").get()card.xpath(".//h2/a/@href").get()。CSS 和 XPath 在这里可以互换;选择对某个字段读起来更清晰的那种。两者的深度对比可参考关于使用 XPath 和 CSS 选择器进行网络抓取的指南。

选择器会漂移

网站标签结构会在没有通知的情况下发生变化,上述选择器是起点模板,而非稳定合约。如果每个卡片的 titleurl 都返回 None,请在浏览器开发工具中打开实时页面,重新检查一个商品卡片,并更新选择器。定期维护选择器是任何生产爬虫的正常工作,不是出了什么问题的信号。

第 3 步:组合并运行完整爬虫

将各部分组合到一个文件中。这是完整的、可运行的爬虫:导入、API 客户端、通过 buildURL 路由的起始 URL,以及 parse 方法。

python
import scrapy
from crawlbase import CrawlingAPI

# Replace YOUR_CRAWLBASE_TOKEN with the token from your dashboard
api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

class AmazonSpider(scrapy.Spider):
    name = "amazonspider"

    targets = ["https://www.amazon.com/s?k=cold+brew+coffee+maker"]
    start_urls = [api.buildURL(url, {}) for url in targets]

    def parse(self, response):
        for card in response.css("div[data-component-type='s-search-result']"):
            title = card.css("h2 a span::text").get()
            href = card.css("h2 a::attr(href)").get()
            if not title or not href:
                continue
            yield {
                "title": title.strip(),
                "url": response.urljoin(href),
            }

运行并用 Scrapy 内置的 feed export 将结果直接写入文件,它会序列化爬虫 yield 的所有内容:

bash
scrapy runspider myspider.py -o products.json

-o products.json 标志告诉 Scrapy 将所有 yield 的项目写入 JSON 文件。去掉该标志,项目则打印到控制台。无论哪种方式,日志中每一行 Scraped from 对应一个商品,最终的统计报告说明本次运行共采集了多少项目。

输出内容示例

每个项目是一条包含你 yield 的两个字段的小记录。JSON 文件是它们的列表,可直接加载到数据库、笔记本或下游管道中。

json
[
  {
    "title": "Airtight Cold Brew Iced Coffee Maker and Tea Infuser with Spout, 1.0L",
    "url": "https://www.amazon.com/Airtight-Coffee-Maker-Infuser-Spout/dp/B01CTIYU60"
  },
  {
    "title": "KitchenAid Cold Brew Coffee Maker, Brushed Stainless Steel",
    "url": "https://www.amazon.com/KitchenAid-KCM4212SX-Coffee-Brushed-Stainless/dp/B06XNVZDC7"
  }
]

爬取多个页面

一个搜索页面只是演示。真实的爬取需要跟踪你刚采集的链接,或遍历后续的结果页面,而 Scrapy 正是为此而生的。不要 yield 一个普通字典,而是为你想跟踪的每个 URL yield 一个 scrapy.Request,通过 buildURL 路由使其继续走 Crawling API,并将其指向解析下一页的回调函数。

python
    def parse(self, response):
        for card in response.css("div[data-component-type='s-search-result']"):
            href = card.css("h2 a::attr(href)").get()
            if href:
                product_url = response.urljoin(href)
                yield scrapy.Request(
                    api.buildURL(product_url, {}),
                    callback=self.parse_product,
                )

    def parse_product(self, response):
        yield {
            "title": response.css("#productTitle::text").get(default="").strip(),
            "url": response.url,
        }

Scrapy 将你 yield 的每个请求加入队列,通过下载器获取,并为每个响应调用对应的回调,因此两层爬取(搜索页面,然后每个商品页面)只需要两个 parse 方法。由于每个后续请求都用 buildURL 封装,它也会通过 Crawling API 传输,使整个爬取过程的 IP 轮换和渲染保持一致。在测试期间用 Scrapy 的 CLOSESPIDER_ITEMCOUNT 等设置限制爬取范围,并用 DOWNLOAD_DELAY 设置礼貌延迟,避免对目标造成压力。对于用 JavaScript 渲染商品列表的网站,同样的路由方式在你请求渲染时也能处理;关于爬取 JavaScript 网站的指南深入介绍了何时需要这样做。

保持不被封锁

通过 Crawling API 路由解决了渲染和可信 IP 这两个最难的部分,但以下几个习惯能让较长时间的爬取保持健康。

  • 控制请求节奏。设置 DOWNLOAD_DELAY,让 Scrapy 的 AutoThrottle 自适应速率,而不是以框架能承受的最快速度发送请求。速度是爬虫被注意到的原因。
  • 善用轮换。住宅 IP 池将请求分散到大量真实用户地址上,使任何单个地址都不会触发速率限制。Crawling API 为你处理了这一点;如果你自己搭建技术栈,这是最需要做好的部分。
  • 关注状态码。爬取开始返回非 200 响应,说明当前的速率或 IP 级别已经不够用了。将其视为需要退后的信号,而非可以忽略的噪音。

同样的模式适用于 Python 之外的场景。如果你想对比在另一种语言中的实现方式,关于如何用 Java 构建网络爬虫的演练以不同的工具链呈现了相同的"通过 API 获取再解析"结构。

负责任地爬取

坚守公开数据,即任何人无需登录就能看到的商品标题和链接,远离任何需要身份验证的内容、个人信息,或你打算再传播的受版权保护的媒体。遵守每个网站的 robots.txt 和服务条款,它们划定了你可以采集的内容和方式的边界,并将请求频率控制在合理范围内,以免对他人的服务器造成压力。当网站为你所需的数据提供官方 API 时,优先使用它:这是经过认可的路径,通常也是更稳定的路径。这里的任何工具都不会改变这些义务,它们只是让技术层面的工作得以实现。

回顾

核心要点

  • Scrapy 提供爬虫框架。继承 start_urlsparse 方法的爬虫子类是核心全部所在,scrapy runspider 可从单个文件运行它。
  • 将每个请求路由到 Crawling API。api.buildURL 封装每个目标 URL,使请求通过轮换可信 IP 传输并返回渲染后的内容,而非从你的数据中心地址直接访问目标网站。
  • 解析前先确认 200 状态。先运行没有解析器的爬虫;针对 API 端点的 Crawled (200) 证明路由在你接触选择器之前就已正常工作。
  • 用 CSS 或 XPath 提取数据。response 同时支持两者;将每个字段映射到耐用的选择器,对缺失匹配做防御处理,并预期选择器会随时间漂移。
  • 负责任地爬取。坚守公开数据,遵守 robots.txt 和服务条款,控制请求节奏,并在有官方 API 时优先使用。

常见问题

为何将 Scrapy 路由到 Crawling API 而非直接抓取?

因为直接的 Scrapy 请求会从你自己的 IP 访问目标,获取的是原始的、通常未渲染的 HTML。在繁忙的商业网站上,这意味着 CAPTCHA、封锁,或者只有一个空的 JavaScript 框架。通过 Crawling API 路由,页面在轮换住宅 IP 后被获取,并在需要时渲染,因此到达 Scrapy 的 HTML 是你实际可以解析的完整页面。

api.buildURL 做了什么?

它接受一个目标 URL,返回包含你的 token 和目标作为查询参数的完整 Crawling API 请求 URL。你将 Scrapy 指向 buildURL 返回的 URL,API 代你获取目标。它让你不必手动拼接 https://api.crawlbase.com/?token=...&url=... 并担心转义问题。

我需要普通 token 还是 JavaScript token?

取决于目标。如果页面在初始 HTML 中就提供了内容,普通 token 就够了。如果商品列表只有在页面的 JavaScript 运行后才出现,你需要 JavaScript token,让 API 在返回页面前在真实浏览器中渲染它。当你在浏览器中能看到的字段在 Scrapy 中返回空值时,通常说明需要切换到 JavaScript token。

我可以在同一个爬虫中混用 CSS 和 XPath 选择器吗?

可以。每个 Scrapy response 都同时提供 response.css(...)response.xpath(...),你可以自由混用,甚至可以逐字段选择。CSS 对于类名和属性匹配通常更简洁,而 XPath 在向上遍历树或按文本匹配时更方便。为眼前的字段选择读起来更清晰的那种。

如何爬取多个页面或跟踪链接?

对每个想跟踪的 URL yield 一个 scrapy.Request 而非普通项目,用 api.buildURL 封装该 URL 使其回到 Crawling API,并为其指定解析下一页的回调函数。Scrapy 会将你 yield 的所有请求加入队列并获取,所以搜索页面加商品页面的两层爬取只需要两个 parse 方法。测试期间用 CLOSESPIDER_ITEMCOUNT 等设置限制运行规模。

我的选择器返回 None,是什么变了?

几乎可以肯定是网站的标签结构发生了变化。类名和容器属性会在没有通知的情况下变更,破坏依赖它们的选择器。在浏览器开发工具中打开实时页面,重新检查元素,尽量优先使用稳定的 data- 属性等耐用锚点,然后更新选择器。定期维护选择器是任何生产爬虫的正常工作。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量