特别是最后一行,猴子,后面没有其它字符了,但是 * 表示可以匹配 0 次,所以表达式也是成立的。我们也用 Python 代码看一下:

import re


content = """苹果,是绿色的
橙子,是橙色的
香蕉,是黄色的
乌鸦,是黑色的
猴子,
"""

for item in re.findall(pattern=',.\*', 
                       string=content):
    print(item)
,是绿色的
,是橙色的
,是黄色的
,是黑色的
,

💡 注意.* 在正则表达式中非常常见,表示匹配任意字符任意次数。

当然这个 * 前面不是非得是 .,也可以是其它字符,如 Fig.5 所示。

python 查找文件中类容并替换完整一行_字符串

Fig.5 其他* 的示例

4.2.3 特殊字符:+(重复匹配多次,不包括 0 次)

+ 表示匹配前面的子表达式一次或多次不包括 0 次

还是上面的例子,我们要从文本中,选择每行逗号后面的字符串内容,包括逗号本身。但是添加一个条件,如果逗号后面没有内容,就不要选择了。

苹果,是绿色的
橙子,是橙色的
香蕉,是黄色的
乌鸦,是黑色的
猴子,

我们可以这样写正则表达式:

,.+

验证结果如 Fig.6 所示。

python 查找文件中类容并替换完整一行_字符串_02

Fig.6 .+ 的使用示例

最后一行,猴子, 后面没有其它字符了,+ 表示至少匹配 1 次,所以最后一行没有子串选中。

4.2.4 特殊字符:?(匹配 0 ~ 1 次)

? 表示匹配前面的子表达式 0 次或 1 次。

还是上面的例子,我们要从文本中,选择每行逗号后面的1个字符,也包括逗号本身。

苹果,绿色的
橙子,橙色的
香蕉,黄色的
乌鸦,黑色的
猴子,

那正则表达式可以这样写:

,.?

验证结果如 Fig.7 所示。

python 查找文件中类容并替换完整一行_python_03

Fig.7 .? 的使用示例

最后一行,猴子, 后面没有其它字符了,但是 `?`` 表示匹配 1 次或 0 次,所以最后一行也选中了一个逗号字符。

4.2.5 特殊字符:{}(匹配指定次数)

示例文本如下所示:

红彤彤,绿油油,黑乎乎,绿油
红彤彤,绿油油,黑乎乎,绿油油
红彤彤,绿油油,黑乎乎,绿油油油
红彤彤,绿油油,黑乎乎,绿油油油油
红彤彤,绿油油,黑乎乎,绿油油油油油
  • 表达式 油{3} 就表示匹配连续的 字 3 次。
  • 表达式 油{3,4} 就表示匹配连续的 字至少 3 次,至多 4 次

示例如 Fig.8 所示。

python 查找文件中类容并替换完整一行_字符串_04

Fig.8 字符{int} 和字符{int,int} 的使用示例

4.2.6 特殊字符:\(转义字符)

反斜杠 \ 用作转义字符,它有两种作用:

  1. 用于转义紧跟其后的特殊字符,使其失去特殊含义,被当作普通字符对待。
  2. 用于创建一些特定的字符类,如换行符 \n、制表符 \t 等。

例如,如果我们想匹配一个实际的点 .,而不是作为通配符的点,我们需要在点前面加上反斜杠 \.。同样,如果我们想匹配一个实际的反斜杠 \,我们需要使用两个反斜杠 \\,因为在字符串中反斜杠本身也是一个转义字符。


我们的示例文本如下:

example.com
example.net
example.org

现在我们想要得到 . 后面的后缀以确定网页的类型(包括 . 本身),那么我们的 Regex 可以这样写:

\..*

我们验证一下,验证结果见 Fig.9。

python 查找文件中类容并替换完整一行_数据库_05

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 中,字符串前加上 rR 表示这是一个原始字符串(raw string)。在原始字符串中,反斜杠 \ 不会被当作转义字符处理,而是保持其字面意义。这意味着在原始字符串中,反斜杠后面的字符不会被特殊解释。💡 如果我们不使用原始字符串,我们需要写四个反斜杠 \\\\ 来表示一个反斜杠 🤣。

4.2.7 特殊字符:[](匹配字符集中任意字符)

方括号 [] 用于创建一个字符集,匹配方括号内列出的任意一个字符。字符集可以包含普通字符和特殊字符,但特殊字符在字符集中将失去其特殊含义,被视为普通字符

例如,字符集 [abc] 将匹配字母 abc 中的任意一个。字符集也可以包含字符范围,如 [a-z] 将匹配从小写 a 到小写 z 的任意字母。

如果字符集的第一个字符是脱字符 ^,则表示取非,匹配任何不在方括号内的字符。例如,[^abc] 将匹配除了 abc 之外的任意字符。


我们的示例文本如下:

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 所示。

python 查找文件中类容并替换完整一行_正则表达式_06

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] 匹配文本中所有的 ace。第二个模式 [^ace] 匹配除了 ace 之外的字符。第三个模式 [a-e] 匹配从 ae 的所有字母。

4.2.8 特殊字符:^(锚定匹配字符串的开头和脱字符)

脱字符 ^ 有两种用途:

  1. 锚定匹配字符串的开头〕当 ^ 出现在正则表达式的开头时,它表示匹配行的开始。也就是说,它指定接下来的模式(pattern)必须出现在被搜索字符串的开头。
  2. 脱字符〕当 ^ 出现在字符集的方括号 [] 内时,它表示取非,用于排除字符集内的字符。在这种情况下,它匹配任何不在方括号内列出的字符(我们刚刚接触过)。

💡 这里对第一种作用再次阐述:当 ^ 出现在模式的开头时,它表示锚定(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 所示:

python 查找文件中类容并替换完整一行_正则表达式_07

Fig.11 “”“”“” 的错误使用

我们再看一下 3 行的写法和效果:

text = """001-苹果价格-60
002-橙子价格-70
003-香蕉价格-80"""

print(text)

结果见 Fig.12。

python 查找文件中类容并替换完整一行_python_08

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 特殊字符:|(或)

在正则表达式中,竖线符号 | 是一个特殊字符,表示逻辑上的“或”操作。它用于指定多个模式中的任意一个匹配。

具体来说,| 用于在正则表达式中创建一个模式组,它表示在该位置可以匹配两个或多个模式中的任意一个。这意味着如果字符串与其中任何一个模式匹配,整个正则表达式就会匹配成功。

下面是一些示例说明 | 的用法:

  1. 匹配多个字符串中的任意一个:
  • 表达式:apple|banana,表示匹配字符串中的 “apple” 或 “banana”。
  1. 匹配多个模式中的任意一个:
  • 表达式:(cat|dog)fish,表示匹配 “catfish” 或 “dogfish”。
  1. 结合使用其他正则表达式元字符:
  • 表达式: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.searchre.findall 有什么区别?

**Answer**re.searchre.findall 是 Python 中正则表达式模块 re 提供的两个不同的函数,它们在查找和匹配文本时有一些区别。

  1. re.search(pattern, string)
  • 功能:在给定的字符串中搜索第一个与正则表达式模式匹配的部分。
  • 返回值:如果找到匹配项,则返回一个匹配对象(Match object),否则返回 None
  • 匹配顺序:re.search 函数只返回第一个匹配项,即使在字符串中有多个匹配。
  1. re.findall(pattern, string)
  • 功能:在给定的字符串中查找所有与正则表达式模式匹配的部分。
  • 返回值:返回一个包含所有匹配项的列表,如果没有匹配项,则返回空列表。
  • 匹配顺序:re.findall 函数会从左到右扫描字符串,并返回所有匹配项的列表。

例如,正则表达式 cat|dog 将匹配包含 “cat” 或 “dog” 的字符串。如果字符串中同时存在 “cat” 和 “dog”,则只会匹配第一个遇到的 “cat” 或 “dog”(相当于使用的是 search 而不是 findall),结果见 Fig.13。

python 查找文件中类容并替换完整一行_python_09

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。

python 查找文件中类容并替换完整一行_特殊字符_10

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 所示。

python 查找文件中类容并替换完整一行_python_11

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 所示。

python 查找文件中类容并替换完整一行_特殊字符_12

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 所示。

python 查找文件中类容并替换完整一行_特殊字符_13

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']