在需要使用正则时搜一搜,复制粘贴改一改。有些时候看不太懂,只要能够跑的通就满意了,心里没有底,到底正不正确。又需要临时抱佛脚,并没有系统的去学习。学过之后过一段时间,就会忘记,反反复复,因此记录下来。
美国一位知名程序员杰米·加文斯基(Jamie Zawinski)说过一句话:
正则很难掌握和利用的工具。
既然这么难,使用的时候搜索以下,就解决问题了。为什么还要学习呢?
如果不熟悉一个技能的时候,遇见问题也想不到可以使用这个技术,根本就不会考虑这个技术。
什么是正则表达式
维基百科中的解释:
正则表达式(英语:Regular Expression,常简写为 regex、regexp 或 RE),又称正则表示式、正则表示法、规则表达式、常规表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些匹配某个模式的文本。
简单来说正则就是用来匹配和处理文本的字符串。
为什么要使用正则表达式
工作时经常用到正则表达式,例如文件格式匹配。
graph LR
A(正则表达式功能) --> B("搜索(爬虫、查找某种类型文件)")
A --> C("替换(比如把一些网址替换成超链接)")
A --> D("校验(手机号、邮箱、身份证、银行可)")
style A fill:#1890ff,color:white,stroke:#1890ff
style B fill:#fa8c16,color:white,stroke:#fa8c16
style C fill:#fa8c16,color:white,stroke:#fa8c16
style D fill:#fa8c16,color:white,stroke:#fa8c16
如何学习正则表达式?
每次学习都去搜索一下,搞定就结束,日积月累,可能会浪费更多的时间。容易忘记。
每天学习一个小时,坚持一周,学习的时间反而更短,还不容易忘记。
需求拆解
拿到需求后,这个需求可以拆分几个子需求。每个子需求是否独立。
例如网址:
"protocol + domain name + port + path to the file + parameters + anchor"
邮箱:
username + @ + domain name
这也是我们日常软件工程最基本的思路。
分析各个子需求
每个子需求可能多个字符(字符组),多个字符串(分支),出现的次数(量词)。
语言规则
查阅语法手册、按照对应语言规则写下来就可以了。
TDD
使用TDD
验证表达式是否正确。一次不一定写正确,从最简单的入手。
红色表示测试失败。
绿色表示测试通过。
测试通过后可以进行重构。并再次确保测试通过。
graph LR
A((红)) --> B((绿)) --> C((重构)) --> A
style A fill:#f5222d,color:white,stroke:#f5222d
style B fill:#52c41a,color:white,stroke:#52c41a
style C fill:#1890ff,color:white,stroke:#1890ff
如何使用正则表达式?
保持克制。
一次性解决所有的问题,但是大家都看不懂 。 就没有可维护性。基本上只能够重写。
能够使用字符串处理的,使用字符串。
一定要写注释
能使用多个简单的正则表达式解决的,一定不要苛求多个正则表达式。
字符
匹配文本
"I love JavaScript!".match(/love/);
匹配多个结果
多大多数正则表达式引擎默认情况下只返回第一个匹配结果。
在 JavaScript 中使用g(global,全局)标志将返回所有匹配的结果数组。
"I love love love JavaScript!".match(/love/g);
匹配不区分字母大小写
默认的情况下,正则表达式是区分大小的。JavaScript 使用i标志强制执行不区分字母大小写。
"I love Love love JavaScript!".match(/love/gi);
匹配任意字符
.
可以匹配任意单个字符、字母、数字。
如果要匹配真正的点,需要转义\.
"Z".match(/./); // [ 'Z', index: 0, input: 'Z', groups: undefined ]
匹配特殊字符
点(.)在正则中有特殊含义的。如果真的需要匹配点,那么就需要使用反斜杠(\ )进行转义。
"test.js".match(/\.js/);
一组字符
匹配一组字符
[]
不匹配任何字符,只负责定义一个字符集合。字符集合是或(OR)
不是和(AND)
的关系
[0123456789]
任何数字
"Mop top".match(/[tm]op/gi);
字符区间
指定一组必须匹配其中之一的字符。
正则表达式频繁利用一些字符区间(0~9, a-z)等等,为了简化,使用-
连字符来定义字符区间。
-
只有在[]
中才是元字符
。字符集合以外是普通的字符-
- 尾字符一定要大于首字符(例如
[9-1]
无效)
[A-Z]
匹配所有的大写字母
[a-z]
匹配所有的小写字母
[0-9]
匹配任何数字
[A-Za-z0-9]
匹配字母与数字
[0-9A-Fa-f]
十六进制颜色
"test a string range".match(/[a-z]/g);
排除
排除字符集合里指定的哪些字符。
使用^
符号。
[^0-9]
: 匹配不是数字的字符
"No numbers here?".match(/[^0-9]/g);
元字符
有特殊含义,代表的不是字符本身。
匹配空白元字符
一般是根据操作系统不同,会用到的换行回车\r\n
。
元字符 | 说明 |
---|---|
[\b] | 回退(并删除)一个字符(Backspace 键) |
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符 |
\v | 垂直制表符 |
匹配空白字符
元字符 | 说明 |
---|---|
\s=[\f\n\r\t\v] | 任何一个空白字符 |
\S=[^\f\n\r\t\v] | 任何一个非空白字符 |
\s
来自 space。
包含空格、制表符\t
、\v
、\n
、\f
、\r
。不包含回退。
\s
不包含[\b]
,\S
也没有排除[\b]
匹配数字
\d
来自digit。
元字符 | 说明 |
---|---|
\d = [0-9] | 任何一个数字字符 |
\D = [^0-9] | 任何一个非数字字符 |
"123-123123-12312".match(/\d/g); // 数字
"123-123123-12312".match(/\D/g); // 非数字
匹配字母数字下划线
元字符 | 说明 |
---|---|
\w = [a-zA-Z0-9_] | 任何一个字母数字或下划线字符_ |
\W = [^a-zA-Z0-9_] | 任何非一个字母数字或下划线字符_ |
\w
来自word
进制数值
\x
十六进制 \x0A
即字符 10
\0
八进制\011
即字符 9
重复匹配
匹配一个或多个字符
+
= {1,}
"123-123123-12312".match(/\d+/g); // [ '123', '123123', '12312' ]
"123-123123-12312".match(/\D+/g); // [ '-', '-' ]
匹配零个或多个字符
*
= {0,1}
"100 10 1".match(/\d0*/g); // 100 10 1
"100 10 1".match(/\d0+/g); // 100 10
匹配零个或一个字符
?
= {0, 1}
符号可选,有点像可选操作符?
"Should I write color or colour?".match(/colou?r/g); // [ 'color', 'colour' ]
匹配具体得重复次数
\d{5}
表示 5 位数字。只能匹配到 5 位数字,如果第六位还是数字是匹配不到的。
"test 12345 test".match(/\d{5}/g); // 12345
匹配区间范围
{m, n}
\d{2,4}
匹配 2 到 4 次
"Today is 5 22, 2022".match(/\d{2,4}/g); // 22 2022
匹配至少重复次数
{n,}
至少匹配 4 次。
"Today is 5 22, 2022".match(/\d{4,}/g); // 2022
位置匹配
单词边界
使用\b
表示,也就是单词与空格间的位置
正则表达式的匹配
有两种概念,一种匹配字符,一种匹配位置。 \b
就是匹配位置。
"lucas lucas1 lucaslz".match(/\blucas\b/); // lucas
\B
不是单词边界(有空格就是单词边界就匹配不上了)。
"lucas lucas1 lucaslz".match(/\Blucas\B/); // null
"lucaslucas1lucaslz".match(/\Blucas\B/g); // lucas lucas 匹配第二个与第三个lucas
字符串边界
^
表示字符串开始
$
表示字符串结束
"1本书,2本书".match(/^\d+/g); // 1
"1book,2book".match(/\w+$/g); // 2book
"1book,2book".match(/\w+$/g); // 2book
"1book,2book".match(/^\d\w+$/g); // null
多行模式
JavaScript 使用/m
标志符表示多行模式
会影响^和$符的行为。
`
第1本书正则
第2本书JavaScript
第3本书HTML
`.match(/\d+/gm); // 1,2,3
`
1book
2book
`.match(/^\d\w+$/gm); // '1book', '2book'
子表达式
用来定义字符或者表达式的集合。
()
定义一个子表达式。
子表达式进行分组
没有括号,o
重复一次或多次
"Googoogoo".match(/go+/gi); // 'Goo', 'goo', 'goo'
有括号,将go
作为整体重复一次或者多次
"Googoogoo".match(/(go)+/gi); // 'Go', 'go', 'go'
子表达式嵌套
一层嵌套一层,可读性很差。
/^(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))$/;
或操作符
使用|
表示,相当于条件选择。
"19902021".match(/(19|20)/g); // 19 20
反向引用
表达式在正则表达式内部被引用就称为反向引用。
可以把反向引用理解为变量
。
反向引用匹配
"This is a test test text.".match(/\s+(\w+)\s+\1/g); // ' test test'
替换操作
JavaScript 在替换中使用$
const p = 'hello test@lucaslz.com';
const regex = /\w+[\w\.]*@[\w\,]+\.\w+)/;
p.replace(regex, '<a href="mailto:$1">$1</a>')
// result
hello <a href="mailto:test@lucaslz.com">test@lucaslz.com</a>
环视
有些时候需要前后字符确定文本匹配的位置,但是又不希望出现在最终结果里面。
也叫做零宽断言。
向前环视
(?=)
为了只匹配后面有日
的数字。需要使用向前环视。
"2022年5月29日".match(/\d+(?=日)/g); // 29
否定向前环视
(?!)
为了匹配除日
值意外的数字。
"2022年5月29日".match(/\d+(?!日)/g); // '2022', '5', '2' // 2后面也没有跟着日,所以会匹配上
"2022年5月29日".match(/\d+(?![\d+日])/g); //'2022', '5'
向后环视
(?<=)
匹配小票上的金额,不要币种。
"2022年5月29日订单金额¥12".match(/(?<=¥)\d+/g); // 12
否定向后环视
(?<!)
匹配除订单金额以外的数字。
"2022年5月29日订单金额¥12".match(/(?!=¥)\d+/g); // '2022', '5', '29', '12'
贪婪匹配
正则表达式默认执行贪婪匹配,匹配尽可能多的字符,多多益善。
"ber beer beeer beeeer".match(/.*r/); // ber beer beeer beeeer
懒惰匹配
匹配尽可能少的字符,第一个匹配上就停止。
"ber beer beeer beeeer".match(/.*?r/); // ber
正则脑图
以^1\d{10}$
为例