网页抓取能将公开网页转化为可分析的结构化数据,而解析步骤决定了数据的质量。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 次免费请求,足以完成本指南的所有操作。请像对待密码一样保管令牌,不要将其提交到版本控制系统。
搭建项目
创建虚拟环境以隔离项目依赖,然后安装脚本所需的两个库。
python --version python -m venv parsel_env source parsel_env/bin/activate pip install parsel crawlbase
在 Windows 上,用 parsel_env\Scripts\activate 替换 source 那行来激活环境。parsel 负责提取,crawlbase 是官方客户端,帮你获取渲染后的页面。json 和 csv 模块随标准库一并提供,导出步骤无需额外安装任何东西。
步骤 1:获取页面并构建 Selector
首先通过 Crawling API 获取一个目录页面,并将其 HTML 加载到 Parsel Selector 中。导入 CrawlingAPI,用你的令牌初始化,请求 URL,并在解析前检查 cb_status(legacy pc_status) 响应头,让失败保持可见而非静默。
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() 将第一个匹配项作为字符串返回。运行文件后你应该能看到目录页标题被打印出来,这就确认了在编写任何字段选择器之前,获取和解析步骤都已正常工作。
上面的 fetch_html 步骤正是 Parsel 自身无法完成的部分,而在真实目标上,渲染和封锁问题也恰恰在这里变得棘手。Crawling API 接受你的令牌,在需要时渲染 JavaScript 页面,在服务端通过住宅 IP 轮换路由请求,并返回完整 HTML,你可以直接将其输入 Selector,无需自行运行无头浏览器集群或代理池。从免费层开始,使用你最多 20,000 次的请求额度。
步骤 2:用 XPath 和 CSS 提取数据
Parsel 允许你用任一语言查询同一个 Selector。XPath 即 XML 路径语言,通过结构导航文档树;CSS 选择器则与样式表中相同,按标签、类名或 id 定位元素。下面两个示例提取同类型的值,方便你直接对比两种写法。
# 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。读取 href 或 src 等属性时,在 XPath 中使用 @attribute,在 CSS 中使用 ::attr(attribute)。所有情况下,.get() 返回第一个匹配项,若无匹配则返回 None,因此缺失元素不会引发错误。
.get() 将第一个匹配值作为字符串返回。.getall() 将所有匹配值作为列表返回。当你期望单个值(如价格)时使用 .get(),当你需要整列数据(如页面上的所有标题)时使用 .getall()。
步骤 3:遍历条目列表
真实页面包含多个重复条目,而不仅仅是一个。标准做法是先选取一次重复容器,然后迭代,对每个元素运行作用域选择器,为每个条目构建一条记录。在书籍沙箱中,每个产品都是一个 <article class="product_pod">,这就是我们要遍历的容器。
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:清洗和规范化值
原始选择器输出在使用前通常需要做一些轻微处理。价格带有货币符号,评分以两个单词构成的类名返回,库存文本带有前后空白。几个标准字符串操作就能将每个值处理成干净的形式。
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。
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.html 到 page-50.html 的循环中,并合并到一个列表里即可,解析逻辑无需任何改动。
输出结果示例
运行 python books_scraper.py,你将得到每本书一条干净的结构化记录,可直接用于分析、数据库或电子表格。
[ { "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 方法链,如 find 和 find_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 次请求免费,无需信用卡。
