Python Module之re-正则表达式

模块目的:使用形式化的模式来搜索字符串或者改变字符串。

正则表达式是由一些形式化的语法规则来描述的文本匹配模式。这些模式被解释成一个指令集,然后使用一个字符串作为输入,最后产生匹配分组或者被修改过的原始字符串。“正则表达式”(regular expressions)为了方便常常被简化说成“正则”(regex 或 regexp)。正则表达式可以包括原始字符匹配、重复、组合模式、分支和其他一些复杂的规则。很多文本分析问题可以很方便地使用正则表达式来解决,而无须自己去创建特殊的解析器或分析器。

正则表达式常常被用在那些包含很多文本处理的应用程序中。比如,它们被开发工程师作为搜索模式而用在文本编辑程序中,包括vi,emacs和一些现代的IDE中。它们也被嵌入Unix的一些命令行工具中,比如sed,grep和awk。许多编程语言在语法上默认支持正则表达式,比如Perl,Ruby,Awk和Tcl。其他的一些语言比如C、C++和Python,通过扩展库支持正则表达式。

存在许多开源的正则表达式的实现,每个实现都共享一个共有的核心语法规则,但是不同的实现又有不同的扩展和修改。Python中re模块的语法规则是基于Perl语言中的语法,并加入了一些Python自己的扩展。


模式文本搜索

re模块最常用的用途就是在文本中搜索指定模式。search()方法使用模式和要搜索的文本作为输入,如果搜索到就返回一个Match对象,如果没有搜索到该模式,就返回None

每一个Match对象保存了匹配的信息,包括原始输入的字符串、使用的正则表达式以及在原始字符串中模式所匹配的位置。

# re_simple_match.py

import re

pattern = 'this'
text = 'Does this text match the pattern'

match = re.search(pattern, text)

s = match.start()
e = match.end()

print('Found "{}"\nin "{}"\nfrom {} to {} ("{}")'.format(match.re.pattern, match.string, s, e, text[s:e]))

start()end()方法分别返回原始字符串中模式匹配的起始位置。

$ python3 re_simple_match.py

Found "this"
in "Does this text match the pattern?"
from 5 to 9 ("this")

编译正则表达式

纵使re模块中包含一些模块级别的方法,这些方法将正则表达式作为字符串参数传递进去。但是更加常用的方法是编译(compile)正则表达式。compile()方法将一个正则字符串转化为一个正则对象(RegexObject)。

# re_simple_compiled.py

import re

# 预编译模式
regexes = [
    re.compile(p) for p in ['this', 'that']
]

text = 'Does this text match the pattern?'

print('Text: {!r}\n'.format(text))

for regex in regexes:
    print('Seeking "{}" ->'.format(regex.pattern), end=' ')

    if regex.search(text):
        print('match!')
    else:
        print('no match')

模块级别的方法会缓存编译后的正则表达式,但是缓存的大小是有限的。使用编译后的正则表达式可以直接避免查找缓存的开销。另一个好处是使用编译的正则表达式,在模块加载的时候对所有的表达式进行预编译,那么所有编译的工作都被移到程序启动的时候,而不是出现在响应用户动作的时间点。

$ python3 re_simple_compiled.py

Text: 'Does this text match the pattern?'

Seeking "this" -> match!
Seeking "that" -> no match

多重匹配

目前为止,所有示例的模式都是使用search()方法去搜索一个单独的匹配。findall()方法则可以返回所有与模式匹配的子字符串。

# re_findall.py

import re

text = 'abbaaabbbbaaaaa'

pattern = 'ab'

for match in re.findall(pattern, text):
    print('Found {!r}'.format(match))

这个例子的输入字符串包含两个ab模式的匹配。

$ python3 re_findall.py

Found 'ab'
Found 'ab'

finditer()方法返回一个迭代器,并且是Match对象,而不是findall()方法直接返回匹配的字符串。

# re_finditer.py
import re

text = 'abbaaabbbbaaaaa'

pattern = 'ab'

for match in re.finditer(pattern, text):
    s = match.start()
    e = match.end()
    print('Found {!r} at {:d}:{:d}'.format(
        text[s:e], s, e))

这个例子同样找到两个模式ab的匹配,并且Match对象可以返回它们在原始字符串中的位置。

$ python3 re_finditer.py

Found 'ab' at 0:2
Found 'ab' at 5:7

模式语法

正则表达式支持比原始字符匹配更强大的匹配模式。模式可以重复,可以锚定到输入字符串的不同逻辑位置,也可以使用更简便的模式表达,而不需要每个字符都出现在模式中。所有的这些特性都是通过原始字符和元字符结合实现的,这都是re模块实现的正则表达式语法规则。

# re_test_patterns.py
import re


def test_patterns(text, patterns):
    """给定原始字符串和一个模式列表,对每个模式,在字符串中寻找
    匹配,并且输出到标准输出。
    """
    for pattern, desc in patterns:
        print("'{}' ({})\n".format(pattern, desc))
        print("  '{}'".format(text))
        for match in re.finditer(pattern, text):
            s = match.start()
            e = match.end()
            substr = text[s:e]
            n_backslashes = text[:s].count('\\')
            prefix = '.' * (s + n_backslashes)
            print("  {}'{}'".format(prefix, substr))
        print()
    return


if __name__ == '__main__':
    test_patterns('abbaaabbbbaaaaa',
                  [('ab', "'a' followed by 'b'"),
                   ])

接下来的示例都将使用test_patterns()方法来探索这些不同的模式对同一个输入字符串是如何改变它们的匹配的。输出字符串输出了原始的输入字符串以及所匹配的子串在原始字符串中的位置。

$ python3 re_test_patterns.py

'ab' ('a' followed by 'b')

  'abbaaabbbbaaaaa'
  'ab'
  .....'ab'

重复

在模式中表达重复有5种方式。元字符星号*表示其前面的模式重复0次或多次(允许一个模式重复0次,表示这个模式不一定要被匹配)。如果元字符星号*被换成了加号+,那么模式必须至少出现1次。使用元字符问号?表示模式要出现0次或者1次。那么如果要重复一个固定的次数,可以在模式之后使用{m},这里的m就表示模式需要重复的次数。最后,如果需要限制重复的次数在固定的区间,则可以使用{m,n},这里m表示最少的重复次数,n表示最多的重复次数。如果省略了n(即{m,}),那么表示模式需要重复至少m次,没有上限。

# re_repetition.py
from re_test_patterns import test_patterns

test_patterns(
    'abbaabbba',
    [('ab*', 'a followed by zero or more b'),
     ('ab+', 'a followed by one or more b'),
     ('ab?', 'a followed by zero or one b'),
     ('ab{3}', 'a followed by three b'),
     ('ab{2,3}', 'a followed by two to three b')],
)

这里模式ab*ab?ab+有更多的匹配。

$ python3 re_repetition.py

'ab*' (a followed by zero or more b)

  'abbaabbba'
  'abb'
  ...'a'
  ....'abbb'
  ........'a'

'ab+' (a followed by one or more b)

  'abbaabbba'
  'abb'
  ....'abbb'

'ab?' (a followed by zero or one b)

  'abbaabbba'
  'ab'
  ...'a'
  ....'ab'
  ........'a'

'ab{3}' (a followed by three b)

  'abbaabbba'
  ....'abbb'

'ab{2,3}' (a followed by two to three b)

  'abbaabbba'
  'abb'
  ....'abbb'

当在处理一个表示重复的指令时,re总是会尽可能多的去匹配字符。这也被称为贪婪匹配,这会造成一些比较独特的结果,或者造成匹配结果中有很多我们不想要的字符。贪婪策略也可以通过在表示重复的元字符后面加上问号?来将其关闭。

# re_repetition_non_greedy.py

from re_test_patterns import test_patterns

test_patterns(
    'abbaabbba',
    [('ab*?', 'a followed by zero or more b'),
     ('ab+?', 'a followed by one or more b'),
     ('ab??', 'a followed by zero or one b'),
     ('ab{3}?', 'a followed by three b'),
     ('ab{2,3}?', 'a followed by two to three b')],
)

关闭贪婪策略意味着,如果你的模式中允许出现0个b字符,那么匹配结果中肯定不包括b字符。

$ python3 re_repetition_non_greedy.py

'ab*?' (a followed by zero or more b)

  'abbaabbba'
  'a'
  ...'a'
  ....'a'
  ........'a'

'ab+?' (a followed by one or more b)

  'abbaabbba'
  'ab'
  ....'ab'

'ab??' (a followed by zero or one b)

  'abbaabbba'
  'a'
  ...'a'
  ....'a'
  ........'a'

'ab{3}?' (a followed by three b)

  'abbaabbba'
  ....'abbb'

'ab{2,3}?' (a followed by two to three b)

  'abbaabbba'
  'abb'
  ....'abb'

字符集

一个字符集表示一个字符的集合,字符集合中的每一个字符都可以在一个匹配点去匹配。比如,字符集[ab]可以匹配a或者b

# re_charset.py

from re_test_patterns import test_patterns

test_patterns(
    'abbaabbba',
    [('[ab]', 'either a or b'),
     ('a[ab]+', 'a followed by 1 or more a or b'),
     ('a[ab]+?', 'a followed by 1 or more a or b, not greedy')],
)

模式a[ab]+的贪婪策略使其匹配了整个字符串,因为第一个字符是a,并且之后的每一个字符不是a就是b

$ python3 re_charset.py

'[ab]' (either a or b)

  'abbaabbba'
  'a'
  .'b'
  ..'b'
  ...'a'
  ....'a'
  .....'b'
  ......'b'
  .......'b'
  ........'a'

'a[ab]+' (a followed by 1 or more a or b)

  'abbaabbba'
  'abbaabbba'

'a[ab]+?' (a followed by 1 or more a or b, not greedy)

  'abbaabbba'
  'ab'
  ...'aa'

字符集也可以用于表示不匹配某些特定的字符。脱字符号^表示去匹配没有出现在字符集中跟在脱字符号之后的字符。

# re_charset_exclude.py

from re_test_patterns import test_patterns

test_patterns(
    'This is some text -- with punctuation.',
    [('[^-. ]+', 'sequences without -, ., or space')],
)

该示例中,模式找到了所有不包含中划线-,点号.或者空格的子字符串。

$ python3 re_charset_exclude.py

'[^-. ]+' (sequences without -, ., or space)

  'This is some text -- with punctuation.'
  'This'
  .....'is'
  ........'some'
  .............'text'
  .....................'with'
  ..........................'punctuation'

但是,随着字符集中字符数量的增多,我们需要去匹配(或者不匹配)字符集中所有的字符变得很麻烦。一个很方便的简便写法是使用范围符号-(也就是中划线),范围符合可以定义一个字符集,字符集包含从范围起始点到结束点的所有连续字符。

# re_charset_ranges.py

from re_test_patterns import test_patterns

test_patterns(
    'This is some text -- with punctuation.',
    [('[a-z]+', 'sequences of lowercase letters'),
     ('[A-Z]+', 'sequences of uppercase letters'),
     ('[a-zA-Z]+', 'sequences of letters of either case'),
     ('[A-Z][a-z]+', 'one uppercase followed by lowercase')],
)

在这里,a-z包括所有的小写字母,A-Z表示所有的大写字母。不同的范围集合可以写在一个字符集中。

$ python3 re_charset_ranges.py

'[a-z]+' (sequences of lowercase letters)

  'This is some text -- with punctuation.'
  .'his'
  .....'is'
  ........'some'
  .............'text'
  .....................'with'
  ..........................'punctuation'

'[A-Z]+' (sequences of uppercase letters)

  'This is some text -- with punctuation.'
  'T'

'[a-zA-Z]+' (sequences of letters of either case)

  'This is some text -- with punctuation.'
  'This'
  .....'is'
  ........'some'
  .............'text'
  .....................'with'
  ..........................'punctuation'

'[A-Z][a-z]+' (one uppercase followed by lowercase)

  'This is some text -- with punctuation.'
  'This'

对于一个特殊的字符集来说,元字符点号.表示在当前位置可以匹配任何的单个字符。

# re_charset_dot.py

from re_test_patterns import test_patterns

test_patterns(
    'abbaabbba',
    [('a.', 'a followed by any one character'),
     ('b.', 'b followed by any one character'),
     ('a.*b', 'a followed by anything, ending in b'),
     ('a.*?b', 'a followed by anything, ending in b')],
)

结合使用元字符点号.和重复元字符可以匹配非常长的结果,除非使用非贪婪模式。

$ python3 re_charset_dot.py

'a.' (a followed by any one character)

  'abbaabbba'
  'ab'
  ...'aa'

'b.' (b followed by any one character)

  'abbaabbba'
  .'bb'
  .....'bb'
  .......'ba'

'a.*b' (a followed by anything, ending in b)

  'abbaabbba'
  'abbaabbb'

'a.*?b' (a followed by anything, ending in b)

  'abbaabbba'
  'ab'
  ...'aab'

转义字符

一种更简略的定义字符集的方法是使用预先定义好的转义字符。re模块中定义的转义字符如下表:

字符意义
\d数字
\D非数字
\s空白字符(包括制表符,空格,换行符等)
\S非空白字符
\w字母、数字
\W非字母、数字

注:转义字符在这里使用反斜杠\来标识,不幸的是,在Python中反斜杠本身也需要转义,所以这样会导致正则表达式非常难于阅读和理解。所以我们可以使用Python中的原始字符串来写正则字符串,即在字符串前面加上字符r,这样就可以解决这个问题。

# re_escape_codes.py

from re_test_patterns import test_patterns

test_patterns(
    'A prime #1 example!',
    [(r'\d+', 'sequence of digits'),
     (r'\D+', 'sequence of non-digits'),
     (r'\s+', 'sequence of whitespace'),
     (r'\S+', 'sequence of non-whitespace'),
     (r'\w+', 'alphanumeric characters'),
     (r'\W+', 'non-alphanumeric')],
)

这些示例结合使用转义字符和重复字符来找到正确的字符序列。

$ python3 re_escape_codes.py

'\d+' (sequence of digits)

  'A prime #1 example!'
  .........'1'

'\D+' (sequence of non-digits)

  'A prime #1 example!'
  'A prime #'
  ..........' example!'

'\s+' (sequence of whitespace)

  'A prime #1 example!'
  .' '
  .......' '
  ..........' '

'\S+' (sequence of non-whitespace)

  'A prime #1 example!'
  'A'
  ..'prime'
  ........'#1'
  ...........'example!'

'\w+' (alphanumeric characters)

  'A prime #1 example!'
  'A'
  ..'prime'
  .........'1'
  ...........'example'

'\W+' (non-alphanumeric)

  'A prime #1 example!'
  .' '
  .......' #'
  ..........' '
  ..................'!'

如果如要匹配正则表达式中的元字符,那么这些字符需要被反斜杠\转义。

# re_escape_escapes.py

from re_test_patterns import test_patterns

test_patterns(
    r'\d+ \D+ \s+',
    [(r'\\.\+', 'escape code')],
)

这个例子对反斜杠\和加号+进行了转义,因为它们都是正则表达式中的元字符,都有特殊的含义。

$ python3 re_escape_escapes.py

'\\.\+' (escape code)

  '\d+ \D+ \s+'
  '\d+'
  .....'\D+'
  ..........'\s+'

锚定位置

我们可以使用锚定(anchoring)指令来指定在原始字符串中模式需要匹配的逻辑位置,下表列出了有效的锚定指令字符。

字符意义
^字符串或一行的开始位置
$字符串或一行的结束位置
\A字符串的开始位置
\Z字符串的结束位置
\b单词边界(单词开始或结束位置)
\B非单词边界
# re_anchoring.py

from re_test_patterns import test_patterns

test_patterns(
    'This is some text -- with punctuation.',
    [(r'^\w+', 'word at start of string'),
     (r'\A\w+', 'word at start of string'),
     (r'\w+\S*$', 'word near end of string'),
     (r'\w+\S*\Z', 'word near end of string'),
     (r'\w*t\w*', 'word containing t'),
     (r'\bt\w+', 't at start of word'),
     (r'\w+t\b', 't at end of word'),
     (r'\Bt\B', 't, not start or end of word')],
)

示例中用来匹配字符串开始位置和结束位置单词的模式不一样,这是因为字符串是由标点符号结尾的,所以模式\w+$无法匹配,因为点号.并不是一个字母数字\w

$ python3 re_anchoring.py

'^\w+' (word at start of string)

  'This is some text -- with punctuation.'
  'This'

'\A\w+' (word at start of string)

  'This is some text -- with punctuation.'
  'This'

'\w+\S*$' (word near end of string)

  'This is some text -- with punctuation.'
  ..........................'punctuation.'

'\w+\S*\Z' (word near end of string)

  'This is some text -- with punctuation.'
  ..........................'punctuation.'

'\w*t\w*' (word containing t)

  'This is some text -- with punctuation.'
  .............'text'
  .....................'with'
  ..........................'punctuation'

'\bt\w+' (t at start of word)

  'This is some text -- with punctuation.'
  .............'text'

'\w+t\b' (t at end of word)

  'This is some text -- with punctuation.'
  .............'text'

'\Bt\B' (t, not start or end of word)

  'This is some text -- with punctuation.'
  .......................'t'
  ..............................'t'
  .................................'t'

搜索约束

在一些我们只需要匹配原始输入的一部分的场景中,我们可以告诉re模块限制我们的搜索范围。比如,如果模式必须出现在输入字符串的开始位置,我们可以使用match()方法而不是search()方法,这样就不需要通过锚定指令来指定搜索的位置。

# re_match.py

import re

text = 'This is some text -- with punctuation.'
pattern = 'is'

print('Text   :', text)
print('Pattern:', pattern)

m = re.match(pattern, text)
print('Match  :', m)
s = re.search(pattern, text)
print('Search :', s)

因为这里is并没有在字符串的开始位置出现,所以我们使用match()方法并没有搜索到结果。相反,它在其他地方出现了两次,所有使用search()方法找到了它。

$ python3 re_match.py

Text   : This is some text -- with punctuation.
Pattern: is
Match  : None
Search : <_sre.SRE_Match object; span=(2, 4), match='is'>

fullmatch()方法需要输入的字符串完全匹配模式。

# re_fullmatch.py

import re

text = 'This is some text -- with punctuation.'
pattern = 'is'

print('Text       :', text)
print('Pattern    :', pattern)

m = re.search(pattern, text)
print('Search     :', m)
s = re.fullmatch(pattern, text)
print('Full match :', s)

这里,search()方法表明模式确实在输入中出现了,但是它并不是输入字符串的全部,所以fullmatch()没有搜索到结果。

$ python3 re_fullmatch.py

Text       : This is some text -- with punctuation.
Pattern    : is
Search     : <_sre.SRE_Match object; span=(2, 4), match='is'>
Full match : None

一个编译后的模式的search()方法,可以接受startend两个参数来限制搜索的范围。

# re_search_substring.py

import re

text = 'This is some text -- with punctuation.'
pattern = re.compile(r'\b\w*is\w*\b')

print('Text:', text)
print()

pos = 0
while True:
    match = pattern.search(text, pos)
    if not match:
        break
    s = match.start()
    e = match.end()
    print('  {:>2d} : {:>2d} = "{}"'.format(
        s, e - 1, text[s:e]))
    # Move forward in text for the next search
    pos = e

这个示例是iterall()方法的一个比较低效的实现,每次找到一个匹配的时候,匹配的结束位置作为下一次搜索的起始位置。

$ python3 re_search_substring.py

Text: This is some text -- with punctuation.

   0 :  3 = "This"
   5 :  6 = "is"

使用分组

搜索匹配模式是正则表达式提供的一个基础且强大的功能。添加分组可以分割匹配到的文本,这更加扩展了正则表达式的功能。分组由括起来的小括号来表示。

# re_groups.py

from re_test_patterns import test_patterns

test_patterns(
    'abbaaabbbbaaaaa',
    [('a(ab)', 'a followed by literal ab'),
     ('a(a*b*)', 'a followed by 0-n a and 0-n b'),
     ('a(ab)*', 'a followed by 0-n ab'),
     ('a(ab)+', 'a followed by 1-n ab')],
)

任何一个完整的正则表达式都可以作为一个分组而插入到一个更大的正则表达式中。所有的表示重复的修饰符可以跟在一个分组后面,用来重复一个完整的分组。

$ python3 re_groups.py

'a(ab)' (a followed by literal ab)

  'abbaaabbbbaaaaa'
  ....'aab'

'a(a*b*)' (a followed by 0-n a and 0-n b)

  'abbaaabbbbaaaaa'
  'abb'
  ...'aaabbbb'
  ..........'aaaaa'

'a(ab)*' (a followed by 0-n ab)

  'abbaaabbbbaaaaa'
  'a'
  ...'a'
  ....'aab'
  ..........'a'
  ...........'a'
  ............'a'
  .............'a'
  ..............'a'

'a(ab)+' (a followed by 1-n ab)

  'abbaaabbbbaaaaa'
  ....'aab'

如果要访问每个特有的分组所匹配到的文本,可以使用Match对象的groups()方法。

# re_groups_match.py

import re

text = 'This is some text -- with punctuation.'

print(text)
print()

patterns = [
    (r'^(\w+)', 'word at start of string'),
    (r'(\w+)\S*$', 'word at end, with optional punctuation'),
    (r'(\bt\w+)\W+(\w+)', 'word starting with t, another word'),
    (r'(\w+t)\b', 'word ending with t'),
]

for pattern, desc in patterns:
    regex = re.compile(pattern)
    match = regex.search(text)
    print("'{}' ({})\n".format(pattern, desc))
    print('  ', match.groups())
    print()

Match.groups()返回模式中所有分组所匹配的字符串所组成的元组。

$ python3 re_groups_match.py

This is some text -- with punctuation.

'^(\w+)' (word at start of string)

   ('This',)

'(\w+)\S*$' (word at end, with optional punctuation)

   ('punctuation',)

'(\bt\w+)\W+(\w+)' (word starting with t, another word)

   ('text', 'with')

'(\w+t)\b' (word ending with t)

   ('text',)

要访问一个单独的分组匹配结果,使用group()方法。这非常有用,尤其当我们需要分组匹配的结果,而不需要其他部分匹配的结果的时候。

# re_groups_individual.py

import re

text = 'This is some text -- with punctuation.'

print('Input text            :', text)

# word starting with 't' then another word
regex = re.compile(r'(\bt\w+)\W+(\w+)')
print('Pattern               :', regex.pattern)

match = regex.search(text)
print('Entire match          :', match.group(0))
print('Word starting with "t":', match.group(1))
print('Word after "t" word   :', match.group(2))

分组0表示由模式匹配的完整字符串,而子分组的编号从1开始,并且按照左括号(出现的顺序依次编号。

$ python3 re_groups_individual.py

Input text            : This is some text -- with punctuation.
Pattern               : (\bt\w+)\W+(\w+)
Entire match          : text -- with
Word starting with "t": text
Word after "t" word   : with

Python在基本分组的基础上又扩展了命名分组(named groups),使用命名分组可以让我们随时方便地修改正则表达式,而不需要同时修改匹配的结果代码。使用命名分组,使用语法(?P<name>pattern)

# re_groups_named.py

import re

text = 'This is some text -- with punctuation.'

print(text)
print()

patterns = [
    r'^(?P<first_word>\w+)',
    r'(?P<last_word>\w+)\S*$',
    r'(?P<t_word>\bt\w+)\W+(?P<other_word>\w+)',
    r'(?P<ends_with_t>\w+t)\b',
]

for pattern in patterns:
    regex = re.compile(pattern)
    match = regex.search(text)
    print("'{}'".format(pattern))
    print('  ', match.groups())
    print('  ', match.groupdict())
    print()

使用groupdict()方法可以得到分组名字到匹配字符串的映射字典。使用groups()方法也可以得到匹配的结果。

$ python3 re_groups_named.py

This is some text -- with punctuation.

'^(?P<first_word>\w+)'
   ('This',)
   {'first_word': 'This'}

'(?P<last_word>\w+)\S*$'
   ('punctuation',)
   {'last_word': 'punctuation'}

'(?P<t_word>\bt\w+)\W+(?P<other_word>\w+)'
   ('text', 'with')
   {'t_word': 'text', 'other_word': 'with'}

'(?P<ends_with_t>\w+t)\b'
   ('text',)
   {'ends_with_t': 'text'}

下面的更新版本的test_patterns()可以输出匹配结果的数字分组或者命名分组,这可以让我们接下来的示例更加容易理解。

# re_test_patterns_groups.py

import re


def test_patterns(text, patterns):
    """Given source text and a list of patterns, look for
    matches for each pattern within the text and print
    them to stdout.
    """
    # Look for each pattern in the text and print the results
    for pattern, desc in patterns:
        print('{!r} ({})\n'.format(pattern, desc))
        print('  {!r}'.format(text))
        for match in re.finditer(pattern, text):
            s = match.start()
            e = match.end()
            prefix = ' ' * (s)
            print(
                '  {}{!r}{} '.format(prefix,
                                     text[s:e],
                                     ' ' * (len(text) - e)),
                end=' ',
            )
            print(match.groups())
            if match.groupdict():
                print('{}{}'.format(
                    ' ' * (len(text) - s),
                    match.groupdict()),
                )
        print()
    return

因为一个分组本身是一个完整的正则表达式,所以分组也可以被嵌入到其他的分组中,从而构造出更复杂的正则表达式。

# re_groups_nested.py

from re_test_patterns_groups import test_patterns

test_patterns(
    'abbaabbba',
    [(r'a((a*)(b*))', 'a followed by 0-n a and 0-n b')],
)

在这个例子中,分组(a*)匹配一个空的字符串,所以groups()方法的返回值包含一个空字符串作为匹配值。

$ python3 re_groups_nested.py

'a((a*)(b*))' (a followed by 0-n a and 0-n b)

  'abbaabbba'
  'abb'        ('bb', '', 'bb')
     'aabbb'   ('abbb', 'a', 'bbb')
          'a'  ('', '', '')

分组在指定可选的模式时也非常有用。使用管道符号竖线|来分割两个模式,这表示被这两个模式其一所匹配。但是也要注意竖线的位置。在下述例子中,第一个表达式匹配一个单独的a,后面跟着一串a或者一串b;第二个表达式匹配一个单独的a,后面跟着一串由a或者b组成的字符串。这两个模式很相似,但是匹配的结果却是完全不同的。

# re_groups_alternative.py

from re_test_patterns_groups import test_patterns

test_patterns(
    'abbaabbba',
    [(r'a((a+)|(b+))', 'a then seq. of a or seq. of b'),
     (r'a((a|b)+)', 'a then seq. of [ab]')],
)

当一个可选分组不被匹配,但是整个表达式却匹配时,groups()方法的返回值会包含一个None值,这个None值就出现在不被匹配的可选分组出现的位置。

$ python3 re_groups_alternative.py

'a((a+)|(b+))' (a then seq. of a or seq. of b)

  'abbaabbba'
  'abb'        ('bb', None, 'bb')
     'aa'      ('a', 'a', None)

'a((a|b)+)' (a then seq. of [ab])

  'abbaabbba'
  'abbaabbba'  ('bbaabbba', 'a')

有时我们并不需要将被分组所匹配的字符串单独提取出来,我们也可以定义这样的分组,称之为非捕获组。非捕获组可以用来定义重复模式或者可选模式,但是并不会将分组匹配的内容分离开来。要创建一个非捕获组,需要用语法(?:pattern)

# re_groups_noncapturing.py

from re_test_patterns_groups import test_patterns

test_patterns(
    'abbaabbba',
    [(r'a((a+)|(b+))', 'capturing form'),
     (r'a((?:a+)|(?:b+))', 'noncapturing')],
)

在这个例子中,我们可以比较一下对于捕获组和非捕获组,groups()方法的返回值有何区别。

$ python3 re_groups_noncapturing.py

'a((a+)|(b+))' (capturing form)

  'abbaabbba'
  'abb'        ('bb', None, 'bb')
     'aa'      ('a', 'a', None)

'a((?:a+)|(?:b+))' (noncapturing)

  'abbaabbba'
  'abb'        ('bb',)
     'aa'      ('a',)

搜索选项

可选的标识可以改变正则引擎在处理一个正则表达式时的行为。不同的标识可以通过异或运算符结合起来,然后传递给compile()search()match()等方法。

忽略大小写的匹配

IGNORECASE可以让模式中的字母忽略大小写,不管大写还是小写字母都可以匹配。

# re_flags_ignorecase.py

import re

text = 'This is some text -- with punctuation.'
pattern = r'\bT\w+'
with_case = re.compile(pattern)
without_case = re.compile(pattern, re.IGNORECASE)

print('Text:\n  {!r}'.format(text))
print('Pattern:\n  {}'.format(pattern))
print('Case-sensitive:')
for match in with_case.findall(text):
    print('  {!r}'.format(match))
print('Case-insensitive:')
for match in without_case.findall(text):
    print('  {!r}'.format(match))

因为模式中包含字母T,如果IGNORECASE没有设置,那么匹配的单词只有一个This。当大小写忽略的时候,单词text也会被匹配。

$ python3 re_flags_ignorecase.py

Text:
  'This is some text -- with punctuation.'
Pattern:
  \bT\w+
Case-sensitive:
  'This'
Case-insensitive:
  'This'
  'text'

多行输入

在这里有两个可以影响多行输入时正则怎么工作的标识:MULTILINEDOTALLMULTILINE标识用来控制当输入字符串包含换行符时怎么处理锚定指令。当设置MULTILINE标识时,那么锚定符^$会匹配每一行的起始位置和结束位置,如果没有设置,就会匹配整个字符串的起始和结束位置。

# re_flags_multiline.py

import re

text = 'This is some text -- with punctuation.\nA second line.'
pattern = r'(^\w+)|(\w+\S*$)'
single_line = re.compile(pattern)
multiline = re.compile(pattern, re.MULTILINE)

print('Text:\n  {!r}'.format(text))
print('Pattern:\n  {}'.format(pattern))
print('Single Line :')
for match in single_line.findall(text):
    print('  {!r}'.format(match))
print('Multline    :')
for match in multiline.findall(text):
    print('  {!r}'.format(match))

这个示例中的模式会匹配输入字符串的第一个单词或者最后一个单词。

$ python3 re_flags_multiline.py

Text:
  'This is some text -- with punctuation.\nA second line.'
Pattern:
  (^\w+)|(\w+\S*$)
Single Line :
  ('This', '')
  ('', 'line.')
Multline    :
  ('This', '')
  ('', 'punctuation.')
  ('A', '')
  ('', 'line.')

DOTALL是另外一个有关多行输入的标识。正常情况下,元字符点号.会匹配所有除了换行符之外的字符。这个标识会让元字符点号.也匹配换行符。

# re_flags_dotall.py

import re

text = 'This is some text -- with punctuation.\nA second line.'
pattern = r'.+'
no_newlines = re.compile(pattern)
dotall = re.compile(pattern, re.DOTALL)

print('Text:\n  {!r}'.format(text))
print('Pattern:\n  {}'.format(pattern))
print('No newlines :')
for match in no_newlines.findall(text):
    print('  {!r}'.format(match))
print('Dotall      :')
for match in dotall.findall(text):
    print('  {!r}'.format(match))

如果没有设置这个标识,输入字符串的每一行都会单独被匹配。相反,如果设置了这个标识,示例中的模式会匹配整个字符串。

$ python3 re_flags_dotall.py

Text:
  'This is some text -- with punctuation.\nA second line.'
Pattern:
  .+
No newlines :
  'This is some text -- with punctuation.'
  'A second line.'
Dotall      :
  'This is some text -- with punctuation.\nA second line.'

Unicode编码

在Python3中,str对象使用完整的Unicode字符集,并且正则表达式都假设输入的模式和字符串都是Unicode编码。前面提到的转义字符也是默认按照Unicode编码。这些假设意味着模式\w+会同时匹配单词“French”和单词“Français”。如果要严格限制转义字符只匹配ASCII编码的字符集(Python2中的默认编码),可以在编译模式或者在调用模块级别的search()match()方法时使用ASCII标识。

# re_flags_ascii.py

import re

text = u'Français złoty Österreich'
pattern = r'\w+'
ascii_pattern = re.compile(pattern, re.ASCII)
unicode_pattern = re.compile(pattern)

print('Text    :', text)
print('Pattern :', pattern)
print('ASCII   :', list(ascii_pattern.findall(text)))
print('Unicode :', list(unicode_pattern.findall(text)))

其他的转义字符(\W\b\B\d\D\s\S)对于ASCII编码的文本也会有不同的处理方式。

$ python3 re_flags_ascii.py

Text    : Français złoty Österreich
Pattern : \w+
ASCII   : ['Fran', 'ais', 'z', 'oty', 'sterreich']
Unicode : ['Français', 'złoty', 'Österreich']

详细的表达式语法

当正则表达式变得越来越复杂的时候,正则语法的缩略形式也会变成一种阻碍。当一个表达式中的分组逐渐增多,我们需要更多的工作来确保正则的每个部分都是我们所需的,来理解正则表达式的各个部分是如何相互影响的。使用命名分组可以在一定程度上缓解这些问题,但是一个更好的方法是使用详细模式的正则表达式(verbose mode expressions),该模式允许我们在表达式中间使用注释和多余的空格。

下述的来验证邮件地址的示例将会向我们展示,详细模式的正则是如何让我们的工作变得更加容易的。第一个版本我们验证以三个顶级域名.com.org.edu结尾的邮件地址。

# re_email_compact.py

import re

address = re.compile('[\w\d.+-]+@([\w\d.]+\.)+(com|org|edu)')

candidates = [
    u'first.last@example.com',
    u'first.last+category@gmail.com',
    u'valid-address@mail.example.com',
    u'not-valid@example.foo',
]

for candidate in candidates:
    match = address.search(candidate)
    print('{:<30}  {}'.format(
        candidate, 'Matches' if match else 'No match')
    )

这个表达式已经比较复杂了,它包含几个字符集,几个分组和表示重复的元字符。

$ python3 re_email_compact.py

first.last@example.com          Matches
first.last+category@gmail.com   Matches
valid-address@mail.example.com  Matches
not-valid@example.foo           No match

将这个表达式转换成一个比较详细的形式将会让我们更容易去扩展。

# re_email_verbose.py

import re

address = re.compile(
    '''
    [\w\d.+-]+       # username
    @
    ([\w\d.]+\.)+    # domain name prefix
    (com|org|edu)    # TODO: support more top-level domains
    ''',
    re.VERBOSE)

candidates = [
    u'first.last@example.com',
    u'first.last+category@gmail.com',
    u'valid-address@mail.example.com',
    u'not-valid@example.foo',
]

for candidate in candidates:
    match = address.search(candidate)
    print('{:<30}  {}'.format(
        candidate, 'Matches' if match else 'No match'),
    )

用这个表达式去匹配相同的输入,这个表达式形式会让我们更容易去阅读。表达式中的注释也可以帮助我们去理解模式的每一个部分,所以这样更容易去扩展,去匹配更多的输入。

$ python3 re_email_verbose.py

first.last@example.com          Matches
first.last+category@gmail.com   Matches
valid-address@mail.example.com  Matches
not-valid@example.foo           No match

下面扩展的版本将会匹配姓名以及邮件地址,这在邮件的开端是可能出现的。姓名首先出现,随后跟着的是邮件地址,并且用尖括号括起来(<>)。

# re_email_with_name.py

import re

address = re.compile(
    '''

    # 姓名会包含字母,一些缩写也会包含点号
    ((?P<name>
       ([\w.,]+\s+)*[\w.,]+)
       \s*
       # 只有存在姓名时,邮件地址才会
       # 被尖括号括起来,所以这里把左
       # 尖括号放入该分组中
       <
    )? # 姓名是可选的

    # 邮件地址本身: username@domain.tld
    (?P<email>
      [\w\d.+-]+       # username
      @
      ([\w\d.]+\.)+    # domain name prefix
      (com|org|edu)    # limit the allowed top-level domains
    )

    >? # 可选的右尖括号
    ''',
    re.VERBOSE)

candidates = [
    u'first.last@example.com',
    u'first.last+category@gmail.com',
    u'valid-address@mail.example.com',
    u'not-valid@example.foo',
    u'First Last <first.last@example.com>',
    u'No Brackets first.last@example.com',
    u'First Last',
    u'First Middle Last <first.last@example.com>',
    u'First M. Last <first.last@example.com>',
    u'<first.last@example.com>',
]

for candidate in candidates:
    print('Candidate:', candidate)
    match = address.search(candidate)
    if match:
        print('  Name :', match.groupdict()['name'])
        print('  Email:', match.groupdict()['email'])
    else:
        print('  No match')

和其他编程语言一样,详细模式下正则表达式允许插入注释的功能可以使程序具有可维护性。最终版本的表达式为以后的维护者提供了详细的注释,并且用空格将每个分组都分开,使它们的嵌套关系更加明了。

$ python3 re_email_with_name.py

Candidate: first.last@example.com
  Name : None
  Email: first.last@example.com
Candidate: first.last+category@gmail.com
  Name : None
  Email: first.last+category@gmail.com
Candidate: valid-address@mail.example.com
  Name : None
  Email: valid-address@mail.example.com
Candidate: not-valid@example.foo
  No match
Candidate: First Last <first.last@example.com>
  Name : First Last
  Email: first.last@example.com
Candidate: No Brackets first.last@example.com
  Name : None
  Email: first.last@example.com
Candidate: First Last
  No match
Candidate: First Middle Last <first.last@example.com>
  Name : First Middle Last
  Email: first.last@example.com
Candidate: First M. Last <first.last@example.com>
  Name : First M. Last
  Email: first.last@example.com
Candidate: <first.last@example.com>
  Name : None
  Email: first.last@example.com

在模式中嵌入标识

有时候,我们在编译正则表达式的时候无法为其设置编译标识,比如我们将模式作为一个参数传递给一个库函数的时候,而编译正则是库函数负责的,这个时候我们可以将编译标识嵌入正则表达式字符串本身。举例来说,比如我们需要设置忽略大小写模式,我们可以在表达式的开头加上(?i)

# re_flags_embedded.py

import re

text = 'This is some text -- with punctuation.'
pattern = r'(?i)\bT\w+'
regex = re.compile(pattern)

print('Text      :', text)
print('Pattern   :', pattern)
print('Matches   :', regex.findall(text))

因为编译标识关系到整个表达式是如何解析的,所以,如有需要,标识一定要出现在表达式的开头。

$ python3 re_flags_embedded.py

Text      : This is some text -- with punctuation.
Pattern   : (?i)\bT\w+
Matches   : ['This', 'text']

下表列出了所有编译标识的缩写。

标识缩写
ASCIIa
IGNORECASEi
MULTILINEm
DOTALLs
VERBOSEx

如果要混合使用嵌入的标识,也可以将它们一起放入一个分组中,比如(?im)设置了多行输入模式和忽略大小写模式。


前向断言和后向断言

在许多情况下,我们只有在一部分匹配的时候才去匹配另一个部分。比如,在验证邮件地址的表达式中,尖括号被认为是可选的,但实际上,尖括号需要成对出现,所以表达式只有在尖括号成对出现或者都没有出现的时候才会匹配。下面的修改版本使用了前向肯定断言来匹配尖括号对。前向肯定断言的语法是(?=pattern)

# re_look_ahead.py

import re

address = re.compile(
    '''
    # 姓名会包含字母,一些缩写也会包含点号
    ((?P<name>
       ([\w.,]+\s+)*[\w.,]+
     )
     \s+
    ) # 姓名不再是可选的了

    # 前向断言
    # 邮件地址被尖括号括起来,但尖括号必须成对出现或者都不出现
    (?= (<.*>$)       # 剩余的部分被尖括号括起来
        |
        ([^<].*[^>]$) # 剩余的部分没有被尖括号括起来
      )

    <? # 可选的左尖括号

    # 邮件地址本身: username@domain.tld
    (?P<email>
      [\w\d.+-]+       # username
      @
      ([\w\d.]+\.)+    # domain name prefix
      (com|org|edu)    # limit the allowed top-level domains
    )

    >? # 可选的右尖括号
    ''',
    re.VERBOSE)

candidates = [
    u'First Last <first.last@example.com>',
    u'No Brackets first.last@example.com',
    u'Open Bracket <first.last@example.com',
    u'Close Bracket first.last@example.com>',
]

for candidate in candidates:
    print('Candidate:', candidate)
    match = address.search(candidate)
    if match:
        print('  Name :', match.groupdict()['name'])
        print('  Email:', match.groupdict()['email'])
    else:
        print('  No match')

这个版本有几处比较重要的变化。第一,姓名不再是可选的了,这意味着只有邮件地址没有姓名是不会被匹配的,但是同时它依然严格限制了姓名和邮件地址的格式。跟在name分组后面的前向肯定断言确保了后面的内容要么被一对尖括号括起来,要么没有被尖括号括起来,而不能出现不成对的尖括号。前向断言也是一个分组,但它是一个零宽断言,也就是说这个分组并不会消耗输入文本,在前向肯定断言匹配成功之后,断言后面的模式也是从相同的位置开始进行匹配。

$ python3 re_look_ahead.py

Candidate: First Last <first.last@example.com>
  Name : First Last
  Email: first.last@example.com
Candidate: No Brackets first.last@example.com
  Name : No Brackets
  Email: first.last@example.com
Candidate: Open Bracket <first.last@example.com
  No match
Candidate: Close Bracket first.last@example.com>
  No match

与前向肯定断言相反的是前向否定断言((?!pattern)),它的含义是在当前位置断言不匹配则视为成功,否则失败。举个例子,邮件地址匹配模式通常会忽略一些自动发出的邮件,这些邮件地址名字为noreply

# re_negative_look_ahead.py

import re

address = re.compile(
    '''
    ^

    # 正常的邮件地址: username@domain.tld

    # 需要忽略的邮件地址
    (?!noreply@.*$)

    [\w\d.+-]+       # username
    @
    ([\w\d.]+\.)+    # domain name prefix
    (com|org|edu)    # limit the allowed top-level domains

    $
    ''',
    re.VERBOSE)

candidates = [
    u'first.last@example.com',
    u'noreply@example.com',
]

for candidate in candidates:
    print('Candidate:', candidate)
    match = address.search(candidate)
    if match:
        print('  Match:', candidate[match.start():match.end()])
    else:
        print('  No match')

因为前向断言匹配失败,这里以noreply开头的邮件地址不被匹配。

$ python3 re_negative_look_ahead.py

Candidate: first.last@example.com
  Match: first.last@example.com
Candidate: noreply@example.com
  No match

上例中,我们在名字username的位置前面使用前向否定断言来排除noreply,同样的,我们也可以在名字username的后面使用后向否定断言,它的语法是(?<!pattern)

# re_negative_look_behind.py

import re

address = re.compile(
    '''
    ^

    # 邮件地址: username@domain.tld

    [\w\d.+-]+       # username

    # 要忽略的邮件名字
    (?<!noreply)

    @
    ([\w\d.]+\.)+    # domain name prefix
    (com|org|edu)    # limit the allowed top-level domains

    $
    ''',
    re.VERBOSE)

candidates = [
    u'first.last@example.com',
    u'noreply@example.com',
]

for candidate in candidates:
    print('Candidate:', candidate)
    match = address.search(candidate)
    if match:
        print('  Match:', candidate[match.start():match.end()])
    else:
        print('  No match')

后向断言和前向断言稍微有些不同,后向断言必须使用固定长度的模式,也可以使用重复的元字符来表示固定数量长度的字符,但是不允许有通配符和范围符。

$ python3 re_negative_look_behind.py

Candidate: first.last@example.com
  Match: first.last@example.com
Candidate: noreply@example.com
  No match

同样的,后向肯定断言可以用来找到跟在一个模式后面的文本,它的语法是(?<=pattern)。下述例子可以找到推特的标签。

# re_look_behind.py

import re

twitter = re.compile(
    '''
    # A twitter handle: @username
    (?<=@)
    ([\w\d_]+)       # username
    ''',
    re.VERBOSE)

text = '''This text includes two Twitter handles.
One for @ThePSF, and one for the author, @doughellmann.
'''

print(text)
for match in twitter.findall(text):
    print('Handle:', match)

这个模式可以找到推特标签,只要其跟在@后面。

$ python3 re_look_behind.py

This text includes two Twitter handles.
One for @ThePSF, and one for the author, @doughellmann.

Handle: ThePSF
Handle: doughellmann

自我引用

匹配的结果也可以被之后的模式所使用。比如,我们可以将验证邮件地址的例子更新为,只有邮件地址是由自己的姓和名组成的才被匹配,这时我们就必须引用前面的分组。最简单的方法就是通过分组的编号引用前面的分组:\num

# re_refer_to_group.py

import re

address = re.compile(
    r'''

    # The regular name
    (\w+)               # first name
    \s+
    (([\w.]+)\s+)?      # optional middle name or initial
    (\w+)               # last name

    \s+

    <

    # The address: first_name.last_name@domain.tld
    (?P<email>
      \1               # first name
      \.
      \4               # last name
      @
      ([\w\d.]+\.)+    # domain name prefix
      (com|org|edu)    # limit the allowed top-level domains
    )

    >
    ''',
    re.VERBOSE | re.IGNORECASE)

candidates = [
    u'First Last <first.last@example.com>',
    u'Different Name <first.last@example.com>',
    u'First Middle Last <first.last@example.com>',
    u'First M. Last <first.last@example.com>',
]

for candidate in candidates:
    print('Candidate:', candidate)
    match = address.search(candidate)
    if match:
        print('  Match name :', match.group(1), match.group(4))
        print('  Match email:', match.group(5))
    else:
        print('  No match')

虽然语法比较简单,但是通过分组的数字编号引用分组有一些不好的地方。从现实的角度出发,如果一个表达式发生了改变,就需要重新去数每个分组的编号,并且相应的引用也需要更改。另一个缺点就是使用\n的语法引用分组最多只能引用99个分组,因为如果一个分组编号是三位数,那么它就会被解释成一个八进制的数字,而不是一个分组引用。当然,如果一个表达式包含不止99个分组,那么更严峻的问题是如何维护这个表达式,而不是如何引用所有的分组。

$ python3 re_refer_to_group.py

Candidate: First Last <first.last@example.com>
  Match name : First Last
  Match email: first.last@example.com
Candidate: Different Name <first.last@example.com>
  No match
Candidate: First Middle Last <first.last@example.com>
  Match name : First Last
  Match email: first.last@example.com
Candidate: First M. Last <first.last@example.com>
  Match name : First Last
  Match email: first.last@example.com

在Python中,可以使用(?P=name)来引用前面定义的命名分组。

# re_refer_to_named_group.py

import re

address = re.compile(
    '''

    # The regular name
    (?P<first_name>\w+)
    \s+
    (([\w.]+)\s+)?      # optional middle name or initial
    (?P<last_name>\w+)

    \s+

    <

    # The address: first_name.last_name@domain.tld
    (?P<email>
      (?P=first_name)
      \.
      (?P=last_name)
      @
      ([\w\d.]+\.)+    # domain name prefix
      (com|org|edu)    # limit the allowed top-level domains
    )

    >
    ''',
    re.VERBOSE | re.IGNORECASE)

candidates = [
    u'First Last <first.last@example.com>',
    u'Different Name <first.last@example.com>',
    u'First Middle Last <first.last@example.com>',
    u'First M. Last <first.last@example.com>',
]

for candidate in candidates:
    print('Candidate:', candidate)
    match = address.search(candidate)
    if match:
        print('  Match name :', match.groupdict()['first_name'],
              end=' ')
        print(match.groupdict()['last_name'])
        print('  Match email:', match.groupdict()['email'])
    else:
        print('  No match')

示例中的表达式打开了IGNORECASE模式,所以,也会匹配名字正常大写而邮件地址是小写的情况。

$ python3 re_refer_to_named_group.py

Candidate: First Last <first.last@example.com>
  Match name : First Last
  Match email: first.last@example.com
Candidate: Different Name <first.last@example.com>
  No match
Candidate: First Middle Last <first.last@example.com>
  Match name : First Last
  Match email: first.last@example.com
Candidate: First M. Last <first.last@example.com>
  Match name : First Last
  Match email: first.last@example.com

使用引用分组的另一个作用是,我们可以根据前面分组是否匹配,而在后面选择不同的模式。验证邮件地址的模式可以更正为:如果前面出现姓名,那么邮件地址需要使用尖括号括起来;如果没有出现姓名,则邮件地址不需要使用尖括号括起来。用来测试一个分组是否匹配的语法是:(?(id)yes-expression|no-expression),这里id是分组名字或者编号,yes-expression是分组匹配时所使用的模式,而no-expression是分组不匹配时使用的模式。

# re_id.py

import re

address = re.compile(
    '''
    ^

    # A name is made up of letters, and may include "."
    # for title abbreviations and middle initials.
    (?P<name>
       ([\w.]+\s+)*[\w.]+
     )?
    \s*

    # Email addresses are wrapped in angle brackets, but
    # only if a name is found.
    (?(name)
      # remainder wrapped in angle brackets because
      # there is a name
      (?P<brackets>(?=(<.*>$)))
      |
      # remainder does not include angle brackets without name
      (?=([^<].*[^>]$))
     )

    # Look for a bracket only if the look-ahead assertion
    # found both of them.
    (?(brackets)<|\s*)

    # The address itself: username@domain.tld
    (?P<email>
      [\w\d.+-]+       # username
      @
      ([\w\d.]+\.)+    # domain name prefix
      (com|org|edu)    # limit the allowed top-level domains
     )

    # Look for a bracket only if the look-ahead assertion
    # found both of them.
    (?(brackets)>|\s*)

    $
    ''',
    re.VERBOSE)

candidates = [
    u'First Last <first.last@example.com>',
    u'No Brackets first.last@example.com',
    u'Open Bracket <first.last@example.com',
    u'Close Bracket first.last@example.com>',
    u'no.brackets@example.com',
]

for candidate in candidates:
    print('Candidate:', candidate)
    match = address.search(candidate)
    if match:
        print('  Match name :', match.groupdict()['name'])
        print('  Match email:', match.groupdict()['email'])
    else:
        print('  No match')

这个模式使用了两个测试。如果name分组匹配,那么前向肯定断言会创建brackets分组,并验证尖括号是否成对出现。如果name分组没有匹配,那么会使用另一个前向断言来验证不要出现尖括号。之后,如果brackets分组被创建,那么(?(brackets)<|\s*)模式和(?(brackets)>|\s*)模式会匹配尖括号,如果没有,则匹配空白字符。

$ python3 re_id.py

Candidate: First Last <first.last@example.com>
  Match name : First Last
  Match email: first.last@example.com
Candidate: No Brackets first.last@example.com
  No match
Candidate: Open Bracket <first.last@example.com
  No match
Candidate: Close Bracket first.last@example.com>
  No match
Candidate: no.brackets@example.com
  Match name : None
  Match email: no.brackets@example.com

使用模式修改字符串

re模块不仅可以搜索一个字符串,它也提供了使用正则表达式去修改字符串的功能,并且替换的内容可以直接引用匹配的分组。使用sub方法可以将所有出现的模式都替换成另一个字符串。

# re_sub.py

import re

bold = re.compile(r'\*{2}(.*?)\*{2}')

text = 'Make this **bold**.  This **too**.'

print('Text:', text)
print('Bold:', bold.sub(r'<b>\1</b>', text))

被正则表达式匹配的文本内容可以直接通过语法\num插入到替换的文本中,即引用分组。

$ python3 re_sub.py

Text: Make this **bold**.  This **too**.
Bold: Make this <b>bold</b>.  This <b>too</b>.

如果在替换时使用命名分组,要使用语法\g<name>

# re_sub_named_groups.py

import re

bold = re.compile(r'\*{2}(?P<bold_text>.*?)\*{2}')

text = 'Make this **bold**.  This **too**.'

print('Text:', text)
print('Bold:', bold.sub(r'<b>\g<bold_text></b>', text))

\g<name>也可以与数字编号引用一起使用,使用它来消除分组编号和字面意义上的数字的歧义。

$ python3 re_sub_named_groups.py

Text: Make this **bold**.  This **too**.
Bold: Make this <b>bold</b>.  This <b>too</b>.

如果给sub方法传递一个count参数,可以限制替换的文本数量。

# re_sub_count.py

import re

bold = re.compile(r'\*{2}(.*?)\*{2}')

text = 'Make this **bold**.  This **too**.'

print('Text:', text)
print('Bold:', bold.sub(r'<b>\1</b>', text, count=1))

只替换了第一个单词,因为count的值是1。

$ python3 re_sub_count.py

Text: Make this **bold**.  This **too**.
Bold: Make this <b>bold</b>.  This **too**.

subn()方法同sub()方法类似,只是它既返回修改后的字符串,也会返回替换的数量。

# re_subn.py

import re

bold = re.compile(r'\*{2}(.*?)\*{2}')

text = 'Make this **bold**.  This **too**.'

print('Text:', text)
print('Bold:', bold.subn(r'<b>\1</b>', text))

这个例子中,搜索模式被替换了2次。

$ python3 re_subn.py

Text: Make this **bold**.  This **too**.
Bold: ('Make this <b>bold</b>.  This <b>too</b>.', 2)

使用模式分割字符串

str.split()方法是一个经常使用的分割字符串的方法。它只支持普通的字符来作为分割符,然而很多时候,因为输入的文本并不规范,我们必须使用正则表达式作为分隔符。比如,许多文本标记语言使用两个或者更多的换行符\n来作为段落的分割,在这个情况下,str.split()无法使用,因为我们无法表示“更多”的换行符。

一个识别段落的策略是使用findall()方法,并且使用这样的模式:(.+?)\n{2:}

# re_paragraphs_findall.py

import re

text = '''Paragraph one
on two lines.

Paragraph two.


Paragraph three.'''

for num, para in enumerate(re.findall(r'(.+?)\n{2,}',
                                      text,
                                      flags=re.DOTALL)
                           ):
    print(num, repr(para))
    print()

这个模式会在最后一个段落匹配失败,也就是例子中的“Paragraph three”,因为它后面没有两个以上的换行符。

$ python3 re_paragraphs_findall.py

0 'Paragraph one\non two lines.'

1 'Paragraph two.'

我们扩展这个模式,认为一个段落是以两个以上的换行符或者结束符$结尾,就可以解决这个问题。但是这样又使得模式变得复杂起来。这里,我们可以使用re.split()而不是re.findall(),就可以让模式变得更简单。

# re_split.py

import re

text = '''Paragraph one
on two lines.

Paragraph two.


Paragraph three.'''

print('With findall:')
for num, para in enumerate(re.findall(r'(.+?)(\n{2,}|$)',
                                      text,
                                      flags=re.DOTALL)):
    print(num, repr(para))
    print()

print()
print('With split:')
for num, para in enumerate(re.split(r'\n{2,}', text)):
    print(num, repr(para))
    print()

传递给split()方法的模式参数可以更精确地表示标记规范,即:两个以上的换行符代表两个段落的分割符。

$ python3 re_split.py

With findall:
0 ('Paragraph one\non two lines.', '\n\n')

1 ('Paragraph two.', '\n\n\n')

2 ('Paragraph three.', '')


With split:
0 'Paragraph one\non two lines.'

1 'Paragraph two.'

2 'Paragraph three.'

如果将表达式使用括号括起来,构成一个分组,可以让split()方法像str.partition()一样工作,它不仅会返回分割后的字符串,还会返回分隔符。

# re_split_groups.py

import re

text = '''Paragraph one
on two lines.

Paragraph two.


Paragraph three.'''

print('With split:')
for num, para in enumerate(re.split(r'(\n{2,})', text)):
    print(num, repr(para))
    print()

现在输出不仅包含每个段落,还包含每个段落中间的分隔符。

$ python3 re_split_groups.py

With split:
0 'Paragraph one\non two lines.'

1 '\n\n'

2 'Paragraph two.'

3 '\n\n\n'

4 'Paragraph three.'

原文点这里

参考:

1.re模块的官方文档

2.Regular Expression HOWTO - 正则表达式介绍 by Andrew Kuchling

3.Kodos - 一个用于测试正则表达式的交互工具 by Phil Schwartz

4.pythex - 一个基于Web的正则表达式测试工具 by Gabriel Rodríguez

5.Wikipedia: Regular expression - 正则表达式的概念和技术的介绍

6.locale - 使用locale模块处理Unicode文本

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值