Python正则表达式爬虫实战:数据提取与性能优化 1. 正则与爬虫的搭档逻辑先搞清楚正则到底解决什么问题先聊个直接的结论爬虫的日常工作本质上就是“拿网页 → 提取数据 → 存下来”。而“提取数据”这一步正则表达式regular expression简称 regex 或 re是最老牌、最稳定、最好上手的武器之一。有人可能会问现在解析 HTML 不都用 BeautifulSoup、lxml、XPath 吗正则是不是过时了我的回答是正则不过时只是场景不同。BeautifulSoup 在处理结构化良好的 HTML 时确实舒服但遇到下面几种情况正则反而是最优解网页代码不规范标签缺胳膊少腿BeautifulSoup 解析出来一团乱你需要的不是 HTML 节点而是一段 JSON、一段 JS 变量、一个接口返回的纯文本目标数据藏在script标签里的某个 JavaScript 变量中比如var pageData {...}根本没有对应的 DOM 结构数据量太大想用最快的速度过滤页面内容正则因为底层是 C 实现的性能比纯 Python 循环高一大截。所以你可以把正则理解成一把“手术刀”而 BeautifulSoup 是一套“夹具”。手术刀不一定适合所有操作但精细活儿基本离不开它。这篇内容我就围绕“python正则、爬虫”这个话题把正则的语法、匹配思路、实际爬虫案例、性能优化和踩坑点完整过一遍。不管你之前有没有接触过正则只要跟着实操走完基本能独立写出“拿 URL → 抓网页 → 用正则提取目标字段”的完整脚本。2. 正则核心语法速成用最小成本掌握匹配的底层逻辑正则看起来像“火星文”一大串符号堆在一起但本质上它只有几个核心概念。我把它们按使用频率排个序你先把这几个搞清楚90% 的爬虫提取需求都能覆盖。2.1 字面匹配与元字符正则的最小组成单元正则匹配的最基本形式就是字面匹配。比如你要在字符串hello world里找world正则就写world就行直接匹配。但随着需求复杂你会发现“匹配任意数字”“匹配邮箱”“匹配中文”这类需求不能靠字面量写死这时候就要用到元字符。我把最常用的列成一张表你可以直接收着当速查卡元字符含义示例匹配结果.匹配任意字符除换行外h.that、hut、h2t都能中\d匹配任意数字等价于[0-9]\d\d123中能匹配出12、23\w匹配字母、数字、下划线\wabc_123整个都能匹配\s匹配空白字符空格、tab、换行name\sname 、name 等^匹配字符串开头^hellohello out there能中$匹配字符串结尾end$this is the end能中[]字符集合匹配其中任意一个[abc]cat中能匹配出a[^]反向字符集合匹配不在其中的字符[^abc]dog中能匹配出d、o、g提示实际爬虫中\d配手机号、\w配用户名、\s配空白这三个组合能解决至少一半的提取问题。2.2 量词与贪婪模式为什么正则有时候会“多匹配”量词控制的是前面的字符出现多少次。核心就三个*0次或多次、1次或多次、?0次或1次。另外还有花括号精确匹配{n}表示恰好 n 次{n,}表示至少 n 次{n,m}表示 n 到 m 次。量词用起来简单但真正的坑在“贪婪”上。我举个例子import re text div classprice199/divdiv classprice299/div result re.findall(rdiv classprice(.*)/div, text) print(result)你猜输出什么直觉上是[199, 299]但实际输出是[199/divdiv classprice299]。因为.*默认是贪婪模式它会尽量多地匹配字符直到最后一个/div为止而不是第一个。解决方式就是让量词“变懒”在量词后面加一个问号改成非贪婪模式也就是用.*?代替.*result re.findall(rdiv classprice(.*?)/div, text) print(result) # [199, 299]这个.*?在爬虫里太常用了基本成了提取 HTML 片段时的标配。2.3 分组与提取括号是正则的“捕获口袋”正则里的圆括号()有两个作用一是把多个字符打包成一个整体来应用量词比如(ab)匹配abab二是捕获即把匹配到的子内容单独提取出来。在爬虫场景里第二个作用最关键。刚才的例子里我已经用了div classprice(.*?)/divfindall返回的就是括号里捕获到的内容。如果有多个括号就返回多个元素的元组。来看一个稍微复杂的例子从一段文本中提取所有“城市温度”的数据import re text 北京: 25°C, 上海: 29°C, 广州: 31°C result re.findall(r([\u4e00-\u9fa5]): (\d)[°]C, text) print(result) # [(北京, 25), (上海, 29), (广州, 31)]这里[\u4e00-\u9fa5]是中文的 Unicode 范围表示至少一个字符。正则就拆成了两段捕获一段匹配中文城市名一段匹配数字温度findall自动把它们打包成元组。2.4 re 模块的核心 API别死记硬背会用这 4 个就够了Python 的re模块方法看着多但爬虫里高频使用的其实就 4 个方法作用返回类型适用场景re.search(pattern, string)扫描整个字符串返回第一个匹配匹配对象或 None判断字符串是否包含目标re.match(pattern, string)从字符串开头匹配匹配对象或 None校验开头格式re.findall(pattern, string)返回所有非重叠匹配列表批量提取数据re.sub(pattern, repl, string)替换匹配到的内容字符串清洗文本我用得最多的是findall和sub。findall用于提取sub用于清理比如很多页面里文本里夹杂着\n、\r、br这些脏标签一条re.sub(r[^], , html_text)就能把 HTML 标签剥干净。我这里还建议把正则模式预编译pattern re.compile(rdiv classprice(.*?)/div) result pattern.findall(html_text)预编译的好处有两个一是代码运行更快因为正则表达式只需要编译一次不用每次匹配都重新编译二是代码可读性更好模式命名清晰后逻辑一目了然。3. 用 requests 正则爬取一个真实网页完整实操流程理论知识过一遍之后现在我们进入正题用 requests 抓网页用正则提取数据。我以一个公开的书籍信息页面为例演示如何把一个静态页面上的书名、价格、作者信息抓下来。注意教程里的网址和数据仅用于技术演示实际抓取前请确认目标网站允许爬取并遵守网站的 robots 协议和服务条款。3.1 第一步环境准备与发送请求首先确认你的 Python 环境已经装上了 requests。没装的话在终端执行pip install requests如果已经有 Python 环境顺便确认一下版本我建议用 Python 3.8 以上因为新版的re模块和requests库在兼容性和性能上表现更好。然后写一段最简单的请求代码import requests url https://books.toscrape.com/ # 一个专门用来练习爬虫的公开网站 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } response requests.get(url, headersheaders, timeout10) response.encoding utf-8 print(response.status_code) print(len(response.text))这里我做了两件事设置了User-Agent请求头伪装成浏览器。很多网站会对没有 UA 的请求直接返回 403这一行能省掉很多麻烦。设置了encoding utf-8确保中文不乱码。如果目标页面不是 utf-8比如 gbk需要根据页面的meta charset...来调整或者在 requests 拿到响应后用response.apparent_encoding自动检测。3.2 第二步用正则分析页面结构确定提取方案拿到页面 HTML 之后不要急着写正则。先用浏览器打开目标网页按 F12 查看元素找到你要提取的数据在 HTML 里的具体位置。我演示的这个图书网站每本书的结构长得像这样简化后article classproduct_pod h3a hrefa-light-in-the-attic_1000/index.html titleA Light in the AtticA Light in the Attic/a/h3 div classproduct_price p classprice_color£51.77/p /div /article观察规律之后我可以设计提取逻辑书名h3a ... title这里是书名相对链接h3a href这里是链接价格p classprice_color这里是价格/p设计正则时我根据 HTML 结构写出对应的模式import re # 书名和链接 book_pattern re.compile(rh3a href(.*?) title(.*?)) # 价格 price_pattern re.compile(rp classprice_color(.*?)/p)为什么用.*?而不是.*刚才讲过了用非贪婪模式防止跨条目匹配。3.3 第三步一次性提取所有数据并整理成结构化结果提取环节用findall批量获取books book_pattern.findall(response.text) prices price_pattern.findall(response.text) for i in range(min(len(books), len(prices))): link, title books[i] price prices[i] print(f{title} | {price} | https://books.toscrape.com/ link)这里有个小细节books的两个字段正好是(relative_link, title)所以循环里用link, title books[i]解包。如果只想要书名可以直接把正则改成捕获一个组title_pattern re.compile(rh3a href.*? title(.*?)) titles title_pattern.findall(response.text)在实际代码里我不会只停留在打印。通常是把数据整理成字典列表方便后续存 CSV 或数据库all_books [] for i in range(min(len(books), len(prices))): link, title books[i] price prices[i].replace(£, ).strip() all_books.append({ title: title, price: float(price), url: https://books.toscrape.com/ link }) print(all_books[:3])到这一步你已经把网页上的非结构化 HTML 转换成了结构化数据这就是正则爬虫的核心价值。3.4 第四步数据清洗与正则的进阶应用爬下来的原始数据极少是干净的经常会有空白符、换行符、HTML 实体等杂质。这时候就需要用re.sub做清洗。我举几个常见场景import re # 去掉所有 HTML 标签 clean_text re.sub(r[^], , raw_html) # 把多种空白符空格、tab、换行压缩为单个空格 clean_text re.sub(r\s, , clean_text) # 解码常见 HTML 实体如 amp; 对应 nbsp; 对应空格 clean_text clean_text.replace(amp;, ).replace(nbsp;, ) # 提取数字有些价格写为 £51.77只提取数字部分 price_num re.search(r[\d.], £51.77).group()这些清洗步骤看着琐碎但它们是让数据“能用”的关键。我在实际项目里统计过爬到原始数据后如果不好好清洗数据准确率可能在 70% 都不到清洗完能做到 98% 以上。3.5 完整代码示例从 URL 到结构化数据的全流程把上面的步骤整合成一个完整函数方便直接复用import requests import re def fetch_books(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } response requests.get(url, headersheaders, timeout10) response.encoding utf-8 html response.text book_pattern re.compile(rh3a href(.*?) title(.*?)) price_pattern re.compile(rp classprice_color(.*?)/p) books book_pattern.findall(html) prices price_pattern.findall(html) results [] for i in range(min(len(books), len(prices))): link, title books[i] price prices[i].replace(£, ).strip() results.append({ title: title, price: float(price), url: url link }) return results if __name__ __main__: data fetch_books(https://books.toscrape.com/) for item in data[:5]: print(item)这个函数拿下来扩展性也很好。想爬多页只需要把 URL 按页码规律拼出来循环调用就行。4. 正则解析的进阶技巧动态文本、嵌套结构与复杂匹配实际爬虫场景会比上面这个示例复杂得多。下面这几种情况是你在真实项目中大概率会遇到的我把对应的正则思路和写法一并整理出来。4.1 从 JavaScript 变量中提取数据现在很多网页的数据不是静态写在 HTML 里的而是由 JavaScript 动态渲染但渲染前会把数据先塞在一个变量里。比如script var pageData {bookName: Python入门, price: 59.9, author: 张三}; /script这种时候你不需要费劲解析整个 JS只需要用正则把pageData后面的内容抠出来import re html scriptvar pageData {bookName: Python入门, price: 59.9, author: 张三};/script match re.search(rvar pageData (\{.*?\});, html, re.S) if match: json_str match.group(1) print(json_str)注意这里用了re.S即re.DOTALL标志它的作用是让.也能匹配换行符。否则如果变量值跨了多行.*?就断掉了。拿到 JSON 字符串后再配合json.loads()转成字典操作起来就非常方便了import json data json.loads(json_str) print(data[bookName], data[price])这个方法我再强调一遍用正则从 JS 里抠出 JSON 片段再交给 json 库解析比硬写一百个正则去匹配每个字段靠谱得多。4.2 匹配嵌套结构正则的边界在哪里有人会问正则到底能不能匹配嵌套的 HTML 标签比如div divinner/div outer /div严格来讲正则不擅长处理任意深度的嵌套结构。因为正则本质上是有限状态机它没有“记忆”当前括号嵌套层级的能力。你最多可以匹配固定深度的嵌套但无法做到通用递归。所以在处理真正的深层嵌套 HTML 时我的建议是深度在 2~3 层以内可以用正则直接处理深度超过 3 层或者结构不规则请用 BeautifulSoup 或 lxml。这不是正则不行而是工具选型问题。正则和解析器本来就各有分工配合使用才是高手打法。4.3 多行匹配的三个关键标志re.S、re.I、re.M这三个标志在正则爬虫里几乎是必用的标志作用典型场景re.SDOTALL让.匹配包括换行在内的所有字符匹配跨越多行的 HTML 块re.IIGNORECASE忽略大小写匹配DIV和div都行re.MMULTILINE让^和$匹配每一行的开头和结尾逐行匹配日志或配置文件举个例子如果页面代码把标签换行写了div classcontent p classauthor张三/p /div这时用re.findall(rp classauthor(.*?)/p, html, re.S)才能正确匹配到张三。不加re.S的话.遇到换行就停了结果就为空。提示如果你不确定是否加了正确标志可以先在 Python 交互环境里小段调试比直接跑整套脚本要省时间。4.4 反向引用与条件匹配反向引用是指引用前面分组捕获到的内容写法是\1、\2等。它能解决一类选择题如果要匹配成对的标签h2.../h2、h3.../h3但开头标签和结尾标签必须一致正则可以这么写import re html h2标题一/h2h3标题二/h3 result re.findall(r(h[1-6])(.*?)/\1, html) print(result) # [(h2, 标题一), (h3, 标题二)]\1引用的就是第一个括号捕获的h2或h3这样能保证我对配对标签的匹配不会出现“开头 h2、结尾 h3”这种错误。反向引用用的频率比前面那些元字符低但一旦遇到需要“配对一致性检查”的场景它是唯一能简洁解决问题的工具。5. 正则性能优化编译、回溯与效率对比正则用顺手之后你可能会开始关心效率问题。尤其当页面超过 1MB、或者需要爬取数万条数据时正则是会用性能说话的工具。5.1 正则为什么会慢核心是“回溯”正则引擎在匹配时如果当前路径走不通会回退到上一个状态换一条路重试这种机制叫回溯。回溯次数一爆炸性能就急剧下降。最经典的性能杀手是“嵌套量词”。比如(a)这种模式在字符串aaaaaaaaaaaaaaaaaaaaaab上匹配时几乎会穷举所有分割方式耗时呈指数级增长。这种写法在爬虫正则里要极力避免。我自己的习惯是能明确字符范围时用[0-9]代替.能用字符集就尽量精确定义比如[\u4e00-\u9fa5]匹配中文不要让.去“试错”对大段可选内容尽量用非贪婪模式.*?减少回溯路径。5.2 用 timeit 实测预编译正则 vs 临时编译正则预编译到底快多少我写了一段简单测试代码来验证import re import timeit text div classprice199/div * 10000 def test_compile(): pattern re.compile(rdiv classprice(\d)/div) return pattern.findall(text) def test_no_compile(): return re.findall(rdiv classprice(\d)/div, text) print(timeit.timeit(test_compile, number100)) print(timeit.timeit(test_no_compile, number100))在我的机器上预编译版的耗时比临时编译版短了不少。虽然单次差距不大但在循环里反复调用场景下累计节省的时间非常可观。注意实际性能受目标文本长度、正则复杂度影响很大建议在真实数据规模上做基准测试别只看小样本结论。5.3 正则 vs 字符串方法 vs HTML 解析器什么时候选谁同样是提取数据有三条路可选正则、字符串方法split、replace等、HTML 解析器BeautifulSoup。我根据自己的经验整理了一个选型参考表对比维度正则字符串方法BeautifulSoup学习成本中等低低灵活性高很低中性能高最高但只适合简单场景低复杂 HTML 兼容性差嵌套容易翻车差好适用场景提取特定模式、JSON、纯文本简单固定格式结构化 HTML我的经验是如果目标字符串格式完全固定比如price:199用split(:)[1]最快如果格式有规律但偶尔变化用正则如果你要提取的是一个完整的 HTML 模块里的多个字段用 BeautifulSoup 更稳。说白了不要迷信正则也不要“一遇到正则就头大”。明确场景选择最合适的工具才是正确的工程判断。6. 常见问题与排查技巧实录正则爬虫刚上手的时候最容易在这几个地方翻车。我把典型问题和排查思路整理成一个速查表你直接对照着看。问题原因分析解决办法明明网页里有目标文本正则却匹配不到可能是换行、空格导致模式被断开加re.S标志或用\s*匹配可能的空白用.*匹配出尺度很大的内容贪婪模式跨过想要的位置把.*改成.*?非贪婪模式数据重复匹配到一大段起始标志和结束标志太宽松把结束标签写明确比如/div不能少中文匹配失败正则没写中文字符范围使用[\u4e00-\u9fa5]或直接写中文Python 支持 UTF-8提取速度太慢正则回溯次数多精简模式用字符集限定范围避免嵌套量词请求被网站拦截没带 UA 或请求频率太高设置User-Agent和timeout控制爬取频率价格、数字提取出来带符号正则模式没把符号去掉用re.search(r[\d.], raw_string)提取纯数字部分6.1 调试正则的正确姿势正则写完之后在跑整个爬虫脚本之前先单独调试匹配逻辑。我的常用套路是把正则和测试文本单独抽出来放到一个脚本里配合re.findall打印结果快速验证。如果你用的是 VS Code可以装一个正则调试插件实时看到哪个部分匹配到了哪些字符。对于复杂正则可视化调试能帮你大幅节省排查时间。6.2 空匹配和 None 处理的坑re.search在没有匹配时会返回None这时候如果直接调用.group()会直接抛AttributeError。我在代码里习惯加一个保护判断match re.search(rtitle(.*?)/title, html, re.S) if match: title match.group(1) else: title None这种写法不仅能避免报错还能在数据缺失时给后续流程留一个处理入口。6.3 编码问题的终极解法用 requests 爬中文网页最烦的就是乱码。我的处理逻辑是response.encoding response.apparent_encodingapparent_encoding是 requests 通过内容自动检测出的编码通常很准。但注意如果页面很大这个检测会比较耗时所以只对不确定编码的页面用已知编码的页面直接硬编码utf-8或gbk就行。7. 正则爬虫的两个避坑心得最后分享两个我在实际项目中沉淀下来的个人习惯。第一个是关于正则的可读性。爬虫脚本写完后过三个月再回来看没准连自己都看不懂当时写的正则是什么意思。所以我强烈建议给每一段复杂度较高的正则加上注释或者用re.VERBOSEre.X模式让正则支持换行和注释pattern re.compile(r h3\s* # 起始标签和可能出现的空白 a\shref(.*?) # 捕获链接 \stitle(.*?) # 捕获书名 , re.VERBOSE)这样正则不是一行“火星文”而是一段能被别人也包括三个月后的自己读懂的代码。第二个是关于容错设计。正则爬虫特别容易因为页面改版而失效。我的经验是在提取关键字段的函数里做一个最小单元测试每次爬取前用已知的小样本数据测试一下确认提取结果数量符合预期再跑全量。如果发现findall返回空列表或数量陡增大概率就是页面结构变了需要及时调整正则。这套“测试先行”的思路帮我避免过很多次“爬到一半才发现数据全是空”的悲剧。正则这个东西初学的时候容易觉得难但用熟之后你会发现在提取数据这个领域几乎没有它搞不定的活。配合 requests 加上一点点容错设计一个轻量又高效的爬虫脚本就能稳稳运行。希望这篇内容能帮你少走点弯路把 python、正则、爬虫这三件事真正串起来。