首页 后端开发 php教程 深入了解正则表达式

深入了解正则表达式

Nov 10, 2016 am 09:36 AM

学习了半年的正则表达式,也不能说一直学习吧,就是和它一直在打交道,如何用正则表达式解决自己的问题,并且还要考虑如何在匹配大量的文本时去优化它。慢慢的觉得正则已经成为自己的一项技能,逐渐的从一个正则表达式小白变成一个伪精通者。

14785753121.jpg

new RegExp 和 // 正则对象创建区别

如果写过 Python 的同学,都一定会知道 Python 中可以在字符串前面加个小写的 r ,来表示防止转义。防止转义的意思就是说: str = r'\t' 等价于 str = '\\t' ,加了 r 会防止 \ 被转义。

为什么要介绍这个,因为这就是 new RegExp 和 // 的区别,因为我们知道在正则表达式中会频繁的使用转义字符 \w\s\d 等,但是它们在内存中的是以 \\w\\s\\d 存储的,看个例子:

//推荐写法
var regex1 = /\w+/g;
regex1 // /\w+/g
//RegExp 写法
var regex2 = new RegExp('\\w+','g');
regex2 // /\w+/g
//错误写法
var regex3 = new RegExp('\w+','g');
regex3 // /w+/g
登录后复制

你也看出来了,错误写法只能匹配 wwwww 这样的字符串,曾经我就见过有人把他们弄混了,还说第一个第三个没有区别。第二种方法的输出,还是 /\w+/g ,中间还是要转换,所以推荐第一种写法。

当然,还有比较奇葩的:

var regex4 = new RegExp(/\w+/g);
regex4 // /\w+/g
登录后复制

i、g、m 修饰符

这几个修饰符只是针对 JS 来说的,像 Python 中还有 re.S 表示 . 可以匹配换行符。

对于 i 表示忽略字母大小写,不是很常用,因为它有很多替代品,比如: /[a-zA-Z]/ 可以用来替代 /[a-z]/i ,至于两者处理长文本的时间效率,我自己没有研究过,不下定论。

使用 i 需要注意的地方,就是 i 会对正则表达式的每一个字母都忽略大小写,当我们需要部分单词的时候,可以考虑一下 /(?:t|T)he boy/ 。

g 表示全局匹配,在印象中,可能很多人会觉得全局匹配就是当使用 match 的时候,把所有符合正则表达式的文本全部匹配出来,这个用途确实很广泛,不过 g 还有其他更有意思的用途,那就是 lastIndex 参数。

var str = '1a2b3c4d5e6f',
    reg = /\d\w\d/g;
str.match(reg); //["1a2", "3c4", "5e6"]
登录后复制

为什么不包括 2b3,4d5 ,因为正则表达式匹配的时候,会用 lastIndex 来标记上次匹配的位置,正常情况下,已经匹配过的内容是不会参与到下次匹配中的。带有 g 修饰符时,可以通过正则对象的 lastIndex 属性指定开始搜索的位置,当然这仅仅局限于函数 exec 和 test(replace 没研究过,没听说过可以控制 lastIndex,match 返回的是数组,无法控制 lastIndex),针对这个题目修改如下:

var str = '1a2b3c4d5e6f',
  reg = /\d\w\d/g;
var a;
var arr = [];
while(a = reg.exec(str)){
  arr.push(a[0]);
  reg.lastIndex -= 1;
}
arr //["1a2", "2b3", "3c4", "4d5", "5e6"]
登录后复制

m 表示多行匹配,我发现很多人介绍 m 都只是一行略过,其实关于 m 还是很有意思的。首先,来了解一下单行模式,我们知道 JavaScript 正则表达式中的 . 是无法匹配 \r\n (换行,各个系统使用不一样) 的,像 Python 提供 re.S 表示 . 可以匹配任意字符,包括 \r\n ,在 JS 中如果想要表示匹配任意字符,只能用 [\s\S] 这种蹩脚的方式了(还有更蹩脚的 [\d\D],[.\s] )。这种模式叫做开启或关闭单行模式,可惜 JS 中无法来控制。

多行模式跟 ^ $ 两兄弟有关,如果你的正则表达式没有 ^$,即时你开启多行模式也是没用的。正常的理解 /^123$/ 只能匹配字符串 123 ,而开启多行模式 /^123$/g 能匹配 ['123','\n123','123\n','\n123\n'] ,相对于 ^$ 可以匹配 \r\n 了。

var str = '\na';
/^a/.test(str); //false
/^a/m.test(str); //true
登录后复制

有人说,m 没用。其实在某些特殊的格式下,你知道你要匹配的内容会紧接着 \r\n 或以 \r\n 结尾,这个时候 m 就非常有用,比如 HTTP 协议中的请求和响应,都是以 \r\n 划分每一行的,响应头和响应体之间以 \r\n\r\n 来划分,我们需要匹配的内容就在开头,通过多行匹配,可以很明显的提高匹配效率。

原理性的东西,我们还是要知道的,万一以后会用到。

(?:) 和 (?=) 区别

在正则表达式中,括号不能乱用,因为括号就代表分组,在最终的匹配结果中,会被算入字匹配中,而 (?:) 就是来解决这个问题的,它的别名叫做非捕获分组。

var str = 'Hello world!';
var regex = /Hello (\w+)/;
regex.exec(str); //["Hello world", "world"]
var regex2 = /Hello (?:\w+)/;
regex2.exec(str); //["Hello world"]
登录后复制

可以看到 (?:) 并不会把括号里的内容计入到子分组中。

关于 (?=),新手理解起来可能比较困难,尤其是一些很牛逼的预查正则表达式。其实还有个 (?!),不过它和 (?=) 是属于一类的,叫做 正向肯定(否定)预查 ,它还有很多别名比如零宽度正预测先行断言。但我觉得最重要的只要记住这两点,预查和非捕获。

预查的意思就是在之前匹配成功的基础上,在向后预查,看看是否符合预查的内容。正因为是预查,lastIndex 不会改变,且不会被捕获到总分组,更不会被捕获到子分组。

var str = 'Hello world!';
var regex = /Hello (?=\w+)/;
regex.exec(str); //["Hello "]
//replace 也一样
var regex2 = /(?:ab)(cd)/
'abcd'.replace(regex2,'$1') //"cd"
登录后复制

和 (?:) 区别是: 我习惯的会把匹配的总结果叫做总分组 ,match 函数返回数组每一项都是总分组,exec 函数的返回数组的第一项是总分组。(?:) 会把括号里的内容计入总分组,(?=) 不会把括号里的内容计入总分组。

说白了,还是强大的 lastIndex 在起作用。(?:) 和 (?=) 差别是有的,使用的时候要合适的取舍。

说了这么多关于 (?=) 的内容,下面来点进阶吧!现在的需求是一串数字表示钱 “10000000”,但是在国际化的表示方法中,应该是隔三位有个逗号 “10,000,000”,给你一串没有逗号的,替换成有逗号的。

var str = "10000000";
var regex = /\d(?=(\d{3})+$)/g;
str.replace(regex, '$&,'); //"10,000,000"
登录后复制

我们分析一下 regex, /\d(?=(\d{3})+$)/g 它是全局 g,实际上它匹配的内容只有一个 \d, (?=(\d{3})+$) 是预判的内容,之前说过,预判的内容不计入匹配结果,lastIndex 还是停留在 \d 的位置。 (?=(\d{3})+$) 到结尾有至少一组 3 个在一起的数字,才算预判成功。

\d = 1 的时候,不满足预判,向后移一位, \d = 0 ,满足预判,replace。

(?!) 前瞻判断

(?=) 和 (?!) 叫做正向预查,但往往是正向这个词把我们的思维给束缚住了。正向给人的感觉是只能在正则表达式后面来预判,那么 预判为什么不能放在前面呢 。下面这个例子也非常有意思。

一个简单密码的验证,要保证至少包含大写字母、小写字母、数字中的两种,且长度 8~20。

如果可以写多个正则,这个题目很简单,思路就是: /^[a-zA-Z\d]{8,20}$/ && !(/[a-z]+/) && !(/[A-Z]+/) && !(/\d+/) ,看着眼都花了,好长一串。

下面用 (?!) 前瞻判断来实现:

var regex = /^(?![a-z]+$)(?![A-Z]+$)(?!\d+$)[a-zA-Z\d]{8,12}$/;
regex.test('12345678'); //false
regex.test('1234567a'); //true
登录后复制

分析一下,因为像 (?!) 预判不消耗 lastIndex,完全可以放到前面进行前瞻。 (?![a-z]+$) 的意思就是从当前 lastIndex (就是^)开始一直到 $,不能全是小写字母, (?![A-Z]+$) 不能全是大写字母, (?!\d+$) 不能全是数字, [a-zA-Z\d]{8,12}$ 这个是主体,判断到这里的时候, lastIndex 的位置仍然是 0,这就是 (?!) 前瞻带来的效率。

非贪婪与贪婪的问题

贪婪出现在 + * {1,} 这种不确定数量的匹配中,所谓的贪婪,表示正则表达式在匹配的时候,尽可能多的匹配符合条件的内容。比如 /hello.*world/ 匹配 'hello world,nice world' 会匹配到第二个 world 结束。

鉴于上面的情况,可以使用 ? 来实现非贪婪匹配。? 在正则表达式中用途很多,正常情况下,它表示前面那个字符匹配 0 或 1 次,就是简化版的 {0,1} ,如果在一些不确定次数的限制符后面出现,表示非贪婪匹配。 /hello.*?world/ 匹配 'hello world,nice world' 的结果是 hello world 。

我刚开始写正则的时候,写出来的正则都是贪婪模式的,往往得到的结果和预想的有些偏差,就是因为少了 ? 的原因。

我初入正则的时候,非贪婪模式还给我一种错觉。还是前面的那个例子,被匹配的内容换一下,用 /hello.*?world/ 匹配 'hello word,nice world' ,因为 word 不等于 world,在第一次尝试匹配失败之后,应该返回失败,但结果却是成功的,返回的是 'hello word,nice world' 。

一开始我对于这种情况是不理解的,但仔细想想也对,这本来就应该返回成功。至于如何在第一次尝试匹配失败之后,后面就不再继续匹配,只能通过优化 .* 。如果我们把 .*?end 这样子来看, .* 会把所有字符都吞进去,慢慢吐出最后几个字符,和 end 比较,如果是贪婪,吐到第一个满足条件的就停止,如果是非贪婪,一直吐到不能吐为止,把离自己最近的结果返回。

所以,贪婪是返回最近的一次成功匹配,而不是第一次尝试。

避免回溯失控

回溯可以杀死一个正则表达式,这一点都不假。关于正则表达式回溯也很好理解,就是正则引擎发现由两条路可以走时,它会选择其中的一条,把另一条路保存以便回溯时候用。

比如正则 /ab?c/ 在成功匹配到 a 之后,后面可以有 b,也可以没有 b,这时候要提供两种选择。还有其他类型的回溯,比如 /to(night|do)/ 。当然影响性能的回溯就要和 .* .+ .{m} 有关。

所谓的回溯失控,就是可供选择的路径太多,看一个常见回溯失控的例子,正则 /(A+A+)+B/ ,如果匹配成功,会很快返回,那么匹配失败,非常可怕。比如来匹配 10 个 A AAAAAAAAAA ,假设第一个 A+ 吞了 9 个 A,整个正则吐出最后一个字符发现不是 B,知道吐完,还不能返回 false,第一个 A+ 吞 8 个 A,….回溯次数的复杂度是 n 的平方。

当然你可能会说,自己不会写这样傻的正则表达式。真的吗?我们来看一个匹配 html 标签的正则表达式, /[\s\S]*?[\s\S]*?[\s\S]*?[\s\S]*?[\s\S]*? (感觉这样写也很傻)。如果一切都 OK,匹配一个正常的 HTML 页面,工作良好。但是如果不是以 结尾,每一个 [\s\S]*? 就会扩大其范围,一次一次回溯查找满足的一个字符串。

在说到回溯的同时,有时候还是要考虑一下 . * {} 查询集合的问题,反正我的建议是尽量避免使用匹配任何字符的 [\s\S] ,这真的是有点太暴力了。因为我们写正则的时候,都是以正确匹配的思路去写的,同时还需要考虑如果匹配不成功,该如何尽快的让 [a-zA-Z]* 集合尽快停止,比如 [^\r\n]* 在匹配单行时效果不错,即时匹配失败也可以快速停止。

总结

感觉这篇文章写的很乱,东扯西扯的,大概我把我这几个月以来所学到的正则表达式知识都写在了这里,当然这并不包括一些基础的知识。我觉得学习正则最主要的还是去练习,只有在实际项目中总结出来的正则经验,才算自己正在掌握的,如果只是简单的少一眼,时间久了,终究会忘记。共勉!

参考

RegExp对象 - 阮一峰

MSDN RegExp

进阶正则表达式

如何找出文件名为 “.js” 的文件,但要过滤掉 “.min.js” 的文件。

代码如下:

var regex = /^(?!.*\.min\.js$).+\.js$/;
regex.test('a.js'); //true
regex.test('b.min.js'); //false
regex.test('c.css'); //false
登录后复制


本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前 By 尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
4 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

php中的卷曲:如何在REST API中使用PHP卷曲扩展 php中的卷曲:如何在REST API中使用PHP卷曲扩展 Mar 14, 2025 am 11:42 AM

PHP客户端URL(curl)扩展是开发人员的强大工具,可以与远程服务器和REST API无缝交互。通过利用Libcurl(备受尊敬的多协议文件传输库),PHP curl促进了有效的执行

在Codecanyon上的12个最佳PHP聊天脚本 在Codecanyon上的12个最佳PHP聊天脚本 Mar 13, 2025 pm 12:08 PM

您是否想为客户最紧迫的问题提供实时的即时解决方案? 实时聊天使您可以与客户进行实时对话,并立即解决他们的问题。它允许您为您的自定义提供更快的服务

解释PHP中晚期静态结合的概念。 解释PHP中晚期静态结合的概念。 Mar 21, 2025 pm 01:33 PM

文章讨论了PHP 5.3中引入的PHP中的晚期静态结合(LSB),从而允许静态方法的运行时分辨率调用以获得更灵活的继承。 LSB的实用应用和潜在的触摸

在PHP API中说明JSON Web令牌(JWT)及其用例。 在PHP API中说明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

JWT是一种基于JSON的开放标准,用于在各方之间安全地传输信息,主要用于身份验证和信息交换。1.JWT由Header、Payload和Signature三部分组成。2.JWT的工作原理包括生成JWT、验证JWT和解析Payload三个步骤。3.在PHP中使用JWT进行身份验证时,可以生成和验证JWT,并在高级用法中包含用户角色和权限信息。4.常见错误包括签名验证失败、令牌过期和Payload过大,调试技巧包括使用调试工具和日志记录。5.性能优化和最佳实践包括使用合适的签名算法、合理设置有效期、

框架安全功能:防止漏洞。 框架安全功能:防止漏洞。 Mar 28, 2025 pm 05:11 PM

文章讨论了框架中的基本安全功能,以防止漏洞,包括输入验证,身份验证和常规更新。

自定义/扩展框架:如何添加自定义功能。 自定义/扩展框架:如何添加自定义功能。 Mar 28, 2025 pm 05:12 PM

本文讨论了将自定义功能添加到框架上,专注于理解体系结构,识别扩展点以及集成和调试的最佳实践。

如何用PHP的cURL库发送包含JSON数据的POST请求? 如何用PHP的cURL库发送包含JSON数据的POST请求? Apr 01, 2025 pm 03:12 PM

使用PHP的cURL库发送JSON数据在PHP开发中,经常需要与外部API进行交互,其中一种常见的方式是使用cURL库发送POST�...

See all articles