正则表达式是程序员的必备技能之一。
我从入行开始就用正则,到现在已经用了三年了。这三年里,写过无数个正则,踩过无数个坑,也积累了一些经验。
刚开始学正则的时候,觉得正则很神奇,几行代码就能完成复杂的字符串匹配,简直是神器。后来用得多了,觉得正则很坑,写出来的正则经常匹配错,调试半天找不到原因,性能也经常出问题。
现在用了三年,才慢慢明白正则的一些道理,知道什么时候该用,什么时候不该用,怎么写才不容易出错,怎么调试才高效。
今天就来分享一下我用了三年正则的经验总结,希望能帮大家少踩一些坑。
一、正则不是万能的
用了三年正则,我明白的第一个道理就是:正则不是万能的。
刚开始用正则的时候,觉得正则什么都能做,匹配字符串、提取内容、替换、验证,什么都想用正则。比如解析HTML、解析JSON、解析XML,都想用正则。
但是后来踩了很多坑之后才明白,正则不是万能的,有些事情正则做不好,甚至根本做不了。
最典型的就是用正则解析HTML。HTML的结构很复杂,标签可以嵌套,可以有属性,可以有注释,可以有大小写,可以有不规范的写法,用正则解析HTML几乎是不可能的,写出来的正则要么匹配不全,要么匹配错误,而且很难维护。
比如你想提取HTML里所有的链接,用正则写/<a\s+href="([^"]+)"/g,看起来没问题,但是实际用的时候会发现:
- 有的标签是大写的
<A - 有的属性用单引号
href='...' - 有的属性没有引号
href=... - 有的href前面有其他属性
- 有的标签里有注释
- 等等
这些情况正则都很难处理,写出来的正则会越来越复杂,最后变成一个谁也看不懂的怪物,而且还是会有遗漏。
正确的做法是用HTML解析器,比如PHP的DOMDocument、Python的BeautifulSoup、JavaScript的DOMParser,这些工具专门用来解析HTML,比正则靠谱多了。
同样,解析JSON、XML、CSV等结构化数据,也应该用专门的解析器,而不是正则。
正则适合处理的是简单的、规则的文本,比如验证邮箱格式、提取手机号、替换敏感词、简单的字符串匹配等。对于复杂的、结构化的数据,应该用专门的解析器。
记住:当你发现自己写的正则越来越复杂,越来越长,越来越难维护的时候,可能就该考虑是不是不该用正则了。
二、能不用正则就不用正则
用了三年正则,我明白的第二个道理就是:能不用正则就不用正则。
很多时候,我们写正则只是为了做一个简单的字符串操作,比如判断字符串是否以某个前缀开头,或者替换某个子串,这些操作用普通的字符串函数就能完成,不需要用正则。
比如:
- 判断是否以"http"开头:用
strpos($str, "http") === 0,比正则/^http/快很多。 - 判断是否包含"abc":用
strpos($str, "abc") !== false,比正则/abc/快。 - 替换所有"a"为"b":用
str_replace("a", "b", $str),比正则/a/替换快。 - 分割字符串:如果分隔符是固定的,用
explode(",", $str),比正则/,/分割快。
普通字符串函数比正则快很多,因为正则需要编译、解析、匹配,开销很大。对于简单的字符串操作,用普通字符串函数不仅更快,而且更简单,更易读,更不容易出错。
我见过很多代码,明明一个strpos就能解决的问题,非要写一个正则,不仅性能差,而且容易写错。比如有人写preg_match("/.abc./", $str)来判断是否包含"abc",这完全是多余的,strpos就够了。
所以,写代码的时候,先想想这个需求能不能用普通字符串函数解决,如果能,就不要用正则。正则是最后的选择,不是第一选择。
三、贪婪匹配和非贪婪匹配的坑
用了三年正则,我踩得最多的坑就是贪婪匹配和非贪婪匹配。
正则默认是贪婪匹配的,也就是说,量词(、+、?、{n,m})会尽可能多地匹配字符。比如/a.b/匹配"a123b456b",会匹配整个"a123b456b",而不是"a123b",因为.*会尽可能多地匹配,直到最后一个b。
这经常会导致匹配错误。比如你想提取HTML标签里的内容,写/<div>(.*)<\/div>/,如果HTML里有多个div,就会匹配从第一个div开始到最后一个div结束的所有内容,而不是每个div分别匹配。
解决方法是用非贪婪匹配,在量词后面加个?,比如?、+?、??,这样量词就会尽可能少地匹配。比如/<div>(.?)<\/div>/就会匹配每个div分别的内容。
但是非贪婪匹配也有坑。比如你想匹配引号里的内容,写/"(.*?)"/,看起来没问题,但是如果内容里有转义的引号\",就会匹配错误,因为非贪婪匹配会在第一个"处停止,不管这个"是不是转义的。
而且非贪婪匹配的性能比贪婪匹配差,因为它需要回溯。对于长文本,非贪婪匹配可能会很慢。
所以,使用贪婪和非贪婪匹配的时候要注意:
- 默认是贪婪匹配,要知道它会尽可能多地匹配。
- 需要尽可能少匹配的时候,用非贪婪匹配(加
?)。 - 非贪婪匹配不是万能的,对于复杂的情况(比如转义字符),可能需要更精确的写法。
- 性能敏感的场景,谨慎使用非贪婪匹配。
更精确的写法是用排除字符类,比如匹配引号里的内容,用/"[^"]"/,而不是/".?"/,这样既不会匹配错误,性能也更好。
四、回溯是性能杀手
用了三年正则,我明白的第四个道理就是:回溯是性能杀手。
正则匹配的时候,如果当前路径匹配失败,会回退到上一个位置,尝试其他可能的匹配,这就是回溯。回溯是正则的基本机制,但是如果回溯太多,会导致性能急剧下降,甚至出现"灾难性回溯",让正则匹配变得极慢,甚至卡死。
最典型的灾难性回溯的例子是/(a+)+$/匹配"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"。这个正则看起来很简单,但是匹配的时候会有大量的回溯,因为(a+)+有很多种分组方式,每一种都要尝试,回溯次数是指数级的,字符串稍微长一点就会卡死。
还有一些常见的容易导致回溯的写法:
- 嵌套的量词,比如
(a)、(a+)+ - 多个可以匹配相同内容的量词相邻,比如
aa - 复杂的 alternation,比如
(a|aa|aaa)* - 非贪婪匹配在长文本上的使用
避免回溯的方法:
- 避免嵌套量词:不要写
(a)这种嵌套量词的正则。 - 用排除字符类代替通配符:比如匹配引号里的内容,用
"[^"]"而不是".?",排除字符类不会回溯。 - 使用原子组:原子组
(?>...)里的内容一旦匹配就不会回溯,能有效减少回溯。比如(?>a+)就不会回溯。 - 使用possessive量词:
a++、a*+这种possessive量词,匹配了就不会释放,不会回溯。 - 避免复杂的alternation:尽量简化alternation,把长的放前面,或者用字符类代替。
- 测试性能:对于复杂的正则,一定要用长文本测试性能,看看有没有回溯问题。
正则的性能问题往往是隐蔽的,测试的时候用短文本可能没问题,但是线上遇到长文本就会卡死。所以写正则的时候一定要有性能意识,避免容易导致回溯的写法。
五、锚点很重要
用了三年正则,我明白的第五个道理就是:锚点很重要。
锚点(^、$、\b)用来匹配位置,而不是字符。很多人写正则的时候忽略锚点,导致匹配错误或者性能问题。
比如验证邮箱格式,很多人写/\w+@\w+\.\w+/,但是这个正则只要字符串里包含邮箱格式就会匹配,比如"abc@def.ghi垃圾内容"也会匹配通过,这显然不是我们想要的。正确的写法应该加上锚点/^\w+@\w+\.\w+$/,这样只有整个字符串都是邮箱格式才会匹配通过。
再比如提取手机号,很多人写/1\d{10}/,但是这个正则会匹配"abc13800138000def"里的手机号,也会匹配"138001380001"里的前11位,这可能不是我们想要的。如果我们想匹配独立的手机号,应该加上单词边界/\b1\d{10}\b/,这样就不会匹配长数字里的一部分了。
锚点不仅能让匹配更精确,还能提升性能。因为有了锚点,正则引擎可以快速定位到匹配的位置,不需要在整个字符串里到处尝试匹配,减少了回溯。
所以,写正则的时候,要养成使用锚点的习惯:
- 验证整个字符串格式的时候,用
^和$锚定开头和结尾。 - 匹配独立单词的时候,用
\b锚定单词边界。 - 只需要匹配开头的时候,用
^锚定开头,这样正则引擎可以从开头开始匹配,不需要遍历整个字符串。
六、正则的可读性很重要
用了三年正则,我明白的第六个道理就是:正则的可读性很重要。
很多人写正则,追求简洁,把正则写得越短越好,结果写出来的正则像天书一样,谁也看不懂,包括自己过一段时间也看不懂。
比如有人写验证邮箱的正则:/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,这个正则其实还算清晰,但是如果再复杂一点,比如验证URL的正则,可能就有几十个字符,各种字符类、量词、分组,看起来就头疼。
正则是用来解决问题的,不是用来炫技的。写出来的正则不仅要能工作,还要让人能看懂,能维护。否则过一段时间,自己都看不懂了,更别说别人了。
提升正则可读性的方法:
- 加注释:很多语言的正则支持注释模式(比如PHP的
x修饰符),可以在正则里加注释,解释每一部分的作用。 - 拆分正则:把复杂的正则拆成几个简单的部分,用字符串拼接起来,每一部分加注释。
- 用命名分组:用
(?P<name>...)命名分组,而不是用数字引用,这样更清晰。 - 不要过度追求简洁:为了可读性,稍微长一点没关系,清晰比简洁重要。
- 写测试用例:给正则写测试用例,不仅能验证正确性,也能通过测试用例说明正则的用途和边界情况。
比如验证邮箱的正则,可以这样写:
$pattern = '/
^ # 开头
[a-zA-Z0-9._%+-]+ # 用户名部分
@ # @符号
[a-zA-Z0-9.-]+ # 域名部分
\. # 点
[a-zA-Z]{2,} # 顶级域名
$ # 结尾
/x';这样加了注释之后,每一部分的作用都很清楚,可读性大大提升。
记住,代码是写给人看的,顺便给机器执行。正则也是代码,也要注重可读性和可维护性。
七、调试正则的技巧
用了三年正则,我积累了一些调试正则的技巧。
正则调试是一件很头疼的事情,因为正则匹配失败的时候,不会告诉你哪里错了,只会返回false或者空,你只能自己猜。
分享几个调试正则的技巧:
1. 用在线正则测试工具
有很多在线正则测试工具,比如regex101.com、regexr.com、debuggex.com等,这些工具可以实时测试正则,显示匹配结果,高亮匹配的部分,还能解释正则的每一部分,非常方便。
我写正则的时候,一般先在在线工具里测试好,确认没问题了再放到代码里。这样比在代码里改来改去高效很多。
2. 从简单到复杂
写正则的时候,不要一开始就写完整的复杂正则,而是从简单的开始,一步步添加复杂度。比如先写匹配开头的部分,测试没问题了,再加中间的部分,再加结尾的部分,每一步都测试,这样如果出了问题,很容易定位是哪一部分的问题。
3. 用捕获组调试
如果正则匹配了但是结果不对,可以用捕获组把每一部分都捕获出来,看看每一部分匹配了什么,这样就能定位是哪一部分匹配错了。
比如/^(\w+)@(\w+)\.(\w+)$/,如果匹配结果不对,可以看看每个捕获组分别匹配了什么,就能知道是用户名错了还是域名错了。
4. 测试边界情况
正则写好之后,一定要测试边界情况,比如空字符串、超长字符串、特殊字符、大小写、不规范的输入等。很多正则在正常输入下没问题,但是遇到边界情况就会出问题。
比如验证邮箱的正则,要测试:正常邮箱、没有@的邮箱、没有域名的邮箱、域名没有点的邮箱、有特殊字符的邮箱、超长邮箱、空字符串等,确保各种情况都能正确处理。
5. 看正则的解释
在线正则工具一般会有正则解释的功能,把正则的每一部分都解释清楚,包括字符类、量词、分组、锚点等。如果正则匹配结果不对,可以看看解释,确认自己写的每一部分是不是自己想要的意思。
很多时候,正则匹配错误是因为自己对正则的语法理解错了,比如以为\d匹配所有数字,其实只匹配0-9;以为\w匹配所有单词字符,其实只匹配字母数字下划线。看解释能帮你发现这些误解。
八、常见的正则错误
用了三年正则,我见过很多常见的错误,分享一下:
1. 忘记转义特殊字符
正则里有很多特殊字符,比如.、*、+、?、()、[]、{}、^、$、\、|等,这些字符在正则里有特殊含义,如果要匹配字面量的这些字符,需要转义,前面加\。
很多人忘记转义,比如想匹配点号.,但是写了.,结果匹配了任意字符,导致匹配错误。比如匹配IP地址,写/\d+.\d+.\d+.\d+/,这里的.没有转义,会匹配任意字符,导致"1a2b3c4"也能匹配通过。正确的写法是/\d+\.\d+\.\d+\.\d+/。
2. 字符类里的特殊字符
字符类[]里的特殊字符和外面的不一样。比如.在字符类里就是普通的点号,不需要转义;^在字符类开头是取反的意思,在中间就是普通的脱字符;-在字符类中间是范围的意思,在开头或结尾就是普通的连字符。
很多人不了解这个,在字符类里乱转义,或者写错位置,导致匹配错误。比如想匹配字母、数字、点号、连字符,写/[a-z0-9.-]/,这里的-在结尾,是普通连字符,没问题。但是如果写/[a-z0-9-.]/,-在中间,就会被解释为范围,可能出错。
3. 量词的范围写错
量词{n,m}表示匹配n到m次,很多人写错,比如{1, 2}中间加了空格,就不是量词了,会被解释为字面量。正确的写法是{1,2},中间不能有空格。
还有人写{,3}表示最多3次,这在有些语言里不支持,应该写{0,3}。{3,}表示至少3次,这个是支持的。
4. 修饰符用错
正则的修饰符(比如i忽略大小写、m多行模式、s点号匹配换行、g全局匹配)很容易用错。
比如m多行模式,会让^和$匹配每一行的开头和结尾,而不是整个字符串的开头和结尾。很多人不知道这个,加了m之后发现匹配结果不对。
再比如s修饰符(dotall),会让.匹配换行符,默认.是不匹配换行的。如果要匹配跨行的内容,需要加s修饰符,或者用[\s\S]代替.。
5. 分组和捕获混淆
()是分组,也是捕获,分组里匹配的内容会被捕获,可以用$1、\1引用。但是有时候我们只是想用分组,不需要捕获,这时候应该用非捕获组(?:...),这样不会捕获,性能也更好。
很多人不知道非捕获组,所有分组都用(),结果捕获了很多不需要的内容,导致引用的时候序号混乱,性能也差。
九、什么时候不该用正则
最后,说说什么时候不该用正则。
前面说了,正则不是万能的,能不用正则就不用正则。具体来说,以下情况不建议用正则:
- 解析结构化数据:HTML、XML、JSON、CSV等结构化数据,用专门的解析器,不要用正则。
- 简单的字符串操作:判断前缀、后缀、包含、替换、分割等,用普通字符串函数,不要用正则。
- 复杂的语法解析:比如解析编程语言、解析表达式、解析配置文件等,用专门的解析器(比如递归下降解析器、PEG解析器),不要用正则。
- 性能要求极高的场景:正则的性能比普通字符串操作差很多,如果性能要求极高,尽量避免用正则。
- 正则变得过于复杂:如果你的正则超过50个字符,或者有多层嵌套,或者你自己都看不懂了,那就该考虑是不是不该用正则了。
当然,这不是说正则没用,正则在合适的场景下还是非常强大的。比如验证输入格式、提取特定模式的内容、批量替换、文本清洗等,正则还是很好用的。
关键是要知道正则的边界,知道什么时候该用,什么时候不该用,用的时候怎么写才好。
十、写在最后
用了三年正则表达式,我才明白这些道理。
正则是一把双刃剑,用好了能事半功倍,用不好会带来无穷的麻烦。它不是万能的,也不是第一选择,但是在合适的场景下,它是非常强大的工具。
这三年里,我从觉得正则很神奇,到觉得正则很坑,到现在能比较熟练地使用正则,踩了很多坑,也积累了很多经验。这些经验说起来都很简单,但是只有真正踩过坑、调过bug、优化过性能,才能真正理解。
希望我的这些经验能帮大家少踩一些坑,更高效地使用正则。如果大家有什么正则的经验或者坑,也欢迎在评论区分享,大家一起学习进步。
最后,用一句话结尾:
"正则如刀,用之得当则事半功倍,用之不当则伤人伤己。知其边界,明其优劣,方能运用自如。"
愿大家都能驾驭正则,而不是被正则驾驭。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录