😝 这次一定 | "学废" 正则表达式 🙋‍♂️(下)

简介: 正则表达式 → 没有一个开发仔会对这个词陌生吧?没印象的话,想想你是如何 判断身份证、手机号码是否合法的 ?Tips:本节代码示例基于Python的re库编写,虽大部分编程语言的正则库都是师从 Perl语言,语法基本一样,但也可能略有差异~

④ 贪婪与非贪婪


正则匹配默认是 贪婪匹配,即匹配尽可能多的字符~


简单得很,写个例子你就懂了:


re.match(r'^(\d+)(0*)$','12345000').groups()
# 原意是想得到('12345', '000')这样的结果的,但输出的却是:('12345000', '')
# 由于贪婪匹配,直接把后面的0都给匹配了,结果0*没东西匹配了
# 若果想尽可能少的匹配,可在\d+后加上一个问号?,采用非贪婪匹配,改成介样:
re.match(r'^(\d+?)(0*)$','12345000').groups()
# 输出结果:('12345', '000')


*0x5、背后的原理


这Part选读哈,不感兴趣可以跳过,多熟悉熟悉上面的正则语法就行了,笔者想弄清楚正则表达式背后原理的原因


网上很多文章都说到:正则表达式令人诟病的性能问题,但却很少人说清具体是什么问题?怎么引起的?如何规避优化~


下述图片和内容大部分来源于:《正则表达式引擎执行原理——从未如此清晰!》


① 正则表达式的工作流程


网络异常,图片无法展示
|


这里的 预编译 指的是提前初始化好正则对象,如Python中的re库,建议:


  • 调用 re.compile(patter) 预编译返回Pattern对象,后续用到正则的地方直接引用;


预编译的方式,在循环中,用同一个正则进行匹配的场景有奇效 (避免重复创建、编译).


② 引擎


程序对正则表达式进行语法分析,建立 语法分析树,再根据分析树结合 正则表达式引擎 生成执行程序 (状态机、又称状态自动机) 用于 字符匹配


这里的 引擎 就是一套 用于建立状态机的核心算法,主要分为下述两大类:


  • DFA (Deterministic finite automaton) → 确定型有穷自动机;
  • NFA (Non-deterministic finite automaton) → 非确定型有穷自动机;


拆词


  • 确定型与非确定型字符串在没编写正则表达式的前提下,能否直接确定字符匹配顺序? 能就是确定型,不能的就是不确定型;
  • 有穷 → 在有限次数内能得到结果;
  • 自动机 → 在设置好匹配规则后由引擎自动完成,不需人为干预;


对比


  • 构造DFA的成本代价 (用内存多、编译速度慢) 远高于NFA,但DFA的执行效率高于NFA;
  • 假设字符串长度为n,DFA的匹配时间复杂度为O(n),NFA因为匹配过程中存在大量分支和回溯,假设状态数为s,NFA的匹配的时间复杂度为O(ns);
  • NFA的优势是支持更多功能,如捕获Group、环视、量词等高级功能,这些功能都是基于子表达式独立进行匹配,因此在编程语言中,使用正则表达式的库都是 基于NFA实现的


DFA自动机是如何进行匹配的?


网络异常,图片无法展示
|


要点:


  • 文本主导 → 按文本顺序执行,稳定
  • 记录当前有效的所有可能 → 如执行到(d|b),会同时比较表达式中的a|b,故需要更多的内存;
  • 每个字符只检查一次 → 提高了执行效率,且速度与正则表达式无关;
  • 不能使用反向引用等功能 → 每个字符只检查一次,位置只记录当前值,故无法使用反向引用、环视等功能;


NFA自动机是如何进行匹配的?


网络异常,图片无法展示
|


要点:


  • 表达式主导 → 按照表达式的一部分执行,不匹配换其他部分继续匹配,直到表达式匹配完成;
  • 会记录某个位置 → 如执行到(d|b)时,记录字符的位置,然后选择其中一个先匹配;
  • 每个字符可能检查多次 → 执行到(d|b)时,比较d后发现不匹配,换成表达式的另外一个分支b,同时文本位置 回退,重新匹配字符b。这也是NFA非确定及效率可能没有DFA高的原因;
  • 能使用反向引用等功能 → 因为有回退功能,所以很容易实现反向引用、环视等功能;


③ NFA自动机的回溯问题


上述 回退 专业术语叫 回溯,原理类似于走迷宫时走过的路设置一个标志物,不对原路返回,换成另一条路。


回溯机制不但要重新计算 正则表达式文本对应位置,还需要维护 括号子表达式所匹配的文本状态,保存到内存中以 数字编号 的组中,这个组也叫 捕获组

捕获组保存括号内的匹配结果,后面的正则表达式中可以使用,就是上面说到的 反向引用


还不是很理解?画个简单的回溯流程示意图~


content_str = "HELLO"
content_regex = "HEL+O"


网络异常,图片无法展示
|


不难看出,回溯问题的导火索就是 贪婪匹配,吃得多,要吐的也多,如果匹配的文本长度几W,引擎可能就要回溯几w次。


如果碰到复杂的正则表达式,有多个部分要回溯,那回溯次数就是 指数级别。比如文本长度为500,表达式有两部分要回溯,次数可能就是500^2=25w次了。够呛的...


④ 优化


优化的方向:减少引擎回溯次数 + 更快更直接地匹配到结果


1) 少用贪婪模式


可使用 非贪婪模式(加?,会首先选择最小的匹配范围)独占模式(加+,不匹配结束匹配,不回溯) 来避免回溯。


2) 减少分支选择


少用,一定要用的话可通过下述几种方式优化:


  • 选择的顺序 → 更常用的选择项放前面,使得它们能较快地被匹配;
  • 提取共用模式 → 如:将(abcd|abef) 替换成→ ab(cd|ef);
  • 简单的分支选择 → 不用正则,直接用字符串查找函数(如index())找,效率还高一些;


3) 使用非捕获型括号


一般一个()就是一个捕获组,如果不需要引用括号中的文本,可使用非捕获组 (?:er),既能减少捕获时间,又能减少回溯使用的状态数量。


4) 一些零碎的优化点


  • 长度判断优化 → 匹配字符串的长度都没正则长度长,就没必要匹配了;
  • 预查必须字符 → 预先扫描必须字符,找都找不到,就没必要匹配了;
  • 用好行和字符串开始、结束符 → 用好 ^$\A\Z 更精确匹配行头、行尾、字符串开头和字符串结尾;
  • 别滥用括号 → 在需要的时候才用括号,在其他时候使用括号会阻止某些优化措施;
  • 别滥用* → 点符号可以匹配任意字符串,但贪婪匹配会导致大量的回溯;
  • 量词等价替换 → a{3} 可比 aaa要快上一些;
  • 拆分表达式 → 有时,多个小正则表达式的速度比一个大正则表达式的速度要快很多;


0x6、小结


本节,系统过了一波正则表达式,从知道是什么,到语法,然后到原理,内容虽少,五脏俱全,希望能帮到想学正则的朋友,别再下次一定了,这次一定,学废 正则表达式~


常用的正则表达式模板网上有很多,菜鸟工具 上还挺全,取需,就不搬运了~


网络异常,图片无法展示
|


再安利一个插件吧:any-rule,同样取需~


网络异常,图片无法展示
|


最后,附上Python中re模块的常用函数速查,有问题欢迎在评论区反馈,谢谢~


0x7、附:Python中re模块的常用函数


import re
# 将正则表达式编译成Pattern对象,好处:预编译+复用
test_pattern = re.compile(正则表达式字符串,标志位修饰符)
# 标志位修饰符(flags) 用于控制匹配模式,支持同时选择多个(|连接),有下述这些:
# 
# re.I  IGNORECASE → 忽略大小写
# re.M  MULTILINE → 多行匹配,影响^和$
# re.S  DOTALL → 使.匹配包括换行在内的所有字符
# re.X  VERBOSE → 忽略空白和注释,并允许使用'#'来引导一个注释
# re.U  UNICODE → 根据Unicode字符集解析字符,影响\w、\W、\b和\B
# re.L  LOCALE → 做本地化识别(locale-aware)匹配
# 匹配:尝试从字符串的开头进行匹配,匹配成功返回匹配对象,否则返回None
re.match(pattern, string, flags=0)
# 匹配:扫描整个字符串,返回第一个匹配对象,否则返回None;
re.search(pattern, string, flags=0)
# 检索:扫描整个字符串,匹配所有能匹配的对象,以列表形式返回;
re.findall(pattern, string, flags=0)
# 检索:同findall,匹配所有能匹配的对象,但是是以迭代器形式返回;
re.finditer(pattern, string, flags=0)参数
# 替换:将匹配的字符串替换为其他字符串,count为替换的最大次数,默认为0,替换所有。
re.sub(pattern, repl, string, count=0, flags=0)
# 分割:将匹配的字符串进行分割,返回列表,maxsplit为分割的最大次数,默认为0,分割所有。
re.split(pattern, string, maxsplit=0, flags=0)
# 分组:获取匹配结果中,每个分组匹配内容,可传入分组序号,不传整个匹配结果,传获取对应分组内容
pattern_result.group()
# 分组:从group(1)开始往后的所有值,返回一个元组
pattern_result.groups()
# 匹配的开始、结束位置
pattern_result.start() # 返回匹配的开始位置
pattern_result.end()  # 返回匹配的结束位置
pattern_result.span() #返回一个元组,表示匹配位置(开始,结束)
# 加载正则表达式字符串前的'r',如re.compile(r'xxx'),作用:
# 告知编译器这个str是raw string(原始字符串),不要转义反斜杠,如r'\n'是两个字符
# 反斜杠 + n,而不是换行!


参考文献:



相关文章
|
搜索推荐 iOS开发 Python
😝 这次一定 | "学废" 正则表达式 🙋‍♂️(中)
正则表达式 → 没有一个开发仔会对这个词陌生吧?没印象的话,想想你是如何 判断身份证、手机号码是否合法的 ?Tips:本节代码示例基于Python的re库编写,虽大部分编程语言的正则库都是师从 Perl语言,语法基本一样,但也可能略有差异~
173 0
|
Web App开发 数据可视化 搜索推荐
😝 这次一定 | "学废" 正则表达式 🙋‍♂️(上)
正则表达式 → 没有一个开发仔会对这个词陌生吧?没印象的话,想想你是如何 判断身份证、手机号码是否合法的 ?Tips:本节代码示例基于Python的re库编写,虽大部分编程语言的正则库都是师从 Perl语言,语法基本一样,但也可能略有差异~
212 0
Python 内置正则表达式库re的使用
正则表达式是记录文本规则的代码,用于查找和处理符合特定规则的字符串。在Python中,常通过原生字符串`r'string'`表示。使用`re.compile()`创建正则对象,便于多次使用。匹配字符串有`match()`(从开头匹配)、`search()`(搜索首个匹配)和`findall()`(找所有匹配)。替换字符串用`sub()`,分割字符串则用`split()`。
|
5月前
|
数据库 Python
Python网络数据抓取(8):正则表达式
Python网络数据抓取(8):正则表达式
54 2
|
5月前
|
自然语言处理 JavaScript 前端开发
Python高级语法与正则表达式(二)
正则表达式描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。
|
5月前
|
安全 算法 Python
Python高级语法与正则表达式(一)
Python提供了 with 语句的写法,既简单又安全。 文件操作的时候使用with语句可以自动调用关闭文件操作,即使出现异常也会自动关闭文件操作。
|
5月前
|
Python
Python使用正则表达式分割字符串
在Python中,你可以使用re模块的split()函数来根据正则表达式分割字符串。这个函数的工作原理类似于Python内置的str.split()方法,但它允许你使用正则表达式作为分隔符。
|
5月前
|
Python
Python中re模块的正则表达式
【6月更文挑战第2天】了解Python的re模块,它是处理正则表达式的核心工具。正则表达式用于在文本中查找特定模式。本文讨论了re模块的用法和技巧,包括导入模块、匹配、分组、替换文本、编译正则表达式以及使用预定义字符类、量词、锚点等高级功能。通过实例展示了如何在Python中执行这些操作,帮助提升文本处理能力。掌握这些技巧将使你更有效地利用正则表达式解决字符串处理问题。
54 2
|
5月前
|
Python
Python正则表达式详解:掌握文本匹配的魔法
Python正则表达式详解:掌握文本匹配的魔法
|
5月前
|
Python
python re 正则表达式库的使用
python re 正则表达式库的使用
42 0