正则表达式前端使用手册
导读你有没有在搜索文本的时候绞尽脑汁,试了一个又一个表达式,还是不行. 你有没有在表单验证的时候,只是做做样子(只要不为空就好),然后烧香拜佛,虔诚祈祷,千万不要出错. 你有没有在使用sed 和 grep 命令的时候,感觉莫名其妙,明明应该支持的元字符,却就是匹配不到. 甚至,你压根没遇到过上述情况,你只是一遍又一遍的调用 replace 而已 (把非搜索文本全部替换为空,然后就只剩搜索文本了),面对别人家的简洁高效的语句,你只能在心中呐喊,replace 大法好. 为什么要学正则表达式. 有位网友这么说: 江湖传说里,程序员的正则表达式和医生的处方,道士的鬼符齐名,曰: 普通人看不懂的三件神器. 这个传说至少向我们透露了两点信息: 一是正则表达式很牛,能和医生的处方,并被大家提起,可见其江湖地位. 二是正则表达式很难,这也从侧面说明了,如果你可以熟练的掌握并应用它,在装逼的路上,你将如日中天 (别问我中天是谁……) ! 显然,有关正则表达的介绍,无须我多言. 这里就借助 Jeffrey Friedl 的《精通正则表达式》一书的序言正式抛个砖.
因此,我们没有理由不去了解正则表达式,甚至是熟练掌握并运用它. 本文以正则基础语法开篇,结合具体实例,逐步讲解正则表达式匹配原理. 代码实例使用语言包括 js,php,python,java(因有些匹配模式,js并未支持,需要借助其他语言讲解). 内容包括初阶技能和高阶技能,适合新手学习和进阶. 本文力求简单通俗易懂,同时为求全面,涉及知识较多,共计12k字,篇幅较长,请耐心阅读,如有阅读障碍请及时联系我. 回顾历史要论正则表达式的渊源,最早可以追溯至对人类神经系统如何工作的早期研究. Warren McCulloch 和 Walter Pitts 这两位神经大咖 (神经生理学家) 研究出一种数学方式来描述这些神经网络. 1956 年,一位叫 Stephen Kleene 的数学家在 McCulloch 和 Pitts 早期工作的基础上,发表了一篇标题为"神经网事件的表示法"的论文,引入了正则表达式的概念. 随后,发现可以将这一工作应用于使用 Ken Thompson 的计算搜索算法的一些早期研究中. 而 Ken Thompson 又是 Unix 的主要发明人. 因此半个世纪以前的Unix 中的 qed 编辑器(1966 qed编辑器问世) 成了第一个使用正则表达式的应用程序. 至此之后,正则表达式成为家喻户晓的文本处理工具,几乎各大编程语言都以支持正则表达式作为卖点,当然 JavaScript 也不例外. 正则表达式的定义正则表达式是由普通字符和特殊字符(也叫元字符或限定符)组成的文字模板. 如下便是简单的匹配连续数字的正则表达式: /[0-9]+/ /d+/ "d" 就是元字符,而 "+" 则是限定符. 元字符
反义元字符
可以看出正则表达式严格区分大小写. 重复限定符限定符共有6个,假设重复次数为x次,那么将有如下规则:
字符组[...] 匹配中括号内字符之一. 如: [xyz] 匹配字符 x,y 或 z. 如果中括号中包含元字符,则元字符降级为普通字符,不再具有元字符的功能,如 [+.?] 匹配 加号,点号或问号. 排除性字符组[^…] 匹配任何未列出的字符,. 如: [^x] 匹配除x以外的任意字符. 多选结构| 就是或的意思,表示两者中的一个. 如: a|b 匹配a或者b字符. 括号括号 常用来界定重复限定符的范围,以及将字符分组. 如: (ab)+ 可以匹配abab..等,其中 ab 便是一个分组. 转义字符即转义字符,通常 * + ? | { [ ( ) ] }^ $ . # 和 空白 这些字符都需要转义. 操作符的运算优先级
测试我们来测试下上面的知识点,写一个匹配手机号码的正则表达式,如下: (+86)?1d{10} ① "+86" 匹配文本 "+86",后面接元字符问号,表示可匹配1次或0次,合起来表示 "(+86)?" 匹配 "+86" 或者 "". ② 普通字符"1" 匹配文本 "1". ③ 元字符 "d" 匹配数字0到9,区间量词 "{10}" 表示匹配 10 次,合起来表示 "d{10}" 匹配连续的10个数字. 以上,匹配结果如下:
修饰符javaScript中正则表达式默认有如下五种修饰符:
常用的正则表达式
密码验证密码验证是常见的需求,一般来说,常规密码大致会满足规律: 6-16位,字母,字符至少包含两种,同时不能包含中文和空格. 如下便是常规密码验证的正则描述: var reg = /(?!^[0-9]+$)(?!^[A-z]+$)(?!^[^A-z0-9]+$)^[^su4e00-u9fa5]{6,16}$/; 正则的几大家族正则表达式分类在 linux 和 osx 下,常见的正则表达式,至少有以下三种:
正则表达式比较
注意
linux/osx下常用命令与正则表达式的关系我曾经尝试在 grep 和 sed 命令中书写正则表达式,经常发现不能使用元字符,而且有时候需要转义,有时候不需要转义,始终不能摸清它的规律. 如果恰好你也有同样的困惑,那么请往下看,相信应该能有所收获. grep,egrep,sed,awk 正则表达式特点grep 支持:BREs、EREs、PREs 正则表达式
egrep 支持:EREs、PREs 正则表达式
sed 支持: BREs、EREs
awk 支持 EREs,并且默认使用 "EREs" 正则表达式初阶技能贪婪模式与非贪婪模式默认情况下,所有的限定词都是贪婪模式,表示尽可能多的去捕获字符; 而在限定词后增加?,则是非贪婪模式,表示尽可能少的去捕获字符. 如下: var str = "aaab",reg1 = /a+/,//贪婪模式 reg2 = /a+?/;//非贪婪模式 console.log(str.match(reg1)); //["aaa"],由于是贪婪模式,捕获了所有的a console.log(str.match(reg2)); //["a"],由于是非贪婪模式,只捕获到第一个a 实际上,非贪婪模式非常有效,特别是当匹配html标签时. 比如匹配一个配对出现的div,方案一可能会匹配到很多的div标签对,而方案二则只会匹配一个div标签对. var str = "<div class='v1'><div class='v2'>test</div><input type='text'/></div>"; var reg1 = /<div.*</div>/; //方案一,贪婪匹配 var reg2 = /<div.*?</div>/;//方案二,非贪婪匹配 console.log(str.match(reg1));//"<div class='v1'><div class='v2'>test</div><input type='text'/></div>" console.log(str.match(reg2));//"<div class='v1'><div class='v2'>test</div>" 区间量词的非贪婪模式一般情况下,非贪婪模式,我们使用的是"*?",或 "+?" 这种形式,还有一种是 "{n,m}?". 区间量词"{n,m}" 也是匹配优先,虽有匹配次数上限,但是在到达上限之前,它依然是尽可能多的匹配,而"{n,m}?" 则表示在区间范围内,尽可能少的匹配. 需要注意的是:
分组正则的分组主要通过小括号来实现,括号包裹的子表达式作为一个分组,括号后可以紧跟限定词表示重复次数. 如下,小括号内包裹的abc便是一个分组: /(abc)+/.test("abc123") == true 那么分组有什么用呢? 一般来说,分组是为了方便的表示重复次数,除此之外,还有一个作用就是用于捕获,请往下看. 捕获性分组捕获性分组,通常由一对小括号加上子表达式组成. 捕获性分组会创建反向引用,每个反向引用都由一个编号或名称来标识,js中主要是通过 var color = "#808080"; var output = color.replace(/#(d+)/,"$1"+"~~");//自然也可以写成 "$1~~" console.log(RegExp.$1);//808080 console.log(output);//808080~~ 以上,(d+) 表示一个捕获性分组,"RegExp.$1" 指向该分组捕获的内容. var url = "www.google.google.com"; var re = /([a-z]+).1/; console.log(url.replace(re,"$1"));//"www.google.com" 以上,相同部分的"google"字符串只被替换一次. 非捕获性分组非捕获性分组,通常由一对括号加上"?:"加上子表达式组成,非捕获性分组不会创建反向引用,就好像没有括号一样. 如下: var color = "#808080"; var output = color.replace(/#(?:d+)/,"$1"+"~~"); console.log(RegExp.$1);//"" console.log(output);//$1~~ 以上,(?:d+) 表示一个非捕获性分组,由于分组不捕获任何内容,所以,RegExp.$1 就指向了空字符串. 命名分组语法: (?<name>...) 命名分组也是捕获性分组,它将匹配的字符串捕获到一个组名称或编号名称中,在获得匹配结果后,可通过分组名进行获取. 如下是一个python的命名分组的例子. import re data = "#808080" regExp = r"#(?P<one>d+)" replaceString = "g<one>" + "~~" print re.sub(regExp,replaceString,data) # 808080~~ python的命名分组表达式与标准格式相比,在 ? 后多了一大写的 P 字符,并且python通过“g<命名>"表示法进行引用. (如果是捕获性分组,python通过"g<编号>"表示法进行引用) 与python不同的是,javaScript 中并不支持命名分组. 固化分组固化分组,又叫原子组. 语法: (?>...) 如上所述,我们在使用非贪婪模式时,匹配过程中可能会进行多次的回溯,回溯越多,正则表达式的运行效率就越低. 而固化分组就是用来减少回溯次数的. 实际上,固化分组(?>…)的匹配与正常的匹配并无分别,它并不会改变匹配结果. 唯一的不同就是: 固化分组匹配结束时,它匹配到的文本已经固化为一个单元,只能作为整体而保留或放弃,括号内的子表达式中未尝试过的备用状态都会被放弃,所以回溯永远也不能选择其中的状态(因此不能参与回溯). 下面我们来通过一个例子更好地理解固化分组. 假如要处理一批数据,原格式为 123.456,因为浮点数显示问题,部分数据格式会变为123.456000000789这种,现要求只保留小数点后2~3位,但是最后一位不能为0,那么这个正则怎么写呢? var str = "123.456000000789"; str = str.replace(/(.dd[1-9]?)d*/,"$1"); //123.456 以上的正则,对于"123.456" 这种格式的数据,将白白处理一遍. 为了提高效率,我们将正则最后的一个"*"改为"+". 如下: var str = "123.456"; str = str.replace(/(.dd[1-9]?)d+/,"$1"); //123.45 此时,"dd[1-9]?" 子表达式,匹配是 "45",而不是 "456",这是因为正则末尾使用了"+",表示末尾至少要匹配一个数字,因此末尾的子表达式"d+" 匹配到了 "6". 显然 "123.45" 不是我们期望的匹配结果,那我们应该怎么做呢? 能否让 "[1-9]?" 一旦匹配成功,便不再进行回溯,这里就要用到我们上面说的固化分组. "(.dd(?>[1-9]?))d+" 便是上述正则的固化分组形式. 由于字符串 "123.456" 不满足该固化分组的正则,匹配会失败,符合我们期望. 下面我们来分析下固化分组的正则 (.dd(?>[1-9]?))d+ 为什么匹配不到字符串"123.456". 很明显,对于上述固化分组,只存在两种匹配结果. 情况①: 若 [1-9] 匹配失败,正则会返回 ? 留下的备用状态. 然后匹配脱离固化分组,继续前进到[d+]. 当控制权离开固化分组时,没有备用状态需要放弃(因固化分组中根本没有创建任何备用状态). 情况②: 若 [1-9] 匹配成功,匹配脱离固化分组之后,? 保存的备用状态仍然存在,但是,由于它属于已经结束的固化分组,所以会被抛弃. 对于字符串 "123.456",由于 [1-9] 能够匹配成功,所以它符合情况②. 下面我们来还原情况②的执行现场.
相应的流程图如下:
遗憾的是,javaScript,java 和 python中并不支持固化分组的语法,不过,它在php和.NET中表现良好. 下面提供了一个php版的固化分组形式的正则表达式,以供尝试. $str = "123.456"; echo preg_replace("/(.dd(?>[1-9]?))d+/","1",$str); //固化分组 不仅如此,php还提供了占有量词优先的语法. 如下: $str = "123.456"; echo preg_replace("/(.dd[1-9]?+)d+/",$str); //占有量词优先 虽然java不支持固化分组的语法,但java也提供了占有量词优先的语法,同样能够避免正则回溯. 如下: String str = "123.456"; System.out.println(str.replaceAll("(.dd[1-9]?+)d+","$1"));// 123.456 值得注意的是: java中 replaceAll 方法需要转义反斜杠. 正则表达式高阶技能-零宽断言如果说正则分组是写轮眼,那么零宽断言就是万花筒写轮眼终极奥义-须佐能乎(这里借火影忍术打个比方). 合理地使用零宽断言,能够能分组之不能,极大地增强正则匹配能力,它甚至可以帮助你在匹配条件非常模糊的情况下快速地定位文本. 零宽断言,又叫环视. 环视只进行子表达式的匹配,匹配到的内容不保存到最终的匹配结果,由于匹配是零宽度的,故最终匹配到的只是一个位置. 环视按照方向划分,有顺序和逆序两种(也叫前瞻和后瞻),按照是否匹配有肯定和否定两种,组合之,便有4种环视. 4种环视并不复杂,如下便是它们的描述.
非捕获性分组由于结构与环视相似,故列在表中,以做对比. 以上4种环视中,目前 javaScript 中只支持前两种,也就是只支持 顺序肯定环视 和 顺序否定环视. 下面我们通过实例来帮助理解下: var str = "123abc789",s; //没有使用环视,abc直接被替换 s = str.replace(/abc/,456); console.log(s); //123456789 //使用了顺序肯定环视,捕获到了a前面的位置,所以abc没有被替换,只是将3替换成了3456 s = str.replace(/3(?=abc)/,3456); console.log(s); //123456abc789 //使用了顺序否定环视,由于3后面跟着abc,不满意条件,故捕获失败,所以原字符串没有被替换 s = str.replace(/3(?!abc)/,3456); console.log(s); //123abc789 下面通过python来演示下 逆序肯定环视 和 逆序否定环视 的用法. import re data = "123abc789" # 使用了逆序肯定环视,替换左边为123的连续的小写英文字母,匹配成功,故abc被替换为456 regExp = r"(?<=123)[a-z]+" replaceString = "456" print re.sub(regExp,data) # 123456789 # 使用了逆序否定环视,由于英文字母左侧不能为123,故子表达式[a-z]+捕获到bc,最终bc被替换为456 regExp = r"(?<!123)[a-z]+" replaceString = "456" print re.sub(regExp,data) # 123a456789 需要注意的是: python 和 perl 语言中的 逆序环视 的子表达式只能使用定长的文本. 比如将上述 "(?<=123)" (逆序肯定环视)子表达式写成 "(?<=[0-9]+)",python解释器将会报错: "error: look-behind requires fixed-width pattern". 场景回顾获取html片段假如现在,js 通过 ajax 获取到一段 html 代码如下: var responseText = "<div data='dev.xxx.txt'></div><img src='dev.xxx.png' />"; 现我们需要替换img标签的src 属性中的 "dev"字符串 为 "test" 字符串. ① 由于上述 responseText 字符串中包含至少两个子字符串 "dev",显然不能直接 replace 字符串 "dev"为 "test". ② 同时由于 js 中不支持逆序环视,我们也不能在正则中判断前缀为 "src='",然后再替换"dev". ③ 我们注意到 img 标签的 src 属性以 ".png" 结尾,基于此,就可以使用顺序肯定环视. 如下: var reg = /dev(?=[^']*png)/; //为了防止匹配到第一个dev,通配符前面需要排除单引号或者是尖括号 var str = responseText.replace(reg,"test"); console.log(str);//<div data='dev.xxx'></div><img src='test.xxx.png' /> 当然,以上不止顺序肯定环视一种解法,捕获性分组同样可以做到. 那么环视高级在哪里呢? 环视高级的地方就在于它通过一次捕获就可以定位到一个位置,对于复杂的文本替换场景,常有奇效,而分组则需要更多的操作. 千位分割符
那么怎么将一串数字转化为千位分隔符形式呢? var str = "1234567890"; (+str).toLocaleString();//"1,234,567,890" 如上,
我们尝试使用环视来处理下. function thousand(str){ return str.replace(/(?!^)(?=([0-9]{3})+$)/g,','); } console.log(thousand(str));//"1,890" console.log(thousand("123456"));//"123,456" console.log(thousand("1234567879876543210"));//"1,879,876,543,210" 上述使用到的正则分为两块.
千位分隔符实例,展示了环视的强大,一步到位. 正则表达式在JS中的应用ES6对正则的扩展ES6对正则扩展了又两种修饰符(其他语言可能不支持):
var s = "abc_ab_a"; var r1 = /[a-z]+/g; var r2 = /[a-z]+/y; console.log(r1.exec(s),r1.lastIndex); // ["abc",index: 0,input: "abc_ab_a"] 3 console.log(r2.exec(s),r2.lastIndex); // ["abc",input: "abc_ab_a"] 3 console.log(r1.exec(s),r1.lastIndex); // ["ab",index: 4,input: "abc_ab_a"] 6 console.log(r2.exec(s),r2.lastIndex); // null 0 如上,由于第二次匹配的开始位置是下标3,对应的字符串是 "_",而使用y修饰符的正则对象r2,需要从剩余的第一个位置开始,所以匹配失败,返回null. 正则对象的 sticky 属性,表示是否设置了y修饰符. 这点将会在后面讲到.
|