当我需要用正则解决问题时,往往先靠搜索“改一改”凑合过去。
后来发现问题常会“跑得通但不放心”,而且隔一段时间就会忘,于是把基础知识整理成一篇可复用的入门笔记,减少重复踩坑。
美国一位知名程序员杰米·加文斯基(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}$ 为例。
DISCUSSION
讨论与反馈