特别是最后一行,猴子,
后面没有其它字符了,但是 *
表示可以匹配 0 次,所以表达式也是成立的。我们也用 Python 代码看一下:
import re
content = """苹果,是绿色的
橙子,是橙色的
香蕉,是黄色的
乌鸦,是黑色的
猴子,
"""
for item in re.findall(pattern=',.\*',
string=content):
print(item)
,是绿色的
,是橙色的
,是黄色的
,是黑色的
,
💡 注意,.*
在正则表达式中非常常见,表示匹配任意字符任意次数。
当然这个 *
前面不是非得是 .
,也可以是其它字符,如 Fig.5 所示。
Fig.5 其他* 的示例
4.2.3 特殊字符:+
(重复匹配多次,不包括 0 次)
+
表示匹配前面的子表达式一次或多次,不包括 0 次。
还是上面的例子,我们要从文本中,选择每行逗号后面的字符串内容,包括逗号本身。但是添加一个条件,如果逗号后面没有内容,就不要选择了。
苹果,是绿色的
橙子,是橙色的
香蕉,是黄色的
乌鸦,是黑色的
猴子,
我们可以这样写正则表达式:
,.+
验证结果如 Fig.6 所示。
Fig.6 .+ 的使用示例
最后一行,猴子,
后面没有其它字符了,+
表示至少匹配 1 次,所以最后一行没有子串选中。
4.2.4 特殊字符:?
(匹配 0 ~ 1 次)
?
表示匹配前面的子表达式 0 次或 1 次。
还是上面的例子,我们要从文本中,选择每行逗号后面的1个字符,也包括逗号本身。
苹果,绿色的
橙子,橙色的
香蕉,黄色的
乌鸦,黑色的
猴子,
那正则表达式可以这样写:
,.?
验证结果如 Fig.7 所示。
Fig.7 .? 的使用示例
最后一行,猴子,
后面没有其它字符了,但是 `?`` 表示匹配 1 次或 0 次,所以最后一行也选中了一个逗号字符。
4.2.5 特殊字符:{}
(匹配指定次数)
示例文本如下所示:
红彤彤,绿油油,黑乎乎,绿油
红彤彤,绿油油,黑乎乎,绿油油
红彤彤,绿油油,黑乎乎,绿油油油
红彤彤,绿油油,黑乎乎,绿油油油油
红彤彤,绿油油,黑乎乎,绿油油油油油
- 表达式
油{3}
就表示匹配连续的油
字 3 次。 - 表达式
油{3,4}
就表示匹配连续的油
字至少 3 次,至多 4 次
示例如 Fig.8 所示。
Fig.8 字符{int} 和字符{int,int} 的使用示例
4.2.6 特殊字符:\
(转义字符)
反斜杠 \
用作转义字符,它有两种作用:
- 用于转义紧跟其后的特殊字符,使其失去特殊含义,被当作普通字符对待。
- 用于创建一些特定的字符类,如换行符
\n
、制表符\t
等。
例如,如果我们想匹配一个实际的点 .
,而不是作为通配符的点,我们需要在点前面加上反斜杠 \.
。同样,如果我们想匹配一个实际的反斜杠 \
,我们需要使用两个反斜杠 \\
,因为在字符串中反斜杠本身也是一个转义字符。
我们的示例文本如下:
example.com
example.net
example.org
现在我们想要得到 .
后面的后缀以确定网页的类型(包括 .
本身),那么我们的 Regex 可以这样写:
\..*
我们验证一下,验证结果见 Fig.9。
Fig.9 转义字符 \ 的使用示例
在这里,\.
表示 .
本身,它是一个普通字符而非特殊字符。.*
表示除了换行符外的所有字符。
我们也用 Python 进行一下验证:
import re
text = "这是一个例子:example.com"
pattern = r"example\.com"
match = re.search(pattern, text)
if match:
print(f"匹配结果:{match.group()}")
else:
print("没有匹配结果")
# 匹配包含反斜杠的文本
text_with_backslash = "路径:C:\\Program Files\\Example"
pattern_with_backslash = r"C:\\Program Files\\Example"
match_with_backslash = re.search(pattern_with_backslash, text_with_backslash)
if match_with_backslash:
print(f"匹配结果:{match\_with\_backslash.group()}")
else:
print("没有匹配结果")
匹配结果:example.com
匹配结果:C:\Program Files\Example
**Question**
:为什么要加 r
?
**Answer**
:在 Python 中,字符串前加上 r
或 R
表示这是一个原始字符串(raw string)。在原始字符串中,反斜杠 \
不会被当作转义字符处理,而是保持其字面意义。这意味着在原始字符串中,反斜杠后面的字符不会被特殊解释。💡 如果我们不使用原始字符串,我们需要写四个反斜杠 \\\\
来表示一个反斜杠 🤣。
4.2.7 特殊字符:[]
(匹配字符集中任意字符)
方括号 []
用于创建一个字符集,匹配方括号内列出的任意一个字符。字符集可以包含普通字符和特殊字符,但特殊字符在字符集中将失去其特殊含义,被视为普通字符。
例如,字符集 [abc]
将匹配字母 a
、b
或 c
中的任意一个。字符集也可以包含字符范围,如 [a-z]
将匹配从小写 a
到小写 z
的任意字母。
如果字符集的第一个字符是脱字符 ^
,则表示取非,匹配任何不在方括号内的字符。例如,[^abc]
将匹配除了 a
、b
和 c
之外的任意字符。
我们的示例文本如下:
abc def ghi jkl mno a b c d aa a a a a a a sdsad sajkjclkx jsadkl dskljnsdlijewqlkjsadj lasdjlkjdwijsalkj lksajd lkasjwd
- 现在我们想要得到字符 ace,那么我们的 Regex 可以这样写:
[ace]
- 如果我们不想要字符 ace,那么我们的 Regex 可以这样写:
[^ace]
- 如果我们想要字符 a 到 e 范围内的所有字符,那么我们的 Regex 可以这样写:
[a-e]
验证如 Fig.10 所示。
Fig.10 字符集的使用示例
可以看到,当我们不想要字符 ace
时([ace]
),空格也被包围了,很合理。当 [a-e]
时,空格并没有被选中,也非常合理。
[]
的 Python 示例:
import re
# 匹配字符集内的任意字符
text = "abc def ghi jkl mno a b c d aa a a a a a a sdsad sajkjclkx jsadkl dskljnsdlijewqlkjsadj lasdjlkjdwijsalkj lksajd lkasjwd"
pattern = r"[ace]"
matches = re.findall(pattern, text)
print(f"匹配结果:{matches}")
# 匹配不在字符集内的任意字符
text = "abc def ghi jkl mno a b c d aa a a a a a a sdsad sajkjclkx jsadkl dskljnsdlijewqlkjsadj lasdjlkjdwijsalkj lksajd lkasjwd"
pattern = r"[^ace]"
matches = re.findall(pattern, text)
print(f"匹配结果:{matches}")
# 匹配字符范围
text = "abc def ghi jkl mno a b c d aa a a a a a a sdsad sajkjclkx jsadkl dskljnsdlijewqlkjsadj lasdjlkjdwijsalkj lksajd lkasjwd"
pattern = r"[a-e]"
matches = re.findall(pattern, text)
print(f"匹配结果:{matches}")
匹配结果:['a', 'c', 'e', 'a', 'c', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'c', 'a', 'e', 'a', 'a', 'a', 'a', 'a']
匹配结果:['b', ' ', 'd', 'f', ' ', 'g', 'h', 'i', ' ', 'j', 'k', 'l', ' ', 'm', 'n', 'o', ' ', ' ', 'b', ' ', ' ', 'd', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 's', 'd', 's', 'd', ' ', 's', 'j', 'k', 'j', 'l', 'k', 'x', ' ', 'j', 's', 'd', 'k', 'l', ' ', 'd', 's', 'k', 'l', 'j', 'n', 's', 'd', 'l', 'i', 'j', 'w', 'q', 'l', 'k', 'j', 's', 'd', 'j', ' ', 'l', 's', 'd', 'j', 'l', 'k', 'j', 'd', 'w', 'i', 'j', 's', 'l', 'k', 'j', ' ', 'l', 'k', 's', 'j', 'd', ' ', 'l', 'k', 's', 'j', 'w', 'd']
匹配结果:['a', 'b', 'c', 'd', 'e', 'a', 'b', 'c', 'd', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'd', 'a', 'd', 'a', 'c', 'a', 'd', 'd', 'd', 'e', 'a', 'd', 'a', 'd', 'd', 'a', 'a', 'd', 'a', 'd']
在这个例子中,re.findall
函数用于找到所有匹配正则表达式的子串。第一个模式 [ace]
匹配文本中所有的 a
、c
和 e
。第二个模式 [^ace]
匹配除了 a
、c
和 e
之外的字符。第三个模式 [a-e]
匹配从 a
到 e
的所有字母。
4.2.8 特殊字符:^
(锚定匹配字符串的开头和脱字符)
脱字符 ^
有两种用途:
- 〔锚定匹配字符串的开头〕当
^
出现在正则表达式的开头时,它表示匹配行的开始。也就是说,它指定接下来的模式(pattern)必须出现在被搜索字符串的开头。 - 〔脱字符〕当
^
出现在字符集的方括号[]
内时,它表示取非,用于排除字符集内的字符。在这种情况下,它匹配任何不在方括号内列出的字符(我们刚刚接触过)。
💡 这里对第一种作用再次阐述:当 ^
出现在模式的开头时,它表示锚定(anchoring)作用,用于指定匹配必须发生在被搜索字符串的开始位置。也就是说,只有当被搜索的字符串以这个模式开始时,匹配才会成功。
也就是说 ^
表示匹配文本的开头位置。
再补充一个知识:正则表达式可以设定单行模式和多行模式,详情见 4.5 Regex 的单行模式和多行模式。
- 如果是单行模式,
^
表示匹配整个文本的开头位置。 - 如果是多行模式,
^
表示匹配文本每行的开头位置。
比如,下面的文本中,每行最前面的数字表示水果的编号,最后的数字表示价格。
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
如果我们要提取所有的水果编号,用这样的正则表达式:
^\d+
其中:
-
\d
可以先看一下 4.4 匹配某种字符类型,简单来说:\d
匹配 0-9 之间任意一个数字字符,等价于表达式[0-9]
。 -
+
表示至少匹配一次
那此时我们有点疑问,如果是 \d+
,那意味着会至少匹配一次及以上的数字,那么它不仅会匹配编号,也会匹配价格,我们用例子看一下:
import re
text = """
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
"""
pattern = r"[\d+]"
matches = re.findall(pattern, text)
print(f"{matches = }")
matches = ['0', '0', '1', '6', '0', '0', '0', '2', '7', '0', '0', '0', '3', '8', '0']
可以看出来,我们的想法是正确的。那么我们怎么才能只匹配一次呢?设定为不贪婪的模式(4.3-贪婪模式和非贪婪模式)?我们实际看一下:
import re
text = """
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
"""
pattern = r"[\d+?]"
matches = re.findall(pattern, text)
print(f"{matches = }")
matches = ['0', '0', '1', '6', '0', '0', '0', '2', '7', '0', '0', '0', '3', '8', '0']
错了!为什么?
这是因为我们使用的是 []
进行的匹配([]
表示匹配字符集中任意字符),在 []
中,所有的特殊字符会被当做普通字符,所以 ?
没有开启不贪婪模式。那我们怎么办?去掉 []
?我们可以试一下:
text = """
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
"""
pattern = r"\d+"
matches = re.findall(pattern, text)
print(f"{matches = }")
matches = ['001', '60', '002', '70', '003', '80']
我们发现这样不仅编号被匹配了,价钱也被匹配了。那我们让其不再贪婪试试?
text = """
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
"""
pattern = r"\d+?"
matches = re.findall(pattern, text)
print(f"{matches = }")
matches = ['0', '0', '1', '6', '0', '0', '0', '2', '7', '0', '0', '0', '3', '8', '0']
确实是不贪婪了,但还是会匹配所有的数字 🤣。
到这里我们就要使用本节的主角 ^
了。它的作用是让匹配范围只在每行的开头,我们试一下:
import re
text = """
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
"""
pattern = r"^\d+"
matches = re.findall(pattern, text)
print(f"{matches = }")
matches = []
这是什么情况,为什么没有匹配到任何内容?出现这种原因是因为我的 text
有问题,如上的方式并不是从第一行开始的,而是从第二行开始的,正确的写法应该是:
text = """001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80"""
这样才是正确的 3 行,否则为 5 行!我们用代码看一下:
text = """
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
"""
print(f"{text}")
结果如 Fig.11 所示:
Fig.11 “”“”“” 的错误使用
我们再看一下 3 行的写法和效果:
text = """001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80"""
print(text)
结果见 Fig.12。
Fig.12 “”“”“” 的正确使用
那我们接下来再看一下在正确使用 """"""
的情况下 Regex 的效果:
import re
text = """001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80"""
pattern = r"^\d+"
matches = re.findall(pattern, text)
print(f"{matches = }")
matches = ['001']
为什么只匹配了一个?这是因为我们使用的是单行模式,如果需要使用多行模式,则:
import re
text = """001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80"""
pattern = r"^\d+"
matches = re.findall(pattern, text, re.M) # 传入参数 re.M 则开启多行模式
print(f"{matches = }")
matches = ['001', '002', '003']
再写一个简单的例子:
import re
# 示例字符串
text1 = "hello world"
text2 = "say hello world"
# 正则表达式,用于匹配以 "hello" 开始的字符串
pattern = r"^hello"
# 使用 re.match 检查匹配
match1 = re.match(pattern, text1)
match2 = re.match(pattern, text2)
# 输出结果
if match1:
print(f"text1: 匹配结果:{match1.group()}")
else:
print("text1: 没有匹配结果")
if match2:
print(f"text2: 匹配结果:{match2.group()}")
else:
print("text2: 没有匹配结果")
# 匹配不在字符集中的字符
pattern = r"[^ol]"
matches = re.findall(pattern, text1)
print(f"匹配结果:{matches}")
text1: 匹配结果:hello
text2: 没有匹配结果
匹配结果:['h', 'e', ' ', 'w', 'r', 'd']
在这个例子中,re.match
函数会检查 text1
是否以 hello
开始,如果是,则返回一个匹配对象。对于 text2
,由于它不是以 hello
开始,所以 re.match
不会返回任何匹配结果。对于取非的用法我们在上一小节刚刚用过,这里不再赘述。
4.2.9 特殊字符:$
(锚定匹配字符串的末尾)
美元符号 $
用于锚定匹配字符串的末尾。当 $
出现在正则表达式的末尾时,它表示匹配必须发生在被搜索字符串的结束位置。也就是说,只有当被搜索的字符串以这个模式结束時,匹配才会成功。
与 ^
类似:
- 如果是 dotall mode,表示匹配整个文本的结尾位置。
- 如果是 multiline mode,表示匹配文本每行的结尾位置。
比如,下面的文本中,每行最前面的数字表示水果的编号,最后的数字表示价格:
001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80
如果我们要提取所有的水果价格,可以使用如下的 regex:
\d+$
⚠️ 注意:$
应该放在最后,而不是像 ^
那样放在最前面。
我们使用 Python 试一下:
import re
text = """001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80"""
pattern = r"\d+$"
matches = re.findall(
pattern=pattern,
string=text,
flags=re.M
)
print(f"{matches = }")
matches = ['60', '70', '80']
如果我们不开启 multiline mode,那么如下:
import re
text = """001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80"""
pattern = r"\d+$"
matches = re.findall(
pattern=pattern,
string=text,
)
print(f"{matches = }")
matches = ['80']
因为单行模式下,$
只会匹配整个文本的结束位置。
4.2.10 特殊字符:|
(或)
在正则表达式中,竖线符号 |
是一个特殊字符,表示逻辑上的“或”操作。它用于指定多个模式中的任意一个匹配。
具体来说,|
用于在正则表达式中创建一个模式组,它表示在该位置可以匹配两个或多个模式中的任意一个。这意味着如果字符串与其中任何一个模式匹配,整个正则表达式就会匹配成功。
下面是一些示例说明 |
的用法:
- 匹配多个字符串中的任意一个:
- 表达式:
apple|banana
,表示匹配字符串中的 “apple” 或 “banana”。
- 匹配多个模式中的任意一个:
- 表达式:
(cat|dog)fish
,表示匹配 “catfish” 或 “dogfish”。
- 结合使用其他正则表达式元字符:
- 表达式:
gr(a|e)y
,表示匹配 “gray” 或 “grey”。
需要注意的是,|
的作用范围是模式组。如果我们希望限定 |
的作用范围,可以使用圆括号 ( )
来明确指定模式组。
以下是一个示例,演示了如何在 Python 中使用 |
进行正则表达式匹配:
import re
pattern = r"apple|banana"
text = "I like bananas and apple"
match = re.search(pattern, text)
if match:
print("Match found:", match.group())
else:
print("No match")
Match found: banana
输出结果将是:Match found: banana
,因为字符串 “bananas” 匹配到了模式中的 “banana”。
总结起来,|
是正则表达式中的特殊字符,用于表示逻辑上的“或”操作,允许匹配多个模式中的任意一个。
**Question**
:re.search
和 re.findall
有什么区别?
**Answer**
:re.search
和 re.findall
是 Python 中正则表达式模块 re
提供的两个不同的函数,它们在查找和匹配文本时有一些区别。
re.search(pattern, string)
:
- 功能:在给定的字符串中搜索第一个与正则表达式模式匹配的部分。
- 返回值:如果找到匹配项,则返回一个匹配对象(Match object),否则返回
None
。 - 匹配顺序:
re.search
函数只返回第一个匹配项,即使在字符串中有多个匹配。
re.findall(pattern, string)
:
- 功能:在给定的字符串中查找所有与正则表达式模式匹配的部分。
- 返回值:返回一个包含所有匹配项的列表,如果没有匹配项,则返回空列表。
- 匹配顺序:
re.findall
函数会从左到右扫描字符串,并返回所有匹配项的列表。
例如,正则表达式 cat|dog
将匹配包含 “cat” 或 “dog” 的字符串。如果字符串中同时存在 “cat” 和 “dog”,则只会匹配第一个遇到的 “cat” 或 “dog”(相当于使用的是 search 而不是 findall),结果见 Fig.13。
Fig.13 | 的示例
import re
# 示例字符串
text = "I have a cat and a dog."
# 正则表达式,用于匹配 "cat" 或 "dog"
pattern = r"cat|dog"
# 使用 re.findall 查找所有匹配项
matches1 = re.findall(pattern, text)
matches2 = re.search(pattern, text)
# 输出结果
print(f"{matches1 = }")
print(f"{matches2 = }")
print(f"{matches2.group()}") if matches2 else ...
print(f"{matches2.groups()}") if matches2 else ...
matches1 = ['cat', 'dog']
matches2 = <re.Match object; span=(9, 12), match='cat'>
cat
()
结果分析如下:
-
matches1
是通过re.findall
函数查找到的所有匹配项的列表,其中包含了字符串中所有匹配到的 “cat” 和 “dog”。 -
matches2
是通过re.search
函数找到的第一个匹配项的匹配对象。匹配对象的span=(9, 12)
表示匹配项在字符串中的起始位置是索引 9,结束位置是索引 12。match='cat'
表示匹配项是字符串中的 “cat”。 -
matches2.group()
返回匹配项的字符串表示,即 “cat”。因为matches2
是一个匹配对象,所以可以使用group()
方法来获取匹配项的字符串。 -
matches2.groups()
返回一个空元组()
。这是因为在正则表达式中没有使用圆括号( )
来创建捕获组,所以没有可以提取的分组信息。
综上所述,结果表示在示例字符串中找到了两个匹配项,分别是 “cat” 和 “dog”。re.findall
返回了所有匹配项的列表,而 re.search
返回了第一个匹配项的匹配对象。我们可以使用匹配对象的 group()
方法来获取匹配项的字符串表示。在这个例子中,没有定义捕获组,所以 groups()
返回一个空元组。
**Question**
:|
相当于是多个元素并列?
**Answer**
:是的,|
在正则表达式中相当于多个元素并列,表示逻辑上的“或”操作。当使用 |
字符时,它允许我们指定多个模式中的任意一个来进行匹配。它会从左到右依次尝试匹配每个模式,并返回第一个匹配到的结果。
例如,正则表达式 apple|banana
表示匹配 “apple” 或者 “banana” 这两个模式中的任意一个。如果目标字符串中包含了其中任意一个词,整个正则表达式就会匹配成功。
另一个例子是,正则表达式 gr(a|e)y
表示匹配以 “gray” 或者 “grey” 开头的单词。它会匹配 “gray” 或者 “grey” 这两个单词。
需要注意的是,|
的作用范围是在它两侧的模式组。如果需要限定 |
的作用范围,可以使用圆括号 ( )
将模式组起来。
综上所述,|
在正则表达式中用于表示多个元素的并列,提供了灵活的匹配选择。它允许我们指定多个模式中的任意一个来进行匹配操作。
**Question**
:那我们是不是也可以使用 [cat,dog]
来代替 cat|dog
?
**Answer**
:实际上,[cat,dog]
并不会产生我们期望的效果。在字符类中,逗号 ,
不会被解释为逻辑上的“或”操作符,而是表示一个普通的逗号字符。所以 [cat,dog]
实际上表示匹配字符 'c'
、'a'
、't'
、逗号 ,
、'd'
、'o'
、'g'
中的任意一个。
4.2.11 特殊字符:()
(分组)
在正则表达式中,括号 ( )
用于创建分组(grouping)。分组允许我们对模式的部分进行分组,并对分组应用特定的操作,如重复、替换等。
组(Group)就是把正则表达式匹配的内容里面其中的某些部分标记为某个组。我们可以在正则表达式中标记多个组。
为什么要有组的概念呢?因为有时,我们需要提取已经匹配的内容里面的某个部分。我从下面的文本中,选择每行逗号前面的字符串,也包括逗号本身。
4.2.11.1 单个分组
苹果,苹果是绿色的
橙子,橙子是橙色的
香蕉,香蕉是黄色的
就可以这样写正则表达式:
^.*,
但是,如果我们要求不要包括逗号呢?我们当然不能直接写成:
^.*
因为最后的逗号是特征所在,如果去掉它,就没法找逗号前面的了。但是把逗号放在正则表达式中,又会包含逗号。解决问题的方法就是使用组选择符:括号。我们可以这样写:
^(.*),
结果见 Fig.14。
Fig.14 组选择符 () 的示例
我们可以发现,要从整个表达式中提取的部分放在括号中,这样水果的名字就被单独的放在一个组(group)中了。对应的 Python 代码如下:
import re
text = """苹果,苹果是绿色的
橙子,橙子是橙色的
香蕉,香蕉是黄色的"""
p = r"^(.\*),"
matches = re.findall(pattern=p, string=text, flags=re.M)
print(f"{matches = }")
matches = ['苹果', '橙子', '香蕉']
我们可能会有疑问,不是应该有两个组吗?为什么只有 Group1 的结果?
findall()
函数只返回每个匹配的第一个分组的内容,而不是返回所有分组的内容。
如果我们希望获取每个分组的内容,可以使用 finditer()
函数来遍历每个匹配对象,并使用其 group()
方法来获取分组的内容:
import re
text = """苹果,苹果是绿色的
橙子,橙子是橙色的
香蕉,香蕉是黄色的"""
p = r"^(.\*),"
matches1 = re.findall(pattern=p, string=text, flags=re.M)
print(f"{matches1 = }")
print('='\*50)
matches2 = re.finditer(p, text, re.M)
for match in matches2:
print(f"Full match: {match.group(0)}")
print(f"Group1: {match.group(1)}")
print('-'\*50)
matches1 = ['苹果', '橙子', '香蕉']
==================================================
Full match: 苹果,
Group1: 苹果
--------------------------------------------------
Full match: 橙子,
Group1: 橙子
--------------------------------------------------
Full match: 香蕉,
Group1: 香蕉
--------------------------------------------------
4.2.11.2 多个分组
分组也可以多次使用。比如,我们要从下面的文本中,提取出每个人的名字和对应的手机号。
张三,手机号码15945678901
李四,手机号码13945677701
王二,手机号码13845666901
可以使用这样的正则表达式:
^(.*),.*(\d{11})
效果如 Fig.15 所示。
Fig.15 两个分组的使用示例
对应代码如下:
import re
text = """张三,手机号码15945678901
李四,手机号码13945677701
王二,手机号码13845666901"""
p = r"^(.\*),.\*(\d{11})"
matches1 = re.findall(pattern=p, string=text, flags=re.M)
print(f"{matches1 = }")
print('='\*50)
matches2 = re.finditer(p, text, re.M)
for match in matches2:
print(f"Full match: {match.group(0)}")
print(f"Group1: {match.group(1)}")
print(f"Group2: {match.group(2)}")
print('-'\*50)
matches1 = [('张三', '15945678901'), ('李四', '13945677701'), ('王二', '13845666901')]
==================================================
Full match: 张三,手机号码15945678901
Group1: 张三
Group2: 15945678901
--------------------------------------------------
Full match: 李四,手机号码13945677701
Group1: 李四
Group2: 13945677701
--------------------------------------------------
Full match: 王二,手机号码13845666901
Group1: 王二
Group2: 13845666901
--------------------------------------------------
4.2.11.3 分组命名
当有多个分组的时候,我们可以使用 (?P<分组名>...)
这样的格式,给每个分组命名。这样做的好处是,更方便后续的代码提取每个分组里面的内容,如 Fig.16 所示。
Fig.16 分组命名示例
import re
text = """张三,手机号码15945678901
李四,手机号码13945677701
王二,手机号码13845666901"""
p = r"^(?P<name>.\*),.\*(?P<phone>\d{11})"
matches1 = re.findall(pattern=p, string=text, flags=re.M)
print(f"{matches1 = }")
print('='\*50)
matches2 = re.finditer(p, text, re.M)
for match in matches2:
print(f"Full match: {match.group(0)}")
print(f"Group-name: {match.group('name')}")
print(f"Group-phone: {match.group('phone')}")
print('-'\*50)
matches1 = [('张三', '15945678901'), ('李四', '13945677701'), ('王二', '13845666901')]
==================================================
Full match: 张三,手机号码15945678901
Group-name: 张三
Group-phone: 15945678901
--------------------------------------------------
Full match: 李四,手机号码13945677701
Group-name: 李四
Group-phone: 13945677701
--------------------------------------------------
Full match: 王二,手机号码13845666901
Group-name: 王二
Group-phone: 13845666901
--------------------------------------------------
4.3 贪婪模式和非贪婪模式
我们要把下面的字符串中的所有 html 标签都提取出来:
source = '<html><head><title>Title</title>'
得到这样的一个列表:
['<html>', '<head>', '<title>', '</title>']
我们很容易想到使用正则表达式 <.*>
,那我们实验一下:
import re
source = '<html><head><title>Title</title>'
p = re.compile(pattern=r'<.\*>')
print(p.findall(source))
['<html><head><title>Title</title>']
我们发现结果并不是我们想要的。这是因为在正则表达式中,'*'
、'+'
、'?'
都是贪婪的,使用它们时,会尽可能多的匹配内容,所以,<.*>
中的 *
(表示任意次数的重复),一直匹配到了字符串最后的 </title>
里面的 e
。
解决这个问题,就需要使用非贪婪模式,也就是在星号后面加上 ?
,变成这样 <.*?>
:
import re
source = '<html><head><title>Title</title>'
p = re.compile(pattern=r'<.\*?>')
print(p.findall(source))
['<html>', '<head>', '<title>', '</title>']
4.4 匹配某种字符类型
转义字符 \
后面接一些字符,表示匹配某种类型的一个字符,例如:
-
\d
匹配 0-9 之间任意一个数字字符,等价于表达式[0-9]
-
\D
匹配任意一个非 0-9 数字的字符,等价于表达式[^0-9]
-
\s
是一个特殊字符,代表任意空白字符。这包括空格、制表符(\t
)、换行符(\n
)、回车符(\r
)等,等价于[\t\n\r\f\v]
-
\S
匹配任意一个非空白字符,等价于表达式[^ \t\n\r\f\v]
\w
匹配任意一个文字字符,包括大小写字母、数字、下划线,等价于表达式[a-zA-Z0-9_]
- 默认情况也包括 Unicode 文字字符,如果指定 ASCII 码标记,则只包括 ASCII 字母
-
\W
匹配任意一个非文字字符,等价于表达式[^a-zA-Z0-9_]
💡 反斜杠 \
也可以用在方括号里面,比如 [\s,.]
,它代表了一组特殊字符的组合。这个字符集合中的每个字符都有特定的含义:
-
,
是一个普通字符,代表逗号。 -
.
也是一个普通字符(在[]
中,特殊字符会被视为普通字符),代表点号。、
所以,[\s,.]
作为一个整体,表示匹配任意空白字符、逗号或点号。
在 Python 中,我们可以这样使用它:
import re
text = 'Hello, World! \nThis is a test.'
pattern = r"[\s,.]" # 匹配空白字符、,、.
matches = re.findall(pattern, text)
print(f"{matches = }")
matches = [',', ' ', ' ', '\n', ' ', ' ', ' ', '.']
在这个例子中,re.findall
函数将返回所有匹配的空白字符、逗号或点号。
4.5 Regex 的单行模式和多行模式
在正则表达式中,单行模式(single line mode)和多行模式(multiline mode)是两种不同的模式,它们影响正则表达式对字符串的处理方式。
前面说过, .
是不匹配换行符的,可是有时候,特征字符串就是跨行的,比如要找出下面文字中所有的职位名称:
<div class="el">
<p class="t1">
<span>
<a>Python开发工程师</a>
</span>
</p>
<span class="t2">南京</span>
<span class="t3">1.5-2万/月</span>
</div>
<div class="el">
<p class="t1">
<span>
<a>java开发工程师</a>
</span>
</p>
<span class="t2">苏州</span>
<span class="t3">1.5-2/月</span>
</div>
如果我们直接使用表达式:
class=\"t1".*?<a>(.*?)</a>
其中,?
表示非贪婪模式。
我们会发现匹配不上,因为 t1
和 <a>
之间有两个空行。这时我们需要 .
也匹配换行符,则可以使用 DOTALL 参数或者 (?s)
,结果如 Fig.17 所示。
Fig.17 让 . 匹配换行符的示例
对应的 Python 代码如下:
import re
text = """<div class="el">
<p class="t1">
<span>
<a>Python开发工程师</a>
</span>
</p>
<span class="t2">南京</span>
<span class="t3">1.5-2万/月</span>
</div>
<div class="el">
<p class="t1">
<span>
<a>java开发工程师</a>
</span>
</p>
<span class="t2">苏州</span>
<span class="t3">1.5-2/月</span>
</div>"""
p1 = r"class=\"t1\">.\*?<a>(.\*?)</a>"
p2 = r"(?s)class=\"t1\">.\*?<a>(.\*?)</a>"
matches1 = re.findall(pattern=p1, string=text, flags=re.DOTALL)
matches2 = re.findall(pattern=p2, string=text)
print(f"{matches1 = }")
print(f"{matches2 = }")
matches1 = ['Python开发工程师', 'java开发工程师']
matches2 = ['Python开发工程师', 'java开发工程师']
4.5.1 dotall mode
在 dotall mode 中,点号(.
)可以匹配任何单个字符,包括换行符(\n
)。
在 4.2.1 特殊字符: .(通配符:匹配除换行符外的所有字符) 中说过,
.
是匹配除换行符外的所有字符,但在单行模式中,这个限制被取消,.
可以匹配一切!
在 Python 的 re
模块中,可以通过在正则表达式中使用 (?s)
来启用 dotall mode,或者在编译正则表达式时使用 re.DOTALL
标志。
示例:
import re
# text = "Hello\nWorld"
text = """Hello
World"""
pattern = r".+" # + 表示至少匹配一次
matches = re.findall(
pattern=pattern,
string=text,
flags=re.DOTALL
)
print(f"{matches = }")
matches = ['Hello\nWorld']