ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python re模块的使用全过程

Python re模块的使用全过程 前言很多人用re模块只用到一个函数随手写re.search(pat, s)就完事。这在小脚本里没问题但当模式要反复用、还想带上注释和命名分组时「编译一次、复用多次」的流程会清晰得多。本文把re模块的完整使用链路走一遍导入、编译、匹配、读取结果、替换与切分、收尾清理。需要先纠正一个常见误解「必须用re.compile()才高效」这种说法并不绝对。官方文档明确写着最近使用过的模式会被缓存所以只在少量正则上跑的程序不必操心编译问题。真正需要显式编译的场景是同一个模式要在循环里用成百上千次、或者你想把模式、标志位、命名分组这些信息集中在一处维护、或者想让代码读起来「一个模式一个对象」。还有一点要提醒本文讲的是 Python 3 的re。Python 2 已于 2020 年 1 月 1 日停止维护请不要再参考iteritems、print语句那一套旧写法。一、完整流程的五步re模块的典型生命周期可以拆成五步导入 → 编译(re.compile) → 匹配(match/search/finditer) → 消费结果(group 等) → 收尾(可选 re.purge)其中第二步是可选的。下面这张表给出手写编译与直接用模块级函数的关系项目模块级函数编译后的 Pattern编译内部自动完成re.compile(pattern, flags0)匹配入口re.search(pat, s, flags)p.search(s[, pos[, endpos]])标志位位置第三个参数编译时固定次数控制无search/match可传pos、endpos复用每次调用传入模式一次编译多次调用缓存有最近的模式不适用re.compile()的签名是re.compile(pattern, flags0)返回一个Pattern对象。它的方法里search(string[, pos[, endpos]])允许你从字符串中间某个下标开始找endpos则限定搜索上界功能上接近「切片后再搜」但^仍然认原始字符串的开头。二、把日志行解析成字典一个能体现「全过程」的实例是解析访问日志。用命名分组(?Pname...)给每段起名字匹配后用groupdict()一次性拿到字典# 适用于 Python 3.8import reLOG re.compile(r(?Pip\d{1,3}(?:\.\d{1,3}){3})\sr(?Pmethod[A-Z])\sr(?Ppath\S*)\sr(?Pstatus\d{3})\sr(?Psize\d))lines [192.168.1.10 GET /index.html 200 5120,10.0.0.7 POST /api/login 401 128,这行不是日志,]for line in lines:m LOG.match(line)if m is None: # 没匹配上时返回 None必须先判断continueinfo m.groupdict()print(info[ip], info[method], info[status])运行后前两行被解析第三行因为match从开头就失败而被跳过。这里(?:\.\d{1,3}){3}用的是非捕获组(?:...)分组只用于配合量词{3}不需要单独取出也就不在groupdict()里占位置。这是减少无谓分组、让结果更干净的小技巧。如果需要逐段匹配位置用finditer得到Match迭代器再用span()定位# 适用于 Python 3.8import repat re.compile(r\d{3}-\d{4})s 电话 010-1234 与 021-5678for m in pat.finditer(s):print(m.group(), m.span())三、替换、切分与清理Pattern.sub(repl, string, count0)与模块级re.sub(pattern, repl, string, count0)行为一致只是模式已经固定在对象里count0表示全部替换。Pattern.split(string, maxsplit0)同样对应re.split。# 适用于 Python 3.8import rephone re.compile(r(\d{3})-(\d{4}))print(phone.sub(r\1* \2, 010-1234)) # 010* 1234words re.compile(r\W)print(words.split(Hello, world; python)) # [Hello, world, python]最后一个环节是可选的清理。re.purge()会清空正则缓存签名是re.purge()无参数、返回None。大多数程序根本不需要调用它它主要给「在长时间运行的进程里动态生成大量不同模式」这类场景用。缓存容量是 CPython 的实现细节不要写代码去依赖某个具体数字需要释放就用公开的re.purge()。四、标志位怎么传flags用按位或|组合。编译时一次性写清比每次调用都传更不容易漏# 适用于 Python 3.8import repat re.compile(r^error, re.IGNORECASE | re.MULTILINE)text Error: 磁盘满\nerror: 内存不足\nokprint(len(pat.findall(text))) # 2re.MULTILINE也可写成行内(?m)让^除了匹配字符串开头也匹配每一行的开头re.IGNORECASE(?i)忽略大小写。常见标志位一览标志简写作用re.IGNORECASEre.I/(?i)忽略大小写re.MULTILINEre.M/(?m)^、$按行生效re.DOTALLre.S/(?s)让.也匹配换行re.VERBOSEre.X/(?x)忽略模式中的空白与#注释re.ASCIIre.A让\w、\d、\s只认 ASCIIre.VERBOSE适合写长模式可以把上面那条日志正则拆成带注释的多行而re.ASCII在需要精确对齐[0-9]语义时很有用——默认的 Unicode 模式里\d会匹配全角等非 ASCII 数字。常见坑点❌ 不判断返回值直接取分组re.search(r\d, abc).group()文本里没有数字时抛AttributeError。✅ 先判断m re.search(...)if m:之后再访问group()。Match对象恒为真、None恒为假。❌ 以为p.match(s)和p.search(s)一样从中间开始的行数据永远匹配不到。✅match只从下标 0 起匹配要任意位置用search要整串用fullmatch。❌ 把标志位当第二个参数传给模块级函数re.search(ra, re.I)把标志位当成了被搜索的字符串。✅re.search(pattern, string, flags)的顺序固定不确定时改用re.compile(pattern, flags)。❌ 对Pattern.split()传flagspat.split(s, re.I)第二参数其实是maxsplit。✅ 标志位只能在re.compile()时给Pattern.split(string, maxsplit0)没有flags。❌ 用findall取带命名分组的结果却拿到一堆元组或单列字符串搞不清结构。✅ 规则是无分组返回整段、单分组返回该组、多分组返回元组要按名字取值就用finditergroupdict()。❌ 在循环里对同一模式反复调用re.compile()或反过来为「保险」每次调re.purge()反而丢掉缓存。✅ 模式固定时编译一次放到模块级或函数外re.purge()只在确实需要释放缓存时调用。❌ 认为「某个组一定参与了匹配」直接拿m.group(optional)去做字符串拼接得到None拼出的TypeError。✅ 可选分组没参与匹配时值为None用m.groups(default)或m.groupdict(default)给默认值。❌ 在 Unicode 文本上指望\d只匹配0-9遇到全角或阿拉伯-印度数字时数量对不上。✅ 需要严格 ASCII 语义就加re.ASCII或干脆把模式写成[0-9]。总结步骤关键 API注意编译re.compile(pattern, flags0)固定模式建议复用匹配search/match/finditerNone必须先判断取结果group/groups/groupdict/span未参与匹配的组为None替换切分sub/subn/splitcount、maxsplit默认 0清理re.purge()可选容量是实现细节把流程固定成「编译一份、匹配到处用、结果按名取」re模块的代码会比到处散落的模块级调用好维护得多。至于缓存的容量、每次调用的耗时都受实现和版本影响需要时才用timeit在自己的数据上量别套用别人给的固定倍数。
返回列表