正则表达式(regex)是文本处理的瑞士军刀。你能用它一行代码解决”从日志里提取所有 IP”、”验证邮箱格式”、”把驼峰命名改成蛇形”、”从 HTML 里抽出所有链接”(虽然 HTML 更该用 BeautifulSoup)。Python 的正则由 re 模块提供。这一篇不做完整语法参考(那要一整本书),而是把日常最常用的模式和 API 讲清楚。
一、re 模块的四个核心函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import rere.match (r"\d+" , "123abc" ) re.match (r"\d+" , "abc123" ) re.search(r"\d+" , "abc123def" ) re.findall(r"\d+" , "a1b22c333" ) re.sub(r"\d+" , "N" , "a1b22c333" )
match:从头匹配;
search:任意位置匹配(一次);
findall:找全部;
sub:替换。
再加两个:
1 2 3 4 5 6 re.split(r"\s+" , "hello world python" ) for m in re.finditer(r"\d+" , "a1b22c333" ): print (m.start(), m.group())
二、正则的基础语法 字符类
. 任意字符(除换行)
\d 数字,等于 [0-9]
\D 非数字
\w 字母/数字/下划线,等于 [a-zA-Z0-9_](Unicode 模式下也含中文)
\W 非上述
\s 空白(空格、tab、换行)
\S 非空白
[abc] a 或 b 或 c
[^abc] 非 a b c
[a-z] a 到 z
量词
* 0 次或多次
+ 1 次或多次
? 0 或 1 次
{n} 恰好 n 次
{n,} 至少 n 次
{n,m} n 到 m 次
贪婪 vs 非贪婪 :默认贪婪(尽量多匹配),加 ? 变非贪婪(尽量少)。
1 2 re.findall(r"<.+>" , "<a><b>" ) re.findall(r"<.+?>" , "<a><b>" )
锚点
分组与捕获
(...) 捕获组
(?:...) 非捕获组(只分组不捕获)
(?P<name>...) 命名组
1 2 3 4 5 6 7 8 9 10 m = re.match (r"(\d{4})-(\d{2})-(\d{2})" , "2025-09-25" ) m.group() m.group(1 ) m.group(2 ) m.groups() m = re.match (r"(?P<year>\d{4})-(?P<mon>\d{2})-(?P<day>\d{2})" , "2025-09-25" ) m.group("year" ) m.groupdict()
或
三、raw string 与转义 永远用 r"..." (raw string)写正则。否则 \d 里的 \d 会被 Python 解释成”转义”,需要写 "\\d":
1 2 re.findall("\\d+" , s) re.findall(r"\d+" , s)
四、编译正则 如果一个正则要用很多次,先 编译 一下:
1 2 3 pattern = re.compile (r"\d+" ) pattern.findall("a1b22" ) pattern.sub("N" , "a1b22" )
好处:
只解析一次;
少写一个参数;
有个”专属对象”,可读性高。
五、常用模式实战 1. 验证格式 1 2 3 4 5 6 7 8 9 10 11 re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.-]+" , email) re.fullmatch(r"1[3-9]\d{9}" , phone) re.fullmatch(r"\d{17}[\dX]" , id_card) re.fullmatch(r"(\d{1,3}\.){3}\d{1,3}" , ip)
fullmatch vs match :fullmatch 要求整个字符串匹配(避免”1234567890abc”被 match 通过)。
2. 抽取字段 1 2 3 4 5 log = '192.168.1.1 - - [2025-09-25:10:11:12] "GET /login HTTP/1.1" 200' m = re.search(r'(\S+) .+? "(\S+) (\S+) .+?" (\d+)' , log) print (m.groups())
3. 拆分 1 2 3 re.split(r"[,.!?、;。!?]\s*" , "hi, 咖飞. how? 学 Python!" )
4. 替换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 re.sub(r"\d+" , "N" , "abc 123 def 456" ) re.sub(r"(\w+)@\w+" , r"\1@匿名" , "user@example.com" ) def to_upper (m ): return m.group().upper() re.sub(r"[a-z]+" , to_upper, "hi 咖飞 abc" ) re.sub(r"\d" , "N" , "1234" , count=2 )
5. 命名组 + sub 1 2 3 re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})" , r"\g<m>/\g<y>" , "2025-09" )
六、re 的 flags 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 re.IGNORECASE / re.I re.MULTILINE / re.M re.DOTALL / re.S re.VERBOSE / re.X re.findall(r"hello" , "Hello" , re.I) pattern = re.compile (r""" (\d{4}) # 年 - (\d{2}) # 月 - (\d{2}) # 日 """ , re.VERBOSE)
VERBOSE 特别适合复杂正则,别忘了实际字符要转义或用字符类包起来(否则空格会被忽略)。
七、常见误区 1. 贪婪陷阱 1 2 re.findall(r"<.+>" , "<h1>hello</h1>" ) re.findall(r"<.+?>" , "<h1>hello</h1>" )
2. \d 只匹配 ASCII 数字 Python 的 \d 默认也匹配 Unicode 数字(如中文数字”零”),如果只想 ASCII:
1 2 3 re.findall(r"\d" , "1 二 3" ) re.findall(r"\d" , "1 二 3" , re.ASCII) re.findall(r"[0-9]" , "1 二 3" )
3. 中文匹配 Unicode 中文范围:[一-龥](常用汉字):
1 2 re.findall(r"[一-龥]+" , "hi 咖飞 abc 学 Python" )
4. HTML 不该用正则 HTML 嵌套结构复杂,用正则容易翻车。用 BeautifulSoup / lxml。
八、re 的性能
编译一次的正则 比字符串正则快(避免重复解析);
大字符串上跑贪婪匹配可能会指数级慢 (catastrophic backtracking);
Python 的 re 是回溯引擎;性能极端场景考虑 regex 第三方库(支持 possessive quantifier)。
注意 :re 内部有 LRU 缓存,即使不 compile 也会缓存约 100 条,但显式 compile 更明确。
九、Walrus + regex:简洁写法 1 2 3 4 if m := re.match (r"(\d+)-(\d+)" , s): print (m.group(1 ), m.group(2 )) else : print ("不匹配" )
海象运算符让”判断 + 取值”合并,比传统写法短。
十、几个日常必备正则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 re.sub(r"\s+" , " " , text).strip() re.sub(r"[^a-zA-Z0-9]" , "" , text) re.sub(r"[^一-龥]" , "" , text) re.sub(r"([A-Z])" , r"_\1" , "UserProfileID" ).lower().lstrip("_" ) re.sub(r"(?<!^)(?=[A-Z])" , "_" , "UserProfileID" ).lower()
十一、一个完整例子:日志敏感信息脱敏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 """对日志行做敏感信息脱敏。""" import rePATTERNS = [ (re.compile (r"1[3-9]\d{9}" ), lambda m: m.group()[:3 ] + "****" + m.group()[-4 :]), (re.compile (r"([\w.+-])[\w.+-]*(@[\w.-]+)" ), r"\1***\2" ), (re.compile (r"\d{6}(\d{8})\d{3}[\dX]" ), lambda m: m.group()[:6 ] + "********" + m.group()[-4 :]), (re.compile (r"\b\d{16,19}\b" ), lambda m: m.group()[:4 ] + " **** **** " + m.group()[-4 :]), ] def mask (line: str ) -> str : for pattern, repl in PATTERNS: line = pattern.sub(repl, line) return line if __name__ == "__main__" : tests = [ "user phone 13812345678 login" , "email kafei.gk@example.com" , "id: 410102199001011234" , "card 6222 0212 3456 7890" , ] for t in tests: print (mask(t))
十二、常见陷阱
忘 raw string :"\d" 会有告警,写 r"\d"。
match 与 search 混淆 :match 只匹配开头。
findall 有捕获组时返回不同 :
无捕获组:返回整体匹配字符串列表;
一个捕获组:返回捕获内容列表;
多个捕获组:返回元组列表。
1 2 3 re.findall(r"\d+" , "a1b2" ) re.findall(r"(\d+)" , "a1b2" ) re.findall(r"(\d+)(\w)" , "a1b2c" )
贪婪吃太多 :加 ? 变非贪婪。
catastrophic backtracking :类似 (a+)+ 遇到长字符串会几秒钟卡死。
在替换里用 \1 但字符串没加 r :"\1" 会被解释成 \x01。用 raw string。
十三、小结与延伸阅读
五大 API:match、search、findall、sub、split;
永远用 r"...";
分组用 (...),命名组 (?P<n>...);
flags:I、M、S、X;
贪婪 +、非贪婪 +?;
HTML 别用正则,用专门的解析库;
复杂正则用 re.compile + VERBOSE;
海象运算符 + re.match 是简洁的匹配-取值合体。
延伸阅读:
下一篇 日期时间 datetime 模块 我们讲怎么优雅地处理时间。