——从入门到熟练)
当 CSS 选择器和 XPath 都搞不定的时候正则表达式就是你最后的杀手锏。它是一种用符号描述字符串模式的语言强大、灵活也容易让人望而生畏。本篇和下一篇我们用两篇的篇幅系统地讲透 Python 中的正则表达式。本篇是入门进阶篇覆盖从基础语法到常用场景下篇是高阶篇覆盖分组、断言、性能优化与实战案例。学完本篇你将能够读懂和书写 80% 的常用正则在 Python 中熟练使用re模块用正则从 HTML、日志、JSON 字符串中提取数据理解正则引擎的基本工作原理。一、正则是什么正则表达式Regular Expression简称 regex / re是一种用来匹配、查找、替换字符串的模式语言。它的本质是一套描述字符串结构的语法。举个例子\d{3}-\d{4}-\d{4}这个正则可以匹配138-0013-8000 186-1234-5678它描述的模式是3 个数字 横杠 4 个数字 横杠 4 个数字。正则的价值在于用极短的语法表达极复杂的文本模式。二、Pythonre模块入门Python 的正则库是标准库re不用额外安装。2.1 第一个正则importre text我的手机号是 138-0013-8000备用号是 186-1234-5678。patternr\d{3}-\d{4}-\d{4}# 查找第一个匹配mre.search(pattern,text)ifm:print(m.group())# 138-0013-8000# 查找所有匹配resultsre.findall(pattern,text)print(results)# [138-0013-8000, 186-1234-5678]注意 pattern 前面的r—— 表示原始字符串避免反斜杠被 Python 转义。正则里反斜杠用得很多一定要加r。2.2 常用方法速查方法作用返回re.search(pattern, string)找第一个匹配Match 对象 / Nonere.match(pattern, string)从开头匹配Match 对象 / Nonere.fullmatch(pattern, string)整个字符串匹配Match 对象 / Nonere.findall(pattern, string)找所有匹配列表re.finditer(pattern, string)找所有匹配迭代器yield Matchre.sub(pattern, repl, string)替换替换后的字符串re.split(pattern, string)分割列表re.compile(pattern)预编译Pattern 对象2.3 Match 对象search、match、fullmatch返回的Match对象mre.search(r(\d{3})-(\d{4})-(\d{4}),电话: 138-0013-8000)m.group()# 138-0013-8000 第 0 组整个匹配m.group(0)# 同上m.group(1)# 138 第 1 个括号m.group(2)# 0013 第 2 个括号m.group(3)# 8000 第 3 个括号m.groups()# (138, 0013, 8000)m.start()# 起始位置m.end()# 结束位置m.span()# (start, end)2.4 预编译 Pattern# 预编译后续反复使用patre.compile(r\d{3}-\d{4}-\d{4})pat.search(text)# 等同于 re.search(pat, text)pat.findall(text)pat.sub(***,text)同一个正则用多次时一定要预编译性能提升明显。三、基础语法元字符正则的魔法来自元字符metacharacter——这些字符不代表字面意思而是特殊功能。3.1 字符类符号含义等价.任意字符除换行[^\n]\d数字0-9[0-9]\D非数字[^0-9]\w单词字符字母、数字、下划线[a-zA-Z0-9_]\W非单词字符[^a-zA-Z0-9_]\s空白字符空格、制表符、换行等-\S非空白字符-3.2 字符集[]用方括号列出一组字符匹配其中任何一个re.findall(r[aeiou],hello world)# [e, o, o]re.findall(r[0-9],abc123)# [1, 2, 3]re.findall(r[a-z],Hello)# [e, l, l, o]re.findall(r[A-Za-z],Hello123)# [H, e, l, l, o]re.findall(r[^0-9],abc123)# [a, b, c] ^ 表示取反3.3 量词指定前面的字符出现多少次符号含义*0 次或多次1 次或多次?0 次或 1 次{n}恰好 n 次{n,}至少 n 次{n,m}n 到 m 次re.findall(rab*c,ac abc abbc adc)# [ac, abc, abbc]re.findall(rabc,ac abc abbc adc)# [abc, abbc]re.findall(rcolou?r,color colour)# [color, colour]re.findall(r\d{3},1234)# [123]re.findall(r\d{2,4},1 12 123 1234)# [12, 123, 1234]3.4 锚点不匹配字符只匹配位置符号含义^行首 / 字符串开头$行尾 / 字符串结尾\b单词边界\B非单词边界re.findall(r^hello,hello world)# [hello]re.findall(rworld$,hello world)# [world]re.findall(r\bcat\b,cat category)# [cat] (只匹配独立单词)3.5 分组与捕获用()把一部分模式包起来形成一个分组mre.search(r(\d{4})-(\d{2})-(\d{2}),2025-05-20)m.groups()# (2025, 05, 20)m.group(1)# 20253.6 或运算|re.findall(rcat|dog,I have a cat and a dog.)# [cat, dog]re.findall(rcol(o|ou)r,color colour)# [o, ou]3.7 转义\要匹配元字符本身如.、*、需要用反斜杠转义re.findall(r3\.14,pi is 3.14)# [3.14]re.findall(ra\b,ab c)# [ab]四、正则的两种模式贪婪 vs 非贪婪这是初学者最容易踩的坑。4.1 贪婪模式默认textdivhello/divdivworld/divre.findall(rdiv.*/div,text)# [divhello/divdivworld/div].*默认贪婪——尽可能多地匹配字符导致从第一个div一直匹配到最后一个/div。4.2 非贪婪模式加?在量词后面加?变成非贪婪re.findall(rdiv.*?/div,text)# [divhello/div, divworld/div]记忆口诀贪婪吃得越多越好非贪婪见好就收。4.3 非贪婪的坑非贪婪虽然方便但性能比贪婪差。能用字符集替代就用字符集# 不够好re.findall(rdiv class.*?,html)# 更好不匹配引号re.findall(rdiv class[^]*,html)五、常用方法详解5.1searchvsmatchvsfullmatchtexthello 123 worldre.match(r\d,text)# None (开头不是数字)re.search(r\d,text)# Match ... 123re.fullmatch(rhello \d world,text)# 整个字符串完全匹配方法匹配范围match必须从开头匹配search任意位置fullmatch必须整个字符串匹配5.2findallvsfinditertexta1 b2 c3 d4re.findall(r\d,text)# [1, 2, 3, 4] 直接返回列表forminre.finditer(r\d,text):print(m.group(),m.span())# 每次返回 Match 对象数据量小时用findall简单数据量大时用finditer省内存。5.3sub替换texthello 123 world 456re.sub(r\d,###,text)# hello ### world ###re.sub(r\d,###,text,count1)# 只替换第一个repl也可以是函数defreplacer(m):return[m.group()]re.sub(r\d,replacer,text)# hello [123] world [456]5.4split分割texta,b;c d\tere.split(r[,;\s],text)# [a, b, c, d, e]5.5compile预编译# 编译一次使用多次phone_patre.compile(r1[3-9]\d{9})phone_pat.findall(text1)phone_pat.findall(text2)phone_pat.search(text3)当正则要执行超过 1 次就应该编译。六、标志位Flags标志位用来修改正则的行为。标志简写含义re.IGNORECASEre.I忽略大小写re.MULTILINEre.M多行模式^和$匹配每行re.DOTALLre.S.匹配换行符re.VERBOSEre.X详细模式可加注释、忽略空白re.ASCIIre.A\w、\d等只匹配 ASCIIre.LOCALEre.L依赖系统区域设置少用6.1re.I忽略大小写re.findall(rpython,Python PYTHON python)# [python]re.findall(rpython,Python PYTHON python,re.I)# [Python, PYTHON, python]6.2re.M多行模式textline1 line2 line3re.findall(r^line\d,text)# [line1] 默认只匹配整个字符串开头re.findall(r^line\d,text,re.M)# [line1, line2, line3]6.3re.S点号匹配换行textdivhello\nworld/divre.search(rdiv.*/div,text)# None (. 不匹配换行)re.search(rdiv.*/div,text,re.S)# Match ...6.4re.X详细模式可以把复杂正则写得可读patre.compile(r (\d{4}) # 年 - # 分隔符 (\d{2}) # 月 - # 分隔符 (\d{2}) # 日 ,re.X)mpat.search(2025-05-20)print(m.groups())# (2025, 05, 20)多个标志位用|组合re.I | re.M | re.S七、爬虫常用正则模板7.1 手机号r1[3-9]\d{9}7.2 邮箱r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}7.3 URLrhttps?://[a-zA-Z0-9.-]\.[a-zA-Z]{2,}[^\s]*7.4 IP 地址r\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}7.5 日期YYYY-MM-DDr\d{4}-\d{2}-\d{2}7.6 中文字符r[\u4e00-\u9fa5]7.7 HTML 标签r[^]7.8 HTML 注释r!--.*?--7.9src/href属性ra\shref([^])rimg\s[^]*src([^])7.10 提取script中的 JSONrwindow\.__INITIAL_STATE__\s*\s*(\{.*?\});注意这些是能用的版本不是最严谨的版本。根据实际场景调整。八、实战用正则抓取网页数据虽然有 BeautifulSoup 和 lxml但正则在某些场景下依然不可替代数据在script里JS 变量数据是一段非结构化文本性能要求极高不想用 DOM 解析器数据格式是日志、CSV 等非 HTML。实战 1从 JS 变量中提取数据importjsonimportre html script window.__DATA__ { title: 爬虫专栏, articles: [ {id: 1, title: 第一篇, views: 1000}, {id: 2, title: 第二篇, views: 2000} ] }; /script mre.search(rwindow\.__DATA__\s*\s*(\{.*?\});,html,re.S)ifm:datajson.loads(m.group(1))print(data[title])# 爬虫专栏print(len(data[articles]))# 2实战 2从日志中提取 IPlog 192.168.1.1 - - [20/May/2025:10:00:00 0800] GET / HTTP/1.1 200 1234 10.0.0.1 - - [20/May/2025:10:00:01 0800] POST /api HTTP/1.1 500 100 ipsre.findall(r\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3},log)print(ips)# [192.168.1.1, 10.0.0.1]实战 3提取所有链接htmla hrefhttps://a.comA/aa hrefhttps://b.comB/alinksre.findall(ra\shref([^]),html)print(links)# [https://a.com, https://b.com]再次提醒能用 DOM 解析就别用正则。正则是核武器威力大但也容易误伤。九、正则的能与不能能做什么验证字符串格式手机号、邮箱从文本中提取结构化信息批量替换文本文本分割日志分析。不能做什么不能解析 HTML理论上不行因为 HTML 不是正则语言不能处理嵌套结构如括号匹配不能做复杂的语义分析。名言如果你有一个问题想用正则解决那你现在有两个问题了。—— Jamie Zawinski十、调试工具regex101.com — 在线正则测试强烈推荐regexr.com — 另一个在线工具Python 内联调试importre texttest 123patr\d# 看看每一步匹配到了哪里forminre.finditer(pat,text):print(f匹配:{m.group()}, 位置:{m.span()})十一、常见坑坑 1忘记r前缀re.findall(\d,123)# 能跑但不推荐re.findall(\\d,123)# 转义了反斜杠re.findall(r\d,123)# ✅ 推荐坑 2.不匹配换行re.findall(rdiv.*/div,html,re.S)# 记得加 re.S坑 3贪婪匹配过度re.findall(rdiv.*/div,html)# ❌ 贪婪re.findall(rdiv.*?/div,html)# ✅ 非贪婪re.findall(rdiv[^]*/div,html)# ✅ 字符集更快坑 4索引越界mre.search(r\d,abc)ifm:# ✅ 先判断print(m.group())坑 5findall带分组的行为如果正则里有分组findall返回的是分组内容不是整个匹配re.findall(r(\d)-(\d),1-2 3-4)# [(1, 2), (3, 4)] 返回元组列表要返回整个匹配用非捕获分组下一篇讲或finditer。十二、动手练习从2025-05-20 10:30:00中提取年、月、日、时、分、秒提取用户张三 说你好 #爬虫# 话题中的用户名和话题标签替换所有数字为*提取所有形如变量名 值的键值对验证一个字符串是否是合法的身份证号18 位最后一位可以是 X。参考答案# 1mre.search(r(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}),2025-05-20 10:30:00)print(m.groups())# 2text用户张三 说你好 #爬虫# 话题print(re.findall(r(\w),text))print(re.findall(r#([^#])#,text))# 3print(re.sub(r\d,*,abc123def456))# 4textname Tom\nage 18\ncity Beijingprint(dict(re.findall(r(\w)\s*\s*(.),text)))# 5defis_id_card(s):returnbool(re.fullmatch(r\d{17}[\dXx],s))print(is_id_card(110101199003077777))print(is_id_card(11010119900307777X))十三、本文小结本篇是正则表达式的入门进阶篇覆盖了re模块核心方法元字符、字符集、量词、锚点、分组贪婪 vs 非贪婪标志位I/M/S/X10 个常用正则模板3 个实战场景常见坑与调试技巧。下一篇第 35 篇《正则表达式完全指南下》——深入分组、断言、回溯、性能优化与高级实战。