Python标准库 - re -- 正则表达式 (2)

 参考:

re --- 正则表达式操作 — Python 3.12.0 文档

正则表达式指南 — Python 3.12.0 文档

正则表达式 – 教程 | 菜鸟教程 (runoob.com)

正则表达式——7种免费测试工具_正则表达式测试工具-CSDN博客

正则表达式对象 (正则对象)

主要是指:由 re.compile() 返回的已编译正则表达式对象。

>>> import re
>>> pattern = re.compile("d")
>>> pattern.search("dog")     # Match at index 0

>>> pattern.search("dog", 1)  # No match; search doesn't include the "d"

search()和match()的区别:match()只在字符串的开头位置检测匹配,search()在字符串中的任何位置检测匹配(这也是 Perl 在默认情况下所做的)。例如:

>>> pattern = re.compile("o")
>>> pattern.search("dog")

>>> pattern.match("dog")

说明:

  • 同样匹配'dog'中的'o',使用search()可以匹配,但使用match()无法匹配。

注意 MULTILINE 多行模式中函数 match() 只匹配字符串的开始,但使用 search() 和以 '^' 开始的正则表达式会匹配每行的开始

>>> re.match("X", "A\nB\nX", re.MULTILINE)  # No match
>>> re.search("^X", "A\nB\nX", re.MULTILINE)  # Match

Python标准库 - re -- 正则表达式 (1)中讲到的函数(re.split(), re.findall(), re.sub()....),都有对应的方法存在于正则表达式对象中。用法类似,只是参数中少了正则表达式对应的字符串(参数pattern),因为这个正则表达式已经在调用re.compile()的时候存在于这个正则表达式对象中了。

匹配对象

就是前文中经常提到的match object。

匹配对象总是有一个布尔值 True。如果没有匹配的话 match() 和 search() 返回 None(判断时作为布尔值False) 所以你可以简单的用 if 语句来判断是否匹配

>>> pattern = re.compile("o")
>>> match = pattern.search("dog")
>>> if match:
...   print("search 'o' in 'dog' OK")
...
search 'o' in 'dog' OK

Match.group([group1, ...])

返回一个或者多个匹配的子组。如果只有一个参数,结果就是一个字符串,如果有多个参数,结果就是一个元组(每个参数对应一个项),如果没有参数,组1默认到0(整个匹配都被返回)。 如果一个组N 参数值为 0,相应的返回值就是整个匹配字符串;如果它是一个范围 [1..99],结果就是相应的括号组字符串。如果一个组号是负数,或者大于样式中定义的组数,就引发一个 IndexError 异常。如果一个组包含在样式的一部分,并被匹配多次,就返回最后一个匹配。

>>> m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist, Bill Clark, sportsman")
>>> m.group()
'Isaac Newton'
>>> m.group(0)
'Isaac Newton'
>>> m.group(1)
'Isaac'
>>> m.group(2)
'Newton'
>>> m.group(1, 2)
('Isaac', 'Newton')

注意:

  • match()或者search()在匹配到一个时,都会停止继续匹配。因此上例中match()函数不会去匹配后面的"Bill Clark"

如果正则表达式使用了 (?P...) 语法, groupN 参数就也可能是命名组合的名字。

>>> m = re.match(r"(?P\w+) (?P\w+)", "Malcolm Reynolds")
>>> m.group('first_name')
'Malcolm'
>>> m.group('last_name')
'Reynolds'

命名组合同样可以通过索引值引用,使用索引时不需要引号

>>> m.group(1)
'Malcolm'
>>> m.group(2)
'Reynolds'

如果一个组匹配成功多次,这个组只返回最后一个匹配。

>>> m = re.match(r"(..)+", "a1b2c3")
>>> m.group(1)
'c3'
>>> print(m)

>>> m = re.match(r"(..)?", "a1b2c3") # This indicates'a1' can be matched
>>> m.group(1)
'a1'
>>> print(m)

注意:

  • 这条规则是针对一般的正则表达式的,而非Python中特有(A repeated capturing group will only capture the last iteration. )
  • match对象本身似乎还是匹配的整个字符串。
  • 这里的"匹配成功多次",我觉得中文翻译得并不准确,这里的多次,重在“重复”,即出现了'+'。注意和下面的例子进行比较:(\d+)其实也可以匹配20/40/60,并且事实上对于一般的正则表达式匹配(默认带'g'修饰)规则而言,确实有3个匹配:20 40 60。但是python的match()只会搜索到第一个匹配项后就停止,因此只出现了一个组。由此可见:应该本条规则的关键在于是否对组使用了+
>>> m = re.match(r"(\d+)", "20 40 60")
>>> m.group(1)
'20'
>>> m.group(2)
Traceback (most recent call last):
  File "", line 1, in 
IndexError: no such group

获取分组也可以用这种形式:m[0]/m[1]/m[2],或者m['name1']/m['name2']

Match.groups(default=None)

返回一个元组,包含所有匹配的子组,在样式中出现的从1到任意多的组合。

>>> m = re.match(r"(\d+)\.(\d+)", "24.1632")
>>> m.groups()
('24', '1632')

Match.groupdict(default=None)

以词典形式,返回所有的命名子组。词典的关键字为组名,元素为该组匹配的字符串。

>>> m = re.match(r"(?P\w+) (?P\w+)", "Malcolm Reynolds")
>>> m.groupdict()
{'first_name': 'Malcolm', 'last_name': 'Reynolds'}

Match.start([group])

Match.end([group])

返回匹配到的字符串的起始和结束位置。

如果没有group参数,或者group=0,则返回整个匹配的字符串的起止位置;否则返回对应组匹配的字符串的起止位置。例如上面的例子中,如果查看m的start()/end(),结果如下:

>>> m.start()
0
>>> m.end()
16
>>> m.start(1)
0
>>> m.end(1)
7

组有可能会匹配一个空字符串,此时这个组的start() = end()

>>> m = re.search('b(c?)', 'cba')
>>> m.start(0)
1
>>> m.end(0)
2
>>> m.start(1)
2
>>> m.end(1)
2

说明:

  • 表达式b(c?)匹配的是字符b,因此组(c?)匹配的是空字符串。所以,如果group=0,start=1(b的位置),end=2(想象为b后面、a前面,我理解实际上就是a的位置)

下面这个例子会从email地址中移除掉 remove_this

>>> email = "tony@tiremove_thisger.net"
>>> m = re.search("remove_this", email)
>>> email[:m.start()] + email[m.end():]
'[email protected]'

正则表达式例子

检查对子

假设你在写一个扑克程序,一个玩家的一手牌为五个字符的串,每个字符表示一张牌,"a" 就是 A, "k" K, "q" Q, "j" J, "t" 为 10, "2" 到 "9" 表示2 到 9。实现如下需求的功能:

  1. 检查输入的扑克牌(字符串)是否有效
  2. 如果有效,检查是否有对子
  3. 如果有对子,打印对几

限制:

  1. 如果有多个对子,只打印一个对子
  2. 不找3张,4张相同的牌,即3/4的相同的牌,也会被当成对子

test_re.py文件源码如下:

import re

def displaymatch(match):
    if match is None:
        return None
    print('' % (match.group(), match.groups()))

def check_pokers(pokers):
    """
    """

    # check if pokers are valid
    valid = re.compile(r"^[a2-9tjqk]{5}$")
    valid_match = valid.match(pokers)
    displaymatch(valid_match)
    if valid_match is None:
        print("pokers: %r not valid" % (pokers))
        return

    # if valid, check if pair exists
    pair = re.compile(r".*(.).*\1")
    pair_match = pair.match(pokers)
    if pair_match is None:
        print("pokers: %r has no pair" % (pokers))
        return

    # if pair exists, display which pair
    print("pokers: %r has pair %r" % (pokers, pair_match.group(1)))

主要技术点就是:使用了对组的反向引用来检查是否存在对子,即正则表达式".*(.).*\1"。其中,这个表达式中的\1就是对组(.)的引用,称为反向引用。

导入这个模块,测试程序如下:

>>> import test_re
>>> test_re.check_pokers("727ak")

pokers: '727ak' has pair '7'
>>> test_re.check_pokers("akt5e")
pokers: 'akt5e' not valid
>>> test_re.check_pokers("aa788")

pokers: 'aa788' has pair '8'
>>> test_re.check_pokers("aa777")

pokers: 'aa777' has pair '7'

模拟 scanf()

下表提供了 scanf() 格式符和正则表达式之间一些大致等价的映射。

scanf() 形符

正则表达式

%c

.

%5c

.{5}

%d

[-+]?\d+

%e, %E, %f, %g

[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?

%i

[-+]?(0[xX][\dA-Fa-f]+|0[0-7]*|\d+)

%o

[-+]?[0-7]+

%s

\S+

%u

\d+

%x, %X

[-+]?(0[xX])?[\dA-Fa-f]+

制作一个电话本

根据文本的特点,使用re.split()可以将内容拆分成表的形式。

例如,有如下文本,我们希望拆分成“姓名/电话/地址”这样的列表

>>> 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"""

分析:先用换行符\n拆行,注意:每两行有效内容之间可能会有一至多个换行符,因此需要'\n+'。然后对每一行,按照": "或者" "来拆分,注意最后的地址部分的空格不能拆,可以通过设定最大拆分的次数来达到这个目的。

>>> entries = re.split("\n+", text)
>>> for entry in entries:
...   print(entry)
...
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
>>> for entry in entries:
...   for item in [re.split(":? ", entry, 3)]:
...    print(item)
...
['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']

文字整理

sub() 替换字符串中出现的样式的每一个实例。因此,可以用来做文字整理。同时,由于sub()函数的替换字符repl可以是函数,因此,可以设计非常强大的文字整理工具。

教材上的例子是:将每个单词除首尾之外的字母随机打乱顺序。

查找所有副词

findall() 匹配样式 所有 的出现,不仅是像 search() 中的第一个匹配。比如,如果一个作者希望找到文字中的所有副词,他可能会按照以下方法用 findall()

>>> text = "He was carefully disguised but captured quickly by police."
>>> re.findall(r"[a-zA-z]+ly\b",text)
['carefully', 'quickly']

写一个词法分析器

一个 词法器或词法分析器 分析字符串,并分类成目录组。 这是写一个编译器或解释器的第一步。

我们将class和功能函数tokenize()放到test_re.py这个模块中,将后面的测试程序放在交互窗口中。

test_re.py中代码如下:

import re
from typing import NamedTuple
class Token(NamedTuple):
    type: str
    value: str
    line: int
    column: int

def tokenize(code):
    keywords = {'IF', 'THEN', 'ENDIF', 'FOR', 'NEXT', 'GOSUB', 'RETURN'}
    token_specification = [
        ('NUMBER',   r'\d+(\.\d*)?'),  # Integer or decimal number
        ('ASSIGN',   r':='),           # Assignment operator
        ('END',      r';'),            # Statement terminator
        ('ID',       r'[A-Za-z]+'),    # Identifiers
        ('OP',       r'[+\-*/]'),      # Arithmetic operators
        ('NEWLINE',  r'\n'),           # Line endings
        ('SKIP',     r'[ \t]+'),       # Skip over spaces and tabs
        ('MISMATCH', r'.'),            # Any other character
    ]
    tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
    print(tok_regex)
    line_num = 1
    line_start = 0
    for mo in re.finditer(tok_regex, code):
        kind = mo.lastgroup
        value = mo.group()
        column = mo.start() - line_start
        if kind == 'NUMBER':
            value = float(value) if '.' in value else int(value)
        elif kind == 'ID' and value in keywords:
            kind = value
        elif kind == 'NEWLINE':
            line_start = mo.end()
            line_num += 1
            continue
        elif kind == 'SKIP':
            continue
        elif kind == 'MISMATCH':
            raise RuntimeError(f'{value!r} unexpected on line {line_num}')
        yield Token(kind, value, line_num, column)

说明:

  • 使用NamedTuple这个内置类型构造Token这个类。这个类的作用是用作tokenize()的返回值。NamedTuple的用法可以查看Python手册的有关内容,这里可以先记住这个用法的基本形式。type/value/line/column是定义的元组成员,str和int都是Python的内置类型,可以直接使用。
  • 注意tok_regex的构造方法,('(?P<%s>%s)' % pair for pair in token_specification)是一个生成器表达式,通过使用旧式的'%'来产生格式化字符串,其中参数"pair"是一个元组,因其里面有2个元素,故可以替代格式化字符串'(?P<%s>%s)' 中的2个'%s'。str.join(iterable)用来使用这个生成器,该函数“返回一个由 iterable 中的字符串拼接而成的字符串”,通过Python教程学习 (6)的学习可知,生成器本身也是一个迭代器,因此str.join()里面的参数正好是一个可迭代的字符串。最后注意:在使用'|'符号连接每一个子正则表达式的时候,我们对每一个子正则表达式都使用了(),这样也可以防止出现拼接时的优先级问题。
  • 和re.findall类似,re.finditer()也会搜索目标字符串中所有匹配的对象,但不同的时候,其产生的是一个Match对象的迭代器(re.findall产生的是一个列表)
  • mo.lastgroup是命名组名字,也就是'(?P<%s>%s)'中的P<%s>中间的%s,当然要替换成真实的字符串,也就是token_specification中的第一列中的元素('NUMBER',‘ASSIGN’...)。mo.group()是所有组匹配出来的字符串,因为'(?P<%s>%s)'只有一个圆括号,即只有一个组,因此每次也只会找出匹配一种样式的字符串。例如,当前找到了一个目标词'IF',会匹配'(?P[A-Za-z]+)'。于是,kind = mo.lastgroup即组名ID,而value=匹配的字符串,也就是目标词'IF'
  • if/elif语句中的语法有2点需要注意一下:其一,kind == 'NEWLINE':时,line_start = mo.end(),这个mo.end()是返回这一行最后一个字符的位置,因为column = mo.start() - line_start,所以每过一行,要把前一行最后一个字符的位置作为基准,下一行的列号通过这个基准计算出来。其二,有几个elif用到了continue/raise,有些则没有,使用了continue/raise的语句会跳过yield那一行继续for循环,而没有的则会执行yield语句;这个具体的区别看后面对yield那一行的说明
  • 在Python教程学习 (6)中,有对yield语句的详细描述,这里针对具体使用场景简单分析一下。执行到yield这一行时,函数会被打断,并返回,等待下一次迭代。于是每匹配一个词,就会返回一个四元元组(kind, value, line_num, column)。因此tokenize实际上就是一个典型的生成器(函数)。

对上述代码的使用如下:

>>> statements = '''
... IF quantity THEN
...   total := total + price * quantity;
...   tax := price * 0.05;
... ENDIF;
... '''
>>> for token in tokenize(statements):
...   print(token)
...
(?P\d+(\.\d*)?)|(?P:=)|(?P;)|(?P[A-Za-z]+)|(?P[+\-*/])|(?P\n)|(?P[ \t]+)|(?P.)
Token(type='IF', value='IF', line=2, column=0)
Token(type='ID', value='quantity', line=2, column=3)
Token(type='THEN', value='THEN', line=2, column=12)
Token(type='ID', value='total', line=3, column=2)
Token(type='ASSIGN', value=':=', line=3, column=8)
Token(type='ID', value='total', line=3, column=11)
Token(type='OP', value='+', line=3, column=17)
Token(type='ID', value='price', line=3, column=19)
Token(type='OP', value='*', line=3, column=25)
Token(type='ID', value='quantity', line=3, column=27)
Token(type='END', value=';', line=3, column=35)
Token(type='ID', value='tax', line=4, column=2)
Token(type='ASSIGN', value=':=', line=4, column=6)
Token(type='ID', value='price', line=4, column=9)
Token(type='OP', value='*', line=4, column=15)
Token(type='NUMBER', value=0.05, line=4, column=17)
Token(type='END', value=';', line=4, column=21)
Token(type='ENDIF', value='ENDIF', line=5, column=0)
Token(type='END', value=';', line=5, column=5)

正如前面所述,对tokenize()的使用,就是生成器的标准用法之一,"for xx in generator:"。

你可能感兴趣的:(Python官方资料学习,正则表达式,学习,python,正则表达式)