尧图精选

爬虫解析HTML:正则表达式与XPath实战指南

🕒 发布时间:2026/10/2 15:29:48 📁 来源:尧图网络
之前有朋友问我爬虫拿到HTML之后怎么把里面的标题、链接、价格一行一行抠出来我第一反应就是你还没吃透正则表达式和XPath。这两个工具是解析网页最基础、也最实用的手段。很多人一开始觉得“正则表达式很难”“XPath是不是要学很多语法”其实把它们放到爬虫场景里拆开看就是一招一式的事。这篇文章就从实际解析网页的需求出发把正则表达式和XPath的简单用法、选型思路、实操代码和排查技巧一次讲清楚适合刚入门爬虫的朋友也适合写了好几个月代码但还是靠复制粘贴提取数据的人。1. 爬虫解析网页的整体思路与工具选型1.1 从HTTP请求到数据提取爬虫的三个环节爬虫看起来高大上本质上就是一条流水线先把网页拿回来再把里面有用的字段挑出来最后存成表格或数据库。按我的习惯整条链路可以拆成三个环节请求、解析、存储。请求阶段用requests或httpx把HTML文本抓下来解析阶段就是从这段HTML文本里找到我们需要的标题、链接、图片地址、价格这类信息存储阶段则是把解析结果写进CSV、Excel或数据库里。很多人把精力全放在“怎么模拟请求、怎么处理Cookie”上结果拿到HTML后一脸懵对着几万行标签不知道从哪里下手。说白了解析才是决定爬虫能不能用的核心环节。因为无论你请求做得多漂亮只要字段提取不准后续所有工作全白做。而正则表达式和XPath就是解析阶段最趁手的两把工具。这里还要提醒一点解析前一定要先搞清楚拿到的是什么。如果是Python的requests库发请求通常用resp.text拿字符串或者用resp.content拿字节流。HTML、XML这种结构化文档更适合用XPath而散落在JavaScript变量、JSON字符串、日志文本里的数据用正则表达式更直接。1.2 正则表达式和XPath的选型逻辑好多新手会纠结“到底学正则还是学XPath”其实这俩不是竞争关系而是互补关系。正则表达式是基于文本模式匹配的它对内容本身“长什么样”敏感不关心内容在哪一层标签里。比如你从一段JavaScript代码里提取name:张三这样的结构或者从纯文本日志里匹配某个日期格式用正则几乎是最优解。XPath则完全不同它面向HTML/XML的树形结构通过节点路径来定位元素。你可以理解成正则是在一堆乱麻里找特定花纹的线XPath是你已经知道线在哪根经纬线上直接顺着路径取出来。因此只要网页结构是常规的HTML用XPath会稳定很多因为你不必关心标签之间的空格、换行、属性顺序等细节结构没变就能提取成功。我个人的选型逻辑很简单能用XPath解决的结构化页面就优先用XPath只有遇到纯文本、字符串片段、动态拼接的数据才会把正则拿出来。两者配合起来基本能覆盖日常爬虫90%以上的解析需求。2. 正则表达式解析网页的核心用法2.1 正则基础语法元字符、量词与分组正则表达式看着晦涩实际核心语法就那么几类。先记住四个最常用的概念元字符、量词、字符类、分组。元字符是正则的基本符号比如.表示任意字符\d表示数字\w表示字母、数字、下划线\s表示空白字符。量词控制出现次数*表示0次或多次表示1次或多次?表示0次或1次。比如\d就能匹配连续的数字串提取价格、ID这类数据特别好用。分组用圆括号()表示它能把匹配到的部分单独捕获出来。比如a href(.*?)(.*?)/a这个表达式第一组(.*?)捕获链接地址第二组(.*?)捕获链接文本。这里还有一个非常关键的技巧.*?是“非贪婪匹配”它会尽量短地匹配内容。如果写成.*默认是贪婪的会一直匹配到最后一个符合条件的结束标签很容易把整段HTML吞掉。我见到的正则解析翻车案例十有八九都是忘了用非贪婪模式。2.2 用Python的re模块提取网页数据Python里做正则解析主要用内置的re模块不需要装第三方库。最常用的三个函数是re.findall、re.search和re.sub。re.findall(pattern, text)返回所有匹配结果适合批量提取。re.search(pattern, text)在字符串里查找第一个匹配结果返回Match对象适合提取单个字段。re.sub(pattern, repl, text)替换匹配到的内容可以用来清洗HTML标签。举个例子假设HTML片段如下div classcontent a href/article/101爬虫入门指南/a a href/article/102正则表达式速查/a /div用正则提取所有链接和标题import re html div classcontent a href/article/101爬虫入门指南/a a href/article/102正则表达式速查/a /div pattern ra href(.*?)(.*?)/a results re.findall(pattern, html, re.S) print(results)运行结果是一个列表每个元素是元组(链接, 标题)[(/article/101, 爬虫入门指南), (/article/102, 正则表达式速查)]注意我在re.findall中加了第三个参数re.S这个参数让.可以匹配换行。如果不加一旦a标签之间出现换行正则就会匹配失败。很多新手在这个地方卡半天明明表达式看起来没问题就是提取不到内容其实就是换行搞的鬼。2.3 正则解析的注意事项与常见误区用正则解析网页最怕的是“看似匹配了实际匹配错了”。这里有三个我踩过的坑提前帮你排掉。第一个坑是HTML标签属性顺序不固定。比如有的页面是a href... classbtn有的页面是a classbtn href...。如果你写死a href.*?第二种情况就匹配不到了。这是我强烈推荐XPath的原因之一标签属性顺序变化在XPath里根本不影响。第二个坑是转义字符。正则里(、)、[、]、.、*这些符号都有特殊含义要匹配它们本身必须加反斜杠。比如你要匹配价格中的小数点\d\.\d里的那个点前面必须加\。写正则时随手把原字符串里的特殊符号都过一遍能省不少调试时间。第三个坑是HTML实体。网页里的nbsp;、amp;、lt;这类字符正则匹配时经常对不上。比如一个标题里显示的是“A B”HTML源码却是A amp; B。此时你可以先对字符串做一次HTML反转义或者干脆在后续清洗时用re.sub把amp;替换成。这些细节不处理提取出来的数据总有奇怪字符存进Excel里特别显眼。3. XPath解析网页的核心用法3.1 XPath基础路径表达式、谓语与轴XPath有点像文件路径只是把文件夹换成了HTML标签。比如/html/body/div表示从根节点一层层往下找//div则表示“不考虑层级直接在整个文档里找所有div节点”。日常解析中用得最多的就是//开头它灵活又简洁不必关心元素嵌得有多深。除了路径谓语[...]是XPath里最强大的筛选功能。它可以按索引、属性、文本内容来筛选节点。举个例子//a[classtitle]/href这段表达式的意思是找到所有a标签并且class属性值等于title然后取这些标签的href属性值。如果要按文本内容筛选可以写//a[contains(text(),爬虫)]意思是“链接文本里包含‘爬虫’两个字的a标签”。XPath轴是进阶用法比如following-sibling可以取当前节点后面的兄弟节点parent可以取父节点。我平时用得不算多但如果遇到“根据某个标签找相邻数据”的需求轴表达式能优雅解决。先掌握//、、[]、text()这四个就足够应付常见解析任务了。3.2 用lxml结合XPath解析HTMLPython里用XPath最佳搭档是lxml库。它底层是C语言实现解析速度快对HTML的容错能力也很强。安装命令很简单pip install lxml基本用法分两步先把HTML字符串传入etree.HTML()得到一个Element对象然后在这个对象上调用xpath()方法。下面是一个完整示例from lxml import etree html div classcontent a href/article/101爬虫入门指南/a a href/article/102正则表达式速查/a /div tree etree.HTML(html) links tree.xpath(//a[classcontent]/href)这里//a/href表示获取所有a标签的href属性返回值是一个列表。如果要同时拿链接和文本可以用索引配合items tree.xpath(//a[classcontent]) for item in items: href item.xpath(./href)[0] text item.xpath(./text())[0] print(href, text)这里./表示相对当前节点继续查找是XPath里非常实用的写法。还有一个小细节如果某天你发现xpath取不到数据先用print(etree.tostring(tree, encodingunicode))看看实际解析成的HTML是什么样的。因为很多网页源码虽然写在HTML文件里但浏览器解析后会补全标签、修正格式跟原始字符串并不完全一致。lxml会尽量按照浏览器的方式解析HTML你debug时打印出它眼中的结构往往能找到问题所在。3.3 浏览器XPath Helper的调试技巧光看代码有时候很难确定XPath表达式写对了没有这时候浏览器插件能救命。我常用的是XPath Helper这个浏览器插件就是Thomas de roo那个版本。安装后在目标页面按快捷键开启插件按住鼠标框选页面元素它会自动生成一个XPath你还能手动修改表达式并立刻看到匹配结果。这个插件最实用的场景是你想定位“网页上显示的第2条新闻链接”但又不想反复改代码刷新看结果。直接在浏览器里用XPath Helper验证确认表达式匹配到了预期元素再把表达式复制到Python代码里成功率会非常高。要注意的是浏览器插件生成的XPath往往很啰嗦类似/html/body/div[2]/div[1]/div[3]/a。这种绝对路径一旦页面结构微调就会失效。我的习惯是把插件生成的表达式当作参考然后手动简化为相对路径尽量用//和class、contains(text())这类语义化条件。比如从//*[idnews-list]/li[1]/a改成//div[idnews-list]//a既简洁又更能抗页面变动。4. 实操案例解析一个静态列表页并提取关键字段4.1 目标页面分析与请求准备纸上谈兵再多不如跑一个完整案例。为了让你能直接复现我用一段嵌入的HTML字符串来模拟一个静态列表页这段源码模拟了博客文章列表包含文章链接、标题和摘要。实际项目中你需要先用requests拿到页面内容再进入解析环节。import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(https://example.com/blog, headersheaders) resp.encoding resp.apparent_encoding html resp.text这里我手动设置了resp.encoding resp.apparent_encoding目的是让requests能够从网页内容里自动猜测编码避免中文乱码。实际开发中你最好先打印resp.encoding看看默认值再结合resp.headers.get(charset)判断。如果网页没有明确charsetapparent_encoding通常是最省事的兜底方案。为了演示稳定下面案例我直接把HTML字符串写成变量省去网络请求的不确定性html html body div classpost-list div classpost h2a href/posts/python-regex正则表达式入门/a/h2 p classsummary学会用正则提取网页数据/p /div div classpost h2a href/posts/xpath-basicsXPath基础教程/a/h2 p classsummary从HTML中定位节点/p /div div classpost h2a href/posts/spider-parse爬虫解析网页实战/a/h2 p classsummary正则与XPath的配合使用/p /div /div /body /html 目标很明确提取每篇文章的链接、标题、摘要。4.2 使用正则表达式提取数据先用正则表达式来解。观察HTML结构每篇文章都被包在div classpost里链接在a href...标题/a摘要在p classsummary.../p。最直觉的写法是分别匹配三个字段import re links re.findall(ra href(.*?)(.*?)/a, html, re.S) summaries re.findall(rp classsummary(.*?)/p, html, re.S) print(links) print(summaries)输出结果[(/posts/python-regex, 正则表达式入门), (/posts/xpath-basics, XPath基础教程), (/posts/spider-parse, 爬虫解析网页实战)] [学会用正则提取网页数据, 从HTML中定位节点, 正则与XPath的配合使用]这种写法简单直接但有个隐患如果页面里还有别的a标签或别的p classsummary匹配结果就会混入无关数据。更严谨的做法是先把每个div classpost作为一个整体切出来然后分别提取内部字段post_blocks re.findall(rdiv classpost(.*?)/div, html, re.S) for block in post_blocks: href re.search(ra href(.*?), block, re.S).group(1) title re.search(ra href.*?(.*?)/a, block, re.S).group(1) summary re.search(rp classsummary(.*?)/p, block, re.S).group(1) print(href, title, summary)这样虽然麻烦了一点但提取精准度更高。用re.search加上.group(1)提取第一个捕获组是正则解析网页的典型套路。4.3 使用XPath提取数据并与正则对比同样的任务用XPath代码会清爽很多。先加载HTML再写XPath表达式from lxml import etree tree etree.HTML(html) post_nodes tree.xpath(//div[classpost]) for node in post_nodes: href node.xpath(./h2/a/href)[0] title node.xpath(./h2/a/text())[0] summary node.xpath(./p[classsummary]/text())[0] print(href, title, summary)输出结果和正则版本完全一样但可以明显看出三个优势语义清晰。./h2/a/href直接表达“当前节点下h2里a标签的href”不用写一长串转义字符。不依赖属性顺序。哪怕a标签里先写class再写href表达式依然有效。容错性更好。HTML标签嵌套即使有多余空格、换行XPath依然能匹配正则的空白字符处理则要谨慎得多。如果页面结构稍微变化比如标题外层多了一个span正则写法很可能匹配不到但XPath可以改成./h2//text()用双斜杠忽略中间层。4.4 完整可运行代码与输出效果给你一个直接可运行的整合脚本既跑正则又跑XPath方便对照import re from lxml import etree html html body div classpost-list div classpost h2a href/posts/python-regex正则表达式入门/a/h2 p classsummary学会用正则提取网页数据/p /div div classpost h2a href/posts/xpath-basicsXPath基础教程/a/h2 p classsummary从HTML中定位节点/p /div div classpost h2a href/posts/spider-parse爬虫解析网页实战/a/h2 p classsummary正则与XPath的配合使用/p /div /div /body /html print( 正则表达式方案 ) post_blocks re.findall(rdiv classpost(.*?)/div, html, re.S) for block in post_blocks: href re.search(ra href(.*?), block, re.S).group(1) title re.search(ra href.*?(.*?)/a, block, re.S).group(1) summary re.search(rp classsummary(.*?)/p, block, re.S).group(1) print(f{title} | {href} | {summary}) print(\n XPath 方案 ) tree etree.HTML(html) post_nodes tree.xpath(//div[classpost]) for node in post_nodes: href node.xpath(./h2/a/href)[0] title node.xpath(./h2/a/text())[0] summary node.xpath(./p[classsummary]/text())[0] print(f{title} | {href} | {summary})输出效果如下 正则表达式方案 正则表达式入门 | /posts/python-regex | 学会用正则提取网页数据 XPath基础教程 | /posts/xpath-basics | 从HTML中定位节点 爬虫解析网页实战 | /posts/spider-parse | 正则与XPath的配合使用 XPath 方案 正则表达式入门 | /posts/python-regex | 学会用正则提取网页数据 XPath基础教程 | /posts/xpath-basics | 从HTML中定位节点 爬虫解析网页实战 | /posts/spider-parse | 正则与XPath的配合使用如果你用这段代码去抓真实页面只需要把html变量替换成requests.get(url).text然后根据目标页面的实际结构调整XPath表达式即可。5. 常见问题与排查技巧实录5.1 编码问题导致乱码爬虫解析网页时最常遇到的就是中文乱码。这个坑的根源在于requests库默认会用HTTP响应头里的charset猜测编码但很多网站并没有在响应头里明确编码或者写错了。比如页面实际是UTF-8响应头却写charsetISO-8859-1于是resp.text返回的就是一堆乱码。我的排查顺序很固定先打印resp.encoding再打印resp.apparent_encoding然后手动把resp.encoding设置为正确的编码。如果还不放心可以直接用resp.content.decode(utf-8, errorsignore)其中errorsignore可以跳过无法解码的字节避免抛异常。对于极少数编码混乱的页面可以尝试gb18030、big5等常见中文编码。解析前搞定编码后续正则和XPath才不会在乱码上做无用功。5.2 HTML结构不规范导致的提取失败真实网页常常会给你“惊喜”比如标签没闭合、属性值带了单双引号、li标签散落各处。用正则表达式处理这些脏结构很容易因为一个换行、一个多余空格就匹配失败。XPath虽然容错性好但也会遇到取不到值的情况。碰到这种情况第一件事不是改表达式而是先打印etree.tostring(tree, encodingunicode)看看lxml解析后的结构。很多时候原始HTML和解析后的DOM树并不一致lxml会自动修复部分标签嵌套错误。打印出来后你会发现原本缺失的/div被补上了或者多出了html、body骨架。这种“先看树再写表达式”的做法能帮你少走很多弯路。还有一种常见情况是动态内容。页面显示的数据其实不在HTML源码里而是通过Ajax请求渲染出来的。此时正则和XPath都不可能提取到你要么去抓接口返回的JSON要么用Selenium等工具模拟浏览器执行JavaScript后再解析。判断方式很简单在浏览器里右键查看页面源代码搜一下你要提取的字符串如果搜不到那基本就是动态加载的。5.3 动态内容与反爬策略的应对思路这里需要强调一个原则解析只是爬虫链条的一部分真正决定爬虫“活不长久”的往往不是解析而是请求策略。对于公开网站至少要做到三点设置合理的User-Agent、控制请求频率、避开明显的高峰时段。很多反爬机制针对的是“一秒请求十几次”的机器行为所以你在写爬虫时哪怕只是练习也要在循环里加上time.sleep(1)让请求间隔变得接近人工操作。更高级的反爬还包括登录校验、验证码、签名参数等这些内容水很深但我不建议初学者一上来就研究绕过手段。先拿无门槛的公开数据练手把正则和XPath用到滚瓜烂熟比什么都重要。如果你自己维护网站或者只写后端接口担心被爬虫刷流量核心思路其实是限流和校验。比如在Controller层对单位时间内的请求次数做限制对频繁访问的IP做临时封禁给接口增加签名参数等。这些属于Web防护的话题但反过来想理解了网站如何防护你也就更清楚爬虫该遵守什么边界。5.4 爬虫的合规边界与建议最后说一说合规问题这是每个爬虫学习者都绕不开的坎。我的建议很朴素只抓公开数据不抓需要登录才能看的隐私数据请求前看一眼目标网站的robots.txt抓数据后不要搞批量贩卖更不要拿来做骚扰、诈骗之类的事。在法律层面不同国家和地区对爬虫的态度并不完全相同但“未经授权大量抓取并商业使用他人数据”这件事在任何地方都是有风险的。对于个人学习和技术研究尽量使用公开的示例网站、测试页面或者网站明确允许爬取的板块。学技术没错但把技术用歪了技术和人都容易走到死胡同。具体到解析环节合规还意味着“提取后不要保留过多无关字段”。比如抓一个商品列表你只需要标题、价格、链接那就只提取这三个字段不要顺手把用户评价、卖家昵称、库存信息全部拉下来存起来。数据最小化既是技术习惯也是保护自己。最后再说点个人经验在我实际写了大量爬虫之后个人体会是正则表达式和XPath从来不是“二选一”的问题而是“该用谁就用谁”的问题。解析HTML结构稳定的页面我会毫不犹豫用XPath处理网页源代码里嵌着的JSON字符串或者从一堆乱糟糟的文本日志里捞数据我还是会老老实实写正则。另外选工具的时候别死磕一个。有时候同一个数据用正则三五行能搞定用XPath反而绕来绕去也有时候XPath一行表达式就能定位到的节点正则写半天还容易出错。最好的状态是两个都会然后根据场景自由切换。建议你从今天这个示例开始把同一份HTML分别用两种方式解析一遍体会它们各自的思考方式。练上几次你会发现解析网页这件事其实比想象中简单。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →