这两天一直在时不时的和 Neo4j 图数据库打交道。它的查询语句可以使用正则表达式,有一段时间没有自己写过正则表达式了,现在处于能看懂别人写的正则表达式,但是自己写不出来,语法规则都忘了。为了方便接下来的工作,所以特地复习复习正则表达式的语法。

正则表达式简介

正则表达式是用来匹配字符串的一系列匹配符,具备简介高效的特点,在很多语言中都有支持(java、python、javascript、php 等等)。在 windows 的 cmd 命令中也同样支持,例如使用命令 dir j*,那么只会罗列出所有以j开头的文件和文件夹。

正则表达式基本语法

正则表达式在在不同语言的支持语法略有不同,本文采用js的进行说明。js 中使用正则表达式的方法为str.match(/表达式/),即需要加两个斜杠。以下所有的代码段第一行为代码,第二行为返回结果,实验是在 chrome 控制台进行的。

一直认为最好的学习方式就是实际操作,理论谁都能讲一大堆,但是实际做没做出来还真不知道。一个奇葩现象就是教软件工程的老师可能并没有在软件行业待过。

普通匹配符

普通匹配符能匹配与之对应的字符,默认区分大小写。

"Hello Regx".match(/H/)
["H", index: 0, input: "Hello Regx", groups: undefined]

正则标记符

  • i :不区分大小写
  • g :全局匹配
  • m :多行匹配(暂不管它,我用的少)

参数直接加在最后一个斜杠的后面,比如"Hello Regx".match(/regx/i),可以加多个参数。

"Hello Regx".match(/regx/i)
["Regx", index: 6, input: "Hello Regx", groups: undefined]

之前是表达式一旦匹配成功,就不再向字符串后面查找了,加上 g 后,表示进行全局查找。最后返回的是一个数组。

"Hello Regx".match(/e/g)
(2) ["e", "e"]

多匹配符

  • \d :匹配数字,即 0~9
  • \w :匹配数字、字母、下划线
  • . :匹配除换行的所有字符

需要注意的是,上面所有的匹配符都只能匹配一个字符。

"Hello 2018".match(/\d/g)
// 使用\d,匹配字符串中的所有数字
(4) ["2", "0", "1", "8"]


"Hello 2018".match(/\w/g)
// 使用\w,匹配所有的数字和字母,需要注意没有匹配到空格
(9) ["H", "e", "l", "l", "o", "2", "0", "1", "8"]


"Hello 2018".match(/./g)
// 使用.,匹配所有字符,包括空格
(10) ["H", "e", "l", "l", "o", " ", "2", "0", "1", "8"]


"Hello 2018".match(/\d\w./g)
// 分析一下这个为什么匹配到的是201,
// 首先\d找到第一个数字2,匹配成功,紧接着\w匹配到0,然后.匹配到1
// 整个正则表达式匹配成功,返回201
["201"]


"Hello 20\n18".match(/\d\w./g)
// 这里匹配不成功,因为.不能匹配换行符,所以返回null
null


"Hello 2018".match(/\w.\d/g)
// 首先看这个正则式,\w.\d,它要求最后一个字符是数字
// \w.能一直匹配到空格,但是因为得满足\d,所以第一个匹配成功的是0 2
// 因为是全局匹配,所以会接着匹配后面的018,也匹配成功
(2) ["o 2", "018"]

自定义匹配符

比如中国的手机号都是以 1 开头,第二位只能是 3、4、5、7、8,第 3 位只要是数字就行。如何匹配这样的字符串?

  • [] :匹配[]中的任意一个字符
"152".match(/1[34578]\d/)
// 第二个字符可以选择中括号中的任意一个
["152", index: 0, input: "152", groups: undefined]

如果在 [] 添加了 ^,代表取反。即 [^] 表示除了中括号中的字符都满足。

"152".match(/1[^34578]\d/)

null


"1a2".match(/1[^34578]\d/)
// 只要不是[]中的字符,都满足,包括回车符
["1a2", index: 0, input: "1a2", groups: undefined]

修饰匹配次数

我们的手机号有 11 位,除了前 2 位有要求,其他9位度没有要求,那么是不是正则表达式就应该这样写呢?

1[^34578]\d\d\d\d\d\d\d\d\d

很明显,这样写太麻烦,肯定有更好的方式,这里就可以修饰一下匹配次数啦。

  • ? :最多出现 1 次
  • + :至少出现 1 次
  • * :出现任意次数
  • {} :分下面四种情况
    • {n}代表前面的匹配符出现 n 次
    • {n, m}出现次数在 n~m 之间
    • {n, }至少出现 n 次
    • {, m}最多出现 m 次

例子很简单,一看就懂,不浪费时间。

"15284750845".match(/1[34578]\d{9}/)
["15284750845", index: 0, input: "15284750845", groups: undefined]


"15".match(/1[34578]\d?/)
["15", index: 0, input: "15", groups: undefined]


"152".match(/1[34578]\d?/)
["152", index: 0, input: "152", groups: undefined]


"152".match(/1[34578]\d+/)
["152", index: 0, input: "152", groups: undefined]


"15".match(/1[34578]\d+/)
null

完整匹配

按照上面的写法会出现下面的问题。

"ya15284750845".match(/1[34578]\d{9}/)
// 不是电话号码,也能匹配成功,需要进一步改进
["15284750845", index: 2, input: "ya15284750845", groups: undefined]
  • ^ :在 [] 中代表取反,但在外面代表从开始匹配
"ya15284750845".match(/^1[34578]\d{9}/)
// 现在就能从一开始匹配而且还得符合正则式才算匹配成功
null


// 但是依旧会出现下面的问题
"1528475084523255".match(/^1[34578]\d{9}/)
// 不是电话号码也能匹配成功,还要改进
["15284750845", index: 0, input: "1528475084523255", groups: undefined]
  • $ :代表持续匹配到结束
"1528475084523255".match(/^1[34578]\d{9}$/)
// 现在就能保证正确了,有^表示从开始匹配;
// 有$表示持续匹配到结束,即完全匹配
null

/*
需要注意的是,一个字符串从开始匹配和从结束匹配都没问题,
不代表整个字符串就没问题,比如 15284750845-15284750845
这个字符串从开始和从结束匹配都能成功,但实际上是错的
*/

特殊符号

到这里发现正则表达式确实很强大,仅仅几个简单的符号就能匹配字符串,但是如果我们要匹配的字符本身就是前面用到的符号怎么办呢?

  • 匹配像$、^等特殊符号时,需要加转义字符\
"1.".match(/./)
//因为.能匹配除换行的所有字符,所以匹配到1
//但实际上我们想匹配.这个字符
["1", index: 0, input: "1.", groups: undefined]


"1.".match(/\./)
// 只需要加一个转义字符就可以了,其他类似
[".", index: 1, input: "1.", groups: undefined]

条件分支

比如现在想匹配图片的文件名,包括 jpg、png、jpeg、gif 等等,这是多个选项,所以需要像编程语言一样,应该具备条件分支结构。

  • | :条件分支
  • () :有两层含义
    • 括号中的内容成为一个独立的整体
    • 括号的内容可以进行分组,单独匹配,若不需要此功能,则( ?: )
"1.jpg".match(/.+\.jpe?g|gif|png/)
// 这样就可以满足条件分支了,不过下面又出问题了
["1.jpg", index: 0, input: "1.jpg", groups: undefined]


"1.png".match(/.+\.jpe?g|gif|png/)
// 这里没有匹配到.和前面的文件名
["png", index: 2, input: "1.png", groups: undefined]


/*
其实我们想告诉它的是,.和后面的每一个条件分支的值都是一个独立的整体
但是它把.+\.jpe?g、gif、png当成了各自独立的整体
我们并不想让它这样切分,所以我们来告诉它怎么分才是正确的
*/


"1.png".match(/.+\.(jpe?g|gif|png)/)
// 现在可以匹配成功了,但是它多匹配了一个
// 因为括号的内容可以进行分组,单独匹配
(2) ["1.png", "png", index: 0, input: "1.png", groups: undefined]


// 所以最终写法如下
"1.png".match(/.+\.(?:jpe?g|gif|png)/)
["1.png", index: 0, input: "1.png", groups: undefined]

贪婪与懒惰

// 首先看一个例子
"aabab".match(/a.*b/)
["aabab", index: 0, input: "aabab", groups: undefined]


/*
上面的匹配没有什么问题,但实际上aab也是可以的
也就是aab也是符合条件的,那又是为什么呢?
*/

因为在正则表达式中,默认是贪婪模式,尽可能多的匹配,可以在修饰数量的匹配符后面添加 ?,则代表懒惰。

// like this (^__^)
"aabab".match(/a.*?b/)
["aab", index: 0, input: "aabab", groups: undefined]

到这里应该就差不多了,再深入的,就自我查询知识了。配一张正则表达式速查表。