越来越体会到,正则表达式,除了可读性差,写表达式困难以外,没有其他任何缺点了。
如果能熟练使用正则表达式,对于处理字符串,提取感兴趣的内容,绝对是效率神器。
引言
越来越体会到,正则表达式,除了可读性差,写表达式困难以外,没有其他任何缺点了。
如果能熟练使用正则表达式,对于处理字符串,提取感兴趣的内容,绝对是效率神器。
之前每次用到正则表达式时,都是现查用法和写测试表达式,不够系统,所以这里研究一下python标准库re的说明文档,进行系统总结一下re模块的内容和用法。内容逻辑如下:
-
首先是搞清楚正则表达式的语法,目的是学会编写正则表达式(即模式对象),掌握模式对象的方法和属性。模式对象使用以后,就可以获得匹配对象。
-
接着就是搞清楚匹配对象的方法和属性。模式对象和匹配对象掌握以后,正则表达式的内容就结束了。
因为python的哲学是简洁明了,所以python标准库中有一个re模块,该模块中提供了几个主要函数,可以更为方便地操作正则对象和匹配对象。再介绍一下正则模式中可以使用的一些编译标志。
最后介绍几个python中正则表达式使用的例子。
正则表达式语法
一个正则表达式(称为RE,或正则,或正则表达式模式)就是一个字符串,只是这个字符串可以指定与之匹配的内容。一个正则表达式,经过编译后就是一个模式对象。
因为是字符串,所以正则表达式也是可以拼接的,也可以使用字符串的format()函数,这一点对正则表达式中包含变量的情况很实用。
简单模式
-
大多数字母和字符只会匹配自己。 例如,正则表达式
test
将完全匹配字符串test
。 -
一些字符是特殊的 metacharacters(元字符) ,并不匹配自己。 相反,它们匹配一些与众不同的东西,这是学习的重点和难点。
匹配字符——元字符
这是元字符的完整列表:
. ^ $ * + ? { } [ ] \ | ( )
下面逐一介绍元字符的使用:
-
第一个介绍的元字符是
[
和]
。 它们用于指定字符类,它是你希望匹配的一组字符。 可以单独列出字符,也可以通过给出两个字符并用'-'
标记一个范围来表示一系列字符。 例如,[abc]
将匹配任何字符a
、b
或c
;这与使用一个范围[a-c]
来表示的相同。 如果你只想匹配小写字母,那正则是[a-z]
。-
注意,在字符类中再使用元字符的话,元字符就表示元字符本身的含义了。例如,
[akm$]
将匹配'a'
,'k'
、'm'
或'$'
中的任意字符;'$'
通常是一个元字符,但在一个字符类中它被剥夺了特殊性。 -
再一个特例是,你可以通过以下方式互补匹配字符类中未列出的字符,用法就是将
'^'
作为该类的第一个字符。 例如,[^5]
将匹配除'5'
之外的任何字符。 如果插入符出现在字符类的其他位置,则它没有特殊含义。 例如,[5^]
将匹配'5'
或'^'
。
-
-
最重要的元字符可能是反斜杠
\
。 反斜杠后面可以跟各种字符,以指示各种特殊序列。它也用于转义所有元字符,实现在模式中匹配元字符本身。例如,如果你需要匹配[
或\
,你可以在它们前面加一个反斜杠来移除它们的特殊含义,\[
或\\
。一些以'\'
开头的特殊序列表示常用的预定义字符集,例如数字集、字母集或任何非空格的集合。 -
反斜杠灾难。从上述2中介绍可以知道,正则表达式使用反斜杠字符 (
'\'
) 来表示特殊形式或允许使用特殊字符而不调用它们的特殊含义。 这与 Python 在字符串文字中对反斜杠字符 ('\'
)的使用目的相冲突。解决方案是使用 Python 的原始字符串表示法来表示正则表达式,字符串前缀加上'r'
,字符串中出现的反斜杠不做任何特殊方式处理。因此r"\n"
是一个包含'\'
和'n'
的双字符字符串,而"\n"
是一个包含换行符的单字符字符串。 -
元字符
.
。 它匹配除换行符之外的任何内容,并且有一个可选模式(re.DOTALL
)设置后可以匹配换行符。.
常用于你想匹配“任何字符”的地方。 -
元字符
|
。或者“or”运算符。 如果 A 和 B 是正则表达式,A|B
将匹配任何与 A 或 B 匹配的字符串。|
具有非常低的优先级。Crow|Servo
将匹配'Crow'
或'Servo'
,而不是'Cro'
、'w'
或'S'
和'ervo'
。如果要匹配字面的'|'
,请使用\|
,或将其括在字符类中,如[|]
。 -
元字符
^
。在行的开头匹配。 除非设置了MULTILINE
标志,否则只会在字符串的开头匹配。 在MULTILINE
模式下,就在字符串中的每个换行符后立即匹配。如果要匹配字面的'^'
,使用\^
。 -
元字符
$
。 匹配行的末尾,定义为字符串的结尾,或者后跟换行符的任何位置。如果要匹配字面的'$'
,使用\$
或者将其括在字符类中,例如[$]
。 -
\A
,仅匹配字符串的开头。 当不在MULTILINE
模式时,\A
和^
实际上是相同的。 在MULTILINE
模式中,它们是不同的:\A
仍然只在字符串的开头匹配,但^
可以匹配在换行符之后的字符串内的任何位置。 -
\Z
,只匹配字符串尾。 -
\b
,字边界。 这是一个零宽度断言,仅在单词的开头或结尾处匹配。 单词被定义为一个字母数字字符序列,因此单词的结尾由空格或非字母数字字符表示。 -
\B
, 是另一个零宽度断言,这与\b
相反,仅在当前位置不在字边界时才匹配。
对\b
使用的代码举例如下:
>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))
<re.Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None
对于上述\b
的使用,需要注意两点,第一,要带有raw字符串的标志 r
; 第二,在一个字符类中,这个断言没有用处,\b
表示退格字符。
重复 次数的设置
能够匹配不同的字符集合是正则表达式可以做的第一件事,这已经是字符串可用的方法很难实现的。
但是,这还不是正则表达式的唯一优势, 另一个强大功能是你可以指定正则的某些部分必须重复一定次数。
- 第一个重复的元字符是
*
。它指定前一个字符可以匹配零次或多次,而不是恰好一次。 - 第二个重复的元字符是
+
,它指定前一个字符可以匹配一次或多次。 要特别注意*
和+
之间的区别;*
匹配 零次 或更多次,因此重复的东西可能根本不存在,而+
至少需要 一次。 使用类似的例子,ca+t
将匹配'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'
,或者有四个的'a////b'
。你可以省略 m 或 n,在这种情况下,将假定缺失值的合理值。 省略 m 被解释为 0 下限,而省略 n 则为无穷大的上限。你可能会注意到上面三个其他限定符都可以用这种表示法表达。{0,}
与*
相同,{1,}
相当于+
,{0,1}
和?
相同。 但是建议你最好使用*
,+
或?
,因为它们更短更容易阅读。
分组
正则表达式可以通过将正则分成几个子组来解析字符串,这些子组匹配不同的感兴趣组件。 组由 '('
,')'
元字符标记。 '('
和 ')'
与数学表达式的含义大致相同;它们将包含在其中的表达式组合在一起,你可以使用重复限定符重复组的内容,例如 *
,+
,?
或 {m,n}
。 例如,(ab)*
将匹配 ab
的零次或多次重复。
>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)
模式对象的方法和属性
一旦你按照上面的正则表达式语法,写好了一个编译为正则表达式的模式对象,你可以用它做什么? 模式对象有几种方法和属性。 这里只介绍最重要的内容;
方法 / 属性 | 目的 |
---|---|
match() |
确定正则是否从字符串的开头匹配。 |
search() |
扫描字符串,查找此正则匹配的任何位置。 |
findall() |
找到正则匹配的所有子字符串,并将它们作为列表返回。 |
finditer() |
找到正则匹配的所有子字符串,并将它们返回为一个 iterator。 |
如果没有找到匹配, match()
和 search()
返回 None
。如果它们成功, 一个 匹配对象 实例将被返回,包含匹配相关的信息:起始和终结位置、匹配的子串以及其它。你应该将结果储存到一个变量中以供稍后使用。
代码举例:
>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')
>>> p.match("") # 空字符串根本不匹配,因为 + 表示“一次或多次重复”。 match() 在这种情况下应返回 None,这将导致解释器不打印输出。 你可以显式打印 match() 的结果,使其清晰。
>>> print(p.match(""))
None
>>> m = p.match('tempo')
>>> m
<re.Match object; span=(0, 5), match='tempo'>
下面紧接着介绍一下对匹配对象的操作。
匹配对象的方法和属性
匹配对象实例也有几个方法和属性;最重要的是:
方法 / 属性 | 目的 |
---|---|
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()
将始终为零。
但是,模式的 search()
方法会扫描字符串,因此在这种情况下匹配可能不会从零开始。
>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)
<re.Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)
在实际的程序中,通常将 匹配对象 结果存储在变量中,然后检查它是否为 None
。代码类似如下:
p = re.compile( ... ) # put in your regex string
m = p.match( 'string goes here' ) # put your strings
if m:
print('Match found: ', m.group())
else:
print('No match')
上面的search()和match()只返回匹配到的第一个对象。
有两种方法返回模式的所有匹配项。
findall()
返回匹配字符串的列表:
>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']
finditer()
方法将一个 匹配对象 的序列返回为一个 iterator
>>> 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)
re模块中的函数
介绍完了上述的模式对象和匹配对象的方法和属性,不过你不是必须要创建模式对象并调用其方法,re模块提供了match()
,search()
,findall()
,sub()
等函数。 这些函数采用与相应模式方法相同的参数,并将正则模式作为第一个参数添加,并仍然返回 None
或 匹配对象 实例。
>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')
<re.Match object; span=(0, 5), match='From '>
-
re.search(*pattern*, *string*, *flags=0*)
等价于
Pattern.search(*string*[, *pos*[, *endpos*]])
, 扫描整个 string 寻找第一个匹配的位置, 并返回一个相应的 匹配对象。如果没有匹配,就返回None
。可选的第二个参数 pos 和参数 endpos 限定了字符串搜索的范围。所以只有从pos
到endpos - 1
的字符会被匹配。 -
re.match(*pattern*, *string*, *flags=0*)
等价于
Pattern.match(*string*[, *pos*[, *endpos*]])
,如果 string 的 开始位置 能够找到这个正则样式的任意个匹配,就返回一个相应的 匹配对象。如果不匹配,就返回None
。可选参数 pos 和 endpos 与search()
含义相同。 -
re.fullmatch(*pattern*, *string*, *flags=0*)
等价于
Pattern.fullmatch(*string*[, *pos*[, *endpos*]])
,如果整个 string 匹配这个正则表达式,就返回一个相应的 匹配对象 。 否则就返回None
。 -
re.findall(*pattern*, *string*, *flags=0*)
等价于
Pattern.findall(*string*[, *pos*[, *endpos*]])
,也可以接收可选参数 pos 和 endpos ,限制搜索范围。返回一个列表。 -
re.finditer(*pattern*, *string*, *flags=0*)
等价于
Pattern.finditer(*string*[, *pos*[, *endpos*]])
,也可以接收可选参数 pos 和 endpos ,限制搜索范围。返回一个可迭代对象。 -
re.sub(*pattern*, *repl*, *string*, *count=0*, *flags=0*)
等价于
Pattern.sub(*repl*, *string*, *count=0*)
,repl参数表示要替换为的内容,可以是字符串,也可以是函数。count表示要替换的次数。 -
re.subn(*pattern*, *repl*, *string*, *count=0*, *flags=0*)
等价于
Pattern.subn(*repl*, *string*, *count=0*)
-
re.split(*pattern*, *string*, *maxsplit=0*, *flags=0*)
等价于
Pattern.split(*string*, *maxsplit=0*)
, maxsplit参数设置为最大分割数。
本质上,这些函数只是为你创建一个模式对象,并在其上调用适当的方法。使用函数,和自己获取模式并调用其方法,这两种用法,没有太大的区别,都可以。
这里举一个例子,就是sub函数中,要替换的内容是一个函数。示例中,替换函数将小数转换为十六进制:
>>> def hexrepl(match):
... "Return the hex string for a decimal number"
... value = int(match.group())
... return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'
编译标志
编译标志允许你修改正则表达式的工作方式。
编译标志在 re
模块中有两个写法,长名称如 IGNORECASE
和一个简短的单字母形式,例如 I
。
多个标志可以 通过按位或运算来指定它们;例如,re.I | re.M
设置 I
和 M
标志。
这是一个可用标志表,以及每个标志的更详细说明。
标志 | 意义 |
---|---|
ASCII , A |
使几个转义如 \w 、\b 、\s 和 \d 匹配仅与具有相应特征属性的 ASCII 字符匹配。 |
DOTALL , S |
使 . 匹配任何字符,包括换行符。 |
IGNORECASE , I |
进行大小写不敏感匹配。 |
LOCALE , L |
进行区域设置感知匹配。 |
MULTILINE , M |
多行匹配,影响 ^ 和 $ 。 |
VERBOSE , X (为 ‘扩展’) |
启用详细的正则,可以更清晰,更容易理解。 |
例如,这里的正则使用 re.VERBOSE,看看是否更容易阅读:
charref = re.compile(r"""
&[#] # Start of a numeric entity reference
(
0[0-7]+ # Octal form
| [0-9]+ # Decimal form
| x[0-9a-fA-F]+ # Hexadecimal form
)
; # Trailing semicolon
""", re.VERBOSE)
如果没有详细设置,正则将如下所示:
charref = re.compile("&#(0[0-7]+"
"|[0-9]+"
"|x[0-9a-fA-F]+);")
在上面的例子中,Python的字符串文字的自动连接已被用于将正则分解为更小的部分,但它仍然比以下使用 re.VERBOSE
版本更难理解。
正则表达式例子
- 建立一个电话本的例子
>>> text = """Ross McFluff: 834.345.1254 155 Elm Street
...
... Ronald Heathmore: 892.345.3428 436 Finley Avenue
... Frank Burger: 925.541.7625 662 South Dogwood Way
...
...
... Heather Albrecht: 548.326.4584 919 Park Place"""
>>> entries = re.split("\n+", text)
>>> entries
['Ross McFluff: 834.345.1254 155 Elm Street',
'Ronald Heathmore: 892.345.3428 436 Finley Avenue',
'Frank Burger: 925.541.7625 662 South Dogwood Way',
'Heather Albrecht: 548.326.4584 919 Park Place']
>>> [re.split(":? ", entry, 4) for entry in entries]
[['Ross', 'McFluff', '834.345.1254', '155', 'Elm Street'],
['Ronald', 'Heathmore', '892.345.3428', '436', 'Finley Avenue'],
['Frank', 'Burger', '925.541.7625', '662', 'South Dogwood Way'],
['Heather', 'Albrecht', '548.326.4584', '919', 'Park Place']]
- 找到所有副词 (findall()函数的使用)
>>> text = "He was carefully disguised but captured quickly by police."
>>> re.findall(r"\w+ly", text)
['carefully', 'quickly']
- 找到所有副词和位置 (finditer()函数的使用)
>>> text = "He was carefully disguised but captured quickly by police."
>>> for m in re.finditer(r"\w+ly", text):
... print('%02d-%02d: %s' % (m.start(), m.end(), m.group(0)))
07-16: carefully
40-47: quickly
- 随机调整句子中每个单词中中间字符的顺序(即不改变首尾字符)
>>> def repl(m):
... inner_word = list(m.group(2))
... random.shuffle(inner_word)
... return m.group(1) + "".join(inner_word) + m.group(3)
>>> text = "Professor Abdolmalek, please report your absences promptly."
>>> re.sub(r"(\w)(\w+)(\w)", repl, text)
'Poefsrosr Aealmlobdk, pslaee reorpt your abnseces plmrptoy.'
>>> re.sub(r"(\w)(\w+)(\w)", repl, text)
'Pofsroser Aodlambelk, plasee reoprt yuor asnebces potlmrpy.'
- 原始字符记法
原始字符串记法 (r"text"
) 保持正则表达式正常。比如,下面两行代码功能就是完全一致的:
>>> re.match(r"\W(.)\1\W", " ff ")
<re.Match object; span=(0, 4), match=' ff '>
>>> re.match("\\W(.)\\1\\W", " ff ")
<re.Match object; span=(0, 4), match=' ff '>
而当需要匹配一个反斜杠字符时,它必须在正则表达式中转义。在原始字符串记法,就是 r"\\"
。