正则表达式
正则表达式用于按照特定规则查找、匹配、提取和替换文本。
例如,判断下面的内容是否是六位股票代码:
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
- 批量文本清洗
- 复杂字符串提取
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() 只从字符串开头开始匹配。
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() 会在整个字符串中寻找第一个匹配。
text = "股票代码:600519,名称:贵州茅台"
result = re.search(
r"\d{6}",
text,
)
if result is not None:
print(result.group())
结果:
600519
search() 只返回第一个匹配结果。
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
pattern = r"[0-9]"
匹配任意一个数字。
result = re.search(
r"[0-9]",
"价格 A10",
)
print(result.group())
结果:
1
小写字母:
r"[a-z]"
大写字母:
r"[A-Z]"
大小写字母:
r"[A-Za-z]"
字母和数字:
r"[A-Za-z0-9]"
r"[A-Za-z0-9_]"
表示匹配:
- 大写字母
- 小写字母
- 数字
- 下划线
在字符类开头使用 ^ 表示取反:
r"[^0-9]"
表示匹配任意非数字字符。
例如:
text = "600519贵州茅台"
result = re.search(
r"[^0-9]+",
text,
)
print(result.group())
结果:
贵州茅台
匹配数字字符:
r"\d"
相当于常见场景中的:
r"[0-9]"
匹配一个或多个数字:
r"\d+"
匹配非数字字符:
r"\D"
匹配单词字符。
通常包括:
- 字母
- 数字
- 下划线
- Unicode 字符
r"\w+"
注意,在 Python 默认 Unicode 模式下,\w 也可能匹配中文。
匹配非单词字符:
r"\W"
匹配空白字符,包括:
- 空格
- 制表符
\t - 换行符
\n - 回车符
\r
r"\s+"
匹配非空白字符:
r"\S+"
数量限定符表示前面的规则重复多少次。
匹配零次或多次:
r"ab*"
可以匹配:
a
ab
abb
abbb
匹配一次或多次:
r"ab+"
可以匹配:
ab
abb
abbb
不能匹配:
a
匹配零次或一次:
r"ab?"
可以匹配:
a
ab
r"\d{6}"
表示正好六个数字。
r"\d{3,}"
表示至少三个数字。
r"\d{2,4}"
表示两个到四个数字。
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}?"
位置锚点不匹配具体字符,只匹配位置。
r"^\d+"
表示字符串必须以数字开头。
print(
re.search(
r"^\d+",
"600519 贵州茅台",
)
)
可以匹配。
下面不能匹配:
re.search(
r"^\d+",
"代码 600519",
)
r"\d+$"
表示字符串必须以数字结尾。
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,
)
\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}"
分组可以:
- 控制规则范围
- 提取部分内容
- 重复一组规则
- 使用反向引用
text = "代码:600519"
result = re.search(
r"(00|60)\d{4}",
text,
)
if result is not None:
print(result.group())
结果:
600519
if result is not None:
print(result.group(1))
结果:
60
group(0) 表示整个匹配:
result.group(0)
group(1) 表示第一个分组:
result.group(1)
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
findall() 返回所有匹配结果。
text = """
600519 贵州茅台
000001 平安银行
300750 宁德时代
"""
提取所有六位数字:
codes = re.findall(
r"\d{6}",
text,
)
print(codes)
结果:
[
"600519",
"000001",
"300750",
]
如果正则中存在捕获分组,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",
]
text = """
600519 贵州茅台
000001 平安银行
"""
result = re.findall(
r"(\d{6})\s+(\S+)",
text,
)
print(result)
结果:
[
("600519", "贵州茅台"),
("000001", "平安银行"),
]
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.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(
pattern,
replacement,
text,
)
text = "价格 1500,数量 100"
result = re.sub(
r"\d+",
"*",
text,
)
print(result)
结果:
价格 *,数量 *
result = re.sub(
r"\d+",
"*",
text,
count=1,
)
只替换第一次匹配。
日期格式转换:
text = "2026-07-10"
result = re.sub(
r"(\d{4})-(\d{2})-(\d{2})",
r"\1年\2月\3日",
text,
)
print(result)
结果:
2026年07月10日
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 更清楚。
替换参数也可以是函数。
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.sub() 类似,但还会返回替换次数。
text = "价格 10,数量 20"
result, count = re.subn(
r"\d+",
"*",
text,
)
print(result)
print(count)
结果:
价格 *,数量 *
2
re.split() 根据正则规则分割字符串。
text = "600519,000001;300750 002594"
codes = re.split(
r"[,;\s]+",
text,
)
print(codes)
结果:
[
"600519",
"000001",
"300750",
"002594",
]
可以同时按:
- 逗号
- 分号
- 空白字符
进行分割。
result = re.split(
r"[-/]",
"2026-07/10",
maxsplit=1,
)
print(result)
如果正则中使用捕获分组,分隔符也会保留在结果中:
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* 长度不固定。
正则标志用于改变匹配行为。
忽略大小写:
result = re.search(
r"python",
"I like Python",
flags=re.IGNORECASE,
)
print(result.group())
也可以写:
re.I
多行模式:
re.MULTILINE
简写:
re.M
在多行模式下:
^匹配每一行开头$匹配每一行结尾
text = """
600519 贵州茅台
000001 平安银行
ABC 错误数据
"""
result = re.findall(
r"^\d{6}",
text,
flags=re.MULTILINE,
)
print(result)
结果:
[
"600519",
"000001",
]
默认情况下,. 不匹配换行符。
使用:
re.DOTALL
简写:
re.S
示例:
text = """
<div>
第一行
第二行
</div>
"""
result = re.search(
r"<div>.*?</div>",
text,
flags=re.DOTALL,
)
print(result.group())
允许正则表达式分行编写,并添加注释。
pattern = re.compile(
r"""
^
(?P<code>\d{6})
\s+
(?P<name>\S+)
\s+
(?P<price>
\d+
(?:\.\d+)?
)
$
""",
flags=re.VERBOSE,
)
简写:
re.X
复杂正则建议使用 re.VERBOSE。
让:
\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 单词字符匹配。
使用 |:
flags = (
re.IGNORECASE
| re.MULTILINE
| re.DOTALL
)
result = re.search(
pattern,
text,
flags=flags,
)
可以把标志写在正则表达式内部:
r"(?i)python"
表示忽略大小写。
常见形式:
(?i) 忽略大小写
(?m) 多行模式
(?s) 点号匹配换行
(?x) 详细模式
局部标志:
r"(?i:python)"
只对括号内部生效。
如果同一个正则表达式需要多次使用,可以预编译:
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"
)
- 代码更清晰
- 可以重复使用
- 便于统一管理
- 复杂规则更容易命名
- 在大量重复调用时更合适
例如:
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() 自动转义。
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})?"
r"\d{6}"
r"(?:00|60)\d{4}"
也可以:
r"(?:0{2}|60)\d{4}"
第一种更直观。
STOCK_CODE_PATTERN = re.compile(
r"""
(?:
00
|
30
|
60
|
68
)
\d{4}
""",
flags=re.VERBOSE,
)
注意,真实证券代码规则可能扩展,业务系统应根据数据源规则维护。
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 = (
"<p>这是<b>重要</b>内容</p>"
)
text = re.sub(
r"<[^>]+>",
"",
html,
)
print(text)
结果:
这是重要内容
但正则不适合可靠解析复杂 HTML。
复杂 HTML 应使用:
- BeautifulSoup
- lxml
- HTMLParser
正则适合处理结构简单、格式稳定的片段。
简单示例:
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 = (
"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}"
)
不推荐在循环中反复创建相同规则:
for text in texts:
re.search(
r"\d{6}",
text,
)
可以预编译:
pattern = re.compile(
r"\d{6}"
)
for text in texts:
pattern.search(text)
不推荐:
r".*600519.*"
如果只是判断是否包含:
"600519" in text
更简单、更快。
某些嵌套重复规则可能非常慢:
r"(a+)+$"
面对长字符串时可能产生大量回溯。
尽量避免:
重复限定符嵌套
过度使用 .*
大量模糊分支
例如,应谨慎使用:
r"(.*)+"
r"(.+)*"
r"(a|aa)+"
不推荐:
r"<.*?>"
如果目标是不跨越 >:
r"<[^>]+>"
通常更明确。
简单判断:
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",
]
不推荐:
pattern = "\d+"
推荐:
pattern = r"\d+"
re.match()
只匹配开头。
re.search()
搜索整个字符串。
不推荐:
re.search(
r"\d{6}",
"代码600519",
)
它会认为匹配成功。
如果要验证整个字符串:
re.fullmatch(
r"\d{6}",
"600519",
)
re.findall(
r"(00|60)\d{4}",
text,
)
只会返回分组内容。
推荐使用非捕获分组:
re.findall(
r"(?:00|60)\d{4}",
text,
)
r".*"
范围过大,容易误匹配。
尽量改成明确字符类:
r"[^,]*"
r"[^>]*"
r"[^\r\n]*"
错误:
match = re.search(
r"\d+",
"abc",
)
print(match.group())
因为 match 是 None,会报错。
正确:
if match is not None:
print(match.group())
正则可以确认:
2026-99-99
符合 数字-数字-数字 格式,但不代表日期真实存在。
业务数据应继续使用专用工具验证。
复杂 HTML 具有:
- 嵌套标签
- 属性顺序变化
- 实体字符
- 注释
- 脚本
- 不规范结构
应使用 HTML 解析器。
提取文本中的所有六位数字。
text = (
"600519 贵州茅台,"
"000001 平安银行"
)
参考答案:
codes = re.findall(
r"(?<!\d)\d{6}(?!\d)",
text,
)
判断字符串是否是 00 或 60 开头的六位代码。
参考答案:
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
第四阶段:
文本解析
日志解析
数据清洗
性能优化
正则封装
自动化测试
学习正则表达式时,建议遵循:
先写简单规则
确认可以匹配
逐步增加条件
准备正确样本
准备错误样本
测试边界情况
最后再优化
正则表达式的核心不是记住所有符号,而是把文本规则拆成:
匹配什么字符
出现多少次
位于什么位置
哪些部分需要提取
哪些条件必须满足
真正实用的原则是:
简单规则优先使用字符串方法,复杂文本模式再使用正则表达式。