跳到主要内容

正则表达式入门

· 阅读需 13 分钟

在需要使用正则时搜一搜,复制粘贴改一改。有些时候看不太懂,只要能够跑的通就满意了,心里没有底,到底正不正确。又需要临时抱佛脚,并没有系统的去学习。学过之后过一段时间,就会忘记,反反复复,因此记录下来。

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