1. 基础字符
1.1 元字符
元字符是一些特殊的metacharactes, 并且不匹配自己。相反,他们表示应该匹配一些与众不同的东西,或者通过重复他们或改变他们的含义来影响正则的其他部分。元字符如下:
. ^ $ * + ? { } [ ] \ | ( )
- 在元字符"[ ]“中,可以单独列出字符,也可以通过给出两个字符并用”-"标记将它们分开来表示一系列字符。例如,[abc]将匹配任何字符a、b或c(匹配单个字符),这与[a-c]相同,它使用一个范围来表示同一组字符。
- 字符类中的元字符不生效。例如,[akm ] 将 匹 配 ′ a ′ , ′ k ′ , ′ m ′ , ′ ]将匹配'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:
- ?字符可以看作是一个可选字符,如home-?grew,其表示home-grew或者homegrew,即当作 ? 的前一个字符是可选的的;
- 对于{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: author@example.com
User-Agent: Thunderbird
MIME-Version: 1.0
To: editor@example
我们想要提取每一个 :后面的信息,因此可以使用分组来进行匹配,
# e.g. 4.1.1
info = "From: author@example.com\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]
>>>
[('author@example.com', '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]: 'author@example.com'
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 前向断言
另一个零宽度断言是前向断言。前向断言以正面和负面形式提供,如下所示:
-
正前向断言 – (?=…)
如果包含的正则表达式,由
...
表示,在当前位置成功匹配,则成功,否则失败。但是,一旦尝试了包含的表达式,匹配的引擎就不会前进,模式其余的部分会在断言开始的地方尝试。# 选出得分大于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'] []
-
负前向断言 – (?!..)
如果包含的表达式在字符串中的当前位置不匹配,则成功。
''' 现有一些文件如下: ["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])
- 使用replacement来替换string中的匹配对象,如果没有找到模式,则string将保持不变。
- 可选参数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)