① 正则表达式之原理篇
背景
最近公司规范出来后,关于字符串不提倡用 “ + ” 进行拼接,于是自己写了个function,利用正则表达式来进行匹配。对于正则表达式,之前不了解原理,每胡和迹次要用的时候查一下,很浪费时间。
内容
基础知识;
正则表达式引擎;
贪婪与非贪婪模式;
DFA与NFA引擎;
回溯机制及常见的回溯形式
基础知识
1. 占有字符:正则表达式匹配过程中,如果子表达式匹配到东西,而并非是一个位置,并最终保存到匹配的结果当中
2. 零宽度:只匹配一个位置,或者是匹配的内容并不保存到匹配结果中
一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配
3.控制权:正则表达式由左到右依次进行匹配,通常情况下是由一个表达式取得控制权,从字符串的的某个位置进行匹配,一个子表达式开始尝试匹配的位置,是从前一子表达匹配成功的结束位置开始的(例如:(表达式一)(表达式二)意思就是表达式一匹配完成后才能匹配表达式二,而匹配表达式二的位置是从表达式一的位置匹配结束后的位置开始)。如果表达式一是零宽度,那表达式一匹配完成后,表达式二匹配的位置还是原来表达式以匹配的位置。也就是说它匹配开始和结束的位置是同一个
4. 元字符
5. 反义元字符
6. 转义字符:\ 使元字符失去它的意义,仅代表其输入中字符的意义
需要转义的字符列表 \ * + ? | { [ ( ) ^ $ . # 和 空白
7. 重复限定符:匹配优先裤并量词,忽略优先量词,即:贪婪与非贪婪
{n,}、 {n, m}、 {, m}、 ’+’ 、‘?’、 '*'
8. 字符类:[ ],区分大小写
9. 分支条件: |
10. 分组 :()指定子表达式,可限制多选项的范围、将若干字符组合为一个单元、受问号或星号之类的量词作用,例:(\d{1,3}){3}\d{3}
断言;(?
11. 括号及反向引用:(子表达式一)(子表达式二)\1 此时括号作用为分组,它具有记忆的功能,即棚山在正则表达式内部仍然能回忆上次匹配到的是什么;\1、\2、\n 是用在正则表达式的匹配环节。在正则表达式的替换环节,则要使用像 $1、$2、$n 这样的语法
12. 平衡组 参考
正则表达式引擎
有两个主要特点:
1. 默认贪婪匹配;( 贪婪匹配与非贪婪匹配 )
2. 返回最先匹配到的结果
针对简单的正则匹配进行分析,例:
当把cat应用到“He captured a catfish for his cat”,引擎先比较c和“H”,结果失败了。于是引擎再比较c和“e”,也失败了。直到第四个字符,c匹配了“c”。a匹配了第五个字符。到第六个字符t没能匹配“p”,也失败了。引擎再继续从第五个字符重新检查匹配性。直到第十五个字符开始,cat匹配上了“catfish”中的“cat”,正则表达式引擎急切的返回第一个匹配的结果,而不会再继续查找是否有其他更好的匹配
Rubular: 基于 Web 的 Ruby 正则表达式编辑器
贪婪与非贪婪(又称惰性、懒惰等)模式
两者影响的是被量词修饰的子表达式的行为。
贪婪模式在整个表达式匹配成功的前提下,尽可能多的匹配;而非贪婪模式(只被部分NFA引擎支持)在整个表达式匹配成功的前提下,尽可能少的匹配。
匹配优先量词(属于贪婪模式的量词):
“{m,n}”、“{m,}”、“?”、“*”和“+”。
忽略优先量词(匹配优先量词后加上“?”:非贪婪模式的量词):
“{m,n}?”、“{m,}?”、“??”、“*?”和“+?”
例:
源字符串:aa
正则表达式一:
正则表达式二:
DFA与NFA引擎(JS的正则引擎是NFA:非确定型有限自动机)
参考: 正则表达式引擎及其分类
DFA引擎:在线性时状态下执行,不要求回溯(因此永远不测试相同的字符两次);确保匹配最长的可能的字符串;因为只包含有限的状态(?),所以它不能匹配具有反向引用的模式;并且因为它不构造显示扩展,所以它不可以捕获子表达式
传统的NFA引擎:运行匹配回溯算法——以指定顺序测试正则表达式的所有可能的扩展并接受第一个匹配项。因为传统的 NFA 构造正则表达式的特定扩展以获得成功的匹配,所以它可以捕获子表达式匹配和匹配的反向引用。但传统 NFA的 回溯使它可以访问完全相同的状态多次(如果通过不同的路径到达该状态)。因此,在最坏情况下,它的执行速度可能非常慢。因为传统的 NFA 接受它找到的第一个匹配,所以它还可能会导致其他(可能更长)匹配未被发现
POSIX NFA 引擎:与传统 NFA 引擎类似,不同点:在可以确保已找到了可能的最长的匹配之前,它们将继续回溯(更慢);并且在使用 POSIX NFA 时,您恐怕不会愿意在更改回溯搜索的顺序的情况下来支持较短的匹配搜索,而非较长的匹配搜索
例:
字符串: this is yansen’s dog
正则表达式: /ya(msen|nsen|nsem)/
NFA工作方式:先在字符串中查找 y, 然后匹配其后是否为 a; 如果是 a 则继续查找其后是否为 m; 如果不是则匹配其后是否为 n (此时淘汰 msen 支分支); 然后继续看其后是否依次为 s,e; 接着测试是否为 n ,是 n 则匹配成功,不是则测试是否为 m 。为什么是 m ?因为 NFA 工作方式是以正则表达式为标准,反复测试字符串,这样同样一个字符串有可能被反复测试了很多次!
DFA:从 this 中 t 开始依次查找 y ,定位到 y ,已知其后为 a ,则查看表达式是否有 a ,此处正好有 a; 然后字符串 a 后为 n ,DFA依次测试表达式,此时 msen 不符合要求淘汰。 nsen 和 nsem 符合要求,然后DFA依次检查字符串,检测到 sen 中的 n 时只有 nsen 分支符合,则匹配成功!
由此两种引擎是完全不同的工作方式:NFA以表达式为主导,更容易操纵;DFA以文本为主导(搜索更快)
回溯机制
引擎是如何来处理那些模糊的条件匹配?
从问题的某一种状态(初始状态)出发,搜索从这种状态出发所能达到的所有“状态”,当一条路走到“尽头”的时候(不能再前进),再后退一步或若干步,从另一种可能“状态”出发,继续搜索,直到所有的“路径”(状态)都试探过。这种不断“前进”、不断“回溯”寻找解的方法,就称作“回溯法”
--来自网络
本质上就是深度优先搜索算法:尝试匹配失败时的下一步通常就是回溯
JS中正则表达式会产生回溯的地方都有哪些呢?
常见的回溯形式
1.贪婪量词
例:正则:/ab{1,3}c/
可视化形式
1. 没有回溯的匹配:当目标字符串是"abbbc"时
匹配过程
2. 有回溯的匹配:当目标字符串是“abbc”时
匹配过程
上图第5步有红颜色(仅表示匹配不成功):此时b{1,3}已经匹配到了2个字符“b”,准备尝试第三个时,结果发现接下来的字符是“c”。那么就认为b{1,3}就已经匹配完毕。然后状态又回到之前的状态(即第6步,与第4步一样),最后再用子表达式c,去匹配字符“c”。当然,此时整个表达式匹配成功了;上图的第6步,就是“回溯”
即:尝试可能的顺序是“从多往少”的方向去尝试:首先会尝试"bbb",然后再看整个正则是否能匹配。不能匹配时,吐出一个"b",即在"bb"的基础上,再继续尝试。如果还不行,再吐出一个,再试。如果还不行呢?只能说明匹配失败了
另一个清晰的回溯:
正则:/".*"/
目标字符串:"acd"ef
省略了尝试匹配双引号失败的匹配过程
其实“.*”最简单但也是非常影响效率的
2.惰性量词
虽然惰性量词不贪,但也会有回溯的现象(为了整体匹配成)
正则表达式
目标字符串:"12345"
匹配过程
3.分支结构
分支也是惰性的,比如/Java|JavaScript/,去匹配字符串"JavaScript",得到的结果是"Java",因为分支会一个一个尝试,如果前面的满足了,后面就不会再试验了。
分支结构中可能前面的子模式会形成了局部匹配,如果接下来表达式整体不匹配时,仍会继续尝试剩下的分支。这种尝试也可以看成一种回溯:
正则表达式
匹配过程
虽然第五步没有回到之前的状态,但仍然回到了分支结构,尝试下一种可能
总结:有回溯的过程,那么匹配效率肯定比DFA相对低一些;别看匹配慢,但是编译快而且还挺有趣
参考: 正则表达式的回溯机制
② 正则表达式原理
首先先讲解下裂清正则表达式的基础知识:
1.字符串的组成
对于字符串”123“而言,包括三个字符四个位置。如下图所示:
2.占有字符和零宽度
正则表达式匹配过程中,如果子表达式匹配到东西,而并非是一个位置,并最终保存到匹配的结果当中。这样的就称为占有字符,而只匹配一个位置,或者是匹配的内容并不保存到匹配结果中,这种就称作零宽度,后续会讲到的零宽度断言等。占有字符是互斥的,零宽度是非互斥的。也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。
3.控制权和传动
正则表达式由左到右依次进行匹配,通常情况下是由一个表达式取得控制权,从字符串的的某个位置进行匹配,一个子表达式开始尝试匹配的位置,是从前一子表达匹配成功的结束位置开始的(例如:(表达式一)(表达式二)意思就是表达式一匹配完成后才能匹配表达式二局粗,而匹配表达式二的位置是从表达式一的位置匹配结束后的位置开始)。如果表达式一是零宽度,那表达式一匹配完成后,表达式二匹配的位置还是原来表达式以匹配的位置。也就是说它匹配开始和结束的位置是同一个。
举一个简单的例子进行说明:正则表达式:123
源数据:123
讲解:首先正则表达式是从最左侧开始进行匹配,也就是位置0处进行匹配,首先得到控制权的是正则表达式中的“1”,而不是源数据中的“1”,匹配源数据中的“1”,匹配成功,将源数据的“1”进行保存到匹配的结果当中,这就表明它占有了一个字符,接下来就将控制权传给正则表达式中的“2”,匹配的位置变成了位置1,匹配源数据中的“2”,匹配成功,将控制权又传动给了正则桐源镇表达式的“3”,这时候匹配的位置变成了位置2,这时候就会将源数据中的“3”进行匹配。又有正则表达式“3”进行传动控制权,发现已经到了正则表达式的末尾,正则表达式结束。
③ 该正则表达式,用于过滤掉什么内容呢:"\\([^()]*\\)"; (PHP)
是指提取括号包裹的内容。
以下是我搜集的正则表达式应用及方法,希望对你有用。
匹配中文字符的正则表达式:[\u4e00-\u9fa5]
匹配双字节字符(包括汉字在内):[^\x00-\xff]
匹配空白行的正则表达式:\n\s*\r
匹配HTML标记的正则表达式:<(\S*?)[^>]*>.*?</\1>|<.*? />
匹配首尾空白字符的正则表达式:^\s*|\s*$
匹配Email地址的正则表达式:\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*
匹配网址URL的正则表达式:[a-zA-z]+://[^\s]*
匹配身份证:\d{15}|\d{18}
匹配ip地址:\d+\.\d+\.\d+\.\d+
匹配特定数字:
^[1-9]\d*$
//匹配正整数
^-[1-9]\d*$ //匹配负整数
^-?[1-9]\d*$ //匹配整数
^[1-9]\d*|0$ //匹配非负整数(正整数 + 0)
^-[1-9]\d*|0$ //匹配非正整数(负整数 + 0)
^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ //匹配正浮点数
^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$
//匹配负浮点数
^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$
//匹配浮点数
^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
//匹配非负浮点数(正浮点数 + 0)
^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$//匹配非正浮点数(负浮点数 + 0)匹配特定字符串:
^[A-Za-z]+$//匹配由26个英文字母组成的字符串
^[A-Z]+$//匹配由26个英文字母的大写组成的字符串
^[a-z]+$//匹配由26个英文字母的小写组成的字符串
^[A-Za-z0-9]+$//匹配由数字和26个英文字母组成的字符串
^\w+$//匹配由数字、26个英文字母或者下划线组成的字符串
只能输入数字:“^[0-9]*$” 只能输入n位的数字:“^d{n}$”
只能输入至少n位数字:“^d{n,}$”
只能输入m-n位的数字:“^d{m,n}$”
只能输入零和非零开头的数字:“^(0|[1-9][0-9]*)$”
只能输入有两位小数的正实数:“^[0-9]+(.[0-9]{2}) $”
只能输入有1-3位小数的正实数:“^[0-9]+(.[0-9]{1,3}) $”
只能输入非零的正整数:“^+ [1-9][0-9]*$”
只能输入非零的负整数:“^-[1-9][0-9]*$”
只能输入长度为3的字符:“^.{3}$”
验证用户密码:“^[a-zA-Z]w{5,17}$”正确格式为:以字母开头,长度在6-18之间, 只能包含 字符、数字和下划线。
验证是否含有^%&',;= $"等字符:“[^%&',;= $x22]+” 只能输入汉字:“^[u4e00-u9fa5],{0,}$”
④ 将正则表达式(aa|b)*a(a|bb)转化成dfa
此正则表达式化简后为a*b*即空字符串,或者仅由a组成的字符串,或者仅有b组成的字符串,或者由源轮若干a后面接若干b组成的字符串。
(A|B)*表示A或者B出现若干次或者不出现。
(A*B*)* A出现若干次或者不出现,B出现若干次或者不出现,一起出现若干次或者不出现
(A*|B*)* A出现若干次或者不出指裂现或者B出现若干次或者不出现,一起出现若干次或者不出现。
任何一个字符串都匹配这个字符串。
简介
正则表达式是对字符串和特殊字符(称为“元字符”))操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。正则表达式是一种文本唯裂闭模式,该模式描述在搜索文本时要匹配的一个或多个字符串。
⑤ 从正则表达式(RE)到最小确定性有限状态自动机(DFA)
RE(Regular Expression)到最小DFA(Deterministic Finite Automaton)的转换是构建正则表达式引擎的基础,并且也是构建词法分析器的基础.
RE描述了一个定义在某个字母表Σ上的字符串集合L,并且空字符串ε也属于L集合.形式化的定义并不好理解,但是相对其他非形式化的定义来说更加简洁和准确.这里的正则表达式和平常所用的处理字符串的正则表达式是同一个,但是这里更加简单.这里的RE只有三个基本的操作:
(1)选择 取并集.符号:|. 比如两个字符串集合R和S的选择操作,记作R|S.
(2)连接 字符串之间的拼接.两个字符串集合R和S的连接为RS.
(3)闭包 符号:* 字符串集合R的闭包R*是指把R与自身连接零次或者多次形成的所有集合的并集.
由这几个简单的操作可以得到我们平常接触的正则表达式的所有扩展.
我说的时候喜欢加上状态两个字,因为FA的关键动作就是状态间的转移.FA有一个状态集S,对于每一个输入都会让FA的状态进行转移.如果能够从起始状态转移到接受状态,那么输入序列就被识州卜别了.不存在空字符串ε的状态转移.
非确定性有限状态自动机(Non-deterministic Finite Automaton,NFA).对于同一输入转移到多个不同的状态或者存在空字符串ε的状态转移的FA.
确定性有限状态自动机(Deterministic Finite Automaton,DFA).对于任何确定的输入都只有唯一确定的转移且不存在空扒芹字符串ε的状态转移的FA.
上面描述的RE的基本操作的简单NFA:
NFA到DFA 是对NFA的简化过程.
NFA到DFA的子集构造算法(The Subset Construction):从将初始状态划分为一个初始状态子集开始,构造状态子集(经过零个或多个空字符串ε转移到的状态和已在子集中的状态都是构造的新的状态子集),存在c属于字母表Σ,经过一个c的转移(必须有c的转移),能够使得从状态子集ni转移到状态子集nj,则在DFA中有在c的输入下从状态子集ni转移到状态子集nj的转移.最后不再有新的状态子集出现.根据状态子集的转移依次构造DFA.
最小化DFA用到的是等价状态集合的划分来构建.一开始只有两个状态集,一个接受状态集合,一个非接受状态集合.对于每一个状态集合Sp,如果存在c属于字母表Σ,使得Sp中的状态转移到不同的状态集合(包括没有册此穗转移的空状态集合),则拆分Sp,使得拆分后的状态集合中的每一个状态不可能转移到不同的状态集合.其中状态集合之间的转移构成最小化DFA中的转移.
⑥ 1-7 正则表表达式—查找功能
正则表达式和js配合可以完成3件事。
1. 查找一个固定的敏感词出现的位置:
以往的方法: str.indexOf()
var i=str.indexOf("敏感词",starti);
在str中,从starti位置开始,查找下一个“敏感词”的位置;
省略第二个参数starti,默认从0位置开始查找;
返回晌前值: 如果找到,返回敏感词第一个字的位置;
如果找不到,返回-1;
问题: 只能查找一种固定的敏感词。
2. 模糊查找符合正则表达式要求的敏感词:
var i=str.search(/正则/);
问题1: 正则默认都是区分大小写的
解决: 在第二个/后加后缀i, ignore
问题2: 只能返回位置i,无法返回敏感词的内容。
3. 查询敏感词的内容,2种情况:
第一种、只查看第一个敏感词的内容和位置:
问题: 正则表达式默认只找到第一个就退出;
解决: 在第二个/后加后缀g, global(全部);
第二中空种、 查找所有敏感词的内容:
问题: 只能获得内容,无法获得位置;
4. 查找每个敏感词的内容和位置: reg.exec();卖谨瞎