内容包括:
平衡法则
构建正则前提
准确性
效率
1、平衡法则
构建正则有一点非常重要,需要做到下面几点的平衡:
匹配预期的字符串
不匹配预期的字符串
可读性和可维护性
效率
2、构建正则前提
2.1 是否能使用正则?
正则太强大了,以至于我们随便遇到一个操作字符串问题时,都会下意识地去想,用正则该怎么做。但我们
始终要提醒自己,正则虽然强大,但不是万能的,很多看似很简单的事情,还是做不到的。
比如匹配这样的字符串:1010010001...
。
虽然很有规律,但是只靠正则就是无能为力。
2.2 是否有必要使用正则?
要认识到正则的局限,不要去研究根本无法完成的任务。同时,也不能走入另一个极端:无所不用正则。能
用字符串 API 解决的简单问题,就不该正则出马。
2.3 是否有必要构建一个复杂的正则?
比如密码匹配问题,要求密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符。
在第2章里,我们写出了正则是:
/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/
其实可以使用多个小正则来做:
var regex1 = /^[0-9A-Za-z]{6,12}$/;
var regex2 = /^[0-9]{6,12}$/;
var regex3 = /^[A-Z]{6,12}$/;
var regex4 = /^[a-z]{6,12}$/;
function checkPassword (string) {
if (!regex1.test(string)) return false;
if (regex2.test(string)) return false;
if (regex3.test(string)) return false;
if (regex4.test(string)) return false;
return true;
}
3、准确性
所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标。
这里提到了“预期”二字,那么我们就需要知道目标的组成规则。
不然没法界定什么样的目标字符串是符合预期的,什么样的又不是符合预期的。
下面将举例说明,当目标字符串构成比较复杂时,该如何构建正则,并考虑到哪些平衡。
3.1 匹配固定电话
比如要匹配如下格式的固定电话号码:
055188888888
0551-88888888
(0511)88888888
第一步,了解各部分的模式规则。
上面的电话,总体上分为区号和号码两部分(不考虑分机号和 "+86" 的情形)。
区号是 "0" 开头的 3 到 4 位数字,对应的正则是:0\d{2,3}
,
号码是非 "0" 开头的 7 到 8 位数字,对应的正则是:[1-9]\d{6,7}
,
因此,匹配 "055188888888" 的正则是:/^0\d{2,3}[1-9]\d{6,7}$/
,
匹配 "0551-88888888" 的正则是:/^0\d{2,3}-[1-9]\d{6,7}$/
,
匹配 "(0551)88888888" 的正则是:/^\(0\d{2,3}\)[1-9]\d{6,7}$/
。
第二步,明确形式关系。
这三者情形是或的关系,可以构建分支:
/^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/
提取公共部分:
/^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/
进一步简写:
/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/
上面的正则构建过程略显罗嗦,但是这样做,能保证正则是准确的。
上述三种情形是或的关系,这一点很重要,不然很容易按字符是否出现的情形把正则写成:
/^\(?0\d{2,3}\)?-?[1-9]\d{6,7}$/
虽然也能匹配上述目标字符串,但也会匹配 "(0551-88888888" 这样的字符串。当然,这不是我们想要的。
其实这个正则也不是完美的,因为现实中,并不是每个 3 位数和 4 位数都是一个真实的区号。
这就是一个平衡取舍问题,一般够用就行。
3.2 匹配浮点数
要求匹配如下的格式:
1.23、+1.23、-1.23
10、+10、-10
.2、+.2、-.2
可以看出正则分为三部分。
符号部分:[+-]
,
整数部分:\d+
,
小数部分:\.\d+
。
上述三个部分,并不是全部都出现。如果此时很容易写出如下的正则:
/^[+-]?(\d+)?(\.\d+)?$/
此正则看似没问题,但这个正则也会匹配空字符""
。
因为目标字符串的形式关系不是要求每部分都是可选的。
要匹配 "1.23"
、"+1.23"
、"-1.23"
,可以用/^[+-]?\d+\.\d+$/
,
要匹配 "10"
、"+10"
、"-10"
,可以用/^[+-]?\d+$/
,
要匹配 ".2"
、"+.2"
、"-.2"
,可以用/^[+-]?\.\d+$/
。
因此整个正则是这三者的或的关系,提取公众部分后是:
/^[+-]?(\d+\.\d+|\d+|\.\d+)$/
如果要求不匹配 "+.2"
和 "-.2"
,此时正则变成:
/^([+-]?(\d+\.\d+|\d+)|\.\d+)/
当然,/^[+-]?(\d+\.\d+|\d+|\.\d+)$/
也不是完美的,我们也是做了些取舍,比如:
它也会匹配 "012"
这样以 "0"
开头的整数。如果要求不匹配的话,需要修改整数部分的正则。一般进行验
证操作之前,都要经过 trim 和判空。那样的话,也许那个错误正则也就够用了。也可以进一步改写成:
/^[+-]?(\d+)?(\.)?\d+$/
,这样我们就需要考虑可读性和可维护性了。
4、效率
保证了准确性后,才需要是否要考虑要优化。大多数情形是不需要优化的,除非运行的非常慢。什么情形正
则表达式运行才慢呢?我们需要考察正则表达式的运行过程(原理)。
正则表达式的运行分为如下的阶段:
1. 编译;
2. 设定起始位置;
3. 尝试匹配
4. 匹配失败的话,从下一位开始继续第 3 步;
5. 最终结果:匹配成功或失败。
4.1 使用具体型字符组来代替通配符,来消除回溯
4.2 使用非捕获型分组
因为括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们。
当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。
例如,/^[-]?(\d\.\d+|\d+|\.\d+)$/
可以修改成:/^[-]?(?:\d\.\d+|\d+|\.\d+)$/
。
4.3 独立出确定字符
例如,/a+/
可以修改成 /aa*/
。
因为后者能比前者多确定了字符 "a"
。这样会在第四步中,加快判断是否匹配失败,进而加快移位的速度。
4.4 提取分支公共部分
比如,/^abc|^def/
修改成 /^(?:abc|def)/
。
又比如, /this|that/
修改成 /th(?:is|at)/
。
这样做,可以减少匹配过程中可消除的重复。
4.5 减少分支的数量,缩小它们的范围
/red|read/
可以修改成 /rea?d/
。
此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!