[Python]正则表达式-不完全使用

1. 基础字符

1.1 元字符

元字符是一些特殊的metacharactes, 并且不匹配自己。相反,他们表示应该匹配一些与众不同的东西,或者通过重复他们或改变他们的含义来影响正则的其他部分。元字符如下:

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

  1. 在元字符"[ ]“中,可以单独列出字符,也可以通过给出两个字符并用”-"标记将它们分开来表示一系列字符。例如,[abc]将匹配任何字符a、b或c(匹配单个字符),这与[a-c]相同,它使用一个范围来表示同一组字符。
  2. 字符类中的元字符不生效。例如,[akm]a,k,m,]将匹配'a', 'k', 'm', '’中的任意字符; '$'通常是一个元字符,但在一个字符类中它被剥夺了特殊性。

1.2 预定义字符集

  • \d 匹配任何十进制数字;这等价于[0-9]
  • \D 匹配任何非数字字符;这等价于[^0-9]
  • \s 匹配任何空白字符;这等价于[ \t\n\r\f\v]
  • \S 匹配任何非空白字符;这等价于[^ \t\n\r\f\v]
  • \w 匹配任何字母与数字字符;这相当于类[a-zA-Z0-9]
  • \W 匹配任何非字母与数字字符;这相当于类[^a-zA-Z0-9]

1.3 常用操作符

操作符 说明 实例
. 表示任意单个字符 对于大小写字母、各种符号都能匹配
[ ] 字符集,对单个字符给出取值范围 [abc]表示a、b、c,[a-z]表示a到z的单个字符
[^ ] 非字符集,对单个字符给出排除范围 [^abc ]表示非a或b或c的单个字符
* 前一个字符0次或无限次扩展 abc*表示ab、abc、abcc、abccc等
+ 前一个字符1次或无限次扩展 abc+表示abc、abcc、abccc等
前一个字符0次或1次扩展 abc?表示ab、abc
| 左右表达式任意一个 abc|def表示abc、def
{m} 扩展前一个字符m次 ab{2}c表示abbc
{m, n} 扩展前一个字符m至n次(包 含n) ab{1, 2}表示ab、abb
^ 匹配字符串开头 ^abc表示abc且在一个字符串的开头
$ 匹配字符串结尾 abc$表示abc且在一个字符串的结尾
( ) 分组标记,内部只能使用 | 操作符 (abc)表示abc,(abc | def)表示abc、def
\d 表示十进制数字
\w 匹配任何字母与数字字符 相当于[a-zA-Z0-9]

tips:

  1. ?字符可以看作是一个可选字符,如home-?grew,其表示home-grew或者homegrew,即当作 ? 的前一个字符是可选的的;
  2. 对于{m, n}来说,如果不限定m和n的数值,即{ , },则认为下限m的值为0,而上限n的值则为无穷,因此:
    • *也就等价于{0, }
    • +也就等价于{1, }
    • ?也就等价于{0, 1}
    • 但在实际中,还是建议使用*、+、?的形式,这样更短更容易阅读

2. 使用正则表达式

有了前面的基础字符知识,我们已经可以使用正则表达式了,在python中如何使用它们呢?re模块提供了正则表达式引擎的接口,允许你将正则编译为对象,然后用它们进行匹配。

2.1 编译正则表达式

正则表达式首先要编译成模式对象,然后才能进行各种操作。

import re
re_pattern = re.compile('ab*')
print("re_pattern : ",re_pattern)
print("type :", type(re_pattern))

[output]
re_pattern :  re.compile('ab*')
type : <class 're.Pattern'>

需要明确的是,正则是作为字符串传递给re.compile()的,正则被处理为字符串,因为正则表达式不是核心python语言的一部分,并且没有创建用于表达它们的特殊语法。

可以理解的是,在正则被re模块编译之前,它就是一个普通的字符串,这与python语言所理解的字符串并无二致,如前所述,正则表达式使用反斜杠字符 \ 来表示特殊形式或允许使用特殊字符而不调用它们的特殊含义,这就会与python字符串中的反斜杠用途发生冲突,导致反斜杠灾难。

例如我们要编写一个与 \expression相匹配的正则,那么编译之后得到的目标模式就应该是 \ \expression,在编译之前,正则就是一个普通的字符串,对于每一个 \,在字符串中表示就是 \ \,也就是说,编译前的字符串就应该是 \ \ \ \ expression,可见,对目标字符串中如果有一个反斜杠,那么传给编译器的正则表达式中就会有4个反斜杠,十分繁琐。

字符 阶段
\expression 被匹配的字符串
\ \ expression 为 re.compile()转义的反斜杠
“\\\\expression” 为字符串字面转义的反斜杠

解决方案是使用python的原始字符串表示法来表示正则表达式;反斜杠不以任何特殊的方式处理前缀为 ‘r’的字符串字面,因此 r’\n’ 是一个包含 ''和 'n’的双字符字符串,而 '\n’是一个包含换行符的单字符字符串。正则表达式通常使用这种原始字符串表示法用python代码编写。

常规字符串 原始字符串
“ab*” r"ab*"
“\\\\expression” r"\\expression"
“\\w+\\s+\\l” r"\w+\s+\l"

tips:

# 带反斜杠的特殊字符可以用\d或者\\d来匹配
pattern = re.compile("\\d")
result = pattern.findall("space hh99")
print(result)

[output] : ['9', '9']

# 匹配一个反斜杠
pattern = re.compile("\\\\")
result = pattern.findall("n\\b\\a")
print(result)

[output] : ['\\', '\\']

3. 应用匹配

3.1 re库的常用函数

函数 说明
search() 扫描字符串,查找此正则匹配的任何位置,返回match对象
match() 从一个字符串的开始位置其匹配正则表达式,返回match对象
findall() 搜索字符串,以列表类型返回全部能匹配的子串
split() 将一个字符串按照正则表达式匹配结果进行分割,返回列表类型
finditer() 搜索字符串,返回一个匹配结果的迭代类型,每个迭代元素是match对象
sub() 在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串

3.2 匹配对象

方法/属性 目的
group() 返回正则匹配的字符串
start() 返回匹配的开始位置
end() 返回匹配的结束位置
span() 返回包含匹配(start, end)位置的元组

group()返回正则匹配的子字符串。start()和end()返回匹配的其实和结束索引。span()在单个元组中返回开始和结束索引。由于match()方法只检查正则是否在字符串的开头匹配,所以start()将始终为0。

注意:

只有match对象才有上述属性,也就是说,对一个模式只有使用search()、match()和finditer()后所返回的match对象才有上述属性,而对于使用findall()等方法,由于其返回对象是一个list,所以没有上述属性。

import re

result1 = pattern.match("little boy found his favorite toy")
result2 = pattern.match("littleboyfoundhisfavoritetoy")
print(result1)
print(result2)

[output]
>>> <re.Match object; span=(0, 6), match='little'>
>>> <re.Match object; span=(0, 28), 	       match='littleboyfoundhisfavoritetoy'>

result2.group()
[output]
>>> 'littleboyfoundhisfavoritetoy'

result2.start()
[output]
>>> 0

result2.end()
[output]
>>> 28

result2.span()
[output]
>>> (0, 28)

但是,模式的search()方法会扫描字符串,因此在这种情况下匹配可能不会从0开始。

result3 = pattern.search("****little boy`s here")
result3.group()
[output]
>>> 'little'

result3.start()
[output]
>>> 4

result3.end()
[output]
>>> 10

result3.span()
[output]
>>> (4, 10)

4. 更多模式能力

4.1 分组

通常,你需要获取更多信息,而不仅仅是正则是否匹配。正则表达式通常用于通过将正则分成几个子组来解析字符串,这些子组匹配不同的感兴趣组件。

例如,现有如下信息:

From: [email protected]

User-Agent: Thunderbird

MIME-Version: 1.0

To: editor@example

我们想要提取每一个 :后面的信息,因此可以使用分组来进行匹配,

# e.g. 4.1.1

info = "From: [email protected]\nUser-Agent: Thunderbird\nMIME-Version: 1.0\nTo: editor@example"

print(info)
[output]
>>>
	From: author@example.com
	User-Agent: Thunderbird
	MIME-Version: 1.0
	To: editor@example

pattern1 = re.compile(r"From:\s+(\w+.\w+.\w+)\s+User-Agent:\s+(\w+)\s+MIME-Version:\s+(\d+.\d+)\s+To:\s+(\w+.\w+)", re.IGNORECASE)

result = pattern1.findall(info)
print(result)

[output]
>>>
[('[email protected]', 'Thunderbird', '1.0', 'editor@example')]

result1 = pattern1.finditer(info)
for result in result1:
    result.group(1)
    result.group(2)
    result.group(3)
    result.group(4)
   
Out[108]: '[email protected]'
Out[108]: 'Thunderbird'
Out[108]: '1.0'
Out[108]: 'editor@example'

再上述4.1.1的例子中,我们在模式中写了四个分组,分别匹配From、User-Agent、MIME-Version和To后面的内容,这四个分组被自动地编号,分别为1、2、3、4,同时被捕获到了相应的内存中,这就引入了分组的后向引用,例如,我要进行一个双词的匹配,请看下例:

#e.g. 4.1.2

raw = "loving loving can hurt"
'''
在匹配之前,(\w+)这个分组可以匹配任何单词内容,一旦当这个分组匹配完毕,在这里,一开始就匹配了loving这个单词,因此,就把loving捕获,放在分组1的内存单元,再经过一个空格后,把分组1中的捕获内容取出,与当前单词比较,看是否匹配,如果匹配,则结束,否则,重新匹配这个分组。
'''
pattern = re.compile(r"\b(\w+)\b\s+\1")
print(pattern.findall(raw))

[output]
>>>
['loving']

4.2 忽略某个分组

有时候给正则的某个子表达式加括号并不是为了分组,而仅仅是为了看起来更清晰,因此我们并不需要捕获该分组,那么,可以使用(?:expression)来忽略该分组。

# e.g. 4.2.1
raw = "age:13, name:Tom"
pattern = re.compile(r"age.(\d+).\s*name.(\w+)")

print(pattern.findall(raw))
[out]: [('13', 'Tom')]

print(pattern.search(raw).group(1))
[out]: 13
    
print(pattern.search(raw).group(2))
[out]: Tom
    
pattern1 = re.compile(r"age.(\d+).\s*name.(?:\w+)")
print(pattern1.findall(raw))
[out]: ['13']
print(pattern1.search(raw).group(1))
[out]: 13
# 由于忽略了第二个分组,因此无法检索该分组
print(pattern1.search(raw).group(2))
[out]: IndexError: no such group

需要注意的是,除了你无法检索组匹配内容的事实外,非捕获组的行为与捕获组完全相同;你可以在里面放任何东西,用重复元字符重复它,比如*,燃火把它嵌入其他组(捕获或不捕获)。(?:…)在修改现有模式的时候特别有用,因为你可以添加新组而不更改所有其他组的编号方式。红色部分不懂什么意思。值得一提的是,捕获和非捕获组之间的搜索没有性能差异,两种形式没有哪一种更快。

4.3 命名分组

命名组的语法是python特定的扩展之一:(?P…)。name是该分组的名称。命名组的行为与捕获组完全相同,并且还将名称与组关联。处理捕获组的的匹配对象方法都接受 1. 按编号引用该分组;2. 按组名字符串引用该分组。

raw = "sheeran sheeran is good"
pattern = re.compile(r"(?P<name>\b\w+\b)\s+(?P=name)")
print(pattern.findall(raw))

[out]: ['sheeran']

pattern = re.compile(r"(?P<name>\b\w+\b)\s+\1")
print(pattern.findall(raw))

[out]: ['sheeran']

4.4 前向断言

另一个零宽度断言是前向断言。前向断言以正面和负面形式提供,如下所示:

  1. 正前向断言 – (?=…)

    如果包含的正则表达式,由...表示,在当前位置成功匹配,则成功,否则失败。但是,一旦尝试了包含的表达式,匹配的引擎就不会前进,模式其余的部分会在断言开始的地方尝试。

    # 选出得分大于30分的球员
    raw = ["James:36.33", "Bryant:33.2", "ONeal:24.33"]
    pattern = re.compile(r".+(?=[:][3][0-9][.][0-9]+)")
    
    for it in raw:
        print(pattern.findall(it))
        
    [out]: ['James']
    	   ['Bryant']
    	   []
    
  2. 负前向断言 – (?!..)

    如果包含的表达式在字符串中的当前位置不匹配,则成功。

    '''
    现有一些文件如下:
    ["sample.txt", "ss.batch", "text.bat", "computer.sss", "name.is.wiki", "haha.bat.ten", "start.end.bat"]
    
    我们需要从中选取出后缀不是bat的文件,因此可以使用负前向断言来匹配后缀不是bat的文件
    '''
    
    raw = ["sample.txt", "ss.batch", "text.bat", "computer.sss", "name.is.wiki", "haha.bat.ten", "start.end.bat"]
    
    pattern = re.compile(r".*[.](?!bat$)[^.]*")
    for it in raw:
        print(pattern.findall(it))
       
    
    [out]: ['sample.txt']
    	   ['ss.batch']
           []
           ['computer.sss']
           ['name.is.wiki']
           ['haha.bat.ten']
           []
    

    在上述代码中,模式是r".*[.](?!bat$)[^.]*",意思是,从结尾匹配bat,如果匹配,则该模式失败,如果结尾不匹配bat,那么开始匹配模式[^.]*$。也就是说,如果结尾不匹配bat,我们使用[^.]*$ 就可以把不匹配的后缀部分输出。如果我们使用的模式是r".*[.](?!bat$),那么就相当于把后缀不是bat的.*[.] 输出,即只是输出后缀前面的部分:

    raw = ["sample.txt", "ss.batch", "text.bat", "computer.sss", "name.is.wiki", "haha.bat.ten"]
    
    pattern = re.compile(r".*[.](?!bat$)")
    for it in raw:
        print(pattern.findall(it))
        
    [out]: ['sample.']
    	   ['ss.']
    	   []
    	   ['computer.']
    	   ['name.is.']
    	   ['haha.bat.']    
    

    tips:

    在平常使用中,更感觉前向断言像是一个筛选条件,我们可以匹配一个字符或字符串,在匹配对象的后面或者前面我们设定一些条件,可以用前向断言来实现。

5. 修改字符串

到目前为止,我们只是针对静态字符串执行搜索。正则表达式通常也用于以各种方式修改字符串,使用以下模式方法:

方法/属性 目的
split() 将字符串拆分成一个列表,在正则匹配的任何地方将其拆分
sub() 找到正则匹配的所有子字符串,并用不同的字符串替换它们
subn() 与sub()相同,但返回新字符串和替换次数

5.1 分割字符串

模式的split()方法在正则匹配的任何地方拆分字符串,返回一个片段列表。它有两种使用方式:

  • pattern.split(string [, maxsplit = 0])
  • re.split(pattern, string [, maxsplit = 0])

通过正则表达式的匹配拆分字符串,如果在正则中使用捕获括号,则它们的内容也将作为结果列表的一部分返回。如果maxsplit非零,则最多执行maxsplit次拆分。

# 分隔符是任意非字母数字字符序列
p = re.compile(r"\W+")
# 不设定maxsplit
result1 = p.split("this is a test, short and sweet, of split()")
print(result1)

[out]:  ['this', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']

# 设定maxsplit = 3,则最多在字符串上分隔3次
result2 = p.split("this is a test, short and sweet, of split()", maxsplit = 3)
print(result2)

[out]:  ['this', 'is', 'a', 'test, short and sweet, of split()']

如果我们不单单想要根据分隔符分隔出的文本,而且还需要知道分隔符是什么,因此,如果在正则中使用捕获括号,则分隔符的内容也会作为列表的一部分返回。

# 在这一句中,分隔符是“, ”
re.split(r"([\W]+)", "words, words, words")

5.2 搜索和替换

另一个常见任务是找到模式的所有匹配项,并用不同的字符串替换它们。sub()方法接受一个替换值,可以是字符串或函数,也可以是要处理的字符串。

  • pattern.sub(replacement, string[, count = 0])
  1. 使用replacement来替换string中的匹配对象,如果没有找到模式,则string将保持不变。
  2. 可选参数count是要替换的模式最大出现次数,count必须是非负整数。默认为0表示替换所有。
raw = "sheeran is a good singer, and I think sheeran is awesome!"
pattern = re.compile(r"sheeran")
pattern.sub("jay", raw)

[out]:  
	'jay is a good singer, and I think jay is awesome!'

pattern.sub("Jay", raw, count = 1)
[out]:
    'Jay is a good singer, and I think sheeran is awesome!'

# subn的用法和sub一致,但返回一个包含新字符串值和一致性的替换次数的2元组。    
pattern.subn("Jay", raw)
[out]:
    ('Jay is a good singer, and I think Jay is awesome!', 2)

需要注意的是,如果replacement是一个字符串,则处理其中的任何反斜杠转义。也就是说,\n被转换为单个换行符,\r 被转换为回车符,依此类推。

还有一种语法用于引用由(?P<name>...)语法定义的命名组。\g<name> 将使用名称为name 的组匹配的子字符串,\g<number> 使用相应的组号(后向引用),因此\g<2> 等同于\2 ,但在诸如\g<2>0 之类的替换字符串中并不模糊。而\20 则被解释为对组20捕获的内容惊醒引用,而不是对组2的引用,因此不建议使用\20 这种用法,以下几种用法都是等效的:

player = "James{Forward} Rondo{Guard} Cousins{Center}"

p = re.compile(r"\w+{(?P<position>[^}]*)}")
# 把所有匹配正则的子字符串替换成 player(\g<position>)
p.sub(r"player(\g<position>)", player)
[out]:
    'player(Forward) player(Guard) player(Center)'

p.sub(r"player(\g<1>)",player)
[out]:
    'player(Forward) player(Guard) player(Center)'

p.sub(r"player(\1)", player)
[out]:
    'player(Forward) player(Guard) player(Center)'

此外,replacement也可以是一个函数,它可以为你提供更多控制。如果replacement是一个函数,则为patter的每次非重叠出现将调用该函数。在每次调用时,函数都会传递一个匹配的匹配对象参数,并可以使用此信息计算所需的替换字符串并将其返回。

# 实现将句中的正数转换为浮点数
def transfer(match):
    value = int(match.group())
    after_transfer = float(value)
    # 字符类型转换
    return str(after_transfer)

raw = "James scored 33 points and Davis scored 29 points"
p = re.compile(r"\d+")

'''
需要注意的时,sub(replacement, string),replacement一般都是一个字符型对象。
'''
p.sub(transfer, raw)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章