当我需要用正则解决问题时,往往先靠搜索“改一改”凑合过去。
后来发现问题常会“跑得通但不放心”,而且隔一段时间就会忘,于是把基础知识整理成一篇可复用的入门笔记,减少重复踩坑。

美国一位知名程序员杰米·加文斯基(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

如何学习正则表达式?

每次学习都去搜索一下,搞定就结束,日积月累,可能会浪费更多时间,而且容易忘记。

每天学习一个小时,坚持一周,花的总时间反而更短,而且不容易忘记。

需求拆解

拿到需求后,可以把需求拆分成几个子需求,并检查每个子需求是否相互独立。

例如网址:

什么是 URL

“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

如何使用正则表达式?

保持克制。

一次性解决所有问题,但是大家都看不懂,就没有可维护性,基本上只能够重写。

  1. 能够用字符串处理解决的,就使用字符串。

  2. 一定要写注释。

  3. 能用多个简单的正则表达式解决的,就不要强求用一个复杂的正则表达式。

字符

匹配文本

"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 等),为了简化书写,使用 - 连字符来定义字符区间。

  1. - 只有在 [] 中才是元字符,在字符集合以外只是普通的字符 -。
  2. 尾字符一定要大于首字符(例如 [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}$ 为例。

在https://regexper.com/中的效果

在https://jex.im/regulex/中的效果

引用

https://regex101.com/

https://www.regextester.com

正则表达式必知必会(修订版)

https://regexper.com/

https://jex.im/regulex/

https://www.debuggex.com/

https://blog.robertelder.org/regular-expression-visualizer/

https://regexlearn.com/