Skip to main content
Septvean's Documents
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

正则表达式

一、什么是正则表达式

正则表达式用于按照特定规则查找、匹配、提取和替换文本。

例如,判断下面的内容是否是六位股票代码:

600519

普通代码可以这样写:

def is_stock_code(text: str) -> bool:
    return len(text) == 6 and text.isdigit()

使用正则表达式:

import re


def is_stock_code(text: str) -> bool:
    return re.fullmatch(
        r"\d{6}",
        text,
    ) is not None

正则表达式适合处理:

  • 手机号码
  • 邮箱地址
  • 日期和时间
  • 股票代码
  • 身份编号
  • 日志内容
  • HTML 文本中的特定数据
  • 文件名
  • URL
  • 批量文本清洗
  • 复杂字符串提取

二、导入 re 模块

Python 使用标准库中的 re 模块处理正则表达式:

import re

不需要安装第三方库。


三、为什么推荐使用原始字符串

正则表达式中经常使用反斜杠:

\d
\s
\w

Python 字符串本身也会处理反斜杠,因此推荐使用原始字符串:

pattern = r"\d{6}"

不推荐:

pattern = "\\d{6}"

两者通常效果相同,但原始字符串更清晰。

正则表达式通常写成:

r"..."

例如:

r"\d+"
r"[A-Za-z]+"
r"^\d{6}$"

四、最常用的匹配函数

Python re 模块常用函数:

函数 作用
re.match() 从字符串开头匹配
re.search() 在整个字符串中查找第一个匹配
re.fullmatch() 整个字符串必须完全匹配
re.findall() 返回所有匹配结果
re.finditer() 返回所有匹配对象
re.sub() 替换匹配内容
re.split() 按正则规则分割字符串
re.compile() 预编译正则表达式

五、re.match

re.match() 只从字符串开头开始匹配。

import re

result = re.match(
    r"\d+",
    "600519 贵州茅台",
)

print(result)

匹配成功时返回匹配对象。

获取匹配内容:

if result is not None:
    print(result.group())

结果:

600519

如果数字不在开头:

result = re.match(
    r"\d+",
    "股票代码:600519",
)

print(result)

结果:

None

因为字符串开头不是数字。


六、re.search

re.search() 会在整个字符串中寻找第一个匹配。

text = "股票代码:600519,名称:贵州茅台"

result = re.search(
    r"\d{6}",
    text,
)

if result is not None:
    print(result.group())

结果:

600519

search() 只返回第一个匹配结果。


七、re.fullmatch

re.fullmatch() 要求整个字符串都符合规则。

result = re.fullmatch(
    r"\d{6}",
    "600519",
)

print(result is not None)

结果:

True

下面不会匹配:

result = re.fullmatch(
    r"\d{6}",
    "代码600519",
)

print(result)

结果:

None

验证输入格式时,通常优先使用 fullmatch()

例如:

def is_valid_stock_code(
    code: str,
) -> bool:
    return re.fullmatch(
        r"\d{6}",
        code,
    ) is not None

八、普通字符匹配

普通字符通常匹配它本身。

pattern = r"Python"

可以匹配:

Python

例如:

text = "我正在学习 Python"

result = re.search(
    r"Python",
    text,
)

print(result.group())

九、点号

. 表示匹配除换行符以外的任意一个字符。

pattern = r"a.c"

可以匹配:

abc
a1c
a-c
a c

不能匹配:

ac
abbc

因为 . 只表示一个字符。

示例:

texts = [
    "abc",
    "a1c",
    "a-c",
    "ac",
]

for text in texts:
    matched = re.fullmatch(
        r"a.c",
        text,
    )

    print(text, matched is not None)

匹配真正的点号

正则中的点号有特殊含义。

要匹配真正的 .,需要转义:

pattern = r"\."

例如:

result = re.search(
    r"\d+\.\d+",
    "价格为 12.50 元",
)

print(result.group())

结果:

12.50

十、字符类 []

字符类表示匹配其中任意一个字符。

pattern = r"[abc]"

可以匹配:

a
b
c

1. 匹配数字范围

pattern = r"[0-9]"

匹配任意一个数字。

result = re.search(
    r"[0-9]",
    "价格 A10",
)

print(result.group())

结果:

1

2. 匹配字母范围

小写字母:

r"[a-z]"

大写字母:

r"[A-Z]"

大小写字母:

r"[A-Za-z]"

字母和数字:

r"[A-Za-z0-9]"

3. 匹配多个范围

r"[A-Za-z0-9_]"

表示匹配:

  • 大写字母
  • 小写字母
  • 数字
  • 下划线

4. 字符类取反

在字符类开头使用 ^ 表示取反:

r"[^0-9]"

表示匹配任意非数字字符。

例如:

text = "600519贵州茅台"

result = re.search(
    r"[^0-9]+",
    text,
)

print(result.group())

结果:

贵州茅台

十一、常用预定义字符

1. \d

匹配数字字符:

r"\d"

相当于常见场景中的:

r"[0-9]"

匹配一个或多个数字:

r"\d+"

2. \D

匹配非数字字符:

r"\D"

3. \w

匹配单词字符。

通常包括:

  • 字母
  • 数字
  • 下划线
  • Unicode 字符
r"\w+"

注意,在 Python 默认 Unicode 模式下,\w 也可能匹配中文。


4. \W

匹配非单词字符:

r"\W"

5. \s

匹配空白字符,包括:

  • 空格
  • 制表符 \t
  • 换行符 \n
  • 回车符 \r
r"\s+"

6. \S

匹配非空白字符:

r"\S+"

十二、数量限定符

数量限定符表示前面的规则重复多少次。


1. 星号 *

匹配零次或多次:

r"ab*"

可以匹配:

a
ab
abb
abbb

2. 加号 +

匹配一次或多次:

r"ab+"

可以匹配:

ab
abb
abbb

不能匹配:

a

3. 问号 ?

匹配零次或一次:

r"ab?"

可以匹配:

a
ab

4. 固定次数 {n}

r"\d{6}"

表示正好六个数字。


5. 至少 n 次 {n,}

r"\d{3,}"

表示至少三个数字。


6. n 到 m 次 {n,m}

r"\d{2,4}"

表示两个到四个数字。


7. 最多 m 次 {,m}

r"\d{,4}"

表示最多四个数字。

实际代码中,为了可读性,通常更常见的是:

r"\d{0,4}"

十三、贪婪匹配

数量限定符默认是贪婪的,会尽可能多地匹配。

text = "<div>第一段</div><div>第二段</div>"

使用:

result = re.search(
    r"<div>.*</div>",
    text,
)

print(result.group())

结果:

<div>第一段</div><div>第二段</div>

因为 .* 会尽可能多地匹配。


十四、非贪婪匹配

在数量限定符后加 ?,表示尽可能少地匹配。

result = re.search(
    r"<div>.*?</div>",
    text,
)

print(result.group())

结果:

<div>第一段</div>

常见非贪婪形式:

*?
+?
??
{n,m}?

例如:

r".*?"
r".+?"
r"\d{2,4}?"

十五、位置锚点

位置锚点不匹配具体字符,只匹配位置。


1. ^ 字符串开头

r"^\d+"

表示字符串必须以数字开头。

print(
    re.search(
        r"^\d+",
        "600519 贵州茅台",
    )
)

可以匹配。

下面不能匹配:

re.search(
    r"^\d+",
    "代码 600519",
)

2. $ 字符串结尾

r"\d+$"

表示字符串必须以数字结尾。


3. 完整格式验证

r"^\d{6}$"

表示整个字符串必须正好是六个数字。

def is_stock_code(
    code: str,
) -> bool:
    return re.match(
        r"^\d{6}$",
        code,
    ) is not None

不过在 Python 中,更推荐:

re.fullmatch(
    r"\d{6}",
    code,
)

4. \A 和 \Z

\A 表示整个字符串开头:

r"\A\d+"

\Z 表示整个字符串结尾:

r"\d+\Z"

在多行模式下,\A\Z^$ 更严格。


十六、单词边界

\b 表示单词边界。

text = "cat category bobcat"
result = re.findall(
    r"\bcat\b",
    text,
)

print(result)

结果:

["cat"]

不会匹配:

category
bobcat

\B 表示非单词边界。


十七、选择符 |

| 表示“或者”。

pattern = r"猫|狗"

可以匹配:

例如:

text = "我喜欢猫,也喜欢狗"

result = re.findall(
    r"猫|狗",
    text,
)

print(result)

结果:

["猫", "狗"]

股票代码前缀:

pattern = r"(00|60)\d{4}"

表示:

  • 00 开头
  • 或者 60 开头
  • 后面再跟四位数字

十八、分组 ()

圆括号用于对正则规则进行分组。

pattern = r"(00|60)\d{4}"

分组可以:

  • 控制规则范围
  • 提取部分内容
  • 重复一组规则
  • 使用反向引用

1. 获取整个匹配

text = "代码:600519"

result = re.search(
    r"(00|60)\d{4}",
    text,
)

if result is not None:
    print(result.group())

结果:

600519

2. 获取指定分组

if result is not None:
    print(result.group(1))

结果:

60

group(0) 表示整个匹配:

result.group(0)

group(1) 表示第一个分组:

result.group(1)

3. 多个分组

text = "600519 贵州茅台 1500.50"

pattern = (
    r"(\d{6})"
    r"\s+"
    r"(\S+)"
    r"\s+"
    r"(\d+\.\d+)"
)

result = re.search(
    pattern,
    text,
)

if result is not None:
    print(result.group(1))
    print(result.group(2))
    print(result.group(3))

结果:

600519
贵州茅台
1500.50

获取所有分组:

print(result.groups())

结果:

(
    "600519",
    "贵州茅台",
    "1500.50",
)

十九、非捕获分组

普通圆括号会创建捕获分组:

r"(00|60)\d{4}"

如果只想组合规则,不需要提取,可以使用非捕获分组:

r"(?:00|60)\d{4}"

(?:...) 表示非捕获分组。

例如:

result = re.fullmatch(
    r"(?:00|60)\d{4}",
    "600519",
)

print(result.groups())

结果:

()

因为没有捕获分组。


二十、命名分组

可以给分组起名字:

pattern = (
    r"(?P<code>\d{6})"
    r"\s+"
    r"(?P<name>\S+)"
    r"\s+"
    r"(?P<price>\d+(?:\.\d+)?)"
)

匹配:

text = "600519 贵州茅台 1500.50"

result = re.fullmatch(
    pattern,
    text,
)

通过名称获取:

if result is not None:
    print(result.group("code"))
    print(result.group("name"))
    print(result.group("price"))

转换为字典:

print(result.groupdict())

结果:

{
    "code": "600519",
    "name": "贵州茅台",
    "price": "1500.50",
}

命名分组比数字分组更容易维护。


二十一、反向引用

反向引用用于匹配前面某个分组已经匹配到的内容。

例如,查找连续重复单词:

text = "Python Python Java"
result = re.search(
    r"\b(\w+)\s+\1\b",
    text,
)

if result is not None:
    print(result.group())

结果:

Python Python

\1 表示引用第一个分组。


命名反向引用

pattern = (
    r"\b"
    r"(?P<word>\w+)"
    r"\s+"
    r"(?P=word)"
    r"\b"
)

(?P=word) 表示引用名为 word 的分组。


二十二、可选分组

问号可以让一个分组变成可选:

pattern = r"(https?://)?example\.com"

其中:

https?

表示:

http
https

整个:

(https?://)?

表示协议部分可以有,也可以没有。

可以匹配:

example.com
http://example.com
https://example.com

二十三、re.findall

findall() 返回所有匹配结果。

text = """
600519 贵州茅台
000001 平安银行
300750 宁德时代
"""

提取所有六位数字:

codes = re.findall(
    r"\d{6}",
    text,
)

print(codes)

结果:

[
    "600519",
    "000001",
    "300750",
]

1. findall 和分组

如果正则中存在捕获分组,findall() 默认返回分组内容。

result = re.findall(
    r"(00|60)\d{4}",
    text,
)

print(result)

结果可能是:

["60", "00"]

它返回的是分组内容,不是完整代码。

如果希望返回完整匹配,应使用非捕获分组:

result = re.findall(
    r"(?:00|60)\d{4}",
    text,
)

结果:

[
    "600519",
    "000001",
]

2. 多个分组

text = """
600519 贵州茅台
000001 平安银行
"""
result = re.findall(
    r"(\d{6})\s+(\S+)",
    text,
)

print(result)

结果:

[
    ("600519", "贵州茅台"),
    ("000001", "平安银行"),
]

二十四、re.finditer

finditer() 返回匹配对象的迭代器。

text = "代码600519,代码000001"

for match in re.finditer(
    r"\d{6}",
    text,
):
    print(
        match.group(),
        match.start(),
        match.end(),
    )

结果类似:

600519 2 8
000001 11 17

finditer() 适合需要获取:

  • 匹配内容
  • 开始位置
  • 结束位置
  • 分组
  • 匹配范围

二十五、匹配对象 Match

匹配成功时,会返回 Match 对象。

常用方法和属性:

match.group()
match.groups()
match.groupdict()
match.start()
match.end()
match.span()
match.string
match.re

示例:

text = "股票代码:600519"

match = re.search(
    r"\d{6}",
    text,
)

if match is not None:
    print(match.group())
    print(match.start())
    print(match.end())
    print(match.span())

结果:

600519
5
11
(5, 11)

切片验证:

print(
    text[
        match.start():
        match.end()
    ]
)

二十六、re.sub 替换

re.sub() 用于替换匹配内容。

基本格式:

re.sub(
    pattern,
    replacement,
    text,
)

1. 替换数字

text = "价格 1500,数量 100"

result = re.sub(
    r"\d+",
    "*",
    text,
)

print(result)

结果:

价格 *,数量 *

2. 限制替换次数

result = re.sub(
    r"\d+",
    "*",
    text,
    count=1,
)

只替换第一次匹配。


3. 使用分组替换

日期格式转换:

text = "2026-07-10"
result = re.sub(
    r"(\d{4})-(\d{2})-(\d{2})",
    r"\1年\2月\3日",
    text,
)

print(result)

结果:

2026年07月10日

4. 命名分组替换

pattern = (
    r"(?P<year>\d{4})-"
    r"(?P<month>\d{2})-"
    r"(?P<day>\d{2})"
)
result = re.sub(
    pattern,
    r"\g<year>年"
    r"\g<month>月"
    r"\g<day>日",
    text,
)

命名引用通常比 \1 更清楚。


5. 使用函数替换

替换参数也可以是函数。

def double_number(
    match: re.Match[str],
) -> str:
    number = int(
        match.group()
    )

    return str(number * 2)
text = "价格 10,数量 20"

result = re.sub(
    r"\d+",
    double_number,
    text,
)

print(result)

结果:

价格 20,数量 40

二十七、re.subn

re.subn()re.sub() 类似,但还会返回替换次数。

text = "价格 10,数量 20"

result, count = re.subn(
    r"\d+",
    "*",
    text,
)

print(result)
print(count)

结果:

价格 *,数量 *
2

二十八、re.split

re.split() 根据正则规则分割字符串。

text = "600519,000001;300750 002594"
codes = re.split(
    r"[,;\s]+",
    text,
)

print(codes)

结果:

[
    "600519",
    "000001",
    "300750",
    "002594",
]

可以同时按:

  • 逗号
  • 分号
  • 空白字符

进行分割。


1. 限制分割次数

result = re.split(
    r"[-/]",
    "2026-07/10",
    maxsplit=1,
)

print(result)

2. 捕获分隔符

如果正则中使用捕获分组,分隔符也会保留在结果中:

result = re.split(
    r"([,;])",
    "A,B;C",
)

print(result)

结果:

[
    "A",
    ",",
    "B",
    ";",
    "C",
]

不需要保留分隔符时,使用非捕获分组:

r"(?:,|;)"

二十九、前向肯定断言

前向肯定断言:

(?=...)

表示当前位置后面必须满足某个条件,但不把它包含在匹配结果中。

例如,提取后面紧跟“元”的数字:

text = "苹果 10 元,数量 5 个"
result = re.findall(
    r"\d+(?=\s*元)",
    text,
)

print(result)

结果:

["10"]

“元”只用于判断,不会包含在结果中。


三十、前向否定断言

前向否定断言:

(?!...)

表示当前位置后面不能满足某个条件。

例如,匹配不以 ST 开头的名称:

names = [
    "贵州茅台",
    "ST某公司",
    "平安银行",
]
pattern = re.compile(
    r"^(?!ST).+$"
)

result = [
    name
    for name in names
    if pattern.fullmatch(name)
]

print(result)

结果:

[
    "贵州茅台",
    "平安银行",
]

三十一、后向肯定断言

后向肯定断言:

(?<=...)

表示当前位置前面必须满足某个条件。

例如,提取“代码:”后面的六位数字:

text = "代码:600519"
result = re.search(
    r"(?<=代码:)\d{6}",
    text,
)

print(result.group())

结果:

600519

三十二、后向否定断言

后向否定断言:

(?<!...)

表示当前位置前面不能满足某个条件。

例如:

text = "价格100,负数-20"
result = re.findall(
    r"(?<!-)\b\d+\b",
    text,
)

print(result)

会匹配没有负号的数字。


后向断言限制

后向断言中的模式通常必须是固定长度。

例如:

r"(?<=代码:)\d{6}"

前面的 代码: 是固定长度,可以使用。

但类似可变长度规则可能报错:

r"(?<=代码\s*:)\d{6}"

因为 \s* 长度不固定。


三十三、正则标志 flags

正则标志用于改变匹配行为。


1. re.IGNORECASE

忽略大小写:

result = re.search(
    r"python",
    "I like Python",
    flags=re.IGNORECASE,
)

print(result.group())

也可以写:

re.I

2. re.MULTILINE

多行模式:

re.MULTILINE

简写:

re.M

在多行模式下:

  • ^ 匹配每一行开头
  • $ 匹配每一行结尾
text = """
600519 贵州茅台
000001 平安银行
ABC 错误数据
"""
result = re.findall(
    r"^\d{6}",
    text,
    flags=re.MULTILINE,
)

print(result)

结果:

[
    "600519",
    "000001",
]

3. re.DOTALL

默认情况下,. 不匹配换行符。

使用:

re.DOTALL

简写:

re.S

示例:

text = """
<div>
第一行
第二行
</div>
"""
result = re.search(
    r"<div>.*?</div>",
    text,
    flags=re.DOTALL,
)

print(result.group())

4. re.VERBOSE

允许正则表达式分行编写,并添加注释。

pattern = re.compile(
    r"""
    ^
    (?P<code>\d{6})
    \s+
    (?P<name>\S+)
    \s+
    (?P<price>
        \d+
        (?:\.\d+)?
    )
    $
    """,
    flags=re.VERBOSE,
)

简写:

re.X

复杂正则建议使用 re.VERBOSE


5. re.ASCII

让:

\w
\d
\s
\b

按照 ASCII 规则匹配。

re.ASCII

简写:

re.A

例如:

text = "Python中文123"

默认:

re.findall(
    r"\w+",
    text,
)

可能包含中文。

使用 ASCII 模式:

re.findall(
    r"\w+",
    text,
    flags=re.ASCII,
)

只按照 ASCII 单词字符匹配。


6. 组合多个标志

使用 |

flags = (
    re.IGNORECASE
    | re.MULTILINE
    | re.DOTALL
)
result = re.search(
    pattern,
    text,
    flags=flags,
)

三十四、行内标志

可以把标志写在正则表达式内部:

r"(?i)python"

表示忽略大小写。

常见形式:

(?i)  忽略大小写
(?m)  多行模式
(?s)  点号匹配换行
(?x)  详细模式

局部标志:

r"(?i:python)"

只对括号内部生效。


三十五、re.compile

如果同一个正则表达式需要多次使用,可以预编译:

stock_code_pattern = re.compile(
    r"\d{6}"
)

使用:

result = stock_code_pattern.fullmatch(
    "600519"
)

查找:

result = stock_code_pattern.search(
    "代码:600519"
)

提取全部:

result = stock_code_pattern.findall(
    "600519 000001"
)

1. 编译正则的优点

  • 代码更清晰
  • 可以重复使用
  • 便于统一管理
  • 复杂规则更容易命名
  • 在大量重复调用时更合适

例如:

STOCK_CODE_PATTERN = re.compile(
    r"(?:00|60)\d{4}"
)
def is_main_board_code(
    code: str,
) -> bool:
    return (
        STOCK_CODE_PATTERN.fullmatch(
            code
        )
        is not None
    )

三十六、转义特殊字符

正则表达式中的特殊字符包括:

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

如果需要匹配它们本身,需要转义。

例如匹配括号:

r"\("
r"\)"

匹配加号:

r"\+"

匹配美元符号:

r"\$"

匹配方括号:

r"\["
r"\]"

三十七、re.escape

如果匹配内容来自用户输入或变量,可以使用 re.escape() 自动转义。

keyword = "a+b"

直接使用:

pattern = keyword

其中 + 会被当作正则语法。

正确:

pattern = re.escape(keyword)
text = "公式是 a+b"

result = re.search(
    pattern,
    text,
)

print(result.group())

结果:

a+b

搜索普通关键词时,可以使用:

def contains_keyword(
    text: str,
    keyword: str,
) -> bool:
    return re.search(
        re.escape(keyword),
        text,
    ) is not None

三十八、手机号匹配

中国大陆常见手机号格式验证示例:

PHONE_PATTERN = re.compile(
    r"1[3-9]\d{9}"
)
def is_phone_number(
    phone: str,
) -> bool:
    return (
        PHONE_PATTERN.fullmatch(
            phone
        )
        is not None
    )

使用:

print(
    is_phone_number(
        "13800138000"
    )
)

注意,号段规则可能变化。实际业务应结合最新运营商规则,而不是只依赖简单正则。


三十九、邮箱地址匹配

简单邮箱格式:

EMAIL_PATTERN = re.compile(
    r"""
    [A-Za-z0-9._%+-]+
    @
    [A-Za-z0-9.-]+
    \.
    [A-Za-z]{2,}
    """,
    flags=re.VERBOSE,
)

验证:

def is_email(
    email: str,
) -> bool:
    return (
        EMAIL_PATTERN.fullmatch(
            email,
        )
        is not None
    )

这只能完成常见格式检查,不能证明邮箱真实存在。


四十、日期匹配

匹配:

2026-07-10
DATE_PATTERN = re.compile(
    r"\d{4}-\d{2}-\d{2}"
)

验证:

def is_date_format(
    value: str,
) -> bool:
    return (
        DATE_PATTERN.fullmatch(
            value
        )
        is not None
    )

但正则只能验证格式,不能保证日期真实有效。

例如:

2026-99-99

也可能匹配。

更可靠的方法是:

from datetime import datetime


def parse_date(
    value: str,
) -> datetime:
    return datetime.strptime(
        value,
        "%Y-%m-%d",
    )

原则:

正则验证文本格式,专用模块验证业务含义。


四十一、金额匹配

匹配整数或两位小数:

MONEY_PATTERN = re.compile(
    r"\d+(?:\.\d{1,2})?"
)

可以匹配:

10
10.5
10.50

如果允许负数:

r"-?\d+(?:\.\d{1,2})?"

如果允许正负符号:

r"[+-]?\d+(?:\.\d{1,2})?"

四十二、股票代码匹配

1. 任意六位代码

r"\d{6}"

2. 00 或 60 开头

r"(?:00|60)\d{4}"

也可以:

r"(?:0{2}|60)\d{4}"

第一种更直观。


3. 常见板块代码

STOCK_CODE_PATTERN = re.compile(
    r"""
    (?:
        00
        |
        30
        |
        60
        |
        68
    )
    \d{4}
    """,
    flags=re.VERBOSE,
)

注意,真实证券代码规则可能扩展,业务系统应根据数据源规则维护。


4. 从文本中提取股票代码

text = """
贵州茅台 600519
平安银行 000001
宁德时代 300750
"""
codes = re.findall(
    r"(?<!\d)\d{6}(?!\d)",
    text,
)

print(codes)

前后断言用于避免从更长数字中截取六位数字。


四十三、从文本中提取键值

文本:

代码:600519
名称:贵州茅台
价格:1500.50

正则:

pattern = re.compile(
    r"""
    代码:(?P<code>\d{6})
    \s*
    名称:(?P<name>[^\r\n]+)
    \s*
    价格:(?P<price>\d+(?:\.\d+)?)
    """,
    flags=re.VERBOSE,
)

使用:

text = """
代码:600519
名称:贵州茅台
价格:1500.50
"""

match = pattern.search(text)

if match is not None:
    data = match.groupdict()

    data["price"] = float(
        data["price"]
    )

    print(data)

结果:

{
    "code": "600519",
    "name": "贵州茅台",
    "price": 1500.5,
}

四十四、清理多余空白

文本:

text = "Python    正则\t表达式\n教程"

将连续空白替换为一个空格:

result = re.sub(
    r"\s+",
    " ",
    text,
).strip()

print(result)

结果:

Python 正则 表达式 教程

四十五、删除 HTML 标签

简单删除标签:

html = (
    "<p>这是<b>重要</b>内容</p>"
)
text = re.sub(
    r"<[^>]+>",
    "",
    html,
)

print(text)

结果:

这是重要内容

但正则不适合可靠解析复杂 HTML。

复杂 HTML 应使用:

  • BeautifulSoup
  • lxml
  • HTMLParser

正则适合处理结构简单、格式稳定的片段。


四十六、提取 HTML 属性

简单示例:

html = (
    '<a href="https://example.com">'
    "链接"
    "</a>"
)
match = re.search(
    r'href="([^"]+)"',
    html,
)

if match is not None:
    print(match.group(1))

结果:

https://example.com

复杂 HTML 仍然推荐使用解析器。


四十七、日志解析

日志:

2026-07-10 09:30:15 INFO 600519 数据更新成功

正则:

LOG_PATTERN = re.compile(
    r"""
    ^
    (?P<date>\d{4}-\d{2}-\d{2})
    \s+
    (?P<time>\d{2}:\d{2}:\d{2})
    \s+
    (?P<level>[A-Z]+)
    \s+
    (?P<code>\d{6})
    \s+
    (?P<message>.+)
    $
    """,
    flags=re.VERBOSE,
)

解析:

line = (
    "2026-07-10 09:30:15 "
    "INFO 600519 数据更新成功"
)

match = LOG_PATTERN.fullmatch(line)

if match is not None:
    print(match.groupdict())

结果:

{
    "date": "2026-07-10",
    "time": "09:30:15",
    "level": "INFO",
    "code": "600519",
    "message": "数据更新成功",
}

四十八、文件名解析

文件名:

stock_600519_20260710.csv

正则:

FILE_PATTERN = re.compile(
    r"""
    ^
    stock_
    (?P<code>\d{6})
    _
    (?P<date>\d{8})
    \.csv
    $
    """,
    flags=re.VERBOSE,
)
match = FILE_PATTERN.fullmatch(
    "stock_600519_20260710.csv"
)

if match is not None:
    print(match.groupdict())

四十九、URL 简单解析

url = (
    "https://example.com/"
    "stock/600519"
)
pattern = re.compile(
    r"""
    ^
    https?://
    (?P<domain>[^/]+)
    /
    stock/
    (?P<code>\d{6})
    /?
    $
    """,
    flags=re.VERBOSE,
)

对于通用 URL 解析,更推荐:

from urllib.parse import urlparse

正则适合固定格式 URL。


五十、密码格式验证

例如规则:

  • 至少八位
  • 包含大写字母
  • 包含小写字母
  • 包含数字
PASSWORD_PATTERN = re.compile(
    r"""
    ^
    (?=.*[a-z])
    (?=.*[A-Z])
    (?=.*\d)
    .{8,}
    $
    """,
    flags=re.VERBOSE,
)

这里使用多个前向断言:

(?=.*[a-z])  至少一个小写字母
(?=.*[A-Z])  至少一个大写字母
(?=.*\d)     至少一个数字

实际系统还应考虑:

  • 密码长度上限
  • 特殊字符
  • 弱密码库
  • 密码哈希
  • 频率限制

五十一、正则表达式调试

复杂正则可以使用:

re.DEBUG

例如:

re.compile(
    r"\d{6}",
    flags=re.DEBUG,
)

会输出正则解析结构。

这适合调试复杂规则。


五十二、检查正则语法错误

错误正则:

pattern = r"(\d+"

编译时会抛出:

re.error

可以捕获:

try:
    compiled = re.compile(pattern)
except re.error as error:
    print(
        f"正则表达式错误:"
        f"{error}"
    )

五十三、性能注意事项

1. 避免重复编译

不推荐在循环中反复创建相同规则:

for text in texts:
    re.search(
        r"\d{6}",
        text,
    )

可以预编译:

pattern = re.compile(
    r"\d{6}"
)

for text in texts:
    pattern.search(text)

2. 尽量明确匹配范围

不推荐:

r".*600519.*"

如果只是判断是否包含:

"600519" in text

更简单、更快。


3. 避免灾难性回溯

某些嵌套重复规则可能非常慢:

r"(a+)+$"

面对长字符串时可能产生大量回溯。

尽量避免:

重复限定符嵌套
过度使用 .*
大量模糊分支

例如,应谨慎使用:

r"(.*)+"
r"(.+)*"
r"(a|aa)+"

4. 使用更明确的字符类

不推荐:

r"<.*?>"

如果目标是不跨越 >

r"<[^>]+>"

通常更明确。


5. 不要用正则解决所有问题

简单判断:

code.isdigit()

通常比:

re.fullmatch(
    r"\d+",
    code,
)

更直观。

判断前缀:

code.startswith(
    ("00", "60")
)

通常比:

re.match(
    r"00|60",
    code,
)

更清晰。


五十四、正则与字符串方法的选择

使用字符串方法:

text.startswith()
text.endswith()
text.split()
text.replace()
text.strip()
text.isdigit()
keyword in text

适合规则简单的场景。

使用正则:

多个分隔符
复杂格式验证
批量提取
模式化替换
多个可选部分
分组和断言

例如只判断六位数字:

len(code) == 6 and code.isdigit()

通常比正则更简单。

但从复杂文本中提取六位代码:

re.findall(
    r"(?<!\d)\d{6}(?!\d)",
    text,
)

正则更合适。


五十五、封装正则函数

不要让正则散落在整个项目中。

推荐:

import re


STOCK_CODE_PATTERN = re.compile(
    r"\d{6}"
)


def validate_stock_code(
    code: str,
) -> str:
    clean_code = code.strip()

    if (
        STOCK_CODE_PATTERN.fullmatch(
            clean_code
        )
        is None
    ):
        raise ValueError(
            "股票代码必须为 6 位数字"
        )

    return clean_code

优点:

  • 规则集中管理
  • 调用方式统一
  • 便于测试
  • 便于修改

五十六、综合案例:解析股票文本

原始文本:

600519 | 贵州茅台 | 1500.50 | 白酒
000001 | 平安银行 | 11.50 | 银行
300750 | 宁德时代 | 300.00 | 电池
错误数据

定义正则:

import re


STOCK_LINE_PATTERN = re.compile(
    r"""
    ^
    \s*
    (?P<code>\d{6})
    \s*
    \|
    \s*
    (?P<name>[^|]+?)
    \s*
    \|
    \s*
    (?P<price>
        \d+
        (?:\.\d+)?
    )
    \s*
    \|
    \s*
    (?P<industry>[^|]+?)
    \s*
    $
    """,
    flags=re.VERBOSE,
)

解析单行:

def parse_stock_line(
    line: str,
) -> dict[str, object]:
    match = (
        STOCK_LINE_PATTERN.fullmatch(
            line
        )
    )

    if match is None:
        raise ValueError(
            f"股票数据格式错误:"
            f"{line!r}"
        )

    data = match.groupdict()

    return {
        "code": data["code"],
        "name": data["name"].strip(),
        "price": float(
            data["price"]
        ),
        "industry": (
            data["industry"].strip()
        ),
    }

批量解析:

def parse_stock_text(
    text: str,
) -> tuple[
    list[dict[str, object]],
    list[tuple[int, str]],
]:
    stocks: list[
        dict[str, object]
    ] = []

    errors: list[
        tuple[int, str]
    ] = []

    for line_number, line in enumerate(
        text.splitlines(),
        start=1,
    ):
        clean_line = line.strip()

        if not clean_line:
            continue

        try:
            stock = parse_stock_line(
                clean_line
            )
        except ValueError as error:
            errors.append(
                (
                    line_number,
                    str(error),
                )
            )
        else:
            stocks.append(stock)

    return stocks, errors

调用:

text = """
600519 | 贵州茅台 | 1500.50 | 白酒
000001 | 平安银行 | 11.50 | 银行
300750 | 宁德时代 | 300.00 | 电池
错误数据
"""

stocks, errors = (
    parse_stock_text(text)
)

print(stocks)
print(errors)

五十七、综合案例:提取新闻中的股票代码和名称

文本:

贵州茅台(600519)上涨,平安银行(000001)成交活跃。
宁德时代(300750)出现调整。

正则:

STOCK_REFERENCE_PATTERN = re.compile(
    r"""
    (?P<name>[\u4e00-\u9fffA-Za-z]+)
    [((]
    (?P<code>\d{6})
    [))]
    """,
    flags=re.VERBOSE,
)

提取:

text = (
    "贵州茅台(600519)上涨,"
    "平安银行(000001)成交活跃。"
    "宁德时代(300750)出现调整。"
)
stocks = [
    match.groupdict()
    for match in (
        STOCK_REFERENCE_PATTERN.finditer(
            text
        )
    )
]

print(stocks)

结果:

[
    {
        "name": "贵州茅台",
        "code": "600519",
    },
    {
        "name": "平安银行",
        "code": "000001",
    },
    {
        "name": "宁德时代",
        "code": "300750",
    },
]

五十八、综合案例:清理混乱股票代码

原始数据:

codes = [
    " 600519 ",
    "代码:000001",
    "SZ300750",
    "abc",
    "股票002594名称",
]

提取六位代码:

CODE_PATTERN = re.compile(
    r"(?<!\d)\d{6}(?!\d)"
)
def extract_stock_code(
    text: str,
) -> str | None:
    match = CODE_PATTERN.search(text)

    if match is None:
        return None

    return match.group()
result = [
    code
    for text in codes
    if (
        code := extract_stock_code(
            text
        )
    )
]

print(result)

结果:

[
    "600519",
    "000001",
    "300750",
    "002594",
]

五十九、常见错误

1. 忘记使用原始字符串

不推荐:

pattern = "\d+"

推荐:

pattern = r"\d+"

re.match()

只匹配开头。

re.search()

搜索整个字符串。


不推荐:

re.search(
    r"\d{6}",
    "代码600519",
)

它会认为匹配成功。

如果要验证整个字符串:

re.fullmatch(
    r"\d{6}",
    "600519",
)

4. findall 中误用捕获分组

re.findall(
    r"(00|60)\d{4}",
    text,
)

只会返回分组内容。

推荐使用非捕获分组:

re.findall(
    r"(?:00|60)\d{4}",
    text,
)

5. 过度使用 .*

r".*"

范围过大,容易误匹配。

尽量改成明确字符类:

r"[^,]*"
r"[^>]*"
r"[^\r\n]*"

6. 忘记判断匹配结果

错误:

match = re.search(
    r"\d+",
    "abc",
)

print(match.group())

因为 matchNone,会报错。

正确:

if match is not None:
    print(match.group())

7. 把格式验证当成业务验证

正则可以确认:

2026-99-99

符合 数字-数字-数字 格式,但不代表日期真实存在。

业务数据应继续使用专用工具验证。


8. 用正则解析复杂 HTML

复杂 HTML 具有:

  • 嵌套标签
  • 属性顺序变化
  • 实体字符
  • 注释
  • 脚本
  • 不规范结构

应使用 HTML 解析器。


六十、练习题

练习一

提取文本中的所有六位数字。

text = (
    "600519 贵州茅台,"
    "000001 平安银行"
)

参考答案:

codes = re.findall(
    r"(?<!\d)\d{6}(?!\d)",
    text,
)

练习二

判断字符串是否是 0060 开头的六位代码。

参考答案:

def is_main_board_code(
    code: str,
) -> bool:
    return (
        re.fullmatch(
            r"(?:00|60)\d{4}",
            code,
        )
        is not None
    )

练习三

提取日期中的年、月、日。

text = "2026-07-10"

参考答案:

match = re.fullmatch(
    r"(?P<year>\d{4})-"
    r"(?P<month>\d{2})-"
    r"(?P<day>\d{2})",
    text,
)

if match is not None:
    print(match.groupdict())

练习四

将日期从:

2026-07-10

转换为:

2026年07月10日

参考答案:

result = re.sub(
    r"(\d{4})-(\d{2})-(\d{2})",
    r"\1年\2月\3日",
    text,
)

练习五

将连续空白替换成一个空格。

参考答案:

result = re.sub(
    r"\s+",
    " ",
    text,
).strip()

练习六

提取所有括号中的内容。

text = "贵州茅台(600519),平安银行(000001)"

参考答案:

result = re.findall(
    r"[((](.*?)[))]",
    text,
)

练习七

查找连续重复的单词。

text = "Python Python is useful"

参考答案:

match = re.search(
    r"\b(\w+)\s+\1\b",
    text,
)

练习八

按逗号、分号或空格分割文本。

参考答案:

result = re.split(
    r"[,;\s]+",
    text,
)

六十一、正则表达式核心速查

任意字符:

.        任意一个非换行字符

字符类型:

\d       数字
\D       非数字
\w       单词字符
\W       非单词字符
\s       空白字符
\S       非空白字符

数量:

*        零次或多次
+        一次或多次
?        零次或一次
{n}      正好 n 次
{n,}     至少 n 次
{n,m}    n 到 m 次

位置:

^        开头
$        结尾
\b       单词边界
\B       非单词边界

字符类:

[abc]    a、b、c 中任意一个
[a-z]    小写字母
[0-9]    数字
[^0-9]   非数字

分组:

(...)            捕获分组
(?:...)          非捕获分组
(?P<name>...)    命名分组
\1               数字反向引用
(?P=name)        命名反向引用

断言:

(?=...)          前向肯定断言
(?!...)          前向否定断言
(?<=...)         后向肯定断言
(?<!...)         后向否定断言

常用函数:

re.match()
re.search()
re.fullmatch()
re.findall()
re.finditer()
re.sub()
re.subn()
re.split()
re.compile()
re.escape()

六十二、学习建议

第一阶段:

普通字符
字符类
\d、\w、\s
数量限定符
match、search、fullmatch

第二阶段:

findall
finditer
sub
split
分组
命名分组

第三阶段:

贪婪和非贪婪
反向引用
前后断言
flags
re.VERBOSE

第四阶段:

文本解析
日志解析
数据清洗
性能优化
正则封装
自动化测试

学习正则表达式时,建议遵循:

先写简单规则
确认可以匹配
逐步增加条件
准备正确样本
准备错误样本
测试边界情况
最后再优化

正则表达式的核心不是记住所有符号,而是把文本规则拆成:

匹配什么字符
出现多少次
位于什么位置
哪些部分需要提取
哪些条件必须满足

真正实用的原则是:

简单规则优先使用字符串方法,复杂文本模式再使用正则表达式。