19 - 正则表达式
19 - 正则表达式正则表达式Regular Expression简称 regex是一种文本匹配的工具。说白了就是用一套暗号来描述你想找的文本模式。基础语法先说个前提正则表达式不是 Python 特有的几乎所有编程语言都支持。所以学会了到处能用。导入 re 模块importre最简单的匹配普通字符就匹配它自己resultre.search(rhello,say hello world)print(result)# re.Match object; span(4, 9), matchhellore.search在字符串里找第一个匹配。找到了返回 Match 对象找不到返回None。注意字符串前面的rraw string因为正则里经常用反斜杠加r就不用双写反斜杠了。元字符这些字符在正则里有特殊含义字符含义例子.任意一个字符除换行h.t→ hat, hot, h3t\d数字[0-9]\d\d→ 匹配两位数字\D非数字\D→ 匹配非数字部分\w字母数字下划线\w→ 匹配一个单词\W非字母数字下划线\s空白字符空格、Tab、换行\S非空白字符^字符串开头^hello→ 以 hello 开头$字符串结尾world$→ 以 world 结尾# 匹配手机号简化版re.search(r\d{11},我的号码是13812345678)# 匹配邮箱开头re.search(r^\w,xiaomingexample.com)量词控制前面的元素出现几次量词含义例子*0 次或多次\d*→ 任意位数字包括 0 位1 次或多次\d→ 至少一位数字?0 次或 1 次colou?r→ color 或 colour{n}恰好 n 次\d{4}→ 4 位数字{n,m}n 到 m 次\d{2,4}→ 2-4 位数字{n,}至少 n 次\d{3,}→ 至少 3 位数字# 匹配年份re.search(r\d{4},发表于2024年)# 匹配价格1-3位数字加可选的小数部分re.search(r\d{1,3}(\.\d{1,2})?,价格99.99元)字符集[]方括号里列出允许的字符# 匹配元音字母re.findall(r[aeiou],hello world)# [e, o, o]# 匹配大写字母re.findall(r[A-Z],Hello World Python)# [H, W, P]# 取反^放在[]里面表示不是这些re.findall(r[^0-9],abc123def)# [a, b, c, d, e, f]# 范围re.findall(r[a-z],Hello World)# [ello, orld]分组()用圆括号把一部分正则包起来形成一个组# 提取日期中的年月日text日期2024-05-25matchre.search(r(\d{4})-(\d{2})-(\d{2}),text)ifmatch:print(match.group(1))# 2024第一个组print(match.group(2))# 05print(match.group(3))# 25print(match.groups())# (2024, 05, 25)命名分组给组起个名字比数字更直观matchre.search(r(?Pyear\d{4})-(?Pmonth\d{2})-(?Pday\d{2}),text)ifmatch:print(match.group(year))# 2024print(match.group(month))# 05print(match.group(day))# 25非捕获分组(?:...)有时候你只是需要用括号来做优先级或量词不想捕获# (?:...) 不创建组matchre.search(r(?:https?://)([\w.]),https://www.example.com)print(match.group(1))# www.example.com第一组就是域名没有协议那组re 模块常用函数re.search — 找第一个匹配matchre.search(r\d,abc 123 def 456)print(match.group())# 123只找到第一个re.match — 从开头匹配# match 只从字符串开头匹配print(re.match(r\d,123abc))# 匹配成功print(re.match(r\d,abc123))# None开头不是数字match跟search的区别match必须从开头开始匹配searchanywhere 都行。re.findall — 找所有匹配# 找所有数字numbersre.findall(r\d,我有 3 个苹果和 5 个橘子)print(numbers)# [3, 5]# 如果有分组返回组的内容datesre.findall(r(\d{4})-(\d{2}),2024-01 2024-02 2024-03)print(dates)# [(2024, 01), (2024, 02), (2024, 03)]re.finditer — 返回迭代器跟findall类似但返回 Match 对象的迭代器适合大量匹配时省内存formatchinre.finditer(r\d,1 22 333):print(f位置{match.start()}-{match.end()}:{match.group()})# 位置 0-1: 1# 位置 2-4: 22# 位置 5-8: 333re.sub — 替换# 把数字替换成 *resultre.sub(r\d,*,我有 3 个苹果和 5 个橘子)print(result)# 我有 * 个苹果和 * 个橘子# 用函数做替换defdouble(match):returnstr(int(match.group())*2)resultre.sub(r\d,double,3 和 5)print(result)# 6 和 10# 脱敏手机号defmask_phone(match):phonematch.group()returnphone[:3]****phone[7:]resultre.sub(r1\d{10},mask_phone,号码13812345678)print(result)# 号码138****5678re.split — 分割# 按数字分割partsre.split(r\d,abc123def456ghi)print(parts)# [abc, def, ghi]# 按多种分隔符分割partsre.split(r[,;|],a,b;c|d)print(parts)# [a, b, c, d]编译正则如果同一个正则要用很多次先编译可以提高性能# 编译一次email_patternre.compile(r[\w.-][\w-]\.[\w.])# 使用多次print(email_pattern.findall(联系我ab.com 或 cd.org))print(email_pattern.search(邮箱testexample.com))贪婪与非贪婪量词默认是贪婪的——尽可能多地匹配texth1标题/h1matchre.search(r.,text)print(match.group())# h1标题/h1匹配了整个字符串加?变成非贪婪尽可能少地匹配matchre.search(r.?,text)print(match.group())# h1只匹配到第一个 贪婪非贪婪**??{n,m}{n,m}?常用标志在正则末尾或re.compile的第二个参数中设置# re.IGNORECASE (re.I) — 忽略大小写re.search(rhello,HELLO,re.IGNORECASE)# 匹配成功# re.MULTILINE (re.M) — 多行模式^ 和 $ 匹配每行text第一行\n第二行\n第三行re.findall(r^\w,text,re.MULTILINE)# [第一行, 第二行, 第三行]# re.DOTALL (re.S) — 让 . 匹配换行符re.search(ra.b,a\nb,re.DOTALL)# 匹配成功# 组合使用re.search(rpattern,text,re.I|re.M)实际例子验证邮箱defis_valid_email(email):patternr^[\w.-][\w-]\.[\w.]$returnbool(re.match(pattern,email))print(is_valid_email(testexample.com))# Trueprint(is_valid_email(not-an-email))# False提取 URLtext请访问 https://www.example.com 或 http://test.org/path?q1urlsre.findall(rhttps?://[\w./\-?],text)print(urls)# [https://www.example.com, http://test.org/path?q1]解析日志log[2024-05-25 14:30:00] ERROR: 数据库连接失败 (timeout30s)patternr\[(?Ptime[\d\- :])\] (?Plevel\w): (?Pmessage.)matchre.search(pattern,log)ifmatch:print(f时间{match.group(time)})print(f级别{match.group(level)})print(f信息{match.group(message)})字符串清理text hello world \n\n python # 多个空白替换为单个空格cleanedre.sub(r\s, ,text).strip()print(cleaned)# hello world python本章小结\d、\w、\s是最常用的元字符*、、?、{n,m}控制匹配次数()分组(?Pname...)命名分组re.search找第一个re.findall找所有re.sub替换量词默认贪婪加?变非贪婪频繁使用的正则先re.compile编译面试题Q1re.search和re.match有什么区别点击查看答案re.search在整个字符串中查找第一个匹配匹配位置不限re.match只从字符串开头匹配开头不匹配就返回 Nonere.search(r\d,abc123)# 匹配成功123re.match(r\d,abc123)# None开头不是数字re.match(r\d,123abc)# 匹配成功123如果需要 search 但只匹配开头可以用^锚点re.search(r^\d, text)。Q2贪婪匹配和非贪婪匹配有什么区别点击查看答案贪婪匹配默认尽可能多地匹配字符非贪婪匹配尽可能少地匹配。textahello/are.search(r.,text).group()# ahello/a贪婪re.search(r.?,text).group()# a非贪婪在量词后加?切换为非贪婪模式*?、?、{n,m}?。常见场景提取 HTML 标签、JSON 字段时通常用非贪婪。Q3re.findall在有分组和没分组时返回值有什么不同点击查看答案没有分组返回匹配到的完整字符串列表re.findall(r\d,a1 b22 c333)# [1, 22, 333]有一个分组返回组内容的列表re.findall(r(\d),a1 b22)# [1, 22]有多个分组返回元组列表re.findall(r(\w)(\d),a1 b2)# [(a, 1), (b, 2)]如果只需要部分匹配结果用分组可以精确控制返回内容。Q4如何匹配一个 IP 地址点击查看答案简单版不验证范围r\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}严格版每段 0-255r(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)实际项目中推荐用ipaddress标准库验证importipaddresstry:ipaddress.ip_address(192.168.1.1)# 合法exceptValueError:# 不合法正则适合从文本中提取疑似 IP 的字符串验证合法性用专用库更可靠。