网页抓取能将公开网页转化为可分析的结构化数据,而解析步骤决定了数据的质量。Python 有多个解析库,但 Parsel 凭借其轻量、快速以及围绕爬虫工程师已熟知的两种选择器语言(XPath 和 CSS)构建的特点脱颖而出。它是 Scrapy 底层使用的同一引擎,在手头有原始 HTML 并想用几行简洁代码提取字段时,也同样好用。

本指南是一个可运行的完整演练。你安装 Parsel,通过 Crawling API 获取渲染后的页面,将 HTML 加载到 Selector 中,并分别使用 XPath 和 CSS 通过 .get().getall() 提取数据。在此基础上,你将遍历一组条目列表,读取文本和属性,清理值,并将结果导出为 JSON 和 CSV。示例目标站点是 books.toscrape.com,这是一个专门为爬虫练习而构建的公开沙箱,你可以端到端运行每个代码片段,而无需接触任何真实的生产站点。

你将构建什么

一个小型 Python 脚本,它抓取一个目录页面,基于返回的 HTML 构建 Parsel Selector,遍历产品卡片,并为每个条目提取一条结构化记录。每张书籍卡片我们提取以下字段:

  • 标题 书名,从链接属性中读取。
  • 价格 标注的价格,清洗为数字。
  • 库存状态 卡片上显示的库存文本。
  • 评分 星级评分,从 CSS 类名中读取。
  • 链接 书籍详情页的绝对 URL。

为何用 Parsel 在 Python 中解析

Parsel 是一个独立的选择器库。你把一段 HTML 字符串传给它,它构建一棵树,然后你用 XPath 或 CSS 表达式查询该树。它处于一个实用的中间位置:比 Scrapy 这样的完整框架轻量,又比 BeautifulSoup 更具选择器导向性(BeautifulSoup 更倾向于 Python 方法链而非选择器字符串)。它在爬虫工具箱中赢得一席之地的原因很直接:

  • 两种选择器语言。 需要按结构导航或按文本匹配时用 XPath,短类名或标签选择器读起来更清晰时用 CSS。Parsel 在同一个对象上同时支持两者。
  • 轻量且快速。 底层基于 lxml 构建,解析大型文档速度很快,除了导入一个类之外几乎不需要任何配置。
  • 简洁的语法。 .get() 返回第一个匹配项,.getall() 返回所有匹配项,链式选择器使提取代码简短易维护。

关于选择器语言本身的深入参考,XPath 和 CSS 选择器一文详细介绍了相关语法。本文则专注于如何将它们配合 Parsel 实际使用。

为何通过 Crawling API 获取页面

Parsel 负责解析 HTML,但它不负责获取页面。你仍然需要某种工具先取回 HTML,而这个获取步骤正是大多数爬虫遇到麻烦的地方。一个裸 HTTP 请求在简单的静态页面上运作良好,但许多现代网站使用 JavaScript 渲染内容,原始响应只是一个空壳,真实数据并不包含其中。另一些站点则监控自动化流量,对不像真实浏览器的请求进行限速或封锁。

通过 Crawling API 获取页面能在一次调用中同时解决这两个问题。你发送一个 URL,它在需要时渲染页面,通过受信任的轮换 IP 路由请求,并返回完整的 HTML,你可以直接将其输入 Parsel Selector。这将获取层面的问题(渲染、轮换、封锁)与解析层面的问题(选择器、字段)分离开来,这种分离正是保持爬虫可维护性的关键。

前置条件

基础 Python。 你应该能够运行脚本并用 pip 安装包。不需要有 Parsel 的使用经验,本指南会随进展介绍 API。

Python 3.8 或更高版本。python --version 检查版本。如果你还没有安装 Python,请从 python.org 安装,并确保它已加入 PATH。

Crawlbase 账号和令牌。 注册后打开控制台,复制你的请求令牌。Crawlbase 最多提供 20,000 次免费请求,足以完成本指南的所有操作。请像对待密码一样保管令牌,不要将其提交到版本控制系统。

搭建项目

创建虚拟环境以隔离项目依赖,然后安装脚本所需的两个库。

bash
python --version

python -m venv parsel_env
source parsel_env/bin/activate

pip install parsel crawlbase

在 Windows 上,用 parsel_env\Scripts\activate 替换 source 那行来激活环境。parsel 负责提取,crawlbase 是官方客户端,帮你获取渲染后的页面。jsoncsv 模块随标准库一并提供,导出步骤无需额外安装任何东西。

步骤 1:获取页面并构建 Selector

首先通过 Crawling API 获取一个目录页面,并将其 HTML 加载到 Parsel Selector 中。导入 CrawlingAPI,用你的令牌初始化,请求 URL,并在解析前检查 cb_status(legacy pc_status) 响应头,让失败保持可见而非静默。

python
from crawlbase import CrawlingAPI
from parsel import Selector

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

def fetch_html(page_url):
    response = api.get(page_url)
    if response["headers"]["cb_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['cb_status']}")
    return None

if __name__ == "__main__":
    url = "https://books.toscrape.com/catalogue/page-1.html"
    html = fetch_html(url)
    if html:
        selector = Selector(text=html)
        print(selector.xpath("//title/text()").get())

Selector(text=html) 是后续一切操作的入口:它一次性解析字符串,并提供一个可用 .xpath().css() 查询的对象。最后一行用 XPath 表达式读取页面标题,其中 /text() 选取文本节点,.get() 将第一个匹配项作为字符串返回。运行文件后你应该能看到目录页标题被打印出来,这就确认了在编写任何字段选择器之前,获取和解析步骤都已正常工作。

Crawlbase Crawling API

上面的 fetch_html 步骤正是 Parsel 自身无法完成的部分,而在真实目标上,渲染和封锁问题也恰恰在这里变得棘手。Crawling API 接受你的令牌,在需要时渲染 JavaScript 页面,在服务端通过住宅 IP 轮换路由请求,并返回完整 HTML,你可以直接将其输入 Selector,无需自行运行无头浏览器集群或代理池。从免费层开始,使用你最多 20,000 次的请求额度。

步骤 2:用 XPath 和 CSS 提取数据

Parsel 允许你用任一语言查询同一个 Selector。XPath 即 XML 路径语言,通过结构导航文档树;CSS 选择器则与样式表中相同,按标签、类名或 id 定位元素。下面两个示例提取同类型的值,方便你直接对比两种写法。

python
# XPath: select the text of the first h1
heading = selector.xpath("//h1/text()").get()

# CSS: select the text inside a known element
price = selector.css("p.price_color::text").get()

# Attributes: @attr in XPath, ::attr() in CSS
link_xpath = selector.xpath("//article//h3/a/@href").get()
link_css = selector.css("article h3 a::attr(href)").get()

两种模式承担了大部分工作。读取文本时,在 XPath 中使用 /text(),在 CSS 中使用 ::text。读取 hrefsrc 等属性时,在 XPath 中使用 @attribute,在 CSS 中使用 ::attr(attribute)。所有情况下,.get() 返回第一个匹配项,若无匹配则返回 None,因此缺失元素不会引发错误。

get vs getall

.get() 将第一个匹配值作为字符串返回。.getall() 将所有匹配值作为列表返回。当你期望单个值(如价格)时使用 .get(),当你需要整列数据(如页面上的所有标题)时使用 .getall()

步骤 3:遍历条目列表

真实页面包含多个重复条目,而不仅仅是一个。标准做法是先选取一次重复容器,然后迭代,对每个元素运行作用域选择器,为每个条目构建一条记录。在书籍沙箱中,每个产品都是一个 <article class="product_pod">,这就是我们要遍历的容器。

python
def parse_books(selector):
    books = []
    for card in selector.css("article.product_pod"):
        title = card.css("h3 a::attr(title)").get()
        price = card.css("p.price_color::text").get()
        availability = card.css("p.instock.availability::text").getall()
        rating = card.css("p.star-rating::attr(class)").get()
        href = card.css("h3 a::attr(href)").get()

        books.append({
            "title": title,
            "price": price,
            "availability": availability,
            "rating": rating,
            "href": href,
        })
    return books

调用 .css("article.product_pod") 返回一个可迭代的 SelectorList,其中每个 card 本身也是一个 Selector,因此内部的 .css() 调用只在该卡片范围内执行。标题位于链接的 title 属性中,价格位于 price_color 段落中,评分则包含在类似 star-rating Three 的类名中,这就是为什么我们读取整个 class 属性,并在下一步清洗它。availability 字段使用 .getall(),因为其文本跨多个空白节点分布,连接并去除空白后才能得到单个干净的字符串。

步骤 4:清洗和规范化值

原始选择器输出在使用前通常需要做一些轻微处理。价格带有货币符号,评分以两个单词构成的类名返回,库存文本带有前后空白。几个标准字符串操作就能将每个值处理成干净的形式。

python
BASE = "https://books.toscrape.com/catalogue/"
WORDS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}

def clean_book(card):
    price_text = card.css("p.price_color::text").get(default="")
    price = float(price_text.replace("£", "").strip() or 0)

    rating_class = card.css("p.star-rating::attr(class)").get(default="")
    rating_word = rating_class.replace("star-rating", "").strip()
    rating = WORDS.get(rating_word)

    stock = " ".join(card.css("p.instock.availability::text").getall())
    href = card.css("h3 a::attr(href)").get(default="")

    return {
        "title": card.css("h3 a::attr(title)").get(),
        "price": price,
        "availability": stock.strip(),
        "rating": rating,
        "link": BASE + href,
    }

两个小习惯让这段代码更健壮。第一,.get(default="") 提供一个回退值,当元素缺失时返回空字符串而非 None,这样后续的 .replace().strip() 调用就不会抛出异常。第二,价格解析去掉货币符号(£ 转义即英镑符号)并转换为 float,使值可以作为数字排序和过滤。评分将类名中的单词映射为整数,相对 href 与基础 URL 拼接生成绝对链接。

步骤 5:组装完整脚本

现在将各部分连接成一个可运行的脚本:获取页面,构建 Selector,通过 clean_book 遍历卡片,并将记录导出为 JSON 和 CSV。

python
import csv
import json
from crawlbase import CrawlingAPI
from parsel import Selector

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"})

BASE = "https://books.toscrape.com/catalogue/"
WORDS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}

def fetch_html(page_url):
    response = api.get(page_url)
    if response["headers"]["cb_status"] == "200":
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['headers']['cb_status']}")
    return None

def clean_book(card):
    price_text = card.css("p.price_color::text").get(default="")
    price = float(price_text.replace("£", "").strip() or 0)
    rating_class = card.css("p.star-rating::attr(class)").get(default="")
    rating = WORDS.get(rating_class.replace("star-rating", "").strip())
    stock = " ".join(card.css("p.instock.availability::text").getall())
    href = card.css("h3 a::attr(href)").get(default="")
    return {
        "title": card.css("h3 a::attr(title)").get(),
        "price": price,
        "availability": stock.strip(),
        "rating": rating,
        "link": BASE + href,
    }

def parse_books(html):
    selector = Selector(text=html)
    return [clean_book(card) for card in selector.css("article.product_pod")]

def save_outputs(records):
    with open("books.json", "w") as f:
        json.dump(records, f, indent=2)
    if not records:
        return
    with open("books.csv", "w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=records[0].keys())
        writer.writeheader()
        writer.writerows(records)

def main():
    url = "https://books.toscrape.com/catalogue/page-1.html"
    html = fetch_html(url)
    if not html:
        return
    records = parse_books(html)
    save_outputs(records)
    print(f"Saved {len(records)} books")

if __name__ == "__main__":
    main()

parse_books 一次性构建 Selector,并通过对卡片的列表推导式返回一组清洗后的记录。save_outputs 写入一个 JSON 文件和一个以第一条记录的键为标题的 CSV,你可以按下游工具的需求选择任一格式。要覆盖整个目录,将 main 包装在一个遍历 page-1.htmlpage-50.html 的循环中,并合并到一个列表里即可,解析逻辑无需任何改动。

输出结果示例

运行 python books_scraper.py,你将得到每本书一条干净的结构化记录,可直接用于分析、数据库或电子表格。

json
[
  {
    "title": "A Light in the Attic",
    "price": 51.77,
    "availability": "In stock",
    "rating": 3,
    "link": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
  },
  {
    "title": "Tipping the Velvet",
    "price": 53.74,
    "availability": "In stock",
    "rating": 1,
    "link": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html"
  }
]

对应的 CSV 包含相同的字段和一行标题,可以直接导入 pandas 或任何电子表格,按价格排序或按评分筛选。

常见错误及注意事项

几个习惯决定了一个爬虫是否经得起下一次运行的考验。

  • 编写选择器前先审查页面。 在浏览器开发者工具中打开页面,确认类名和结构。针对不存在元素的选择器什么都不返回,而这是最常见的爬取结果为空的原因。
  • 始终处理缺失数据。 使用 .get(default="") 或防范 None,这样单个字段缺失就不会导致整个循环崩溃。页面很少像看起来那样整齐一致。
  • 去除并规范化文本。 网页文本带有多余空白和货币符号。在解析时用 .strip().replace() 清洗,确保存储的值是一致的。
  • 控制请求速率。 在紧密循环中快速抓取页面是被限速的最快方式。在请求之间添加短暂延迟,并将请求量保持在合理范围内。

负责任地抓取

Parsel 只解析你已经持有的 HTML,但你获取 HTML 的方式同样重要。无论目标是什么,以下几条原则能让任何爬虫项目保持在正确的边界内。

在收集任何数据前,检查站点的服务条款和 robots.txt,并将两者都视为边界而非建议。只收集任何访客无需登录即可看到的公开数据,并将请求速率保持在合理范围内,以免给站点服务器造成压力。当项目涉及个人数据时,责任随之增大:GDPR 和 CCPA 等法规规定了个人信息的收集和使用方式,这些情况需格外谨慎处理,或完全回避。本例使用专为练习而构建的沙箱,正是为了让你能学习技术细节而无需担忧上述问题,同样的原则在将爬虫指向真实站点时同样适用。更多关于在站点限制内操作的内容,请参阅如何在不被封锁的情况下抓取网站

回顾

核心要点

  • Parsel 以选择器为核心。 构建一个 Selector(text=html),然后用 XPath 或 CSS 查询,选用哪种取决于哪种对该元素的表达更清晰。
  • get 和 getall 涵盖大多数提取场景。 .get() 将第一个匹配项作为字符串返回,.getall() 将所有匹配项作为列表返回,.get(default="") 防止缺失字段导致程序崩溃。
  • 文本和属性的提取模式是固定的。 分别在 XPath 和 CSS 中使用 /text()::text 读取文本,使用 @attr::attr() 读取属性。
  • 遍历容器而非整个页面。 一次性选取重复元素,然后对每个条目运行作用域选择器,构建每条干净的记录,并导出为 JSON 和 CSV。
  • 将获取与解析分离。 让 Crawling API 处理渲染、轮换和封锁,然后将完成的 HTML 交给 Parsel,使提取代码保持简洁。

常见问题

Parsel 是什么,为何用它进行网页抓取?

Parsel 是一个小型、快速的 Python 库,用于通过 XPath 和 CSS 选择器从 HTML 和 XML 中提取数据。它是 Scrapy 使用的同一选择器引擎,当你已经有 HTML 并想从中提取字段时,也可以作为独立工具使用。人们选择它的原因在于其简洁的语法、在同一对象上对两种选择器语言的支持,以及它与现有数据处理流水线的良好集成。

Parsel 和 BeautifulSoup 的区别是什么?

两者都能解析 HTML,但风格不同。Parsel 以选择器为驱动:你编写 XPath 或 CSS 表达式并调用 .get().getall()。BeautifulSoup 则依赖 Python 方法链,如 findfind_all。Parsel 还原生支持 XPath,而 BeautifulSoup 不支持。选择哪个取决于你更喜欢哪种表达选择器的方式。

Parsel 中 get 和 getall 的区别是什么?

.get() 将第一个匹配值作为字符串返回,若无匹配则返回 None.getall() 将所有匹配值作为列表返回。对于单一字段(如价格或标题)使用 .get(),当你需要整个集合(如页面上的所有链接)时使用 .getall()。传入 .get(default="value") 可为缺失元素提供回退值。

如何处理通过 JavaScript 加载内容的页面?

Parsel 解析你传给它的任何 HTML,所以问题在于如何获取该 HTML。如果页面通过 JavaScript 渲染内容,裸请求返回的将是一个没有数据的空壳。通过 Crawling API 获取时,它会先渲染页面并返回完整 HTML,你再将其加载到 Selector 中,就像本文所展示的一样,解析代码无需做任何改动。

可以将 Parsel 的结果导出为 JSON 或 CSV 吗?

可以。Parsel 返回普通 Python 值,因此一旦你构建了一个字典列表,就可以用标准的 json 模块写入 JSON,用 csv.DictWriter 写入 CSV,就像完整脚本所做的那样。此后数据无需任何额外转换即可导入 pandas 或数据库。

为何要配合 Crawling API 而非直接使用普通请求?

普通请求往往在 Parsel 运行之前就已失败:页面可能是客户端渲染的,或者站点可能封锁了不像真实浏览器的流量。Crawling API 负责处理渲染、IP 轮换和 CAPTCHA 挑战,然后返回干净的 HTML。这将获取层的问题从解析代码中剥离出来,让 Parsel 专注于它擅长的事:将 HTML 转化为结构化字段。

开始构建

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

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

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