首页 > 技术文章 > python的re模块学习

songbiao 2020-03-07 22:33 原文

python的re模块学习

引言

越来越体会到,正则表达式,除了可读性差,写表达式困难以外,没有其他任何缺点了。

如果能熟练使用正则表达式,对于处理字符串,提取感兴趣的内容,绝对是效率神器

之前每次用到正则表达式时,都是现查用法和写测试表达式,不够系统,所以这里研究一下python标准库re的说明文档,系统总结一下re模块的用法。内容逻辑如下:

  1. 首先是搞清楚正则表达式的语法,目的是学会编写正则表达式(即模式对象),掌握模式对象的方法和属性。模式对象使用以后,就可以获得匹配对象。

  2. 接着就是搞清楚匹配对象的方法和属性。模式对象和匹配对象掌握以后,正则表达式的核心内容就结束了。

因为python的哲学是简洁明了,所以python标准库中有一个re模块,该模块中提供了几个主要函数,可以更为方便地操作正则对象和匹配对象。然后介绍一下正则模式中可以使用的一些编译标志,编译标志可以补充正则模式匹配时的行为。

最后介绍几个python中正则表达式使用的例子。

正则表达式语法

一个正则表达式(称为RE,或正则,或正则表达式模式)就是一个字符串,只是这个字符串可以指定与之匹配的一组内容。一个正则表达式,经过编译后就是一个模式对象。

因为是字符串,所以正则表达式也是可以拼接的,也可以使用字符串的format()函数,这一点对正则表达式中包含变量的情况很实用。

简单模式

  • 大多数字母和字符只会匹配自己。 例如,正则表达式 test 将完全匹配字符串 test

  • 一些字符是特殊的 metacharacters(元字符) ,并不匹配自己。 相反,它们匹配一些与众不同的东西,这是学习的重点和难点。

匹配字符——元字符

这是元字符的完整列表:

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

下面逐一介绍元字符的使用:

  1. 第一个介绍的元字符是 [] 。 它们用于指定字符类,它是你希望匹配的一组字符。 可以单独列出字符,也可以通过给出两个字符并用 '-' 标记一个范围来表示一系列字符。 例如, [abc] 将匹配任何字符 abc ;这与使用一个范围 [a-c] 来表示的相同。 如果你只想匹配小写字母,那正则是 [a-z]

    • 注意,在字符类中再使用元字符的话,元字符就表示元字符本身的含义了。例如,[akm$] 将匹配 'a''k''m''$' 中的任意字符; '$' 通常是一个元字符,但在一个字符类中它被剥夺了特殊性。

    • 再一个特例是,你可以通过以下方式互补达到只匹配字符类中未列出的字符,用法就是将 '^' 作为该类的第一个字符。 例如,[^5] 将匹配除 '5' 之外的任何字符。 如果插入符出现在字符类的其他位置,则它没有特殊含义。 例如,[5^] 将匹配 '5''^'

  2. 最重要的元字符可能是反斜杠\。 反斜杠后面可以跟各种字符,以指示各种特殊序列。它也用于转义所有元字符,实现在模式中匹配元字符本身。例如,如果你需要匹配 [\,你可以在它们前面加一个反斜杠来移除它们的特殊含义,\[\\。一些以 '\' 开头的特殊序列表示常用的预定义字符集,例如数字集、字母集或任何非空格的集合,这些集合使用的cheatsheet见regex在线测试工具regexr

  3. 反斜杠灾难。从上述2中介绍可以知道,正则表达式使用反斜杠字符 ('\') 来表示特殊形式或允许使用特殊字符而不调用它们的特殊含义。 这与 Python 在字符串文字中对反斜杠字符 ('\')的使用目的相冲突。解决方案是使用 Python 的原始字符串表示法来表示正则表达式,字符串前缀加上 'r' ,字符串中出现的反斜杠不做任何特殊方式处理。因此 r"\n" 是一个包含 '\''n' 的双字符字符串,而 "\n" 是一个包含换行符的单字符字符串。

  4. 元字符 . 。 它匹配除换行符之外的任何内容,并且有一个可选模式( re.DOTALL )设置后可以匹配换行符。 . 常用于你想匹配“任何字符”的地方。

  5. 元字符 |。或者“or”运算符。 如果 AB 是正则表达式,A|B 将匹配任何与 AB 匹配的字符串。 | 的优先级非常低。 Crow|Servo 将匹配 'Crow''Servo',而不是 'Cro''w''S''ervo'。如果要匹配字面的 '|',请使用 \|,或将其括在字符类中,如 [|]

  6. 元字符 ^ 。在行的开头匹配。 除非设置了 MULTILINE 标志,否则只会在字符串的开头匹配。 在 MULTILINE 模式下,就在字符串中的每个换行符后立即匹配。如果要匹配字面的 '^',使用 \^

  7. 元字符 $ 。 匹配行的末尾,定义为字符串的结尾,或者后跟换行符的任何位置。如果要匹配字面的 '$',使用 \$ 或者 [$]

  8. \A ,仅匹配字符串的开头。 当不在 MULTILINE 模式时,\A^ 实际上是相同的。 在 MULTILINE 模式中,它们是不同的: \A 仍然只在字符串的开头匹配,但 ^ 可以匹配在换行符之后的字符串内的任何位置。

  9. \Z,只匹配字符串尾。

  10. \b,字边界。 这是一个零宽度断言,仅在单词的开头或结尾处匹配。 单词被定义为一个字母数字字符序列,因此单词的结尾由空格或非字母数字字符表示。

  11. \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 表示退格字符。

重复 次数的设置

能够匹配不同的字符集合是正则表达式可以做的第一件事,这已经是字符串可用的方法很难实现的。

但是,这还不是正则表达式的唯一优势, 另一个强大功能是你可以指定正则的某些部分必须重复一定次数。

  1. 第一个重复的元字符是 *。它指定前一个字符可以匹配零次或多次,而不是恰好一次。
  2. 第二个重复的元字符是 +,它指定前一个字符可以匹配一次或多次。 要特别注意 *+ 之间的区别;* 匹配 零次 或更多次,因此重复的东西可能根本不存在,而 + 至少需要 一次。 使用类似的例子,ca+t 将匹配 'cat' (1 个 'a'),'caaat' (3 个 'a'),但不会匹配 'ct'
  3. 重复限定符,问号字符 ? ,它匹配一次或零次。你可以把它想象成是可选的。 例如,home-?brew 匹配 'homebrew''home-brew'
  4. 最复杂的重复限定符是 {m,n},其中 mn 是十进制整数。 这个限定符意味着必须至少重复 m 次,最多重复 n 次。 例如,a/{1,3}b 将匹配 'a/b''a//b''a///b' 。 它不匹配没有斜线的 'ab',或者有四个的 'a////b'。你可以省略 mn,在这种情况下,将假定缺失值的合理值。 省略 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()只返回匹配到的第一个对象。

有两种方法返回模式的所有匹配项。

>>> p = re.compile(r'\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']
>>> 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 限定了字符串搜索的范围。所以只有从 posendpos - 1 的字符会被匹配。

  • re.match(*pattern*, *string*, *flags=0*)

    等价于Pattern.match(*string*[, *pos*[, *endpos*]]) ,如果 string开始位置 能够找到这个正则样式的任意个匹配,就返回一个相应的 匹配对象。如果不匹配,就返回 None 。可选参数 posendpossearch() 含义相同。

  • re.fullmatch(*pattern*, *string*, *flags=0*)

    等价于Pattern.fullmatch(*string*[, *pos*[, *endpos*]]) ,如果整个 string 匹配这个正则表达式,就返回一个相应的 匹配对象 。 否则就返回 None

  • re.findall(*pattern*, *string*, *flags=0*)

    等价于Pattern.findall(*string*[, *pos*[, *endpos*]]) ,也可以接收可选参数 posendpos ,限制搜索范围。返回一个列表。

  • re.finditer(*pattern*, *string*, *flags=0*)

    等价于Pattern.finditer(*string*[, *pos*[, *endpos*]]) ,也可以接收可选参数 posendpos ,限制搜索范围。返回一个可迭代对象。

  • 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 设置 IM 标志。

这是一个可用标志表,以及每个标志的更详细说明。

标志 意义
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"\\"

致谢

推荐阅读