☰
XPath与Parsel实战:从爬虫HTML中高效提取结构化数据
2026/10/1 11:22:53 网站建设 项目流程

爬虫拿到HTML之后,真正让人头疼的其实是"怎么从这一堆标签里把我要的东西抠出来"。前面我们试过正则,写起来是真的爽,但也是真的脆——样式稍微换个空格、加个属性,你的findall可能就全军覆没。这一节我们来啃解析环节的第二块硬骨头:XPath配合Parsel库。我先把结论放在这里:学会XPath之后,你会发现之前那些凑正则的时间,基本都可以省下来去干点别的。

XPath不是一门编程语言,而是一门"在XML/HTML文档里定位节点"的查询语言。它不依赖"字符串长什么样",而是依赖"节点在文档树里的位置和关系",所以只要页面结构没有发生根本性调整,哪怕样式的文字变了一百遍,你的解析代码依然稳如老狗。而Parsel这个库,是Scrapy团队从Scrapy框架里抽出来的独立解析模块,底层基于lxml,速度、稳定性都有保证,API设计得也相当顺手。

这篇文章我不会只甩几个XPath语法给你背,那样学完就忘。我会从"为什么需要树状思维"开始,把XPath的路径、谓词、函数讲透,再用Parsel写真实案例,最后把我实际踩过的坑挨个摊开说。内容定位是"进阶但新手友好"——我默认你已经能用requests拿到HTML文本了,但没学过也没关系,这节的重点全在解析,前面的部分能跑通就行。

1. 解析这件事,为什么越往后越要"按树找节点"

很多新手在学解析时都会有个困惑:一开始觉得正则挺简单的,re.findall(r'<title>(.*?)</title>', html)这不就完事了吗?干嘛还要搞XPath、CSS选择器这些东西?

这个想法我太理解了,因为我也是从正则走过来的。但真到了复杂页面,正则就原形毕露了。

1.1 正则的本质是"平面匹配",撑不起"层级关系"

正则的原理是在字符串里按模式去扫,它看到的是从头到尾一长串字符,没有"这个div套着那个div"的概念。举个实际例子,你想提取一个列表页里所有商品的名称和价格,HTML大概长这样:

<ul class="product-list"> <li> <div class="name">机械键盘</div> <div class="price">299</div> </li> <li> <div class="name">无线鼠标</div> <div class="price">89</div> </li> </ul>

用正则你会怎么写?很可能是两个findall,一个抓name,一个抓price,然后靠zip硬拼。

names = re.findall(r'<div class="name">(.*?)</div>', html) prices = re.findall(r'<div class="price">(.*?)</div>', html)

这么干有两个隐患。第一,如果某个商品没有价格标签,names和prices的长度就对不上,zip会悄悄丢掉数据或者错位。第二,如果页面里其他地方也出现了class="name"(比如某个推荐模块),你会发现正则把不该抓的东西也抓进来了。为什么会这样?因为正则不知道"这个name标签属于哪个li",它没有"作用域"的概念。

树状解析的思路就完全不同。它先把HTML整个转换成一棵DOM树——ul是根,两个li是它的子节点,每个li下面又有name和price两个叶子节点。然后你告诉解析器:"去每个li节点下面,找到它的name子节点",它就能精确地沿着结构去取,永远不会跨列表匹配。

你只要记住这个心法:正则适合处理"平铺的、格式松散"的文本,比如从一段话里抓手机号、邮箱;页面结构提取,请优先交给树状解析方案。

1.2 在浏览器里"看见"DOM树:一开始就别盲写

学习XPath最有效的方式,不是对着文档记语法,而是先学会用浏览器开发者工具看HTML结构。我教学生时反复强调一句话:"解析器眼里没有'页面',只有一棵树。"所以你必须先学会在代码之外"看见"这棵树。

你在Chrome里打开任意一个页面,按F12切到Elements面板,看到的就是浏览器解析出来的DOM树。这棵树和你在代码里用requests拿到的HTML是同一个东西,只是浏览器帮你渲染成了可折叠的可视化结构。你点开某个节点,左右两侧能看到它的标签名、属性、文本内容。

一个非常实用的技巧是:**在Elements面板里选中某个元素,右键 → Copy → Copy XPath,就能得到一条指向该元素的XPath表达式。**比如你在上面那个商品列表里右键点"机械键盘",复制出来的往往是类似/html/body/div[1]/div[2]/ul/li[1]/div[1]这样的绝对路径。

但请注意——**浏览器生成的XPath只能当草稿,不要直接粘进代码里用。**原因有两点:

  • 它是绝对路径,从<html>一路写到底。如果页面在它上面多套了一层div,整条路径就失效了。
  • 它经常带数字下标(比如div[1]),而页面里模块的插入顺序一变,下标可能就全乱了。

我推荐的做法是:对着浏览器生成的这条路径,把它缩短成一个抓住"锚点"的相对XPath。比如看到class="name"这个特征,你就可以不用管前面那串/html/body/div[1]/div[2],直接写//div[@class="name"]。又稳又简洁。这就是后面要讲的"相对路径"思想,先在心里埋个种子。

2. XPath的路径表达:从"盘根错节"到"一格一站"

理解了"树"的概念,XPath的语法就好学了。它就是一套描述"怎么从树的某个位置走到另一个位置"的语言。你把HTML想象成一个巨大的文件柜,XPath就是写在纸条上的取件路线。

先上一张最基础的符号表,我保证每一个都会用到:

表达式含义生活类比
/从根节点开始走(绝对路径)从档案室门口开始走
//从任意位置往下找,不管在哪层在整栋楼里搜关键词
.当前节点"我现在站的位置"
..当前节点的父节点"上一级"
@取属性看门牌号
*匹配任意节点通配符

2.1 优先用//开头的相对路径,少用绝对路径

很多初学者看到XPath第一眼就被吓到,觉得那一串//div[@class='...']/ul/li/a像天书。把它拆开就简单了:它其实就是"从任意位置,找div,div的class属性要等于xxx,然后找它下面的ul,再到li,再到a"。

为什么我强烈建议你以//开头?因为绝大多数字段在页面里出现的位置是"不固定的深度"。比如你要的标题可能有时候嵌在三层div里面,有时候又嵌在五层里面,如果用绝对路径/html/body/div/div/div[3]/h1,结构一变就断。而//h1[@class='title']表达的是"不管在哪,只要你是h1且class是title,我就要你"。这就是稳定的来源。

# 绝对路径写法——页面结构稍微一动就挂 html.xpath('/html/body/div[2]/div/div[1]/h1/text()') # 相对路径写法——抓住特征属性,稳得多 html.xpath('//h1[@class="title"]/text()')

在写爬虫的时候,我有个习惯:**一个XPath里但凡超过3个层级,我就会停下来想,有没有可能压缩成两步。**因为每多一个层级,就多一个可能被改动的中间节点。别迷信"写得多精确",要追求"写得多稳定"。

2.2 谓词:用[ ]做筛选,让节点"按条件上岗"

XPath里最强大的东西,我认为是方括号谓词。它相当于给XPath加了if条件。还是拿商品列表举例,你可能遇到这么几种需求:

  • 找第二个li://ul[@class="product-list"]/li[2]
  • 找class属性等于name的节点://div[@class="name"]
  • 找带price类的节点://div[contains(@class, "price")]
  • 找包含"键盘"文本的节点://div[contains(text(), "键盘")]

这里有一个非常容易踩的坑,我要单独拎出来说。**同一个标签往往有多个class,比如<div class="product-item hot">。**如果你写//div[@class="product-item"],是匹配不到它的,因为属性值严格等于"product-item hot"。这时候必须用contains(@class, "product-item")来模糊匹配。

另外,contains(text(), "关键词")和contains(., "关键词")也有区别。前者只匹配直接的文本节点,后者会把当前节点下面所有子节点的文本都算进来。在一段文字被多个内联标签(比如<span>、<a>)拆散的情况下,contains(., "关键词")更好用。我一开始不懂这个区别,写出来的XPath经常返回空列表,后来发现是文本被嵌套标签分割了,改用.就解决了。

2.3 XPath坐标轴:不只是"向下找",还能"找兄弟"

大部分教程讲到谓词就结束了,但我觉得有四个"轴"概念非常实用,新手也应该知道,因为它们能让XPath的表达能力上一个档次:

  • following-sibling:::后面的兄弟节点
  • preceding-sibling:::前面的兄弟节点
  • parent:::父节点(简写是..)
  • ancestor:::祖先节点

举个真实场景。你提取一个文章列表,想拿到"每篇文章标题下的发布时间"。HTML结构是:

<article> <h2 class="news-title">文章标题</h2> <span class="date">2024-11-01</span> </article>

你已经定位到了//h2[@class="news-title"],怎么拿对应的date?可以这样:

//h2[@class="news-title"]/following-sibling::span[@class="date"]/text()

这个表达式读起来很自然:"标题后面的那个date兄弟的文本"。遇到类似"同一个模块下多个平行字段"的场景,用兄弟轴比从根节点重新找一遍要可靠得多,因为它是基于你已定位的那个节点出发的,天然和它同一组。

3. Parsel库上手:把XPath变成Python代码的桥

XPath语法本身是一回事,真正在Python里用它又是另一回事。市面上能解析HTML的库不少,BeautifulSoup、lxml、Parsel各有拥趸。我为什么在"解析与清洗"这一章里专门选Parsel来讲?有三个原因。

3.1 为什么从BeautifulSoup换到Parsel

我不是说BeautifulSoup不好,它对于偶尔解析一两个页面的人来说确实很友好,API简单直接,网上代码也多。但爬虫写多了你会发现几个痛点:

  • BeautifulSoup的查找API(find、find_all)在写复杂筛选时很啰嗦,一层套一层。
  • 它默认的解析器在不同环境下行为有差异,有时候需要额外安装lxml作为解析器才能正确处理某些畸形HTML。
  • 它是"汤式"API,"先find再find"的链条式写法,不如XPath一句来得直观。

Parsel设计的哲学正好反着来:**它把"用什么方式找节点"和"找到之后怎么处理"分得清清楚楚。**底层用lxml解析,速度和容错性都有保障,又提供了和Scrapy一脉相承的API——你以后如果接触Scrapy,会发现Selector的用法完全一致,等于提前学了框架的核心技能。

我用一个对比让你感受差异。同样是提取商品列表的所有name,BeautifulSoup写出来大概是:

soup = BeautifulSoup(html, 'lxml') names = [div.text for div in soup.select('ul.product-list li div.name')]

而Parsel写出来是:

sel = parsel.Selector(text=html) names = sel.xpath('//ul[@class="product-list"]/li/div[@class="name"]/text()').getall()

看起来好像差不多?但XPath的表达能力在于,当条件复杂以后,它的伸缩性远好于CSS。最典型的例子是"找包含特定文本的节点"这种需求,CSS几乎办不到,XPath用contains(text(), 'xxx')一行搞定。

3.2 从text=到Selector对象:核心三步走

Parsel的基本用法真的就三步,我建议你把它当成肌肉记忆来练。

第一步,导入库并创建Selector对象。

import parsel selector = parsel.Selector(text=html)

这里的text=接受的是一段HTML或XML字符串。如果你已经用requests拿到了response.text,直接丢进去就行。还有一个容易忽略的细节——parsel.Selector默认会对输入文本做文本规范化处理,包括解码、修复不完整的标签等,底层靠的就是lxml的容错能力。所以哪怕你拿到的HTML有标签没闭合,它一般也能硬解析出来,不会像正则那样直接错乱。

第二步,调用.xpath()方法进行定位。

# 返回的是一个list[Selector]对象列表 result = selector.xpath('//ul[@class="product-list"]/li')

这里返回的不是["机械键盘", "无线鼠标"]这样的字符串列表,而是一个Selector对象的列表。每个Selector代表一个<li>节点。这个"先拿到节点,再从节点里提取"的思维非常重要,因为它天然支持"分组提取"。

第三步,用.get()或.getall()真正拿到数据。

# get() 返回第一个匹配的结果,没有就是None first_name = selector.xpath('//div[@class="name"]/text()').get() # getall() 返回所有匹配结果组成的列表 all_names = selector.xpath('//div[@class="name"]/text()').getall()

这是Parsel和XPath结合时最核心的API分界。新手最容易翻车的地方就是搞不清这两个方法的区别:写getall()却只想要一个,结果拿到的列表还得自己取[0];或者写get()但页面里有多个匹配,结果永远只拿到第一个却不自知。你要养成一个习惯:先问自己"这个页面上该字段可能出现几次",单值用get(),多值用getall()。

3.3 节点对象再次xpath时,路径开头别乱加//

这个坑我见得太多次了,必须强调。当你拿到了一个Selector节点对象,再在它内部做二次查询时,路径的写法是有讲究的。

items = selector.xpath('//ul[@class="product-list"]/li') for item in items: name = item.xpath('./div[@class="name"]/text()').get() price = item.xpath('./div[@class="price"]/text()').get()

注意这里我写的是./div,而不是//div。./div的意思是"从当前节点出发,找它的子节点中的div"。而//div的意思是"从当前节点出发,在它下面所有层级的节点里找div"。如果HTML里这个li下面还有更深的嵌套,而你又用了//div[@class="name"],它依然能匹配到正确节点,好像没出问题。但一旦页面某个li内部多了一个不属于本商品的"推荐商品"div,//就会把那个干扰项也匹配进来,让你的数据多抓或者错抓。

还有一点:**对同一个Selector变量反复调用xpath,得到的是新Selector列表,不会污染原对象。**所以你可以放心地在一个循环里拆分组再取子字段,这比BeautifulSoup在一棵树上反复find更加安全,也更容易调试。

4. 综合实战:抓一个新闻列表页的标题、链接和时间

讲了这么多概念,我们直接上一个完整的实战。我挑一个典型的新闻列表页结构来模拟,因为新闻列表是爬虫练手最常见的场景——结构清晰、字段固定,很适合把XPath和Parsel串起来用。

页面HTML结构我先贴出来(我简化过,保留核心骨架):

<!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="utf-8"><title>示例新闻列表</title></head> <body> <main class="container"> <article class="news-item"> <h2 class="news-title"><a href="/news/20241101-a">Python 3.13发布,性能提升明显</a></h2> <span class="date">2024-11-01</span> <p class="summary">新版本引入了若干优化……</p> </article> <article class="news-item"> <h2 class="news-title"><a href="/news/20241031-b">爬虫技术再引热议:数据合规成焦点</a></h2> <span class="date">2024-10-31</span> <p class="summary">行业正在探索更规范的采集方式……</p> </article> <!-- 更多条目…… --> </main> </body> </html>

好消息是:新闻列表这种"一篇文章一个article块"的结构非常规整,用XPath做分组提取简直不要太顺手。

4.1 先拆需求,再定XPath,别急着写代码

很多人写爬虫的习惯是"打开页面,右键复制XPath,粘贴进代码,跑一下看结果"。我强烈建议你先花两分钟在纸上把目标字段和页面结构对应起来。以这个列表为例,目标字段有三个:

目标字段页面结构特征定位思路
标题<h2 class="news-title">下的<a>文本先定位article,再找h2里的a的text
链接同一个<a>的href属性先定位article,再取a的@href
发布时间<span class="date">的文本先定位article,再取span的text

关键决策点在于:到底是以"全部字段全局搜索"来写XPath,还是"先分组定位到article,再在每组内部取字段"?

我用后一种方案。逻辑也很好理解:目标是"每篇文章的标题、链接、时间",这三者天然属于同一个<article>节点。如果全局写三个互不相干的XPath,一旦某个article缺了时间,三个列表的长度就对不齐,数据就错位了。先分组再提取,等于把"打包一条记录"这件事交给了解析器,天然对齐。

对应的XPath就是:

  • 定位到所有article://article[@class="news-item"]
  • 每组内部取标题:./h2[@class="news-title"]/a/text()
  • 每组内部取链接:./h2[@class="news-title"]/a/@href
  • 每组内部取时间:./span[@class="date"]/text()

这种写法非常稳定。如果页面结构变了,比如标题外面又多套了一个div,你只需要改这一项的路径,其他项完全不受影响。

4.2 完整代码:从HTML字符串到结构化数据

下面是完整的Parsel解析代码,我把注释写得密一点,方便你对着上面HTML看:

import parsel html = open('news_list.html', encoding='utf-8').read() selector = parsel.Selector(text=html) # 第一步:定位到所有新闻条目节点 items = selector.xpath('//article[@class="news-item"]') # 第二步:遍历每个节点,提取所需字段 results = [] for item in items: # 标题:取a标签内的文本 title = item.xpath('./h2[@class="news-title"]/a/text()').get() # 链接:取a标签的href属性 link = item.xpath('./h2[@class="news-title"]/a/@href').get() # 发布时间:取span标签的文本 publish_date = item.xpath('./span[@class="date"]/text()').get() # 拼接URL:页面里是相对路径,需要补全 if link and not link.startswith('http'): link = 'https://example.com' + link results.append({ 'title': title.strip() if title else None, 'link': link, 'publish_date': publish_date.strip() if publish_date else None, }) # 打印结果 for item in results: print(item)

这里有几个清洗细节我要展开讲,因为它们直接影响最终数据的质量。

**第一个细节:strip()处理空白。**从HTML标签里拿出来的text节点,经常会带着缩进空格、换行符,尤其是用getall()批量提取时,一堆\n和空格混在结果里,看着头大。我习惯在拿到文本后立刻做strip()。但注意,如果你直接用text()提取,它返回的是该元素的直接文本节点,不会再往下递归。如果想提取某个元素的"全部文本",包括子标签里的文字,要用string(.)或者//*[text()]的组合,这个后面单独讲。

**第二个细节:相对URL的拼接。**新闻站点的链接设计成相对路径很常见。你在浏览器里看起来是完整可点的,因为浏览器自动帮你基于当前页面URL补全了。但爬虫拿到的就是光秃秃一个/news/20241101-a,你不处理,落库就是一条无效链接。最简单的处理方式是准备一个base_url,手工startswith('http')判断后拼上去。更健壮的方式是交给urllib.parse.urljoin(),它能处理各种奇怪的相对路径写法:

from urllib.parse import urljoin base_url = 'https://example.com' full_link = urljoin(base_url, link)

urljoin的好处是,如果link已经是完整绝对URL,它会原样返回;如果是//static.example.com/logo.png这种协议相对地址,它也能正确处理。爬虫里处理链接,我几乎不用字符串拼接,全部交给urljoin。

**第三个细节:字段缺失时的兜底策略。**这里比较关键。item.xpath(...).get()在匹配不到内容时,返回的是None而不是报错。上面代码里我用了title.strip() if title else None这种写法,就是为了避免None.strip()直接抛AttributeError。如果你是在写批量入库的爬虫,建议所有字段都按这个模式处理,宁可把缺失字段记为None,也不要让一个坏数据打崩整条流水线。

如果字段在页面里可能出现多种情况,比如"有的文章没有摘要",但你想把摘要也抓下来,这时候有一个更优雅的处理:让XPath替你做默认值判断。

summary = item.xpath('./p[@class="summary"]/text()').get(default='')

.get()方法支持default参数,匹配不到时返回你指定的默认值,比"先判断再给默认"少写好几行。这也是Parsel API里一个很良心的设计。

4.3 结果验证:打印出来用眼睛核对一遍再入库

代码跑完,输出大致长这样:

{'title': 'Python 3.13发布,性能提升明显', 'link': 'https://example.com/news/20241101-a', 'publish_date': '2024-11-01'} {'title': '爬虫技术再引热议:数据合规成焦点', 'link': 'https://example.com/news/20241031-b', 'publish_date': '2024-10-31'}

很多新手到这一步就急着写数据库存储代码了。我的建议是:**先别往数据库里灌,把解析结果打印出来,人肉检查一遍。**我见过太多案例,解析逻辑看着对,结果打印出来字段错位、时间格式不统一、标题里混入了分类名,这种垃圾数据入库之后再来清洗,成本翻好几倍。在解析结果输出这一步多花两分钟,你会发现后面清洗环节省下来的是大把时间。

5. 我踩过的解析坑:空列表、动态加载和边界问题

到这一节,我想把"调试XPath和Parsel时真正踩过的坑"集中讲一遍。很多坑不是语法问题,而是"想当然"问题。我一个个说。

5.1 返回空列表的几种可能:先怀疑结构,再怀疑语法

如果你在写Parsel代码时发现xpath()返回了空列表或者get()返回了None,别急着觉得自己XPath写错了。按照我下面这个顺序排查,命中率极高:

第一,HTML结构里是不是根本没有你要找的标签?有时候你眼睛在浏览器里看到了内容,但那是JS动态渲染出来的。requests直接拿到的源码里压根没有这些标签。验证方法很简单:在Python里把html内容打印前500个字符,或者存成一个文件,用编辑器打开搜一下你要找的class名,搜不到就说明静态源码里没有,XPath必然空。

第二,你的XPath是不是被空格或引号坑了?我犯过最蠢的错误就是复制了浏览器生成的XPath,里面带了tbody这种浏览器自动补全的标签,但原始HTML里根本没写<tbody>,结果怎么匹配都是空。还有一类常见错误是属性值里有单引号、双引号混用,XPath表达式是用引号包属性值的,如果你的属性值里有同款引号,整条表达式就断了。遇到这种情况,可以试试用双方括号配合concat(),不过这属于进阶,新手可以先把单双引号统一成一种。

第三,节点是不是在iframe里?这是一个极其隐蔽的坑。有些页面会把内容塞进<iframe>子框架里,浏览器里看起来是正常内容,但requests拿到的主HTML里只有一个<iframe src="...">标签,真正的数据在src指向的另一个页面里。这种情况你需要先拿到iframe的src,再发起一次请求去解析子页面。XPath和Parsel处理不了"跨文档"的内容,这是结构限制,不是你的代码问题。

第四,你的text()用对了没有?text()匹配的是直接的文本节点。如果你要提取的文本被<span>、<b>之类内联标签分割了,text()只能拿到一段段碎片,get()拿到的是第一段碎片,看起来像是"丢了内容"。这时候建议改用string(.)或者normalize-space(.)来提取当前节点的全部字符串内容。

# 提取div下的全部文本(包括子标签里的),并压缩空白 full_text = item.xpath('string(./div[@class="content"])').get().strip()

注意哈,string(.)返回的是一个字符串,不是一个节点列表。想拿全部文本又保持段落结构,这个方法是比较省力的。

5.2 动态加载内容:XPath的边界得承认

这是新手最容易产生"我的代码坏了"错觉的地方。很多异步渲染的页面,比如商品评论、瀑布流列表、微博时间线,requests拿到的HTML里只有一坨初始化代码和空壳容器,内容全是JS跑完后才塞进去的。你用XPath去解析静态HTML,当然什么都拿不到——不是XPath不好用,是"货"还没上架。

遇到这类页面,你首先想到的应该是"数据在不在接口里"。打开浏览器开发者工具的Network面板,刷新页面,找到XHR请求,看看有没有返回JSON或HTML片段的数据接口。如果有,直接请求那个接口,用requests去拿JSON,比用Selenium/Playwright渲染整个页面轻量得多。这是最优雅的方案,可惜很多新手不知道。

如果确实找不到接口,或者页面逻辑太复杂没法模拟,那就只能上浏览器渲染方案(比如Selenium或Playwright),等页面加载完成后再通过page.content()把渲染后的HTML拿出来,然后再交给Parsel去解析。要注意的是,此时的HTML是浏览器渲染后的快照,里面会有很多JS注入的属性,XPath匹配时相对更宽松,一般用//div[@class="comment-item"]这种抓特征的写法依然有效。

我特意把动态加载这个坑放在这里,是因为它和XPath本身的坑性质完全不同——前者是"数据源问题",后者是"定位语法问题"。排查问题的第一步永远是定位问题层级,先确认数据在不在,再考虑解析方式。

5.3 解析结果里全是\n和空格?用normalize-space做兜底清洗

这一节叫"解析与清洗",解析完之后,清洗往往是个被低估的环节。你用text()拿到的字符串,常常附带缩进、换行、连续空格。比如上面<p class="summary">里的文本,如果在HTML里换行了,提取出来就会变成\n Python 3.13版本在性能上有显著提升……\n。

手动strip()只能去掉首尾,中间的换行和多余空格它管不了。这里有一个非常推荐的XPath函数:normalize-space()。它会做三件事:去掉首尾空白、把连续多个空白字符压缩成一个空格、把换行符变成普通空格。

# 一个表达式同时完成文本定位和清洗 summary = item.xpath('normalize-space(./p[@class="summary"])').get()

normalize-space()接收一个节点作为参数,返回清洗后的字符串。这样你从XPath拿到的就是干干净净的句子,不需要再回Python里做二次re.sub(r'\s+', ' ', text)操作。我在写爬虫时,凡是提取类似段落文本、摘要、描述这类内容,都会优先用normalize-space(),它把"提取"和"清洗"一步到位。

那是不是所有字段都用normalize-space()更好?也不是。比如你要提取的关键词列表本身就是用空格分隔的,或者你要保留内容的换行段落结构,那就不适合用这个函数。清洗方案要跟着业务需求走,不是一套逻辑打天下。

5.4 调试XPath的习惯:学会在浏览器Console里用$x()初筛

最后分享一个调试层面的提速技巧。在Chrome的Console里,你可以直接输入$x()来测试XPath表达式,不需要写任何Python代码。

比如你想确认//article[@class="news-item"]/h2[@class="news-title"]/a/text()能不能匹配到标题,打开Console输入:

$x('//article[@class="news-item"]/h2[@class="news-title"]/a/text()')

回车之后,浏览器会列出所有匹配的节点文本。不行就改,改到满意为止。这个方法的优势是零成本迭代——你不用每次改完都跑一遍Python脚本,而且浏览器会基于真实的DOM树来判断,结果非常可靠。等表达式在Console里验证OK了,再粘回Parsel代码里,命中率会高很多。

还有一个我常用的技巧:如果XPath里含有双引号,在Console里就别用双引号包整个表达式了,省得转义。你可以写成:

$x("//article[@class='news-item']/h2[@class='news-title']/a/text()")

属性值用单引号,外层用双引号,一眼就能看清,不会乱。这个小习惯在写复杂XPath的时候特别管用。


实际写解析代码这么多年,我最大的体会是:**XPath是一个"一次学会,终身受用"的技能,但前提是你别把它当成一套死语法去背,而是当成一种"描述位置"的思维方式。**而且Parsel和XPath的组合不只在requests这个小场景里有用,你以后写Scrapy爬虫、处理XML配置、甚至解析SVG向量数据,这套技能全部都能复用。这一节能把"先分组定位再逐字段提取"的思维练熟,后续接触复杂的嵌套页面时会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询