Python正则表达式完全指南:从字符串处理到爬虫实战

Nora
Nora
发布时间:2026-10-13 | 2人浏览过
原创

本文深入解析Python中正则表达式的核心概念,涵盖原始字符串定义、基础与预设元字符、边界与重复匹配、分组及贪婪与非贪婪模式。结合爬虫场景,详细演示如何利用re模块提取邮箱、处理相对路径及批量替换,助力开发者高效进行文本处理与数据抓取。

前言

正则表达式在文本处理方面效率极高,绝大多数编程语言都原生支持这一功能。它广泛应用于表单验证、文本提取与替换等场景。在爬虫开发中,熟练掌握正则表达式往往能显著提升数据抓取效率,达到事半功倍的效果。

在正式介绍正则表达式之前,先提出一个实际问题:假设有一段来自豆瓣网页的文本(已做简化),如何从中提取所有出现的邮箱地址?

html = """
  <style>
   .qrcode-app{
    display: block;
    background: url(/pics/qrcode_app4@2x.png) no-repeat;
   }
  </style>

  <div class="reply-doc content">
   <p class="">34613453@qq.com,谢谢了</p>
   <p class="">30604259@qq.com麻烦楼主</p>
  </div>
  <p class="">490010464@163.com<br/>谢谢</p>
  """

若未接触过正则表达式,面对此类问题可能会感到无从下手,且难以找到比正则更优的解决方案。暂且搁置该问题,待掌握正则表达式相关知识后,再来探讨其解决方法。

字符串的表现形式

Python中的字符串有多种表现形式。以u开头的字符串称为Unicode字符串,本文暂不讨论。此外,你可能还见过以下两种写法:

>>> foo = "hello"
>>> bar = r"hello"

前者为常规字符串,后者以r开头,称为原始字符串。二者有何区别?在上述示例中,由于均由普通文本字符组成,因此看似无差异,但通过以下代码可以验证:

>>> foo is bar
True
>>> foo == bar
True

然而,当字符串中包含特殊字符时,情况会有所不同。请看另一个示例:

>>> foo = "\n"
>>> bar = r"\n"

>>> foo, len(foo)
('\n', 1)
>>> bar, len(bar)
('\\n', 2)
>>> foo == bar
False
>>>

"\n"是转义字符,在ASCII码中表示换行符。而r"\n"是原始字符串,原始字符串不对特殊字符进行转义,它仅表示字面意思,即由反斜杠"\"和字符"n"组成的字符串。

定义原始字符串时,可以使用小写r或大写R作为前缀,例如r"\b"或R"\b"均为合法写法。在Python中,正则表达式通常推荐使用原始字符串形式定义,原因如下:

以字符"\b"为例,在ASCII码中它代表退格键,而在正则表达式中,它是匹配单词边界的特殊元字符。为了让正则编译器正确识别其含义,必须使用原始字符串。当然,也可以通过在常规字符串中使用反斜杠"\"进行转义来实现,但原始字符串更为直观。

>>> foo = "\\b"
>>> bar = r"\b"
>>> foo == bar
True

正则基本介绍

正则表达式由普通文本字符和特殊字符(即元字符)两部分组成。元字符赋予了正则表达式强大的表达能力。例如,在正则表达式r"a.d"中,'a'和'd'是普通字符,而'.'是元字符,代表任意单个字符。因此,它能匹配'a1d'、'a2d'或'acd'。其匹配流程如下:

Python内置的re模块专门用于处理正则表达式。

>>> rex = r"a.d" # 正则表达式文本
>>> original_str = "and" # 原始文本
>>> pattern = re.compile(rex) # 正则表达式对象
>>> m = pattern.match(original_str) # 匹配对象
>>> m 
<_sre.SRE_Match object at 0x101c85b28>

# 等价于
>>> re.match(r"a.d", "and")
<_sre.SRE_Match object at 0x10a15dcc8>

若原文本字符串与正则表达式匹配,match方法将返回一个Match对象;若不匹配,则返回None。通过判断返回值是否为None,即可实现表单验证等功能。

接下来,我们将学习更多的元字符及其用法。

基本元字符

  • .:匹配除换行符以外的任意单个字符。例如,"a.c"可以完全匹配"abc",也能在"abcef"中匹配出"abc"。
  • \:转义字符,用于使特殊字符恢复其原本意义。例如,正则表达式1\.2可以匹配字符串"1.2"。
  • [...]:匹配方括号内的任意一个字符。例如,a[bcd]e可以匹配abe、ace或ade。该语法还支持范围操作,如"a-z"表示a到z,"0-9"表示0到9。需要注意的是,在"[]"内部,特殊字符不再具有特殊含义,仅表示字面意思,例如[.*]即匹配字符.或*。
  • [^...]:字符集取反,表示匹配任何不在括号内的字符。例如,a[^bcd]e可以匹配aee、afe等。
>>> re.match(r"a.c", "abc").group()
'abc'
>>> re.match(r"a.c", "abcef").group()
'abc'
>>> re.match(r"1\.2", "1.2").group()
'1.2'
>>> re.match(r"a[0-9]b", "a2b").group()
'a2b'
>>> re.match(r"a[0-9]b", "a5b11").group()
'a5b'
>>> re.match(r"a[.*?]b", "a.b").group()
'a.b'
>>> re.match(r"abc[^\w]", "abc!123").group()
'abc!

group方法用于返回原字符串中与正则表达式匹配的子字符串。需要注意的是,只有当match方法匹配成功并返回Match对象后,才能调用group方法。

预设元字符

  • \w:匹配任意单词字符(包括数字和下划线),等价于[A-Za-z0-9_]。例如,a\wc可以匹配abc或acc。
  • \W:匹配任意非单词字符,与\w相反,等价于[^A-Za-z0-9_]。例如,a\Wc可以匹配a!c。
  • \s:匹配任意空白字符(包括空格、回车等)。例如,a\sc可以匹配a\nc,其中\n表示回车符。
  • \S:匹配任意非空白字符。
  • \d:匹配任意数字,等价于[0-9]。例如,a\dc可以匹配a1c或a2c等。
  • \D:匹配任意非数字字符。

边界匹配

边界匹配相关的符号专门用于修饰字符的位置。

  • ^:匹配字符串的开头。例如,^abc表示匹配以a开头且紧随bc的字符串,如"abc"。
  • $:匹配字符串的结尾。例如,hello$表示匹配以hello结尾的字符串。
>>> re.match(r"^abc","abc").group()
'abc'
>>> re.match(r"^abc$","abc").group()
'abc'

重复匹配

前述元字符主要针对单个字符进行匹配。若需匹配重复出现的字符,例如长度为18位的身份证号码,则需要使用重复匹配相关的元字符。

  • *:重复匹配零次或多次。
  • ?:重复匹配零次或一次。
  • +:重复匹配一次或多次。
  • {n}:精确重复匹配n次。
  • {n,}:重复匹配至少n次。
  • {n, m}:重复匹配n到m次。
# 简单匹配身份证号码,前面17位是数字,最后一位可以是数字或者字母X
>>> re.match(r"\d{17}[\dX]", "42350119900101153X").group()
'42350119900101153X'

# 匹配5到12的QQ号码
>>> re.match(r"\d{5,12}$", "4235011990").group()
'4235011990'

逻辑分支

匹配固定电话号码时,不同地区的规则可能不同:部分地区区号为3位、电话为8位,另一些地区区号为4位、电话为7位,且区号与号码间用-分隔。应对此类需求时,需使用逻辑分支条件字符|。它将表达式分为左右两部分,优先尝试匹配左侧部分,若成功则不再匹配右侧,体现逻辑“或”的关系。

# abc|cde 可以匹配abc 或者 cde,但优先匹配abc
>>> re.match(r"aa(abc|cde)","aaabccde").group()
'aaabc'

表达式0\d{2}-\d{8}|0\d{3}-\d{7}以0开头,既能匹配3位区号加8位号码的组合,也能匹配4位区号加7位号码的组合。

>>> re.match(r"0\d{2}-\d{8}|0\d{3}-\d{7}", "0755-4348767").group()
'0755-4348767'
>>> re.match(r"0\d{2}-\d{8}|0\d{3}-\d{7}", "010-34827637").group()
'010-34827637'

分组

前述匹配规则主要针对单个字符。若需对多个字符进行重复匹配,可使用子表达式(即分组)来表示,分组用小括号"()"标识。例如,(abc){2}表示匹配两次"abc"。在匹配IP地址时,可使用(\d{1,3}\.){3}\d{1,3}。由于IP地址由4组数字和3个点组成,前3组数字及点可视为一个分组重复3次,最后一部分为1到3位数字组成的字符串,如"192.168.0.1"。

关于分组,group方法可用于提取匹配的字符串分组。默认情况下,不带参数的group()或group(0)返回整个表达式的匹配结果;第一对括号中的分组可通过group(1)获取,以此类推。

>>> m = re.match(r"(\d+)(\w+)", "123abc")
#分组0,匹配整个正则表达式
>>> m.group()
'123abc'
#等价
>>> m.group(0)
'123abc'
# 分组1,匹配第一对括号
>>> m.group(1)
'123'
# 分组2,匹配第二对括号
>>> m.group(2)
'abc'
>>>

通过分组,我们可以从字符串中提取所需信息。此外,分组还支持命名方式获取。

# 第一个分组的名字是number
# 第二个分组的名字是char
>>> m = re.match(r"(?P<number>\d+)(?P<char>\w+)", "123abc")
>>> m.group("number")
'123'
# 等价
>>> m.group(1)
'123'

贪婪与非贪婪

默认情况下,正则表达式在使整个表达式匹配成功的前提下,尽可能多地匹配字符,这称为贪婪模式,是一种“贪得无厌”的模式。例如,r"a.*b"表示匹配以a开头、b结尾,中间包含任意多个字符的字符串。若用它匹配"aaabcb",它将匹配整个字符串"aaabcb"。

>>> re.match(r"a.*b", "aaabcb").group()
'aaabcb'

若希望尽可能少地匹配,只需在量词后添加问号"?"。在保证匹配成功的前提下,尽可能少地匹配字符。例如,若希望仅匹配"aaab",只需将正则表达式修改为r"a.*?b"。

>>> re.match(r"a.*?b", "aaabcb").group()
'aaab'
>>>

非贪婪模式在爬虫应用中极为常见。例如,在爬取网站并转换为PDF的场景中,若网页img标签涉及相对路径,需将其替换为绝对路径。

>>> html = '<img src="/images/category.png"><img src="/images/js_framework.png">'

# 非贪婪模式就匹配的两个img标签
# 你可以改成贪婪模式看看可以匹配几个
>>> rex = r'<img.*?src="(.*?)">'
>>> re.findall(rex, html)
['/images/category.png', '/images/js_framework.png']
>>>
>>> def fun(match):
...  img_tag = match.group()
...  src = match.group(1)
...  full_src = "http://foofish.net" + src
...  new_img_tag = img_tag.replace(src, full_src)
...  return new_img_tag
...
>>> re.sub(rex, fun, html)
<img src="http://foofish.net/images/category.png"><img src="http://foofish.net/images/js_framework.png">

sub函数可接受一个函数作为替换目标,该函数的返回值将用于替换正则表达式匹配的部分。在此示例中,我们将整个img标签定义为一个正则表达式r'',group()返回的值是<img src="/images/category.png"> ,而group(1)的返回值是/images/category.png。最后,利用replace方法将相对路径替换为绝对路径。

至此,你应该对正则表达式有了初步了解,现在你应该能够解决文章开篇提出的邮箱提取问题了。

正则表达式的基本介绍到此告一段落。虽然代码示例中使用了re模块的多种方法,但尚未正式详细介绍该模块。考虑到文章篇幅,这部分内容将放在下篇,下篇将对re模块的常用方法进行系统介绍。

总结

以上就是本文的全部内容,希望这些内容能对大家的Python学习或工作有所帮助。如有任何疑问,欢迎留言交流,感谢大家对本站的支持。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表