文章目录
- 正则基础
- 正则应用
- 正则性能优化
- 特殊含义
- ?.
- ?=
正则基础
一、正则表达式基础知识
-
什么是正则表达式?
- 正则表达式是一种用于匹配字符串中字符组合的模式。在JavaScript中,正则表达式是对象。它就像一个模板,可以帮助你在文本中查找、替换或验证符合特定规则的字符串内容。例如,你可以用正则表达式来检查一个字符串是否是有效的电子邮件地址、电话号码或者网址等。
-
正则表达式的语法组成
- 字符类(Character Classes)
- 方括号
[]
用于定义字符类。例如,[abc]
匹配字符a
、b
或c
中的任意一个。还可以使用范围,如[a - z]
匹配所有小写字母,[A - Z]
匹配所有大写字母,[0 - 9]
匹配所有数字。 - 有一些预定义的字符类:
\d
等价于[0 - 9]
,表示数字。\D
等价于[^0 - 9]
,表示非数字。\w
等价于[a - zA - Z0 - 9_]
,表示单词字符(字母、数字、下划线)。\W
等价于[^a - zA - Z0 - 9_]
,表示非单词字符。\s
表示空白字符(空格、制表符、换行符等),\S
表示非空白字符。
- 方括号
- 量词(Quantifiers)
*
:匹配前面的元素零次或多次。例如,a*
可以匹配空字符串、a
、aa
、aaa
等。+
:匹配前面的元素一次或多次。例如,a+
可以匹配a
、aa
、aaa
等,但不能匹配空字符串。?
:匹配前面的元素零次或一次。例如,a?
可以匹配空字符串或a
。{n}
:匹配前面的元素恰好n
次。例如,a{3}
匹配aaa
。{n,}
:匹配前面的元素至少n
次。例如,a{2,}
匹配aa
、aaa
、aaaa
等。{n,m}
:匹配前面的元素至少n
次且最多m
次。例如,a{1,3}
匹配a
、aa
、aaa
。
- 锚点(Anchors)
^
:匹配字符串的开头。例如,^abc
只匹配以abc
开头的字符串。$
:匹配字符串的结尾。例如,xyz$
只匹配以xyz
结尾的字符串。
- 分组(Grouping)
- 圆括号
()
用于分组。例如,(ab)+
匹配ab
、abab
、ababab
等。分组还可以用于捕获匹配的内容,方便后续提取和处理。
- 圆括号
- 或操作(Alternation)
- 竖线
|
用于表示或操作。例如,a|b
匹配a
或者b
。可以结合分组来构建更复杂的或关系,如(abc|def)
匹配abc
或者def
。
- 竖线
- 字符类(Character Classes)
-
创建正则表达式对象
- 在JavaScript中,可以使用两种方式创建正则表达式对象。
- 字面量方式:
- 例如,
/abc/
是一个匹配字符串abc
的正则表达式。可以将它赋值给一个变量,如let reg = /abc/;
- 例如,
- 构造函数方式:
- 例如,
let reg = new RegExp("abc");
。这种方式在需要动态构建正则表达式时很有用,比如根据用户输入来生成匹配模式。
- 例如,
二、理解正则表达式
- 匹配过程
- 当使用正则表达式进行匹配时,引擎会从字符串的开头(如果没有使用
m
(多行)模式)开始,按照正则表达式的模式逐个字符进行比较。例如,对于正则表达式/abc/
和字符串abcdef
,引擎首先比较第一个字符a
,匹配成功后继续比较第二个字符b
,再比较第三个字符c
,全部匹配成功后就找到了一个匹配项。 - 如果在匹配过程中某个字符不符合正则表达式的模式,引擎会尝试从下一个字符位置重新开始匹配(这取决于正则表达式的具体设置,如是否使用了
g
(全局)模式)。
- 当使用正则表达式进行匹配时,引擎会从字符串的开头(如果没有使用
- 贪婪模式与非贪婪模式
- 量词默认是贪婪模式。这意味着它们会尽可能多地匹配字符。例如,对于正则表达式
/a.*b/
和字符串aaabbb
,它会匹配整个aaabbb
,因为.*
会一直匹配直到最后一个b
。 - 非贪婪模式可以通过在量词后面添加
?
来实现。例如,/a.*?b/
对于字符串aaabbb
会匹配aab
,因为.*?
会尽可能少地匹配字符,只要能满足后面的b
被匹配即可。
- 量词默认是贪婪模式。这意味着它们会尽可能多地匹配字符。例如,对于正则表达式
三、正则表达式的应用
- 字符串验证
- 验证电子邮件地址
- 一个简单的电子邮件验证正则表达式可以是
/^[a-zA-Z0 - 9_.+-]+@[a-zA-Z0 - 9 -]+\.[a-zA-Z0 - 9-.]+$/
。 - 解释:
^
表示从字符串开头匹配。[a - zA - Z0 - 9_.+-]+
匹配邮件地址的用户名部分,可以包含字母、数字、点、下划线、加号和减号,并且至少出现一次。@
匹配@
符号。[a - zA - Z0 - 9 -]+
匹配域名部分,可以包含字母、数字和减号,并且至少出现一次。\.[a - zA - Z0 - 9-.]+
匹配域名后缀,点号需要转义(\
),后缀部分可以包含字母、数字、点和减号,并且至少出现一次。$
表示匹配到字符串结尾。
- 一个简单的电子邮件验证正则表达式可以是
- 验证手机号码(以中国手机号码为例)
- 简单的正则表达式可以是
/^1[3 - 9]\d{9}$/
。 - 解释:
^
开头。1
表示手机号码以1开头。[3 - 9]
表示第二位数字是3到9中的一个。\d{9}
表示后面跟着9位数字。$
结尾。
- 简单的正则表达式可以是
- 验证电子邮件地址
- 字符串查找和替换
- 查找
- 在JavaScript中,可以使用
string.match(regex)
方法来查找匹配正则表达式的内容。例如,let str="abcabc"; let regex = /abc/; let matches = str.match(regex);
。matches
会是一个包含匹配结果的数组,在这个例子中,它会包含abc
。如果使用g
(全局)模式,如/abc/g
,matches
会包含所有的abc
匹配项(在这个例子中是["abc","abc"]
)。
- 在JavaScript中,可以使用
- 替换
- 可以使用
string.replace(regex, replacement)
方法来替换匹配正则表达式的内容。例如,let str="abcabc"; let regex = /abc/; let newStr = str.replace(regex,"def");
。newStr
会是defabc
,因为只替换了第一个匹配项。如果使用g
(全局)模式,如/abc/g
,则会替换所有匹配项,得到defdef
。
- 可以使用
- 查找
- 提取信息
- 假设你有一个包含日期格式为
yyyy - mm - dd
的字符串,如"活动日期是2024-11-21"
,你可以使用正则表达式/(\d{4})-(\d{2})-(\d{2})/
来提取年、月、日信息。- 例如,
let str="活动日期是2024-11-21"; let regex = /(\d{4})-(\d{2})-(\d{2})/; let matches = str.match(regex);
。matches
数组中,matches[0]
是整个匹配的日期字符串2024 - 11 - 21
,matches[1]
是提取的年份2024
,matches[2]
是提取的月份11
,matches[3]
是提取的日期21
。
- 例如,
- 假设你有一个包含日期格式为
正则应用
- 表单验证
- 用户名验证
- 许多网站要求用户名只能包含字母、数字和下划线,并且长度在一定范围内。例如,要求用户名长度在3到16个字符之间。可以使用正则表达式
/^[a-zA-Z0 - 9_]{3,16}$/
进行验证。 - 解释:
^
表示从字符串开头匹配。[a - zA - Z0 - 9_]
定义了字符类,包含所有字母、数字和下划线。{3,16}
表示前面的字符类中的字符出现的次数至少为3次,最多为16次。$
表示匹配到字符串结尾,确保整个字符串都符合要求。
- 许多网站要求用户名只能包含字母、数字和下划线,并且长度在一定范围内。例如,要求用户名长度在3到16个字符之间。可以使用正则表达式
- 密码强度验证
- 一个简单的密码强度验证可以要求密码至少包含一个大写字母、一个小写字母、一个数字和一个特殊字符,并且长度不少于8个字符。
- 正则表达式可以是
/^(?=.*[a - z])(?=.*[A - Z])(?=.*\d)(?=.*[@#$%^&+=!]).{8,}$/
- 解释:
^
开头,$
结尾确保整个字符串都符合要求。(?=.*[a - z])
是一个正向肯定预查,确保字符串中包含至少一个小写字母。(?=.*[A - Z])
确保包含至少一个大写字母。(?=.*\d)
确保包含至少一个数字。(?=.*[@#$%^&+=!])
确保包含至少一个指定的特殊字符。.{8,}
表示除了满足前面的条件外,字符串长度至少为8个字符。
- 用户名验证
- 文本内容提取
- 提取网页中的链接
- 在处理网页内容(例如从HTML字符串中)提取链接时,可以使用正则表达式。假设网页内容是一个简单的HTML字符串,链接格式为
<a href="https://example.com">链接文本</a>
。 - 可以使用正则表达式
/<a\s+href="([^"]*)">([^<]*)<\/a>/g
来提取链接地址和链接文本。 - 解释:
<a\s+href="
匹配<a
标签和href
属性的开头部分,\s+
匹配一个或多个空白字符,这是因为href
属性前面可能有空格等。([^"]*)
是一个捕获组,用于捕获双引号内的链接地址,[^"]*
表示匹配除双引号以外的任意字符零次或多次。">
匹配>
符号,用于结束href
属性部分。([^<]*)
是另一个捕获组,用于捕获<a>
和</a>
之间的链接文本,[^<]*
表示匹配除<
以外的任意字符零次或多次。<\/a>
匹配</a>
标签结束部分。g
(全局)模式用于匹配所有符合条件的链接。
- 在处理网页内容(例如从HTML字符串中)提取链接时,可以使用正则表达式。假设网页内容是一个简单的HTML字符串,链接格式为
- 提取文章中的标签
- 假设文章内容中有一些以
#
开头的标签,如这是一篇关于#JavaScript和#正则表达式的文章
。 - 可以使用正则表达式
/#[a-zA - Z0 - 9]+/g
来提取这些标签。 - 解释:
#
匹配#
符号。[a - zA - Z0 - 9]+
匹配一个或多个字母或数字,用于提取标签内容。g
(全局)模式用于提取所有标签。
- 假设文章内容中有一些以
- 提取网页中的链接
- 数据清洗和格式化
- 电话号码格式化
- 例如,用户输入的电话号码可能有多种格式,如
13812345678
、+8613812345678
、(010) - 12345678
等。可以将它们统一格式化为+86 - 138 - 1234 - 5678
的格式。 - 首先使用正则表达式识别电话号码的各种格式,对于手机号码可以使用
/^(\+?86)?(1[3 - 9]\d{9})$/
来匹配。 - 然后通过提取和重新拼接来格式化。代码示例如下:
function formatPhoneNumber(phone) { let match = phone.match(/^(\+?86)?(1[3 - 9]\d{9})$/); if (match) { let countryCode = match[1]? match[1] : "+86"; let number = match[2]; return countryCode + " - " + number.slice(0, 3) + " - " + number.slice(3, 7) + " - " + number.slice(7); } return phone; }
- 例如,用户输入的电话号码可能有多种格式,如
- 日期格式化
- 假设日期数据有多种格式,如
2024/11/21
、2024 - 11 - 21
、11/21/2024
等,要将它们统一格式化为yyyy - mm - dd
。 - 可以使用正则表达式
/(\d{4})[\/-](\d{2})[\/-](\d{2})/
来匹配日期,然后提取并重新拼接。
function formatDate(dateStr) { let match = dateStr.match(/(\d{4})[\/-](\d{2})[\/-](\d{2})/); if (match) { return match[1] + " - " + match[2] + " - " + match[3]; } return dateStr; }
- 假设日期数据有多种格式,如
- 电话号码格式化
- 代码文本处理
- 提取JavaScript函数名
- 在处理JavaScript代码文本时,假设要提取代码中的所有函数名。函数定义格式可能是
function functionName() {... }
或者const functionName = function() {... }
等。 - 可以使用正则表达式
/function\s+([a-zA - Z_$][a-zA - Z0 - 9_$]*)|\s*const\s*([a-zA - Z_$][a-zA - Z0 - 9_$]*)\s*=\s*function/g
来提取函数名。 - 解释:
function\s+([a - zA - Z_$][a - zA - Z0 - 9_$]*)
匹配function
关键字后面跟着一个函数名,函数名的命名规则是由字母、数字、下划线和美元符号组成,并且第一个字符不能是数字。([a - zA - Z_$][a - zA - Z0 - 9_$]*)
是一个捕获组用于捕获函数名。|\s*const\s*([a - zA - Z_$][a - zA - Z0 - 9_$]*)\s*=\s*function
用于匹配通过常量定义的函数,同样([a - zA - Z_$][a - zA - Z0 - 9_$]*)
捕获函数名。g
(全局)模式用于提取所有函数名。
- 在处理JavaScript代码文本时,假设要提取代码中的所有函数名。函数定义格式可能是
- 检查代码中的语法错误(简单示例)
- 对于一些简单的语法错误检查,比如检查是否缺少分号。可以使用正则表达式
/([^;])\s*$
来检查每行代码的末尾是否缺少分号。 - 解释:
([^;])
捕获一个不是分号的字符。\s*$
匹配零个或多个空白字符直到行尾。如果捕获到了不是分号的字符,并且后面没有分号直到行尾,就可能存在缺少分号的问题。不过这只是一个简单的初步检查,实际的语法检查工具会更复杂。
- 对于一些简单的语法错误检查,比如检查是否缺少分号。可以使用正则表达式
- 提取JavaScript函数名
正则性能优化
- 避免过度使用贪婪匹配
- 贪婪匹配会尽可能多地匹配字符,这可能导致性能问题。例如,对于正则表达式
/a.*b/
和一个很长的字符串,引擎会一直匹配直到最后一个b
出现。 - 优化方法是在合适的时候使用非贪婪匹配。非贪婪匹配通过在量词后添加
?
来实现。例如,将/a.*b/
改为/a.*?b/
。非贪婪匹配会尽可能少地匹配字符,只要能满足后续的匹配条件即可。这样在处理长字符串时可以减少不必要的匹配尝试,提高性能。
- 贪婪匹配会尽可能多地匹配字符,这可能导致性能问题。例如,对于正则表达式
- 减少回溯次数
- 回溯是正则表达式引擎在匹配过程中,当某个匹配分支失败时,回退到之前的状态重新尝试其他可能的匹配路径的过程。过多的回溯会导致性能下降。
- 例如,对于正则表达式
/(a|ab)+c/
和字符串abc
,引擎首先会匹配a
,然后因为+
量词会尝试再次匹配a
或ab
,当发现无法匹配时会回溯并尝试其他组合。 - 优化可以通过优化正则表达式的结构来减少回溯。一种方法是将更具体的模式放在前面。对于上面的例子,可以改为
/(ab|a)+c/
,这样在匹配时可以更快地找到正确的路径,减少回溯次数。
- 使用合适的字符类范围
- 在定义字符类时,尽量精确地指定范围。例如,如果只需要匹配数字,使用
\d
(等价于[0 - 9]
)比使用[0 - 9a - zA - Z]
更高效。 - 另外,避免使用不必要的复杂字符类组合。如果可以使用简单的字符类来完成匹配,就不要使用复杂的组合。例如,要匹配所有英文字母,使用
[a - zA - Z]
比使用([a - b]|[c - d]|[e - f]|...|[y - z]|[A - B]|[C - D]|...|[Y - Z])
要高效得多。
- 在定义字符类时,尽量精确地指定范围。例如,如果只需要匹配数字,使用
- 利用锚点(Anchors)来限制匹配范围
- 锚点
^
(匹配字符串开头)和$
(匹配字符串结尾)可以帮助限制正则表达式的匹配范围。例如,如果要检查一个字符串是否完全由数字组成,使用/^\d+$/
比使用/\d+/
更好。 - 因为
/\d+/
会在字符串中任何包含数字的部分匹配,而/^\d+$/
会确保整个字符串都是数字,减少了不必要的匹配尝试,特别是在处理长字符串时可以提高性能。
- 锚点
- 避免在循环中重复编译正则表达式
- 在JavaScript中,每次创建一个新的正则表达式对象(无论是通过字面量还是构造函数),引擎都会进行编译。如果在循环中频繁使用相同的正则表达式,这种编译会导致性能下降。
- 例如:
for (let i = 0; i < 100; i++) { let reg = /abc/; let str = "abcdef"; let result = str.match(reg); }
- 优化方法是将正则表达式对象在循环外定义,例如:
let reg = /abc/; for (let i = 0; i < 100; i++) { let str = "abcdef"; let result = str.match(reg); }
- 使用
sticky
模式(y
模式)代替g
(全局)模式(在某些情况下)g
(全局)模式会从上次匹配结束的位置继续匹配,这可能会导致一些不必要的回溯。而y
(sticky
)模式会从lastIndex
属性指定的位置开始匹配,并且如果匹配不成功,lastIndex
会重置为0。- 例如,在需要精确控制匹配位置的场景下,
y
模式可能会更高效。但要注意y
模式的兼容性,它在一些旧版本的浏览器中可能不支持。
- 测试和性能分析
- 使用性能测试工具(如
console.time()
和console.timeEnd()
)来比较不同正则表达式的性能。例如:
let str = "a long string for testing"; console.time("regex1"); let reg1 = /a.*b/; str.match(reg1); console.timeEnd("regex1"); console.time("regex2"); let reg2 = /a.*?b/; str.match(reg2); console.timeEnd("regex2");
- 这样可以直观地看到不同正则表达式在特定字符串上的匹配时间,从而选择性能更好的表达式。同时,对于复杂的应用场景,可以使用专业的性能分析工具来深入分析正则表达式的性能瓶颈。
- 使用性能测试工具(如
特殊含义
?.
-
可选的非贪婪匹配(?.)
- 在正则表达式中,
?.
用于表示可选的非贪婪匹配。其中,?
在这里有两种作用组合在一起。 - 首先,
?
作为量词,表示前面的元素(在这里是点.
)是可选的,即可以出现0次或者1次。其次,和单独的?
量词用于可选匹配不同,?.
中的?
还改变了点.
(匹配除换行符外的任意单个字符)的匹配方式为非贪婪匹配。 - 非贪婪匹配意味着在匹配过程中,它会尽可能少地匹配字符,只要能满足后续的正则表达式规则即可。
- 在正则表达式中,
-
示例说明
- 考虑正则表达式
a(.*?)b
和a(.*)b
,以及待匹配的字符串aabb
。 - 对于
a(.*)b
,因为*
是贪婪匹配,它会匹配最长的字符串,所以整个aabb
会被(.*)
匹配,得到的结果是aabb
。 - 而对于
a(.*?)b
,(.*?)
是非贪婪匹配,它会尽可能少地匹配。在aabb
这个字符串中,(.*?)
只会匹配a
和第一个b
之间的a
,得到的结果是a
。 - 如果是
a(?.b)
和字符串a
或者ab
。对于字符串a
,(?.b)
可以匹配成功,因为b
是可选的,这里就没有匹配b
;对于字符串ab
,(?.b)
也能匹配成功,并且匹配了b
。
- 考虑正则表达式
-
实际应用场景
- 在处理HTML标签内容提取等场景中,可能会用到这种可选的非贪婪匹配。例如,想要提取
<p>
标签内可能存在也可能不存在的某个属性值。假设HTML片段是<p class="test">内容</p>
或者<p>内容</p>
,如果要提取class
属性值(如果存在),可以使用类似class="(?.[^"]*)"
的正则表达式部分(完整的正则表达式还需要考虑标签的开头和结尾等部分)。 - 在这个例子中,
(?.[^"]*)
表示class
属性值是可选的(如果没有class
属性也能匹配),并且非贪婪地匹配双引号内的内容(如果有class
属性的话),这样可以更灵活地处理不同情况的HTML标签内容。
- 在处理HTML标签内容提取等场景中,可能会用到这种可选的非贪婪匹配。例如,想要提取
?=
-
零宽度正预测先行断言(?=)的定义
- 在正则表达式中,
?=
是一种零宽度正预测先行断言。它用于指定一个位置,该位置之后的字符必须满足?=
后面的模式,但匹配过程中并不消耗(匹配)这些字符。 - 简单来说,它就像一个“前瞻性检查”,只检查某个位置之后是否符合特定条件,而不把符合条件的字符包含在实际的匹配内容中。
- 在正则表达式中,
-
示例解释
- 例如,有正则表达式
a(?=b)
和字符串abc
。当进行匹配时,引擎会先找到字符a
,然后检查a
之后的字符是否为b
。在这个例子中,a
之后是b
,满足(?=b)
的条件,所以a
被匹配。但是,b
以及后面的c
不会被当作匹配结果的一部分,因为?=
只是进行了位置检查,并没有真正匹配这些字符。 - 再看另一个例子,对于正则表达式
/(\w)(?=\d)/
和字符串abc1
,(\w)
会匹配第一个字母a
,然后(?=\d)
会检查a
之后是否是数字。因为a
之后是b
不是数字,所以这个位置不满足。接着引擎会继续寻找下一个位置,当(\w)
匹配到c
时,(?=\d)
检查c
之后是1
,满足条件,所以最终(\w)
匹配的是c
,但整个匹配结果只是c
,1
不会包含在其中。
- 例如,有正则表达式
-
实际应用场景
- 密码验证
- 在验证密码强度时,如要求密码必须包含数字和字母。可以使用
/^(?=.*\d)(?=.*[a - zA - Z]).+$/
。其中(?=.*\d)
用于检查密码字符串中(从开头位置开始检查)是否存在数字,(?=.*[a - zA - Z])
用于检查是否存在字母。这两个断言只是检查密码是否符合条件,并不影响后续对整个密码字符串(.+
)的匹配,而且这样可以在一个正则表达式中同时检查多个条件。
- 在验证密码强度时,如要求密码必须包含数字和字母。可以使用
- 提取符合特定条件的子串
- 假设要从一个文本中提取所有以
http
开头且后面跟着数字的网址部分。可以使用/(http)(?=\d)/
。这样就可以找到符合条件的http
部分,而不包括后面用于检查条件的数字。例如对于文本http1://example.com和https://example.com
,这个正则表达式只会匹配第一个http
部分,因为只有它后面跟着数字。
- 假设要从一个文本中提取所有以
- 密码验证