介绍
这周开始学习老姚大佬的《JavaScript 正则表达式迷你书》 , 然后习惯性的看完一遍后,整理一下知识点,便于以后自己重新复习。
我个人觉得:自己整理下来的资料,对于知识重现,效果不错。
感谢原书作者老姚,本文无意抄袭,只是作为自己知识点的整理,后续也会整理到自己的 JavaScript知识库——《Cute-JavaScript》 网站中。
另外,请读者们注意,这篇文章是知识点的整理,方便复习,所以不会介绍太详细,因为毕竟原书写得非常棒,刚入门的朋友,我还是建议看下原书。
然后可以看看这篇文章,来回顾重要知识点。
目录
- 一、正则表达式字符匹配
- 二、正则表达式位置匹配
- 三、正则表达式括号的使用
- 四、正则表达式回溯法原理
- 五、正则表达式的拆分
- 六、正则表达式的构建
- 七、正则表达式编程
文章推荐(补充中)
工具推荐
一、正则表达式字符匹配
原书这么一句话,特别棒:正则表达式是匹配模式,要么匹配字符,要么匹配位置,要记住。
1. 两种模糊匹配
正则表达式的强大在于它的模糊匹配,这里介绍两个方向上的“模糊”:横向模糊和纵向模糊。
- 横向模糊匹配
即一个正则可匹配的字符串长度不固定,可以是多种情况。
如 /ab{2,5}c/
表示匹配: 第一个字符是 "a"
,然后是 2 - 5 个字符 "b"
,最后是字符 "c"
:
1 | let r = /ab{2,5}c/g; |
- 纵向模糊匹配
即一个正则可匹配某个不确定的字符,可以有多种可能。
如 /[abc]/
表示匹配 "a", "b", "c"
中任意一个。
1 | let r = /a[123]b/g; |
2. 字符组
- 范围表示法
可以指定字符范围,比如 [1234abcdUVWXYZ]
就可以表示成 [1-4a-dU-Z]
,使用 -
来进行缩写。
如果要匹配 "a", "-", "z"
中任意一个字符,可以这么写: [-az]
或 [a\-z]
或 [az-]
。
- 排除字符组
即需要排除某些字符时使用,通过在字符组第一个使用 ^
来表示取反,如 [^abc]
就表示匹配除了 "a", "b", "c"
的任意一个字符。
- 常见简写形式
字符组 | 具体含义 |
---|---|
\d |
表示 [0-9] ,表示一位数字。 |
\D |
表示 [^0-9] ,表示除数字外的任意字符。 |
\w |
表示 [0-9a-zA-Z_] ,表示数字、大小写字母和下划线。 |
\W |
表示 [^0-9a-zA-Z_] ,表示非单词字符。 |
\s |
表示 [\t\v\n\r\f] ,表示空白符,包含空格、水平制表符、垂直制表符、换行符、回车符、换页符。 |
\S |
表示 [^\t\v\n\r\f] ,表示非空白字符。 |
. |
表示 [^\n\r\u2028\u2029] 。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。 |
然后表示任意字符,就可以使用 [\d\D]
、[\w\W]
、[\s\S]
和 [^]
任意一个。
3. 量词
量词也称重复,常用简写如下:
量词 | 具体含义 |
---|---|
{m,} |
表示至少出现 m 次。 |
{m} |
等价于 {m, m} ,表示出现 m 次。 |
? |
等价于 {0, 1} ,表示出现或不出现。 |
+ |
等价于 {1, } ,表示至少出现1次。 |
* |
等价于 {0, } ,表示出现任意次,也有可能不出现。 |
- 贪婪匹配和惰性匹配
在正则 /\d{2,4}/
,表示数字连续出现 2 - 4 次,可以匹配到 2 位、 3 位、4 位连续数字。
但是在 贪婪匹配 如 /\d{2,4}/g
,会尽可能多匹配,如超过 4 个,就只匹配 4 个,如有 3 个,就匹配 3 位。
而在 惰性匹配 如 /\d{2,4}?/g
,会 尽可能少 匹配,如超过 2 个,就只匹配 2 个,不会继续匹配下去。
1 | let r1 = /\d{2,4}/g; |
惰性量词 | 贪婪量词 |
---|---|
{m,m}? |
{m,m} |
{m,}? |
{m,} |
?? |
? |
+? |
+ |
*? |
* |
4. 多选分支
即提供多个子匹配模式任选一个,使用 |
(管道符)分隔,由于分支结构也是惰性,即匹配上一个后,就不会继续匹配后续的。
格式如:(r1|r2|r3)
,我们就可以使用 /leo|pingan/
来匹配 "leo"
和 "pingan"
。
1 | let r = /leo|pingan/g; |
5. 案例分析
匹配字符,无非就是字符组、量词和分支结构的组合使用。
- 十六进制颜色值匹配
1 | let r = /#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}/g; |
- 时间和日期匹配
1 | // 时间 12:23 或 01:09 |
- Windows操作系统文件路径匹配
盘符使用 [a-zA-Z]:\\
,这里需要注意 \
字符需要转义,并且盘符不区分大小写;
文件名或文件夹名,不能包含特殊字符,使用 [^\\:*<>|"?\r\n/]
表示合法字符;
并且至少有一个字符,还有可以出现任意次,就可以使用 ([^\\:*<>|"?\r\n/]+\\)*
匹配任意个 文件夹\
;
还有路径最后一部分可以是 文件夹
,即没有 \
于是表示成 ([^\\:*<>|"?\r\n/]+)?
。
1 | let r = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/; |
- id匹配
如提取 <div id="leo" class="good"></id>
中的 id="leo"
:
1 | let r1 = /id=".*"/; // tips1 |
tips1:由于 .
匹配双引号,且 *
贪婪,就会持续匹配到最后一个双引号结束。
tips2:使用惰性匹配,但效率低,有回溯问题。
tips3:最终优化。
二、正则表达式位置匹配
位置匹配,就是要匹配每个字符两边的位置。
在 ES5
中有6个位置: ^
,$
,\b
,\B
,(?=p)
和 (?!p)
。
另外把位置理解成空字符是非常有用的:
1 | /^^hello$$/.test('hello'); // true |
1. ^ 和 $
^
匹配开头,多行中匹配行开头。$
匹配结尾,多行中匹配行结尾。
1 | "hello".replace(/^|$/g, "#"); // "#hello#" |
多行匹配模式使用 m
修饰符。
2. \b
和 \B
\b
匹配单词边界,即 \w
和 \W
之间的位置,包括 \w
和 ^
之间的位置,和 \w
和 $
之间的位置。\B
和 \b
相反,即非单词边界,匹配中除去 \b
,剩下的都是 \B
的。
也就是 \w
与 \w
、 \W
与 \W
、^
与 \W
,\W
与 $
之间的位置。。
1 | "[HI] Leo_1.mp4".replace(/\b/g,"#"); |
3. (?=p)
和 (?!p)
p
为一个子模式,即 (?=p)
匹配前面是 p
的位置,而 (?!p)
则匹配前面不是 p
的位置。
1 | "hello".replace(/(?=l)/g, "#"); |
4. 相关案例
- 匹配数字千位分隔符
1 | // 匹配最后一个逗号 |
- 数据格式化
1 | let num = 1888; |
- 验证密码
1 | // 密码长度 6-12 位数字或字母 |
三、正则表达式括号的使用
简单理解:括号提供了分组,便于我们使用它。
通常有两种引用情况:在JS代码中引入,和在正则表达式中引入。
分组和分支结构,主要是强调括号内是一个整体,即提供子表达式。
- 分组如
/(ab)+/g
匹配连续出现的ab
。 - 分支结构如
/(a|b)+/g
匹配出现的a
或b
表达式。
1.分组引用
如在日期匹配的时候,就可以这么改造:
1 | // 原来 |
- 提取数据
1 | "2019-03-14".match(r); |
- 替换
将 yyyy-mm-dd
转成 mm/dd/yyyy
。
1 | "2019-03-14".replace(r, "$2/$3/$1"); |
2. 反向引用
使用 \n
表示第 n
个分组,比如 \1
表示第 1
个分组:
1 | let r = /\d{4}(-|\/|\.)\d{2}\1\d{2}/; |
- 多个括号嵌套
按照开括号的顺序:
1 | let r = /^((\d)(\d(\d)))\1\2\3\4$/; |
- 特殊情况
\10
表示的是第 10 个分组,若要匹配 \
和 0
时,使用 (?:\1)0
或 \1(?:0)
。
1 | let r = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/; |
- 当引用不存在的分组
如匹配 \2
是前面不存在,则匹配 \2
本身,即对 2
的转义,不同浏览器可能不同:
1 | let r = /\1\2\3\4/; |
- 分组后面有量词
当分组后面有量词的话,则捕获的是最后一次的匹配:
1 | "12345".match(/(\d)+/); // ["12345", "5", index: 0, input: "12345"] |
3. 相关案例
这里只写出核心代码。
- 模拟字符串
trim
方法
1 | // 1 匹配首尾空白符,替换成空字符 |
- 每个单词首字母大写
1 | "hi leo hi boy!".toLowerCase().replace( |
- 驼峰化 和 中划线化
1 | "-leo-and-pingan".replace(/[-_\s]+(.)?/g, |
- 匹配成对HTML标签
匹配成对标签 <h1>leo<\h1>
,而不匹配不成对标签 <h1>leo<\h2>
。
1 | let r = /<([^>]+)>[\d\D]*<\/\1>/; |
四、正则表达式回溯法原理
概念理解起来比较容易。
比如用 /ab{1,3}c/
去匹配下面两个字符串。
当匹配
abbbc
,按顺序匹配,到了第 3 个b
后,直接匹配c
,这样就没有回溯。当匹配
abbc
,按顺序匹配,到了第 2 个b
后,由于规则是b{1,3}
,则会继续往下匹配,然后发现下一位是c
,于是回退到前一个位置,重新匹配,这就是回溯。
另外像 /".*"/
来匹配 "abc"de
的话,就会有三个回溯情况,为了减少不必要的回溯,我们可以把正则修改为 /"[^"]*"/
。
介绍
回溯法,也称试探法,本质上是深度优先探索算法,基本思路是:匹配过程中后退到之前某一步重新探索的过程。
1. 常见的回溯形式
- 贪婪量词
多个贪婪量词挨着存在,并相互冲突时,会看匹配顺序,深度优先搜索:
1 | "12345".match(/(\d{1,3})(\d{1,3})/); |
- 惰性量词
有时候会因为回溯,导致实际惰性量词匹配到的不是最少的数量:
1 | "12345".match(/(\d{1,3}?)(\d{1,3})/); |
- 分支结构
分支机构,如果一个分支整体不匹配,会继续尝试剩下分支,也可以看成一种回溯。
1 | "candy".match(/can|candy/); // ["can", index: 0, input: "candy"] |
2. 本章小结
简单总结:一个个尝试,直到,要么后退某一步整体匹配成功,要么最后试完发现整体不匹配。
贪婪量词:买衣服砍价,价格高了,便宜点,再便宜点。
懒惰量词:卖衣服加价,价格低了,多给点,再多给点。
分支结构:货比三家,一家不行换一家,不行再换。
五、正则表达式的拆分
拆分正则代码块,是理解正则的关键。
在 JavaScrip 正则表达式有以下结构:
- 字面量: 匹配一个具体字符,如
a
匹配字符a
。 - 字符组: 匹配一个有多种可能性的字符,如
[0-9]
匹配任意一个数字。 - 量词: 匹配一个连续出现的字符,如
a{1,3}
匹配连续最多出现 3 次的a
字符。 - 锚: 匹配一个位置,如
^
匹配字符串的开头。 - 分组: 匹配一个整体,如
(ab)
匹配ab
两个字符连续出现。 - 分支: 匹配一个或多个表达式,如
ab|bc
匹配ab
或bc
字符。
另外还有以下操作符:
优先级 | 操作符描述 | 操作符 |
---|---|---|
1 | 转义符 | \ |
2 | 括号和方括号 | (...) /(?:...) /(?=...) /(?!...) /[...] |
3 | 量词限定符 | {m} /{m,n} /{m,} /? /* /+ |
4 | 位置和序列 | ^ /$ /\元字符 /一般字符 |
5 | 管道符 | ` |
Tips:优先级从上到下,由高到低。
1. 注意要点
- 匹配字符串整体
不能写成 /^abc|bcd$/
,而是要写成 /^(abc|bcd)$/
。
- 量词连缀问题
需要匹配:每个字符是 a
/b
/c
中其中一个,并且字符串长度是 3 的倍数:
不能写成 /^[abc]{3}+$/
,而是要写成 /([abc]{3})+/
。
- 元字符转义问题
元字符就是正则中的特殊字符,当匹配元字符就需要转义,如:
^
、$
、.
、*
、+
、?
、|
、\
、/
、(
、)
、[
、]
、{
、}
、=
、!
、:
、-
。
1 | // "[abc]" => /\[abc\]/ 或者 /\[abc]/ |
2. 案例分析
- 身份证号码
1 | /^(\d{15}|\d{17})[\dxX]$/.test("390999199999999999");// true |
- IPV4地址
需要好好分析:
1 | let r = /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/ |
六、正则表达式的构建
正则的构建需要考虑以下几点的平衡:
- 匹配预期的字符串
- 不匹配非预期的字符串
- 可读性和可维护性
- 效率
我们还需要考虑这么几个问题:
- 是否需要使用正则
如能使用其他 API 简单快速解决问题就不需要使用正则:
1 | "2019-03-16".match(/^(\d{4})-(\d{2})-(\d{2})/); // 间接获取 ["2019", "03", "16"] |
- 是否需要使用复杂正则
/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/
将这个正则拆分成多个小块,如下:
1 | var regex1 = /^[0-9A-Za-z]{6,12}$/; |
1. 准确性
即需要匹配到预期目标,且不匹配非预期的目标。
- 匹配固定电话
如需要匹配下面固定电话号码,可以分别写出对应正则:
1 | 055188888888 => /^0\d{2,3}[1-9]\d{6,7}$/ |
然后合并:
1 | let r = /^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}$/ |
然后提取公共部分:
1 | let r = /^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/ |
再优化:
1 | let r = /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/ |
- 匹配浮点数
先确定,符号部分([+-]
)、整数部分(\d+
)和小数部分(\.\d+
)。
1 | 1.23、+1.23、-1.23 => /^[+-]?\d+\.\d+$/ |
整理后:
1 | let r = /^[+-]?(\d+\.\d+|\d+|\.\d+)$/; |
2. 效率
正则表达式运行过程:
- 编译
- 设定起始位置
- 尝试匹配
- 若匹配失败则返回前一步重新匹配
- 返回匹配成功失败的结果
我们常常优化对 3 和 4
步进行优化:
- 使用具体字符组替代通配符,消除回溯
如 /"[^"]*"/
代替 /".*?"/
。
- 使用非捕获型分组
当不需要使用分组引用和反向引用时,此时可以使用非捕获分组。
如 /^[-]?(?:\d\.\d+|\d+|\.\d+)$/
代替 /^[-]?(\d\.\d+|\d+|\.\d+)$/
。
- 独立出确定字符
加快判断是否匹配失败,进而加快移位的速度。
如 /aa*/
代替 /a+/
。
- 提取分支公共部分
减少匹配过程中可消除的重复。
如 /^(?:abc|def)/
代替 /^abc|^def/
。
- 减少分支的数量,缩小它们的范围
如 /rea?d/
代替 /red|read/
。
七、正则表达式编程
这里要掌握正则表达式怎么用,通常会有这么四个操作:
- 验证
- 切分
- 提取
- 替换
1. 四种操作
- 验证
匹配本质上是查找,我们可以借助相关API操作:
1 | // 检查字符串是否包含数字 |
- 切分
1 | "leo,pingan".split(/,/); // ["leo", "pingan"] |
- 提取
1 | // 提取日期年月日 |
- 替换
1 | // yyyy-mm-dd 替换成 yyyy/mm/dd |
2. 相关API注意
search
和match
参数问题
这两个方法会把字符串转换成正则,所以要加转义
1 | let s = "2019.03.16"; |
match
返回结果的格式问题
match
参数有 g
会返回所有匹配的内容,没有 g
则返回标准匹配格式:
1 | let s = "2019.03.16"; |
test
整体匹配时需要使用^
和$
1 | /123/.test("a123b"); // true |
split
的注意点
split
第二个参数是 结果数组的最大长度:
1 | "leo,pingan,pingan8787".split(/,/, 2); // ["leo", "pingan"] |
使用正则分组,会包含分隔符:
1 | "leo,pingan,pingan8787".split(/(,)/); // ["leo", ",", "pingan", ",", "pingan8787"] |
- 修饰符
修饰符 | 描述 |
---|---|
g |
全局匹配,即找到所有匹配的,单词是 global 。 |
i |
忽略字母大小写,单词是 ingoreCase 。 |
m |
多行匹配,只影响 ^ 和 $ ,二者变成行的概念,即行开头和行结尾。单词是 multiline 。 |
文章到这结束,感谢阅读,也感谢老姚大佬的这本书
Author | 王平安 |
---|---|
pingan8787@qq.com | |
博 客 | www.pingan8787.com |
微 信 | pingan8787 |
每日文章推荐 | https://github.com/pingan8787/Leo_Reading/issues |
ES小册 | js.pingan8787.com |
微信公众号