循序渐进学习 Python 正则表达式(1)

循序渐进学习 Python 正则表达式(1)


1. 简介


正则表达式(re)模块从 Python 1.5  开始加入,并且采用了 Perl 风格的正则表达式匹配模式(patterns)。在早期的 Python 版本中,正则表达式使用的是 regex 模块,它使用的是 Emacs 风格的匹配模式。在 Python 2.5 中,regex 模块被完全移除了。

正则表达式(称为,REs,regexes,或 regex patterns)本质上是一门小的,非常特别的嵌入到 Python 中的编程语言,在 Python 的 re 模块中。你可以使用这门小的语言来指定你想匹配的字符串的集合符合的规则;这个字符串集合可能包含英文句子,或电子邮件地址,Tex 命令,或其他你想要的东西。然后你就可以问类型这样的问题:“这个字符串匹配给定的模式吗?”,或者:“这个字符串中有某些部分匹配这个模式吗”。你也可以使用正则表达式(REs)通过多种方式来修改或分割字符串。

正则表达式模式会被编译成一串字节码,然后传给一个由 C 言语写的匹配引擎来执行。对于高级用法而言,可能需要更多留意对于一个给定的 RE 正则表达式引擎会如何执行,以何种方式写出的正则表达式产生的字节码执行更快。优化不是本文要讨论的内容,因为这需要你对匹配引擎内部机制有很好的理解。

正则表达式言语非常小,且有限制,并不是所有字符串处理的任务都能使用正则表达式完成。还有一些任务可以使用正则表达式完成,但是却很复杂。这种情况下,你还是最好写Python 代码来处理;尽管这些代码处理速度比复杂的正则表达式慢,但是却更好理解。


2. 简单的模式(patterns)


我们将从最简单的正则表达式开始学起。因为正则表达式是用来处理字符串的,我们就从最常见的任务——匹配字符,开始。


匹配字符

大多数字母和字符都会简单的匹配它们自身。例如,正则表达式 test 会精确的匹配字符串 test(你可以开启大小写不敏感模式,来使得这个 RE 也可以匹配字符串 Test 或 TEST,这个后面会再提)。

这条规则也有例外:一些字符是特殊的元字符,它们不会匹配其自身。它们的出现表明应该匹配一些不同寻常的东西,或它们会通过重复正则表达式的其他部分或改变其他部分的意义来影响正则表达式。本文的大部分是在讨论这些元字符和它们的用途。

下面是完整的元字符列表,本文余下部分将讨论他们的意义和用法:

. ^ $ * + ? { } [ ] \ | ( )

我们要看的第一个元字符是方括号:[],它们用于指定一个字符类,这里所谓的字符类就是你想匹配的字符的集合。在 [] 中,字符可以单个的列出,也可以使用首尾字符加上连字符来表示一个字符范围,例如,[abc] 这个字符类会匹配字符,a,b,c 中的任意一个;这与 [a-c] 这个表示方法是相同的。因此如果你想匹配小写字母,正则表达式就可以这样写:[a-z]

在字符类中,元字符是不起特殊作用的,例如,[akm$] 会匹配:a,k,m,$ 这些字符里的任意一个。$ 通常是个元字符,但是在字符类里,他的特殊意义就被去掉了。

你可以通过取字符类的补集来匹配那些不再字符类中的字符:这是通过将 ^ 字符作为字符类中的首字符来实现的。例如,[^5] 会匹配除了 5 以外的字符。在字符类之外的 ^ 字符,一般会匹配其自身。

也许最重要的元字符就是反斜线:\ 。 就像在 Python 字符串字面值中,\ 后面可以跟着许多各种各样d字符来表示特殊的序列。在正则表达式中,它也同样用来转义所有的元字符,使你可以在模式中匹配这些元字符。例如,如果你需要匹配 [ 或 \ ,你可以在他们前面加上反斜线进行转义,来消除特们的特殊意义:\[ 或 \\

一些以 \ 开始的特殊序列代表常用的预定义的字符集:如,数字集合,字母集合,非空白字符集合等。下面列出来其中的一些。与它们等价的字符类表示也同时给出在后面:

\d :匹配任意十进制数字,与 [0-9] 等价

\D :匹配任意非数字字符,与 [^0-9] 等价

\s :匹配任意空白字符,与 [ \t\n\r\f\v] 等价

\S :匹配任意非空白字符,与 [^ \t\n\r\f\v] 等价

\w :匹配任意字母或数字字符,与 [a-zA-Z0-9] 等价

\W :匹配任意非字母或数字字符,与 [^a-zA-Z0-9] 等价

这些序列可以包含在字符类的内部,如:[\s,.] 表示会匹配任何空白字符,或 ',' 或 '.' 的字符类。

本节最后一个元字符是点:‘.’ 。它会匹配除了换行符以外的任意字符,在替代模式(re.DOTALL)中,它甚至可以匹配换行符。


重复指定部分


能够匹配可变的字符集合是正则表达式能够做而普通的字符串方法无法做的第一件事。正则表达式的另一个能力是你可以指定表达式中的某个部分重复的次数。

我们要看的第一个指定重复次数的元字符是星号:*,星号不会匹配字面值 *,它是指定其前面的字符可以出现零次或多次,而不是仅仅一次。

例如,ca*t,将会匹配 ct (0 个 a),cat (1 个 a),caaat (3 个 a)等。重复(如,*)是贪婪的,正则表达式引擎会尽可能多的重复。如果模式(pattern)后面的部分不匹配了,引擎就会回退一个位置,以更少次的重复来尝试匹配。

下面是一个一步一步匹配的例子:我们来看这个正则表达式 a[bcd]*b,它会匹配字母 a,字符类 [bcd] 里面的领个或多个字符,最后是一个结尾的字符 b。现在我们假设用这个 RE 来匹配字符串:abcbd

匹配步骤已匹配的字符串解释
1a匹配引擎开始匹配字符 a ,结果匹配
2abcbd匹配引擎尝试匹配 [bcd]*,尽可能多的匹配,直到字符串结束
3失败引擎尝试匹配模式中的字符 b,但是当前位置已经到了字符串的尾部,因此匹配失败
4abcb引擎在字符串当前匹配的位置回退一步,使得 [bcd]* 少匹配一个字符
5失败引擎再次尝试匹配字符 b,但是当前位置在最后一个字符 d 上,匹配仍旧失败
6abc引擎再回退一步,使得 [bcd]* 再少匹配一个字符
7abcb引擎再次尝试匹配 b,这次当前位置上是 b,匹配成功

现在正则表达式已经到达了尾部,它匹配了字符串 abcb。这展现了匹配引擎首先尽可能多的向前匹配,如果到某个点不再匹配,引擎会逐步回退来重新尝试匹配余下的正在表达式。它会一直回退,直到 [bcd]* 匹配零次,如果随后的匹配还失败的话,匹配引擎就会断定所给的字符串与指定的正则表达式不匹配。

另一个指定重复次数的元字符是加号:+,它会匹配一次或多次重复。请注意它和星号的区别,星号匹配零次或多次,所以指定要重复的东西可能根本不存在,但是加号要求指定的部分至少出现一次。举个简单的例子来说,ca+t 这个 RE 会匹配 cat (1 个 a),caaat (3 个 a),但是却不会匹配 ct

还有两个指定重复次数的限定符,其中一个是问号:?,问号会匹配一次或零次,你可以认为它是在说,它前面的部分是可选的。例如,正则表达式 home-?brew 会匹配,homebrew 或 home-brew

最复杂的重复限定符是 {m, n},其中 m,n 都是十进制数字。这个限定符的意思是,指定部分出现至少 m 次,至多出现 n 次。例如,a/{1,3}b 这个正则表达式,会匹配 a/b,a//b,a///b。但是不会匹配 ab (没有 / 字符) 或 ab (有 4 个 / 字符)。


你可以省略 m 或 n 中的一个,这种情况下就会给省略的部分假定一个合理的值。省略 m,就意味着下限是 0,省略 n 就意味着上限是无穷大。

读者可能注意到,其他三种指定重复的方式都可以用这个方式来表示:* 等价于 {0,},+ 等价于 {1,},? 等价于 {0,1}。但是我们应该尽量使用 *,+,?,因为它们更加短小易读。


3. 使用正则表达式


既然我们已经看过了一些正则表达式,那么要怎样在 Python 中使用它们呢?re 模块提供了使用正则表达式的接口,允许你将正则表达式编译成对象,然后使用它们来进行匹配。


编译正则表达式



正则表达式编译后得到模式对象(pattern object),改对象的方法可以提供多种操作,如,模式匹配搜索,进行字符串替换等

>>> import re
>>> p = re.compile('ab*')
>>> p  
<_sre.SRE_Pattern object at 0x...>

re.compile() 方法可以接受一个额外的 flag 参数,该参数用来是能多种特性和语法变化。我们后面会谈有哪些可用设置,下面先给出一个简单的例子:

>>> p = re.compile('ab*', re.IGNORECASE)

正则表达式以字符串的形式被传递给了 re.compile()。REs 被当作字符串处理是因为正则表达式不是 Python 语言核心的一部分,因此没有没正则表达式创造特殊的语法。re 模块仅仅是被 Pyhton 包含的一个 C 扩展模块,就行 socket 或 zlib 模块一样。

把正则表达式当作字符串处理,使 Python 语言更简单,但是这会带来一个缺点,就是下一小节要讲的。


反斜线带来的麻烦


如前面所述,正则表达式使用反斜线来转义特殊元字符,以去掉他们的特殊含义。这与 Python 字符串字面值中同样使用反斜线转义的用法构成了冲突。

我们假设你想写一个匹配 \section 字符串的正则表达式。你必须通过在前面加上反斜线来转义反斜线自身和其他的元字符,因此你写出的正在表达式像是这样:\\section。所以要传递给 re.compile() 函数的字符串应该是 \\section,然而,为了将 \\section 表示成 Python 字符串字面值,你必须要再次对反斜线进行转义。

字符处理阶段
\section需要被匹配的字符串
\\section转义反斜线,得到的要传递给 re.compile() 的正在表达式
\\\\section转义反斜线得到正则表达式在 Python 中的字符串字面值表示,这是最终传递给 re.compile() 的参数

简而言之,为了匹配一个字面上的反斜线 \,你需要些 '\\\\' 作为正则表达式字符串,因为正在表达式必须是 \\,每个反斜线在 Python 的字符串字面值中又必须表示成 \\。这使得正则表达式中出现许多的反斜线,正则表达式变得难以理解。

解决这个问题的方法是使用 Python 的原始字符串的记法来写正则表达式。在原始字符串(字符串前面加了 r 作为前缀)中,反斜线不会进行特殊处理。因此 r'\n' 是一个包含 \ 和 n 的两个字符的字符串,而 ‘\n’ 则是包含一个换行符的一个字符的字符串。正则表达式在 Python 中经常使用原始字符串记法。


正则表达式(普通字符串字面值表示法)原始字符串表示法
“ab*”r"ab*"
“\\\\section”r"\\section"
“\\w+\\s+\\1”r"\w+\s+\1"


进行正则表达式匹配


一旦你有了一个代表编译后的正则表达式的对象,你会拿它来干什么呢?模式对象有一些方法和属性。本文只会讲其中那些最重要的部分。

方法/属性
用途
match()判断给定正则表达式是否在字符串开始处匹配
search()扫面字符串,寻找字符串中匹配给定正则表达式的位置
findall()找到所有匹配正则表达式的子字符串,并把它们作为一个列表返回
finditer()找到所有匹配正则表达式的子字符串,并把它们作为一个迭代器返回

如果字符g串与正则表达式不匹配,match() 和 search() 方法就会返回 None。如果匹配,它们就会返回一个匹配对象(match object)的实例,该实例包含匹配信息:匹配的起始位置和结束位置,匹配到的子字符串等。

你可以通过与 re 模块进行交互性的实验来学习。如果你可以使用 Tkinter,你可能还行看看 Python 安装目录下的 Tools/scripts/redemo.py,这时一个包含在 Python 发行包中的演示程序。它允许你输入正则表达式和字符串,并且显示正则匹配是成功还是失败。当尝试调试一个复杂的正则表达式时,redemo.py 是相当有用的。

本文使用标准的 Python 解释器来做演示。首先我们打开 Python 解释器,导入 re 模块,并编译一个正则表达式:

Python 2.2.2 (#1, Feb 10 2003, 12:57:01)
>>> import re
>>> p = re.compile('[a-z]+')
>>> p  #doctest: +ELLIPSIS
<_sre.SRE_Pattern object at 0x...>

现在你可以尝试使用这个正则表达式 [a-z]+ 来匹配各种各样的字符串。空字符串不会匹配,因为 + 意味着重复至少一次。这时 match() 会返回 None,解释器不会打印任何输出,为了清晰,你可以显式的打印出 match() 的结果:

>>> p.match("")
>>> print p.match("")
None

现在我们来尝试一个会匹配的字符串,例如 tempo。这时 match() 会返回一个匹配对象,你应该把结果保存到一个变量中,待后面使用:

>>> m = p.match('tempo')
>>> m  
<_sre.SRE_Match object at 0x...>

现在你可以从得到的匹配对象中查询相关信息。匹配对象的实例有一些方法和属性,其中最重要的列举如下:

方法/属性
用途
group()返回被正则表达式匹配的子字符串
start()返回匹配开始的位置
end()返回匹配结束的位置
span()返回一个包含匹配开始和结束位置的元组(start, end)

试试这些方法,很快就会明白它们的意义:

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() 返回被正则表达式匹配到的子字符串。start() 和 end() 分别返回匹配的开始和结束索引。span() 以一个元组的方式返回开始和结束的索引。因为 match() 方法只检测正则表达式是否在字符串的开头匹配,所以 start() 总是返回 0 。而模式对象的 search() 方法会沿着字符串扫描,所以匹配可以不再字符串开头:

>>> print p.match('::: message')
None
>>> m = p.search('::: message'); print m  
<_sre.SRE_Match object at 0x...>
>>> m.group()
'message'
>>> m.span()
(4, 11)

在实际程序中,最常见的使用方式是把一个匹配对象保存到变量中,然后检查它是否为 None,看起来就像这样:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print 'Match found: ', m.group()
else:
    print 'No match'

模式对象的其他两个方法会找出正则表达式的所有匹配,findall() 会返回一个匹配到的子字符串的列表:

>>> p = re.compile('\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

findall() 在返回前必须创建好这整个列表。finditer() 方法则是返回 匹配对象实例的迭代器:

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator  
<callable-iterator object at 0x...>
>>> for match in iterator:
...     print match.span()
...
(0, 2)
(22, 24)
(29, 31)

模块级函数



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值