专栏导读 :大模型数据清洗与文本结构化提取的关键利器。本篇系统涵盖正则表达式完整语法树:字符类、数量词、边界匹配、分组捕获与非捕获、贪婪与非贪婪;深入演示 Python 内置 re 模块常用方法(search/match/findall/sub/split)以及高频实战验证正则。
第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 pattern)
与常规的圆括号类似,但分组所匹配到了子字符串可通过符号分组名称 name 来访问。
(?P=name)
引用一个命名组合。
\num
向后引用一个子字符串,该子字符串与正则表达式的第num个用括号围起来的子表达式匹配。其中num从1开始。例如:(.)\1 匹配两个连续的相同字符。
14.2.5 元字符 注意:. ^ $ * + ? { } [ ] \ | ( )属于元字符,[ 和 ] 这两个元字符用于指定一个字符类,也就是你希望匹配的字符的一个集合。元字符 (除了 \) 在字符类中是不起作用的。 例如,[akm$] 将会匹配以下任一字符 ‘a’, ‘k’, ‘m’ 或 ‘$’;’$’ 通常是一个元字符,但在一个字符类中它的特殊性被消除了。
14.3 原始字符串 Python中字符串前面加上 r 表示原始字符串,忽略转义。原始字符串非常适合用于正则表达式,因为正则表达式中通常包含很多反斜杠(例如 \d 或 \w),使用原始字符串可以避免反斜杠带来的转义问题。
例如:
1 pattern = r"^1[345789]\d{9}$"
14.4 re模块 Python的re模块提供了正则表达式匹配操作。re模块中提供了一些方法用于查找或处理字符串。
14.4.1 search方法
re.search(pattern, string, flags):扫描整个 string 查找正则表达式 pattern 产生匹配的第一个位置,并返回相应的 Match。如果字符串中没有与模式匹配的位置则返回 None。
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 """ search方法 """ import retext_one = "今天是2026年1月25日,天气不错。" result_one = re.search(r'\d+' , text_one) if result_one: print (f"找到匹配: {result_one.group()} " ) print (f"匹配位置: {result_one.span()} " ) 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()} " ) print (f"第一个分组: {result_two.group(1 )} " ) print (f"第二个分组: {result_two.group(2 )} " ) print (f"第三个分组: {result_two.group(3 )} " ) print (f"起始位置: {result_two.start()} " ) print (f"结束位置: {result_two.end()} " ) print (f"位置范围: {result_two.span()} " ) text_three = "helloWORLD" result_three = re.search(r'[a-z]+' , text_three, re.IGNORECASE) if result_three: print (f"匹配到的字符串: {result_three.group()} " ) email = "[email protected] " 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(): 在整个字符串中搜索匹配。非常适合用于验证输入格式、提取特定模式的文本内容等场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 """ match方法 """ import retext_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()} " ) print (f"匹配位置: {result_two.span()} " ) else : print ("未找到匹配" )
14.4.3 findall
re.findall(pattern, string, flags):返回 pattern 在 string 中的所有非重叠匹配,以字符串列表或字符串元组列表的形式。对 string 的扫描从左至右,匹配结果按照找到的顺序返回。空匹配也包括在结果中。
re.search(): 返回第一个匹配的 Match 对象 re.match(): 从字符串开头开始匹配,返回第一个匹配的 Match 对象 re.findall(): 返回所有匹配的字符串列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 """ findall方法 """ import retext_one = "今天是2026年1月25日,天气不错。" result_one = re.findall(r'\d+' , text_one) if result_one: print (f"找到匹配: {result_one} " ) 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,所有的匹配都会被替换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 """ sub方法 """ import retext = "123hello89world45python472" text = re.sub(r'^\d+|\d+$' , '' , text) print (text) result = re.sub(r'\d+' , '-' , text) print (result) words = "hello world python atguigu" result = re.sub(r'\b\w+\b' , lambda m: m.group().capitalize(), words) print (result)
14.4.5 split
re.split(pattern, string, maxsplit=0):用 pattern 分开 string 。 如果在 pattern 中捕获到括号,那么所有的组里的文字也会包含在列表里。如果 maxsplit 非零, 最多进行 maxsplit 次分隔, 剩下的字符全部返回到列表的最后一个元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 """ split方法 """ import retext = "apple,banana;orange:grape" result = re.split(r'[,;:]' , text) print (result) text = "apple,banana;orange:grape" result = re.split(r'([,;:])' , text) print (result)
14.5 案例 14.5.1 校验手机号码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import retel_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 校验邮箱
14.5.3 校验0-255之间的数字 1 2 3 4 5 6 7 8 9 10 11 import renumbers = ["0" , "9" , "50" , "100" , "199" , "200" , "255" , "256" , "-1" , "01" , "001" ] 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 提取网址 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import rehtml = """<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 替换文本中的所有数字为对应的词 1 2 3 4 5 6 7 8 import resentence = "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))