Python正则表达式完全指南:从基础到实战
本文系统讲解Python中正则表达式的核心概念,涵盖原始字符串定义、基础与预设元字符、边界与重复匹配、逻辑分支及分组技巧。重点解析贪婪与非贪婪模式,并结合爬虫场景演示如何利用sub函数与正则表达式进行HTML标签处理与路径替换,帮助开发者高效处理文本提取与验证任务。
正则表达式在处理文本时效率极高,如同疾风扫落叶。绝大多数编程语言都内置支持正则表达式,其应用场景广泛,包括表单验证、文本提取和字符串替换等。特别是在爬虫系统中,正则表达式不可或缺,熟练运用往往能实现事半功倍的效果。
在正式介绍正则表达式之前,先提出一个问题。以下文本节选自豆瓣某网页链接,内容已做简化。请问:如何从这段文本中提取出所有的邮箱地址?
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。通过判断m是否为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方法返回原字符串(abcef)中与正则表达式相匹配的那部分子字符串(abc)。前提是要匹配成功,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$ >>> re.match(r"^abc","abc").group() 'abc' >>> re.match(r"^abc$","abc").group() 'abc'
重复匹配
前面的元字符都是针对单个字符进行匹配的。如果希望匹配的字符重复出现,例如匹配长度为18位的身份证号码,就需要用到重复匹配的元字符。
*:重复匹配零次或者更多次。 ?:重复匹配零次或者一次。 +:重复匹配1次或者多次。 {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个点可以作为一个分组重复3次,最后一部分是一个1到3个数字组成的字符串,如:192.168.0.1。
关于分组,group方法可用于提取匹配的字符串分组。默认情况下,它会把整个表达式的匹配结果当做第0个分组,即不带参数的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,那么它会匹配整个字符串。
>>> re.match(r"a.*b", "aaabcb").group() 'aaabcb'
有时,我们希望尽可能少地匹配,该怎么办?只需在量词后面加一个问号“?”,在保证匹配的情况下尽可能少地匹配。比如刚才的例子,我们只希望匹配aaab,那么只需修改正则表达式为r"a.*?b"。
>>> re.match(r"a.*?b", "aaabcb").group() 'aaab' >>>
非贪婪模式在爬虫应用中非常频繁。例如之前在公众号「Python之禅」曾写过一篇爬取网站并将其转换为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'<img.*?src="(.*?)">',group() 返回的值是 <img src="/images/category.png">,而group(1)的返回值是/images/category.png。最后,我用replace方法把相对路径替换成绝对路径。
到此,你应该对正则表达式有了初步了解,现在我想你应该能解决文章开篇提出的问题。
正则表达式的基本介绍也到这里告一段落。虽然代码示例中用了re模块中的很多方法,但我还没正式介绍该模块。考虑到文章篇幅,我把这部分放在下篇,下篇将对re的常用方法进行介绍。
以上所述是小编给大家介绍的Python正则表达式完全指南,希望对大家有所帮助。如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













