Skip to content

第14章 正则表达式

14.1 什么是正则表达式

正则表达式(regular expression,常简写为regex、regexp或re),是一种用于匹配和操作文本的强大工具,它是由一系列字符和特殊字符组成的模式,用于描述要匹配的文本模式。正则表达式可以在文本中查找、替换、提取和验证特定的模式。

14.2 正则表达式的语法组件

14.2.1 字符类

字符描述
.匹配除 \r,\n 之外的任何单个字符。要匹配包括 \r,\n 在内的任何字符,请使用像 `(.
\d匹配一个数字字符。等价于[0-9]
\D匹配一个非数字字符。等价于 [^0-9]
\w匹配包括下划线的任何单词字符。等价于 [A-Za-z0-9_],注意Unicode正则表达式会匹配中文字符。
\W匹配任何非单词字符。等价于 [^A-Za-z0-9_]
\s匹配任何空白字符,包括空格、制表符、换页符等。等价于 [ \f\n\r\t\v]
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]
[xyz]匹配所包含的任意一个字符。如 [abc] 可以匹配“plain”中的“a”。特殊字符仅有反斜线 \ 保持特殊含义,用于转义字符。其它特殊字符如星号、加号、各种括号等均作为普通字符。脱字符 ^ 如果出现在首位则表示负值字符集合;如果出现在字符串中间就仅作为普通字符。连字符 - 如果出现在字符串中间表示字符范围描述;如果出现在首位(或末尾)则仅作为普通字符。右方括号应转义出现,也可以作为首位字符出现。
[^xyz]匹配未列出的任意字符。
[a-z]字符范围。匹配在Unicode编码表指定范围内的任意字符。 例如,[a-z]可以匹配“a”到“z”范围内的任意小写字母字符。

14.2.2 数量词

字符描述
*****匹配前面的子表达式零次或多次。等价于 {0,}。
+匹配前面的子表达式一次或多次。等价于 {1,}。
?匹配前面的子表达式零次或一次。等价于 {0,1}。
{n}n是一个非负整数。匹配确定的n次。例如, o{2} 不能匹配“Bob”中的“o”,但是能匹配“food”中的两个“o”。
{n,}至少匹配n次。
{n,m}其中n<=m。最少匹配n次且最多匹配m次。
?非贪心量化:当该字符紧跟在任何一个其他重复修饰符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如对于字符串“oooo”,o+? 将匹配单个“o”,而 o+ 将匹配所有“o”。

14.2.3 边界匹配器

字符描述
^匹配字符串的开始位置。
$匹配字符串的结束位置。
\b匹配一个单词边界,也就是指单词和空格间的位置。
\B匹配非单词边界。

14.2.4 匹配分组

字符描述
x|y匹配 x 或 y
(pattern)匹配 pattern 并获取这一匹配的子字符串。
(?P<name> pattern)与常规的圆括号类似,但分组所匹配到了子字符串可通过符号分组名称 name 来访问。
(?P=name)引用一个命名组合。
\num向后引用一个子字符串,该子字符串与正则表达式的第num个用括号围起来的子表达式匹配。其中num从1开始。例如:(.)\1 匹配两个连续的相同字符。

14.2.5 元字符

注意:. ^ $ * + ? { } [ ] \ | ( )属于元字符,[ 和 ] 这两个元字符用于指定一个字符类,也就是你希望匹配的字符的一个集合。元字符 (除了 \) 在字符类中是不起作用的。 例如,[akm$] 将会匹配以下任一字符 'a', 'k', 'm' 或 '$';'$' 通常是一个元字符,但在一个字符类中它的特殊性被消除了。

14.3 原始字符串

Python中字符串前面加上 r 表示原始字符串,忽略转义。原始字符串非常适合用于正则表达式,因为正则表达式中通常包含很多反斜杠(例如 \d 或 \w),使用原始字符串可以避免反斜杠带来的转义问题。

例如:

python
pattern = r"^1[345789]\d{9}$"  # 以1开头,第二位为3,4,5,7,8,9,后面是9位数字

14.4 re模块

Python的re模块提供了正则表达式匹配操作。re模块中提供了一些方法用于查找或处理字符串。

14.4.1 search方法

  • re.search(pattern, string, flags):扫描整个 string 查找正则表达式 pattern 产生匹配的第一个位置,并返回相应的 Match。如果字符串中没有与模式匹配的位置则返回 None。
python
"""
    search方法
"""
import re

# 在字符串中搜索第1个数字
text_one = "今天是2026年1月25日,天气不错。"
result_one = re.search(r'\d+', text_one)
if result_one:
    print(f"找到匹配: {result_one.group()}")  # 输出: 找到匹配: 2026
    print(f"匹配位置: {result_one.span()}")   # 输出: 匹配位置: (3, 7)

# 在字符串中搜索第1个手机号码
text_two = "联系电话:138-1234-5678,备用号码:159-8765-4321"
result_two = re.search(r'(\d{3})-(\d{4})-(\d{4})', text_two)
if result_two:
    print(f"完整匹配: {result_two.group()}")     # 完整匹配: 138-1234-5678
    print(f"第一个分组: {result_two.group(1)}")   # 第一个分组: 138
    print(f"第二个分组: {result_two.group(2)}")   # 第二个分组: 1234
    print(f"第三个分组: {result_two.group(3)}")   # 第三个分组: 5678
    print(f"起始位置: {result_two.start()}")      # 起始位置: 5
    print(f"结束位置: {result_two.end()}")        # 结束位置: 17
    print(f"位置范围: {result_two.span()}")       # 位置范围: (5, 17)

# 忽略大小写
text_three = "helloWORLD"
result_three = re.search(r'[a-z]+', text_three, re.IGNORECASE)
if result_three:
    print(f"匹配到的字符串: {result_three.group()}")

# 验证邮箱格式
email = "shangguigu@atguigu.com"
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if re.search(email_pattern, email):
    print("邮箱格式正确")
else:
    print("邮箱格式错误")

14.4.2 match

  • re.match(pattern, string, flags):如果 string 开头的零个或多个字符与正则表达式 pattern 匹配,则返回相应的 Match。如果字符串与模式不匹配则返回 None。

re.match(): 仅从字符串开头开始匹配。适合用于验证字符串开头是否符合特定格式。 re.search(): 在整个字符串中搜索匹配。非常适合用于验证输入格式、提取特定模式的文本内容等场景。

python
"""
    match方法
"""
import re

# 验证字符串是否以数字开头
text_one = "今天是2026年1月25日,天气不错。"
result_one = re.match(r'\d+', text_one)
if result_one:
    print(f"找到匹配: {result_one.group()}")
    print(f"匹配位置: {result_one.span()}")
else:
    print("未找到匹配")

# 验证字符串是否以数字开头
text_two = "2026年2月16日是除夕"
result_two = re.match(r'\d+', text_two)
if result_two:
    print(f"找到匹配: {result_two.group()}")  # 输出: 找到匹配: 2026
    print(f"匹配位置: {result_two.span()}")   # 输出: 匹配位置: (0, 4)
else:
    print("未找到匹配")

14.4.3 findall

  • re.findall(pattern, string, flags):返回 pattern 在 string 中的所有非重叠匹配,以字符串列表或字符串元组列表的形式。对 string 的扫描从左至右,匹配结果按照找到的顺序返回。空匹配也包括在结果中。

re.search(): 返回第一个匹配的 Match 对象 re.match(): 从字符串开头开始匹配,返回第一个匹配的 Match 对象 re.findall(): 返回所有匹配的字符串列表

python
"""
    findall方法
"""
import re

# 在字符串中搜索所有数字
text_one = "今天是2026年1月25日,天气不错。"
result_one = re.findall(r'\d+', text_one)
if result_one:
    print(f"找到匹配: {result_one}")  # 输出: 找到匹配: ['2026', '1', '25']

# 在字符串中匹配所有日期
text_two = "出生日期:2002年5月6日,毕业日期:2025-7-1,入职日期:2026/3/8"
result_two = re.findall(r'(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})?', text_two)
for tel in result_two:
    print(f"完整匹配: {tel}")
    print(f"第一个分组: {tel[0]}")
    print(f"第二个分组: {tel[1]}")
    print(f"第三个分组: {tel[2]}")

14.4.4 sub

  • re.sub(pattern, repl, string, count=0):返回通过使用 repl 替换在 string 最左边非重叠出现的 pattern 而获得的字符串。如果样式没有找到,则不加改变地返回 string。

    • repl 可以是字符串或函数;如为字符串,则其中任何反斜杠转义序列都会被处理。 也就是说,\n 会被转换为一个换行符,\r 会被转换为一个回车符,依此类推。如果 repl 是一个函数,则它会针对每次 pattern 的非重叠出现的情况被调用。 该函数接受单个 Match 参数,并返回替换字符串。

    • 可选参数 count 是要替换的最大次数;count 必须是非负整数。如果省略这个参数或设为 0,所有的匹配都会被替换。

python
"""
    sub方法
"""
import re

text = "123hello89world45python472"
# 去除字符串首尾的数字
text = re.sub(r'^\d+|\d+$', '', text)
print(text) # hello89world45python
# 替换字符串中的数字为 -
result = re.sub(r'\d+', '-', text)
print(result) # hello-world-python

# 使用 lambda 函数将字符串中的单词首字母大写
words = "hello world python atguigu"
result = re.sub(r'\b\w+\b', lambda m: m.group().capitalize(), words)
print(result) # Hello World Python Atguigu

14.4.5 split

  • re.split(pattern, string, maxsplit=0):用 pattern 分开 string 。 如果在 pattern 中捕获到括号,那么所有的组里的文字也会包含在列表里。如果 maxsplit 非零, 最多进行 maxsplit 次分隔, 剩下的字符全部返回到列表的最后一个元素。
python
"""
    split方法
"""
import re

# 按多个分隔符分割
text = "apple,banana;orange:grape"
result = re.split(r'[,;:]', text)
print(result)  # 输出: ['apple', 'banana', 'orange', 'grape']

# 捕获组会保留分隔符
text = "apple,banana;orange:grape"
result = re.split(r'([,;:])', text)
print(result)  # 输出: ['apple', ',', 'banana', ';', 'orange', ':', 'grape']

14.5 案例

14.5.1 校验手机号码

python
import re

# 校验手机号码
tel_numbers = [
    "13812345678",  # 合法
    "11456817239",  # 非法
    "19912345678",  # 合法
    "17138412356",  # 合法
    "12345678908",  # 非法
    "14752345673",  # 合法
    "1800123456",  # 非法
]
pattern = r"^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$"
for i in tel_numbers:
    print(f"{i:20}{"合法" if re.match(pattern, i) else "非法"}")

14.5.2 校验邮箱

python
import re

# 校验邮箱的合法性
email_addresses = [
    "example@example.com",   # 合法
    "user.name@subdomain.example.co", # 合法
    "username@.com",         # 非法
    "@missingusername.com",  # 非法
    "-dasd@qq.com",           # 合法
]
# pattern = r"[\w!#$%&'*+-/=?^`{|}~.]+@[\w!#$%&'*+-/=?^`{|}~.]+\.[a-zA-Z]{2,}$"
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
for i in email_addresses:
    print(f"{i:40}{"合法" if re.match(pattern, i) else "非法"}")

14.5.3 校验0-255之间的数字

python
import re

# 检验数字是否在[0,255]范围
numbers = ["0", "9", "50", "100", "199", "200", "255", "256", "-1", "01", "001"]
# 十位为1-9,?表示可以没有十位,个位是0-9
# 或 百位是1,十位是0-9,个位是0-9
# 或 百位是2,十位是0-4,个位是0-9
# 或 百位是2,十位是5,个位是0-5
pattern = r"^([1-9]?\d|1\d{2}|2[0-4]\d|25[0-5])$"
for num in numbers:
    print(f"{num:5} {"合法" if re.match(pattern, num) else "非法"}")

14.5.4 提取网址

python
import re

# 获取所有href中网址
html = """<link rel="alternate" hreflang="zh" href="https://zh.wikipedia.org/wiki/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hans" href="https://zh.wikipedia.org/zh-hans/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hans-CN" href="https://zh.wikipedia.org/zh-cn/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hans-MY" href="https://zh.wikipedia.org/zh-my/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hans-SG" href="https://zh.wikipedia.org/zh-sg/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hant" href="https://zh.wikipedia.org/zh-hant/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hant-HK" href="https://zh.wikipedia.org/zh-hk/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hant-MO" href="https://zh.wikipedia.org/zh-mo/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="zh-Hant-TW" href="https://zh.wikipedia.org/zh-tw/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">
<link rel="alternate" hreflang="x-default" href="https://zh.wikipedia.org/wiki/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F">"""

pattern = r"href=\"(.+)\""
for url in re.findall(pattern, html):
    print(url)

14.5.5 替换文本中的所有数字为对应的词

python
import re

# 使用 re.sub() 方法将数字替换为对应的单词
sentence = "I have 2 apples and 3 oranges."
# 定义数字到词的映射
num_map = {"1": "one", "2": "two", "3": "three", "4": "four", "5": "five"}
print(re.sub(r"(\d)", lambda x: num_map[x[0]], sentence))
# I have two apples and three oranges.