当前位置:   article > 正文

《Python进阶系列》十六:详解Python中的正则表达式_python 正则表达式 匹配提取包含换行

python 正则表达式 匹配提取包含换行

正则表达式

概述

正则表达式为高级的文本模式匹配抽取与/或文本形式的搜索和替换功能提供了基础。简单地说,正则表达式是一些由字符和特殊符号组成的字符串,它们描述了模式的重复或者表述多个字符,于是正则表达式能按照某种模式匹配一系列有相似特征的字符串。

Python 通过标准库中的 re 模块来支持正则表达式。

在Python 术语中,主要有两种方法完成模式匹配

  • “搜索”(searching),即在字符串任意部分中搜索匹配的模式;搜索通过search()函数或方法来实现。匹配通过调用match()函数或方法实现。
  • “匹配”(matching)是指判断一个字符串能否从起始处全部或者部分地匹配某个模式。

当涉及模式时,全部使用术语“匹配”;我们按照 Python 如何完成模式匹配的方式来区分“搜索”和“匹配”。

前面讲到,正则表达式是包含文本和特殊字符的字符串,该字符串描述一个可以识别各种字符串的模式。对于通用文本,用于正则表达式的字母表是所有大小写字母及数字的集合。可能也存在一些特殊字母;例如,指仅包含字符“0”和“1”的字母表。该字母表可以表示所有二进制字符串的集合,即“0”、“1”、“00”、“01”、“10”、“11”、“100”等。

把标准字母表用于通用文本,我们展示了一些简单的正则表达式以及这些模式所表述的字符串。下面所介绍的正则表达式都是最基本、最普通的。它们仅仅用一个简单的字符串构造成一个匹配字符串的模式:该字符串由正则表达式定义。

正则表达式模式匹配的字符串
foofoo
abc123abc123
PythonPython

上面的第一个正则表达式模式是“foo”。该模式没有使用任何特殊符号去匹配其他符号,而只匹配所描述的内容,所以,能够匹配这个模式的只有包含“foo”的字符串。同理,对于字符串“Python”和“abc123”也一样。

正则表达式的强大之处在于引入特殊字符来定义字符集、匹配子组和重复模式。正是由于这些特殊符号,使得正则表达式可以匹配字符串集合,而不仅仅只是某单个字符串。

特殊符号和字符

下面列出最常见的符号和字符。

符号:

表示法描述正则表达式示例
literal匹配文本字符串的字面值literalfoo
re1|re2匹配正则表达式re1 或者 re2foo|bar
.匹配任何字符(除了\n之外)b.b
^匹配字符串起始部分^Dear
$匹配字符串终止部分/bin/*sh$
*匹配 0 次或者多次前面出现的正则表达式[A-Za-z0-9]*
+匹配 1 次或者多次前面出现的正则表达式[a-z]+.com
?匹配 0 次或者 1 次前面出现的正则表达式goo?
{N}匹配 N 次前面出现的正则表达式[0-9]{3}
{M,N}匹配 M~N 次前面出现的正则表达式[0-9]{5,9}
[…]匹配来自字符集的任意单一字符[aeiou]
[..x−y..]匹配 x~y 范围中的任意单一字符[0-9], [A-Za-z]
[^…]不匹配此字符集中出现的任何一个字符,包括某一范围的字符[^aeiou], [^A-Za-z0-9]
(*|+|?|{})?用于匹配上面频繁出现/重复出现符号的非贪婪版本(*+?{}.*?[a-z]
(…)匹配封闭的正则表达式,然后另存为子组([0-9]{3})?,f(oo|u)bar

特殊字符

表示法描述正则表达式示例
\d匹配任何十进制数字,与[0-9]一致(\D\d相反,不匹配任何非数值型的数字)data\d+.txt
\w匹配任何字母数字字符,与[A-Za-z0-9_]相同(\W与之相反)[A-Za-z_]\w+
\s匹配任何空格字符,与[\n\t\r\v\f]相同(\S与之相反)of\sthe
\b匹配任何单词边界(\B与之相反)\bThe\b\N匹配已保存的子组Nprice:\16
\c逐字匹配任何特殊字符c(即仅按照字面意义匹配,不匹配特殊含义).,\,*
\A(\Z)匹配字符串的起始(结束)\ADear

使用择一匹配符号匹配多个正则表达式模式

表示择一匹配的管道符号(|),也就是键盘上的竖线,表示一个“从多个模式中选择其一”的操作。它用于分割不同的正则表达式。例如,在下面的表格中,左边是一些运用择一匹配的模式,右边是左边相应的模式所能够匹配的字符。

正则表达式模式匹配的字符串
at |homeat、home
r2d2 | c3po r2d2、c3po
bat | bet |bitbat、bet、bit

有了这个符号,就能够增强正则表达式的灵活性,使得正则表达式能够匹配多个字符串而不仅仅只是一个字符串。择一匹配有时候也称作(union)或者逻辑或(logical OR)。

匹配任意单个字符

点号或者句点(.)符号匹配除了换行符\n以外的任何字符。无论字母、数字、空格(并不包括“\n”换行符)、可打印字符、不可打印字符,还是一个符号,使用点号都能够匹配它们。

Python 正则表达式有一个编译标记[S或者DOTALL],该标记能够推翻这个限制,使点号能够匹配换行符。

正则表达式模式匹配的字符串
f.o匹配在字母“f”和“o”之间的任意一个字符;例如 fao、f9o、f#o 等
..任意两个字符
.end匹配在字符串 end 之前的任意一个字符

要显式匹配一个句点符号本身,必须使用反斜线转义句点符号的功能,例如“\.”。

从字符串起始或者结尾或者单词边界匹配

还有些符号和相关的特殊字符用于在字符串的起始和结尾部分指定用于搜索的模式。如果要匹配字符串的开始位置,就必须使用脱字符(^)或者特殊字符\A。后者主要用于那些没有脱字符的键盘(例如,某些国际键盘)。同样,美元符号($)或者\Z将用于匹配字符串的末尾位置。

使用这些符号的模式与本文描述的其他大多数模式是不同的,因为这些模式指定了位置或方位。之前的“核心提示”记录了匹配(试图在字符串的开始位置进行匹配)和搜索(试图从字符串的任何位置开始匹配)之间的差别。正因如此,下面是一些表示“边界绑定”的正则表达式搜索模式的示例。

正则表达式模式匹配的字符串
^From任何以 From 作为起始的字符串
/bin/tcsh$ 任何以 /bin/tcsh 作为结尾的字符串
^Subject: hi$任何由单独的字符串 Subject: hi 构成的字符串

再次说明,如果想要逐字匹配这些字符中的任何一个(或者全部),就必须使用反斜线进行转义。例如,如果你想要匹配任何以美元符号结尾的字符串,一个可行的正则表达式方案就是使用模式.*\$$

特殊字符\b\B 可以用来匹配字符边界。而两者的区别在于\b将用于匹配一个单词的边界,这意味着如果一个模式必须位于单词的起始部分,就不管该单词前面(单词位于字符串中间)是否有任何字符。同样,**\B**** 将匹配出现在一个单词中间的模式**(即,不是单词边界)。下面为一些示例。

正则表达式模式匹配的字符串
the任何包含 the 的字符串
\bthe 任何以 the 开始的字符串
\bthe\b仅仅匹配单词 the
\Bthe任何包含但并不以 the 作为起始的字符串

创建字符集

尽管句点可以用于匹配任意符号,但某些时候,可能想要匹配某些特定字符。正因如此,发明了方括号。该正则表达式能够匹配一对方括号中包含的任何字符。下面为一些示例。

正则表达式模式匹配的字符串
b[aeiu]t bat、bet、bit、but
[cr][23][dp][o2]一个包含四个字符的字符串,第一个字符是“c”或“r”,然后是“2”或“3”,后面是“d”或“p”,最后是“o”或者“2”。例如,c2do、r3p2、r2d2、c3po 等

方括号仅仅表示逻辑或的功能,如果仅允许“r2d2”或者“c3po” 作为有效字符串,就需要更严格限定的正则表达式。唯一的方案就是使用择一匹配,例如, r2d2|c3po

然而,对于单个字符的正则表达式,使用择一匹配和字符集是等效的。举个例子,a|b[ab]等效。

限定范围和否定

除了单字符以外,字符集还支持匹配指定的字符范围。方括号中两个符号中间用连字符(-)连接,用于指定一个字符的范围;例如,A-Za-z或者 0-9 分别用于表示大写字母、小写字母和数值数字。这是一个按照字母顺序的范围,所以不能将它们仅仅限定用于字母和十进制数字上。另外,如果脱字符(^)紧跟在左方括号后面,这个符号就表示不匹配给定字符集中的任何一个字符

正则表达式模式匹配的字符串
z.[0-9]字母“z”后面跟着任何一个字符,然后跟着一个数字
[r-u][env-y][us]字母“r”、“s”、“t”或者“u”后面跟着“e”、“n”、“v”、“w”、“x”或者“y”,然后跟着“u”或者“s”
[^aeiou]一个非元音字符
[^\t\n]不匹配制表符或者\n
[“-a]在一个 ASCII 系统中,所有字符都位于“”和“a”之间,即 34~97 之间

使用闭包操作符实现存在性和频数匹配

本节介绍最常用的正则表达式符号,即特殊符号*+?,所有这些都可以用于匹配一个、多个或者没有出现的字符串模式

  • 星号或者星号操作符(*)将匹配其左边的正则表达式出现零次或者多次的情况(在计算机编程语言和编译原理中,该操作称为 Kleene 闭包);
  • 加号(+)操作符将匹配一次或者多次出现的正则表达式(也叫做正闭包操作符);
  • 问号(?)操作符将匹配零次或者一次出现的正则表达式

还有大括号操作符({}),里面或者是单个值或者是一对由逗号分隔的值。这将最终精确地匹配前面的正则表达式 N** **(如果是{N})或者一定范围的次数;例如,{M, N}将匹配 M~N次出现。这些符号都能够由反斜线符号转义,例如\*匹配星号,等等。

注意,在之前的表格中曾经多次使用问号,这意味着要么匹配 0 次,要么匹配 1 次,或者其他含义:如果问号紧跟在任何使用闭合操作符的匹配后面,它将直接要求正则表达式引擎匹配尽可能少的次数

“尽可能少的次数”是什么意思?当模式匹配使用分组操作符时,正则表达式引擎将试图“吸收”匹配该模式的尽可能多的字符。这通常被叫做贪婪匹配。问号要求正则表达式引擎去“偷懒”,如果可能,就在当前的正则表达式中尽可能少地匹配字符,留下尽可能多的字符给后面的模式(如果存在)。

正则表达式模式匹配的字符串
[dn]ot?字母“d”或者“n”,后面跟着一个“o”,然后是最多一个“t”,例如,do、no、dot、not
0?[1-9]任何数值数字,它可能前置一个“0”,例如,匹配一系列数(表示从 1~9 月的数值),不管是一个还是两个数字
[0-9]{15,16}匹配 15 或者 16 个数字(例如信用卡号码)
</?[^>]+>匹配全部有效的(和无效的)HTML 标签
[KQRBNP][a-h][1-8]-[a-h][1-8]在“长代数”标记法中,表示国际象棋合法的棋盘移动(仅移动,不包括吃子和将军)。 即“K”、“Q”、“R”、“B”、“N”或“P”等字母后面加上“a1”~“h8”之间的棋盘坐标。前面的坐标表示从哪里开始走棋,后面的坐标代表走到哪个位置(棋格)上

表示字符集的特殊字符

与使用“0-9”这个范围表示十进制数相比,可以简单地使用 \d 表示匹配任何十进制数字。另一个特殊字符(\w)能够用于表示全部字母数字的字符集,相当于[A-Za-z0-9_]的缩写形式,\s 可以用来表示空格字符。这些特殊字符的大写版本表示不匹配;例如,\D 表示任何非十进制数(与[^0-9]相同),等等。

使用这些缩写,可以表示如下一些更复杂的示例。

正则表达式模式匹配的字符串
\w+-\d+一个由字母数字组成的字符串和一串由一个连字符分隔的数字
[A-Za-z]\w*第一个字符是字母;其余字符(如果存在)可以是字母或者数字
\d{3}-\d{3}-\d{4}美国电话号码的格式,前面是区号前缀,例如 800-555-1212
\w+@\w+\.comXXX@YYY.com 格式表示的简单电子邮件地址

使用圆括号指定分组

现在,我们已经可以实现匹配某个字符串以及丢弃不匹配的字符串,但有些时候,我们可能会对之前匹配成功的数据更感兴趣。我们不仅想要知道整个字符串是否匹配我们的标准,而且想要知道能否提取任何已经成功匹配的特定字符串或者子字符串。答案是可以,要实现这个目标,只要用一对圆括号包裹任何正则表达式。

当使用正则表达式时,一对圆括号可以实现以下任意一个(或者两个)功能:

  • 对正则表达式进行分组;
  • 匹配子组。

关于为何想要对正则表达式进行分组的一个很好的示例是:当有两个不同的正则表达式而且想用它们来比较同一个字符串时。另一个原因是对正则表达式进行分组可以在整个正则表达式中使用重复操作符(而不是一个单独的字符或者字符集)。

使用圆括号进行分组的一个副作用就是,匹配模式的子字符串可以保存起来供后续使用。这些子组能够被同一次的匹配或者搜索重复调用,或者提取出来用于后续处理。

为什么匹配子组这么重要呢?主要原因是在很多时候除了进行匹配操作以外,我们还想要提取所匹配的模式。例如,如果决定匹配模式\w+-\d+,但是想要分别保存第一部分的字母和第二部分的数字,该如何实现?我们可能想要这样做的原因是,对于任何成功的匹配,我们可能想要看到这些匹配正则表达式模式的字符串究竟是什么。

如果为两个子模式都加上圆括号,例如(\w+)-(\d+),然后就能够分别访问每一个匹配子组。我们更倾向于使用子组,这是因为择一匹配通过编写代码来判断是否匹配,然后执行另一个单独的程序(该程序也需要另行创建)来解析整个匹配仅仅用于提取两个部分。

正则表达式模式匹配的字符串
\d+(\.\d*)?表示简单浮点数的字符串;也就是说,任何十进制数字,后面可以接一个小数点和零个或者多个十进制数字,例如“0.004”、“2”、“75.”等
(Mr?s?\.)?[A-Z][a-z]*[A-Za-z-]+名字和姓氏,以及对名字的限制,全名前可以有可选的“Mr.”、“Mrs.”、“Ms.”或者“M.”作为称谓,以及灵活可选的姓氏,可以有多个单词、横线以及大写字母

正则表达式和 Python 语言

re 模块支持非常强大而且更通用的 Perl 风格(Perl5 风格)的正则表达式,该模块允许多个线程共享同一个已编译的正则表达式对象,也支持命名子组。

re 模块:核心函数和方法

下面列出来自re 模块的更多常见函数和方法。它们中的大多数函数也与已经编译的正则表达式对象(regex object)和正则匹配对象(regex match object)的方法同名并且具有相同的功能。

函数/方法函数/方法
compile(pattern,flags = 0)使用任何可选的标记来编译正则表达式的模式,然后返回一个正则表达式对象

re模块函数和正则表达式对象的方法

函数/方法函数/方法
match(pattern,string,flags=0)尝试使用带有可选标记的正则表达式的模式来匹配字符串。如果匹配成功,就返回匹配对象;如果失败,就返回 None
search(pattern,string,flags=0)使用可选标记搜索字符串中第一次出现的正则表达式模式。如果匹配成功,则返回匹配对象;如果失败,则返回 None
findall(pattern,string [,flags])查找字符串中所有(非重复)出现的正则表达式模式,并返回一个匹配列表
finditer(pattern,string [,flags])findall()函数相同,但返回的不是一个列表,而是一个迭代器。对于每一次匹配,迭代器都返回一个匹配对象
split(pattern,string,max=0)根据正则表达式的模式分隔符,split 函数将字符串分割为列表,然后返回成功匹配的列表,分隔最多操作 max 次(默认分割所有匹配成功的位置)
sub(pattern,repl,string,count=0)使用 repl 替换所有正则表达式的模式在字符串中出现的位置,除非定义 count,否则就将替换所有出现的位置
purge()清除隐式编译的正则表达式模式

常用的匹配对象方法

函数/方法函数/方法
group(num=0)返回整个匹配对象,或者编号为 num 的特定子组
groups(default=None)返回一个包含所有匹配子组的元组(如果没有成功匹配,则返回一个空元组)
groupdict(default=None)返回一个包含所有匹配的命名子组的字典,所有的子组名称作为字典的键(如果没有成功匹配,则返回一个空字典)

常用的模块属性(用于大多数正则表达式函数的标记)

函数/方法函数/方法
re.Ire.IGNORECASE不区分大小写的匹配
re.Lre.LOCALE根据所使用的本地语言环境通过\w\W\b\B\s\S 实现匹配
re.Mre.MULTILINE^$分别匹配目标字符串中行的起始和结尾,而不是严格匹配整个字符串本身的起始和结尾
re.Srer.DOTALL.”(点号)通常匹配除了\n(换行符)之外的所有单个字符;该标记表示“.”(点号)能够匹配全部字符
re.Xre.VERBOSE通过反斜线转义,否则所有空格加上#(以及在该行中所有后续文字)都被忽略,除非在一个字符类中或者允许注释并且提高可读性

核心提示:编译正则表达式(编译还是不编译?)

  • 在模式匹配发生之前,正则表达式模式必须编译成正则表达式对象。由于正则表达式在执行过程中将进行多次比较操作,因此强烈建议使用预编译。而且,既然正则表达式的编译是必需的,那么使用预编译来提升执行性能无疑是明智之举。re.compile()能够提供此功能。
  • 其实模块函数会对已编译的对象进行缓存,所以不是所有使用相同正则表达式模式的 search()match()都需要编译。即使这样,你也节省了缓存查询时间,并且不必对于相同的字符串反复进行函数调用。在不同的 Python 版本中,缓存中已编译过的正则表达式对象的数目可能不同,而且没有文档记录。purge()函数能够用于清除这些缓存。

使用compile()函数编译正则表达式

后续要介绍的几乎所有的 re 模块函数都可以作为 regex 对象的方法。注意,尽管推荐预编译,但它并不是必需的。如果需要编译,就使用编译过的方法;如果不需要编译,就使用函数。幸运的是,不管使用函数还是方法,它们的名字都是相同的。因为这在大多数示例中省去一个小步骤,所以我们将使用字符串替代。我们仍将会遇到几个预编译代码的对象,这样就可以知道它的过程是怎么回事。

对于一些特别的正则表达式编译,可选的标记可能以参数的形式给出,这些标记允许不区分大小写的匹配,使用系统的本地化设置来匹配字母数字,等等。它们可以通过按位或操作符(|)合并。 这些标记也可以作为参数适用于大多数 re 模块函数。如果想要在方法中使用这些标记,它们必须已经集成到已编译的正则表达式对象之中,或者需要使用直接嵌入到正则表达式本身的(?F)标记,其中 F 是一个或者多个i(用于 re.I/IGNORECASE)、m(用于 re.M/MULTILINE)、s(用于 re.S/DOTALL)等。如果想要同时使用多个,就把它们放在一起而不是使用按位或操作,例如,(?im)可以用于同时表示 re.IGNORECASEre.MULTILINE

匹配对象以及group()groups()方法

当处理正则表达式时,除了正则表达式对象之外,还有另一个对象类型:匹配对象。这些是成功调用 match()或者 search()返回的对象。匹配对象有两个主要的方法:group()groups()

  • group()要么返回整个匹配对象,要么根据要求返回特定子组。
  • groups()则仅返回一个包含唯一或者全部子组的元组。

如果没有子组的要求,那么当group()仍然返回整个匹配时,groups()返回一个空元组。

使用match()方法匹配字符串

match()函数试图从字符串的起始部分对模式进行匹配。

  • 如果匹配成功,就返回一个匹配对象;
  • 如果匹配失败,就返回 None。

匹配对象的 group()方法能够用于显示那个成功的匹配。

下面是如何运用match()以及 group()的一个示例:

>>> import re
>>> m = re.match('foo', 'foo')
>>> if m is not None:
       m.group()
'foo'
  • 1
  • 2
  • 3
  • 4
  • 5

如下为一个失败的匹配示例,它返回 None。

>>> m = re.match('foo', 'bar')
>>> if m is not None: m.group()

>>> m.group()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

因为上面的匹配失败,所以 m 被赋值为 None,而且以此方法构建的 if 语句没有指明任何操作。对于剩余的示例,如果可以,为了简洁起见,将省去 if 语句块,但在实际操作中,最好不要省去以避免AttributeError 异常(None是返回的错误值,该值并没有 group()属性)。

只要模式从字符串的起始部分开始匹配,即使字符串比模式长,匹配也仍然能够成功。例如,模式“foo”将在字符串“food on the table”中找到一个匹配,因为它是从字符串的起始部分进行匹配的。

>>> m = re.match('foo', 'food on the table')
>>> m.group()
'foo'
  • 1
  • 2
  • 3

可以看到,尽管字符串比模式要长,但从字符串的起始部分开始匹配就会成功。子串“foo” 是从那个比较长的字符串中抽取出来的匹配部分。 甚至可以充分利用 Python 原生的面向对象特性,忽略保存中间过程产生的结果。

>>> re.match('foo', 'food on the table').group()
'foo'
  • 1
  • 2

使用search()在一个字符串中查找模式(搜索与匹配的对比)

事实上,想要搜索的模式出现在一个字符串中间部分的概率,远大于出现在字符串起始部分的概率。
search()的工作方式与 match()完全一致,不同之处在于 search()会用它的字符串参数,在任意位置对给定正则表达式模式搜索第一次出现的匹配情况。如果搜索到成功的匹配,就会返回一个匹配对象;否则,返回 None。

search()函数不但会搜索模式在字符串中第一次出现的位置,而且严格地对字符串从左到右搜索。看一个例子:

>>> m = re.search('foo', 'seafood')
>>> m.group()
'foo'
>>> m = re.match('foo', 'seafood')
>>> m.group()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

需要注意的是,等价的正则表达式对象方法使用可选的 posendpos 参数来指定目标字符串的搜索范围。

匹配多个字符串

>>> bt = 'bat|bet|bit'
>>> m = re.match(bt, 'bat')
>>> if m is not None: m.group()
...
'bat'

>>> m = re.match(bt, 'He bit me!')
>>> if m is not None: m.group()
...
>>> m = re.search(bt, 'He bit me!')
>>> if m is not None: m.group()
...
'bit
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

匹配任何单个字符

点号(.)不能匹配一个换行符\n或者非字符,也就是说, 一个空字符串。

>>> anyend = '.end'
>>> m = re.match(anyend, 'bend')
>>> if m is not None: m.group()
...
'bend'
>>> m = re.match(anyend, 'end')
>>> if m is not None: m.group()  # 不匹配空字符
...
>>> m = re.match(anyend, '\nend') 
>>> if m is not None: m.group() # 不匹配\n
...
>>> m = re.search(anyend, 'The end.')
>>> if m is not None: m.group()
...
' end'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在正则表达式中搜索一个真正的句点(小数点),而我们通过使用一个反斜线对句点的功能进行转义:

>>> m = re.match('3\.14', '3.14')
>>> if m is not None: m.group()
...
'3.14'
>>> m = re.match('3.14', '3214')
>>> if m is not None: m.group()
...
'3214'
>>> m = re.match('3.14', '3.14')
>>> if m is not None: m.group()
...
'3.14'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

创建字符集([]

对于 r2d2|c3po 的限制将比[cr][23][dp][o2]更为严格。

>>> m = re.match('[cr][23][dp][o2]', 'c3po')
>>> if m is not None: m.group()
...
'c3po'
>>> m = re.match('[cr][23][dp][o2]', 'c3do')
>>> if m is not None: m.group()
...
'c3do'
>>> m = re.match('r2d2|c3po', 'c2do')
>>> if m is not None: m.group()
...
>>> m = re.match('r2d2|c3po', 'r2d2')
>>> if m is not None: m.group()
...
'r2d2'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

重复、特殊字符以及分组

正则表达式中最常见的情况包括特殊字符的使用、正则表达式模式的重复出现,以及使用圆括号对匹配模式的各部分进行分组和提取操作。

上文中提到了一个关于简单电子邮件地址的正则表达式(\w+@\w+\.com)。或许我们想要匹配比这个正则表达式所允许的更多邮件地址。 为了在域名前添加主机名称支持,例如www.xxx.com,仅仅允许 xxx.com 作为整个域名,必须修改现有的正则表达式。为了表示主机名是可选的,需要创建一个模式来匹配主机名,使用“?”操作符来表示该模式出现零次或者一次,然后按照如下所示的方式, 插入可选的正则表达式到之前的正则表达式中:\w+@(\w+\.)?\w+\.com。从下面的示例中可见, 该表达式允许.com 前面有一个或者两个名称:

>>> patt = '\w+@(\w+\.)?\w+\.com'
>>> re.match(patt, 'gaga@xxx.com').group()
'gaga@xxx.com'
>>> re.match(patt, 'gaga@www.xxx.com').group()
'gaga@www.xxx.com'
  • 1
  • 2
  • 3
  • 4
  • 5

接着,用以下模式来进一步扩展该示例,允许任意数量的中间子域名存在。将“?”改为“*”: “\w+@(\w+\.)*\w+\.com”。

>>> patt = '\w+@(\w+\.)*\w+\.com'
>>> re.match(patt, 'gaga@www.xxx.yyy.zz.com').group()
'gaga@www.xxx.yyy.zz.com'
  • 1
  • 2
  • 3

之前讨论过使用圆括号来匹配和保存子组,以便于后续处理,而不是确定一个正则表达式匹配之后,在一个单独的子程序里面手动编码来解析字符串。比如一个简单的正则表达式模式\w+-\d+,它由连字符号分隔的字母数字字符串和数字组成,可以通过添加()来添加一个子组,构造一个新的正则表达式 (\w+)-(\d+)来完成这项工作。

下面的例子说明了如何使用group()方法访问每个独立的子组以及groups()方法以获取一 个包含所有匹配子组的元组。

>>> m = re.match('(\w+)-(\d+)', 'abc-123')
>>> m.group()
'abc-123'
>>> m.group(1)
'abc'
>>> m.group(2)
'123'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

由以上代码可见,group()通常用于以普通方式显示所有的匹配部分,但也能用于获取各个匹配的子组。可以使用groups()方法来获取一个包含所有匹配子字符串的元组。

>>> m = re.match('ab', 'ab')
>>> m.group()
'ab'
>>> m.groups()  # 没有子组
()

>>> m = re.match('(ab)', 'ab')
>>> m.group()
'ab'
>>> m.group(1)
'ab'
>>> m.groups()  # 一个子组
('ab',)

>>> m = re.match('(a)(b)', 'ab')
>>> m.group()
'ab'
>>> m.groups()  # 两个子组
('a', 'b')

>>> m = re.match('(a(b))', 'ab')
>>> m.group()
'ab'
>>> m.groups()  # 两个子组
('ab', 'b') 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

匹配字符串的起始和结尾以及单词边界

如下示例突出显示表示位置的正则表达式操作符。该操作符更多用于表示搜索而不是匹配,因为match()总是从字符串开始位置进行匹配。

>>> m = re.search('^The', 'The end.')
>>> if m is not None: m.group()
...
'The'
>>> m = re.search('^The', 'end. The')
>>> if m is not None: m.group()
...
>>> m = re.search(r'\bthe', 'bite the dog') # 在边界
>>> if m is not None: m.group()
...
'the'
>>> m = re.search(r'\bthe', 'bitethe dog') # 有边界
>>> if m is not None: m.group()
...
>>> m = re.search(r'\Bthe', 'bitethe dog') # 没有边界
>>> if m is not None: m.group()
...
'the'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

核心提示:使用 Python 原始字符串

  • 正则表达式对于探索原始字符串有着强大的动力,原因就在于 ASCII 字符和正则表达式的特殊字符之间存在冲突。作为 一个特殊符号,\b表示 ASCII 字符的退格符,但是\b同时也是一个正则表达式的特殊符号, 表示匹配一个单词的边界。对于正则表达式编译器而言,若它把两个\b视为字符串内容而不是单个退格符,就需要在字符串中再使用一个反斜线转义反斜线,就像这样:\b
  • 这样显得略微杂乱,特别是如果在字符串中拥有很多特殊字符,就会让人感到更加困惑。 而该原始字符串可以用于(且经常用于)帮助保持正则表达式查找某些可托管的东西。事实上,很多 Python 程序员总是抱怨这个方法,仅仅用原始字符串来定义正则表达式 。
>>> m = re.match('\bblow', 'blow')
>>> if m: m.group()
...
>>> m = re.match('\bblow', 'blow')
>>> if m: m.group()
...
'blow'
>>> m = re.match(r'\bblow', 'blow')
>>> if m: m.group()
...
'blow'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

使用 findall()和 finditer()查找每一次出现的位置

findall()查询字符串中某个正则表达式模式全部的非重复出现情况。这与search()在执行字符串搜索时类似,但与match()search()的不同之处在于,findall()总是返回一个列表。如果 findall()没有找到匹配的部分,就返回一个空列表,但如果匹配成功,列表将包含所有成功的匹配部分(从左向右按出现顺序排列)。

>>> re.findall('car', 'car')
['car']
>>> re.findall('car', 'scary')
['car']
>>> re.findall('car', 'carry the barcardi to the car')
['car', 'car', 'car']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

子组在一个更复杂的返回列表中搜索结果,而且这样做是有意义的,因为子组是允许单个正则表达式中抽取特定模式的一种机制,例如匹配一个完整电话号码中的一部分(例如区号),或者完整电子邮件地址的一部分(例如登录名称)。

对于一个成功的匹配,每个子组匹配是由findall()返回的结果列表中的单一元素;对于多个成功的匹配,每个子组匹配是返回的一个元组中的单一元素,而且每个元组(每个元组都对应一个成功的匹配)是结果列表中的元素。

finditer()函数是一个与findall()函数类似但是更节省内存的变体。两者之间以及和其他变体函数之间的差异在于,和返回的匹配字符串相比,finditer()在匹配对象中迭代。如下是在单个字符串中两个不同分组之间的差别。

>>> re.findall(r'(th\w+) and (th\w+)', s, re.I)
[('This', 'that')]

>>> s = 'This and that.'
>>> [m.groups() for m in re.finditer(r'(th\w+) and (th\w+)', s, re.I)]
[('This', 'that')]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在单个字符串中执行单个分组的多重匹配:

>>> [g.group(1) for g in re.finditer(r'(th\w+)', s, re.I)]
['This', 'that']
  • 1
  • 2

注意,使用finditer()函数完成的所有额外工作都旨在获取它的输出来匹配findall()的输出。

最后 ,与match()search()类似,findall()finditer()方法的版本支持可选的 posendpos 参数,这两个参数用于控制目标字符串的搜索边界。

使用sub()subn()搜索与替换

有两个函数/方法用于实现搜索和替换功能:sub()subn()。两者几乎一样,都是将某字符串中所有匹配正则表达式的部分进行某种形式的替换。用来替换的部分通常是一个字符串, 但它也可能是一个函数,该函数返回一个用来替换的字符串。subn()sub()一样,但 subn() 还返回一个表示替换的总数,替换后的字符串和表示替换总数的数字一起作为一个拥有两个元素的元组返回。

>>> re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
'attn: Mr. Smith\n\nDear Mr. Smith,\n'
>>> re.subn('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
('attn: Mr. Smith\n\nDear Mr. Smith,\n', 2)

>>> re.sub('[ae]', 'X', 'abcdef')
'XbcdXf'
>>> re.subn('[ae]', 'X', 'abcdef')
('XbcdXf', 2)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

前面讲到,使用匹配对象的group()方法除了能够取出匹配分组编号外,还可以使用\N, 其中 N 是在替换字符串中使用的分组编号。下面的代码仅仅只是将美式的日期表示法 MM/DD/YY{,YY}格式转换为其他国家常用的格式DD/MM/YY{,YY}

>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})', r'\2/\1/\3', '2/20/91')
'20/2/91'
>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})', r'\2/\1/\3', '2/20/1991')
'20/2/1991'
  • 1
  • 2
  • 3
  • 4

在限定模式上使用split()分隔字符串

re模块和正则表达式的对象方法split()对于相对应字符串的工作方式是类似的,但是与分割一个固定字符串相比,它们基于正则表达式的模式分隔字符串,为字符串分隔功能添加 一些额外的威力。如果你不想为每次模式的出现都分割字符串,就可以通过为 max 参数设定 一个值(非零)来指定最大分割数。
如果给定分隔符不是使用特殊符号来匹配多重模式的正则表达式,那么re.split()str.split()的工作方式相同,如下所示(基于单引号分割)。

>>> re.split(':', 'str1:str2:str3')
['str1', 'str2', 'str3']
  • 1
  • 2

对于一个复杂的例子,例如,一个用于 Web 站点的简单解析器,该如何实现?用户需要输入城市和州名,或者城市名加上 ZIP 编码,还是三者同时输入?这就需要比仅仅是普通字符串分割更强大的处理方式,具体如下。

>>> DATA = (
... 'Mountain View, CA 94040',
... 'Sunnyvale, CA',
... 'Los Altos, 94023',
... 'Cupertino 95014',
... 'Palo Alto CA',
... )
>>> for datum in DATA:
... print(re.split(', |(?= (?:\d{5}|[A-Z]{2})) ', datum)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

参考

  • 《Python核心编程 第三版》
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/113619
推荐阅读
相关标签
  

闽ICP备14008679号