XPath与Parsel实战:告别正则,打造稳定HTML解析方案

发布时间:2026/10/12 8:56:40
XPath与Parsel实战:告别正则,打造稳定HTML解析方案 解析HTML这件事在你刚开始写爬虫的时候可能觉得特简单拿到网页源码正则一瓢数据就出来了。但等你真正大量跑数据、频繁换网站、或者隔段时间回头维护老脚本时会发现正则方案又脆又难改。这就是我在整个爬虫入门系列里把解析这块单独拿出来讲的原因也是本章想帮你解决的核心问题如何用XPath和Parsel写出一套更稳定、更抗网页改版、且对新手友好的解析方式。做爬虫和写业务代码不一样同一页面今天能跑、明天就能挂最常见的原因不是反爬而是页面结构变了。正则靠的是字符串模式匹配页面动一个小标签就可能废掉整条规则而XPath基于HTML文档树形结构定位哪怕改动一两个属性通常也只是小修。这篇内容会带你从“为什么要换解析思路”开始把XPath的核心语法拆透再把Parsel这个封装好的解析库讲明白最后用一段真实可跑的代码走通整个流程。没有正则基础完全没关系只要你能看懂前端页面的结构哪怕第一次接触XPath也能跟下来。1. 为什么要换掉正则XPath解析的思想转变1.1 正则解析的三大痛点正则表达式确实是个好工具尤其在处理纯文本、日志、字符串匹配的时候它又快又精准。但放到HTML解析场景里它的短板会随着页面复杂度无限放大。第一个痛点是可读性差。一个稍微复杂点的匹配规则比如要提取一个列表页里所有链接的URL、标题、时间正则写完几乎是一串“天书”稍微隔几天自己都难读懂。更别提后期维护新同事看到这堆规则基本无从下手。第二个痛点是脆弱。HTML标签嵌套、属性顺序变化、不规范的自闭合标签、JS动态注入的节点……任何一个细微变动都可能导致正则匹配不到预期内容而且不容易排查到底哪里断了。正则默认匹配是面向“字符串形态”的它不关心这段字符串在文档结构里是什么位置所以你没办法说“我只取这个区块里的链接”——除非你把“这个区块”也全部写进正则那规则会膨胀到失控。第三个痛点是数据清洗成本高。正则拿到的是带各种空白符、换行、标签残留的裸字符串你得再叠加一组清理逻辑。做一次两次还可以做成百上千个规则的时候你会浪费大量时间在“擦屁股”上。那有没有一种工具是直接帮我们“按结构取内容”的有就是XPath。它把HTML当成一棵树你要做的就是告诉它我要找什么类型的节点、这个节点有什么特征、路径怎么走。思路一旦转过来后面所有东西都会顺很多。1.2 XPath是怎么“框”住数据的XPathXML Path Language本身是一门查询语言它的设计目标是为了在XML文档中定位节点。HTML虽然不是严格的XML但结构上和XML一样都是标签嵌套的树状结构所以XPath完全可以作用于HTML。这个“树”的概念特别重要。你看一个html页面最外层是html里面分head和bodybody里有很多div每个div里又有标题、段落、链接……这些元素天然形成父子、兄弟、祖先和后代关系。XPath做的事情就是沿着这棵树的枝干去走用路径表达式告诉程序从哪个节点出发经过什么关系最终落在哪个节点上。你可以把它理解成去一个大型仓库里找货仓库有A区、B区A区有货架1到20货架上有不同层每层有不同货位。你可以不关心货物长什么样只要按路径走过去就能精准拿到对应位置的货物。XPath里面的/html/body/div[2]/a就是一个类似的“仓库定位路线”从根节点出发一层一层往下走经过第2个div最终找到a标签。理解了这层思想你就明白为什么它稳定了只要树的主要骨架没变哪怕某个节点内部多了一堆东西、属性值顺序变了定位路线依然有效。而且XPath也允许你“按条件找”比如“所有class包含‘title’的节点”这样就算页面属性有小调整规则也不会立刻失效。1.3 XPath和CSS选择器的关系你可能听说过前端里更常用的是CSS选择器比如div.title a或者#content p。CSS选择器和XPath在功能上有重叠很多解析库两种都支持。如果你熟悉CSS选择器上手XPath不难因为逻辑是共通的只是语法不同。在我的实际使用里两者各有优势。CSS选择器写起来简短适合结构稳定的页面XPath表达能力更强特别是需要按文本内容定位、需要用到轴比如取父节点、前一个兄弟节点时CSS选择器会比较吃力。Parsel这个库两者都支持我的习惯是优先用XPath因为它更统一遇到复杂页面也能应付不会出现CSS表达式写不了只能混用的情况。如果你以前只写过正则那完全不用慌。XPath的语法量很小核心就几个表达式配合实践很快就能形成肌肉记忆。下面这一节就带你过一遍最常用的基础。2. 先把XPath语法啃明白节点、路径、谓词2.1 HTML文档就是一棵树要让XPath用得好你得先学会看DOM树。打开浏览器F12Elements面板里那一层一层的缩进结构就是树的直观体现。每个标签是一个节点标签里的文本是文本节点属性是属性节点。XPath能操作的节点类型大概就这几种元素节点、属性节点、文本节点以及少用的命名空间节点。我建议新手在写XPath之前先养一个习惯在开发者工具里先展开目标元素的父级、兄弟级搞清楚它在树里的实际位置。很多新手一上来就写//div这样确实能匹配到所有div但你要的东西通常不在第一个div里结果写了一长串却拿不到数据原因就是对树结构不熟。看树结构时重点关注三样东西标签名、关键属性一般看class或id、文本位置。有了这三个坐标XPath基本就能定了。一份典型的HTML结构大概是这样的div classnews-list div classnews-item h3a href/news/1001.html标题一/a/h3 span classdate2024-01-01/span /div div classnews-item h3a href/news/1002.html标题二/a/h3 span classdate2024-01-02/span /div /div在这段结构里news-list是父容器每个news-item是一条新闻。我们要提取每条新闻的链接和标题就要学会用XPath来描述“在class为news-item的div里找h3下的a标签”。2.2 XPath常用表达式速查表我整理了一个新手最该记熟的速查表你写代码时可以先贴这份出来照着用用多了自然就记住了。表达式含义示例/从根节点直接选取/html/body/div//从任意位置选取忽略层级//div/a.当前节点./a..父节点//a/..选取属性//a/hreftext()选取文本//h3/text()*通配符匹配任意元素//div/*[n]谓词按顺序筛选//div[2][classx]按属性值筛选//div[classitem][contains(text(),关键词)]按文本内容模糊匹配//p[contains(text(),Python)][contains(class,item)]按属性模糊匹配//div[contains(class,item)]这里面的核心规则我展开说一下//开头的叫相对路径起点意思是“不管在哪一层从任意位置向下找”。写爬虫时绝大多数表达式都用//开头因为绝对路径/html/body/div/...太长而且页面只要在外面包一层就失效了。用来取属性值比如//a/href取到的是一堆URL字符串。这是最常用的需求务必记牢。[n]的索引从1开始不是0。这个坑很多新手踩过//div[1]选的是第一个div不是第0个。contains()是救命函数因为网页上很多class带多个值比如classtitle red big你用[classtitle]反而匹配不到改成[contains(class,title)]才稳妥。2.3 谓词与轴定位的进阶玩法谓词就是中括号里的筛选条件上面提到的是最基础的用法。稍微进阶一点的是组合谓词比如同时限定标签、属性和位置//div[contains(class,item)][2]这个表达式的意思是“在class包含item的div里找第二个”。注意[2]是先用前面的条件筛出一组节点再取这一组里的第二个。轴是XPath里定位关系比较丰富的一部分但实际爬虫中用得不算太多我挑两个实用的讲解following-sibling取当前节点后面的兄弟节点。典型场景是某个标题后面跟着日期但日期没有独立class你可以用//h3/following-sibling::span拿到它后面的span。ancestor取所有祖先节点。典型场景是你找到了一个链接想回退到它所在的整个容器拿其他数据就可以用//a[contains(href,news)]/ancestor::div[contains(class,item)]。轴的完整列表还有child、parent、preceding、descendant等但说实话日常用到最多的就是前面的兄弟和祖先这一个来回。懂这两个配合谓词绝大多数页面都能覆盖了。3. Parsel让XPath更好用的“翻译官”3.1 为什么选Parsel而不是纯lxml如果你在搜索引擎里搜XPath解析会看到两种主流方案一种直接用lxml库一种用Parsel。lxml确实底层强大但它比较“裸”——你拿到HTML后得自己html.fromstring()然后到处tree.xpath()每一步都要手动管理。而对新手来说Parsel的优势在于它对XPath和CSS选择器做了统一封装接口简洁返回对象又自带继续筛选的能力用起来顺手得多。另外一个很现实的理由是Parsel的底层本身就是lxml所以性能上并不吃亏它只是在上面包了一层更友好的API。你在爬虫里用Parsel等于既享受了lxml的速度又不用跟繁琐的底层操作较劲。我在生产脚本里也是更喜欢用Parsel尤其是要连续在同一个页面上做多轮解析时返回的Selector对象可以直接再调.xpath()可以非常自然地“链式取数”。安装也很简单pip install parsel安装好后核心的导入和初始化程序如下from parsel import Selector html htmlbodyh1你好爬虫/h1/body/html sel Selector(texthtml) print(sel.xpath(//h1/text()).get())这一步做完你就拿到了一个可用的解析对象。下面看一下它到底能做哪些操作。3.2 Selector的基本操作Selector对象有三个高频方法新手一定要分清楚.xpath()用XPath语法提取.css()用CSS选择器语法提取.re()用正则表达式对已提取的内容进一步处理.xpath()返回的仍然是一个Selector列表所以你可以继续对它调.xpath()这就是链式操作的基础。什么时候会用到链式假设列表页里每条新闻是一个div我先取所有div再在div内部继续取a链接和标题这样逻辑清晰代码也好维护。items sel.xpath(//div[contains(class,news-item)]) for item in items: title item.xpath(.//h3/a/text()).get() link item.xpath(.//h3/a/href).get() print(title, link)这里有一个非常关键的小细节在循环内部再次使用xpath时路径开头要加上.。.//h3/a/text()表示“从当前节点往下找h3里的a”如果不加这个点它会从整个文档根节点重新查找很可能查到的不是当前这一条数据的内容。这个坑几乎每个新手都会踩后面问题排查部分我会再强调。.re()方法适合在已经拿到URL或者文本之后再做一层精细提取。比如链接里带了参数你只想要其中的id就可以这样item.xpath(.//h3/a/href).re(r(\d))它会返回所有匹配到的数字。用.re()做二次加工能让解析规则更短不用专门写一个清理函数。3.3 get()、getall()、extract()到底啥区别这部分是Parsel新手最容易混淆的。先看传统的extract()和extract_first()它们返回的是字符串列表或单个字符串但Parsel官方现在已经不太推荐这种写法了更推荐用get()和getall()。三者的对应关系如下方法作用替换旧写法.get()取第一个匹配到的字符串没有则返回Noneextract_first().getall()取所有匹配到的字符串返回列表extract().extract()取所有匹配到的字符串旧写法不推荐它们在日常写代码里的区别体现在两个方面。第一是空值时get()拿不到内容时返回None而extract_first()在旧版本里可能报错或返回None写法上get()更安全。第二是返回类型getall()一定返回列表哪怕只有一个结果也是列表这让你后续的循环处理更统一。我给你一个写代码的默认规范可以少踩很多坑# 推荐 title item.xpath(.//h3/a/text()).get() links item.xpath(//a/href).getall() # 不推荐 title item.xpath(.//h3/a/text()).extract_first() links item.xpath(//a/href).extract()另外从SelectorList对象里取元素还有一个[0]的方式比如sel.xpath(//title/text())[0]。这种方式在你明确结果只有一个时可用但它不会做边界处理一旦xpath没匹配到就会报IndexError。我用get()就是为了让脚本在页面结构有小变动时不至于崩掉最多返回None方便下一步做空值判断。4. 实操用XPath/Parsel写一个新闻列表爬虫4.1 先看目标定义任务和解析需求纸上谈兵没有用我们直接跑一个贴近生产的例子。假设要抓取一个新闻列表页目标数据是每条新闻的标题、链接和发布日期。我准备一段示例HTML来模拟这个页面结构然后把XPath一步一步写出来。div classcontainer h2今日要闻/h2 ul classlist li classitem a href/news/101.html classtitle-linkPython 3.12发布/a span classtime2024-02-01/span /li li classitem a href/news/102.html classtitle-link爬虫框架对比/a span classtime2024-02-02/span /li li classitem a href/news/103.html classtitle-linkParsel使用技巧/a span classtime2024-02-03/span /li /ul /div在这个页面里每条新闻都包在li.item里标题是a.title-link的文本链接是它的href属性时间是span.time的文本。解析需求很清晰接下来就按照“先定位容器、再取具体字段”的顺序来设计XPath。4.2 三步写出稳定XPath第一步定位外层容器。很多页面外层容器没有语义化可能一个div套一个div所以我会先找有没有含关键词的class。这里容器是ul.list那第一步的XPath就是//ul[contains(class,list)]或者//ul[classlist]。用contains是考虑到这个class可能还带了别的值稳妥一点。第二步定位每条数据的父级。继续往下一层每条新闻在li.item里所以在容器内部找li[contains(class,item)]。如果要写成一个表达式那就是//ul[contains(class,list)]/li[contains(class,item)]。第三步分别提取字段。在父级内部标题是./a[contains(class,title-link)]/text()链接是./a[contains(class,title-link)]/href时间是./span[contains(class,time)]/text()。把这三步组合起来就跑出完整的取数逻辑了。你可以看到整个过程是有迹可循的容器 → 数据行 → 字段每层之间的关系都是明确的后期维护时只要对照页面结构很快就能定位哪里需要改。4.3 完整代码和效果演示下面给出一个可以直接跑的脚本。这里我用了parsel的Selector然后用requests库获取真实页面时会加上headers但演示时直接用这段HTML字符串避免请求失败干扰解析学习import requests from parsel import Selector # 模拟实际请求时的headers headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } # 实际项目里这里用 requests.get(url, headersheaders).text html div classcontainer h2今日要闻/h2 ul classlist li classitem a href/news/101.html classtitle-linkPython 3.12发布/a span classtime2024-02-01/span /li li classitem a href/news/102.html classtitle-link爬虫框架对比/a span classtime2024-02-02/span /li li classitem a href/news/103.html classtitle-linkParsel使用技巧/a span classtime2024-02-03/span /li /ul /div selector Selector(texthtml) items selector.xpath(//ul[contains(class,list)]/li[contains(class,item)]) for item in items: title item.xpath(./a[contains(class,title-link)]/text()).get() link item.xpath(./a[contains(class,title-link)]/href).get() publish_time item.xpath(./span[contains(class,time)]/text()).get() print(f标题: {title} | 链接: {link} | 日期: {publish_time})运行后你会在终端看到三条清晰的数据记录标题: Python 3.12发布 | 链接: /news/101.html | 日期: 2024-02-01 标题: 爬虫框架对比 | 链接: /news/102.html | 日期: 2024-02-02 标题: Parsel使用技巧 | 链接: /news/103.html | 日期: 2024-02-03到这里一个小而全的解析流程就跑通了。实际使用中你只需要把Selector(texthtml)换成Selector(textrequests.get(url, headersheaders).text)就可以应对动态请求到的页面。但要注意一点如果页面内容是JavaScript动态渲染的requests拿到的HTML里可能并没有这些节点这时候你需要先确认页面数据是否直接写在源码中或者另用浏览器自动化方案这属于爬虫进阶范畴这里先不提。5. 常见问题与排查技巧5.1 XPath明明没错但抓不到数据这是我被问得最多的问题。表达式在浏览器Console里测试能选中但到了Python里用Parsel却拿不到结果。排查思路按顺序来第一步打印你的响应内容确认拿到的确实是你想解析的HTML。有可能是你请求到了一个重定向页面、登录页、或者反爬验证页。你要解析的节点根本不在这个响应里那XPath写得再好也没用。第二步检查你用的是不是Selector对象而不是字符串。如果你直接对字符串调.xpath()是会报AttributeError的。确认你已经初始化了Selector(texthtml)。第三步注意text()的写法。很多人写//a/text()去取链接文本结果返回空原因可能是标签内部不只一层比如aspan标题/span/a这时/text()只能拿到直接子文本节点拿不到span里面的内容。解决办法是改用//a//text()双斜杠表示取所有后代文本节点或者直接//a/string()后者是XPath 2.0的特性Parsel也能用会返回当前节点内拼接后的全部文本。第四步查看有没有命名空间或者HTML解析异常。这种情况比较少但如果你直接解析XML格式的页面可能需要在Selector里指定typexml。HTML页面一般不需要。5.2 网页改版后瞬间失效爬虫脚本维护过程中最烦的不是反爬而是页面结构改版。假设原来新闻标题在h3 a后来变成了div.title a你的XPath就失效了。这不是bug而是页面变了解析规则必须跟着变。为了减少这类问题带来的维护成本我的实践有两条。第一XPath尽量用语义和属性定位不用绝对层级。比如优先//a[contains(class,title)]而不要写/html/body/div[3]/div[1]/h3/a。前者是“我叫什么、我有哪些特征”后者是“我住在哪条街第几个路口”。页面改版时特征通常变化不大纯路径则往往夭折。第二给解析写一个独立的配置函数。比如把每条数据的XPath集中放在一个字典里改版时只动配置不动主逻辑。我自己的项目里通常是这样的结构xpath_config { item: //ul[contains(class,list)]/li[contains(class,item)], title: ./a[contains(class,title-link)]/text(), link: ./a[contains(class,title-link)]/href, time: ./span[contains(class,time)]/text(), }这样维护起来非常省事页面小改动时改一行配置就行不会因为一个XPath变动导致整个脚本重写。5.3 数据带空白和换行的清理网页里的文本节点经常会夹杂着换行、缩进空格和HTML实体比如nbsp;。你提取出来的标题可能是\n Python 3.12发布 \n用print看不出来但入库或存文件时会很脏。Parsel本身不负责清洗这里我的经验是给提取后的字符串统一做一步标准化处理而不是逐个改。我会写一个小函数def clean_text(value): if not value: return return .join(value.split())value.split()会按任意空白字符切分再重新用单个空格拼接这样能把换行、缩进、多空格一次性清理干净。日期字段还可以进一步用strip()去首尾空白。如果你的提取结果用了.getall()返回列表那可以对列表里的每个元素map一下这个函数。这样做的好处是逻辑统一不管哪个字段需要清洗都走同一套规则不会出现“标题清了时间没清”的遗漏。5.4 如何提升解析性能爬虫如果跑的是一个小脚本解析性能无所谓但当你的请求频率高、页面大、字段多时解析速度会成为瓶颈。Parsel底层是lxml性能本身就够用但我在代码层面还有几个优化习惯。其一尽量减少重复解析。如果你要对同一个页面的多个字段做提取不要反复Selector(texthtml)初始化初始化一次就够了之后在同一个Selector上反复调用.xpath()。我见过有人每取一个字段就重新请求一次页面这是极大的浪费。其二用getall()批量提取减少循环。假如只需要链接列表不要写一个循环然后逐个get()直接在初始Selector上xpath(//a/href).getall()一次性拿到列表速度快得多。循环交给后续的数据处理解析阶段尽量“一步到位”。其三避免在XPath里写过于宽泛的通配符。//div//a//text()这种表达式会扫描大量节点如果页面几千个节点数量级还好但几万个节点时影响就比较明显了。尽量缩小范围先定位到容器再在容器内做提取。这些优化看起来只是小改动但在跑大规模数据采集时会显著降低整体耗时和CPU占用。6. 一些经验和心得这一节算是我个人在写大量爬虫脚本之后沉淀下来的几条体会不一定在教材里能看到但对新手挺有帮助。第一不要盲信浏览器复制的XPath。F12里右键复制XPath很方便那段路径通常是以/html/body/...开头的绝对路径又长又脆。它可以用在调试上但别直接扔进生产代码。我会拿它当作定位参考然后自己改写成一个短小、带语义特征、用//开头的表达式。改写的过程也是加深对结构理解的过程。第二解析之前先看robots和网站条款。做技术归做技术但爬虫不是只要代码能跑就行。我在实际做采集项目时一般都会先确认目标网站是否允许爬取注意请求频率。这个习惯会帮你避开很多麻烦。第三调试XPath时用一个临时脚本或在线工具。我常用的是把一段HTML保存成文件然后在本地反复跑Selector测试改表达式刷新结果比每次都发请求要快很多。Parsel也支持直接对字符串操作这正好方便本地调试。等到表达式稳定了再集成进正式脚本。第四始终给自己留后路。解析结果里总会存在脏数据、空值、异常值我在写代码时习惯在每个字段提取后做一个if not title: continue之类的保护判断保证单条数据出问题时不影响整个任务。这个习惯在批量采集里非常重要否则一条脏数据可能导致整个任务中断。第五解析和存储解耦。这一章节虽然只讲解析但好的爬虫设计里解析模块的输出应该是干净、结构化的列表或字典与后续的数据库写入、CSV导出完全解耦。我一般会在解析完成后先打印或保存几行结果看看确认无误再进入存储环节。先肉眼验证再落库能省下后期大量清洗工作。结尾碎碎念折腾完这一章你应该能感受到XPath和正则之间完全不同的思维方式了。正则是在字符串里“找形状”而XPath是在树里“找位置”这种结构化思维一旦建立起来再看复杂页面就不会发怵了。我在自己带新人时总会说一句话解析代码写得稳不稳不是看语法记了多少而是看遇到页面变动时能不能快速定位修改点。把XPath的常用语法练熟配合Parsel的简单接口你日常90%以上的解析需求都可以轻松覆盖。最后再分享一个小习惯每次写完一个解析脚本我都会顺手把页面的结构截图或者Markdown注释放在代码头部标注好关键节点的位置和当时的XPath。这样哪怕过了半年再回来看这个脚本也能一眼知道当初是怎么设计解析规则的。爬虫这条路前期拼的是会用工具中期拼的是设计思路后期拼的是维护能力。XPath和Parsel只是起点但打好这部分基础后面学Scrapy、做分布式采集都会轻松很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询