正则是什么

在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

最基础语法参考

  1. . - 除换行符以外的所有字符。
  2. ^ - 字符串开头。
  3. $ - 字符串结尾。
  4. \d,\w,\s - 匹配数字、字符、空格。
  5. \D,\W,\S - 匹配非数字、非字符、非空格。
  6. [abc] - 匹配 ab c 中的一个字母。
  7. [a-z] - 匹配 a z 中的一个字母。
  8. [^abc] - 匹配除了 ab c 中的其他字母。
  9. aa|bb - 匹配 aa bb
  10. ? - 0 次或 1 次匹配。
  11. * - 匹配 0 次或多次。
  12. + - 匹配 1 次或多次。
  13. {n} - 匹配 n次。
  14. {n,} - 匹配 n次以上。
  15. {m,n} - 最少 m 次,最多 n 次匹配。
  16. (expr) - 捕获 expr 子模式,以 \1 使用它。
  17. (?:expr) - 忽略捕获的子模式。
  18. (?=expr) - 正向预查模式 expr
  19. (?!expr) - 负向预查模式 expr

参考资料:
菜鸟教程 runoob 正则在线 http://c.runoob.com/front-end/854正则表达式一直是困扰很多程序员的一门技术,当然也包括曾经的我。大多数时候我们在开发过程中要用到某些正则表达式的时候,都会打开谷歌或百度直接搜索然后拷贝粘贴。当下一次再遇到相同问题的时候,同样的场景又再来一遍。作为一门用途很广的技术,我相信深入理解正则表达式并能融会贯通是值得的。所以,希望这篇文章能帮助大家理清思路,搞懂正则表达式各种符号之间的内在联系,形成知识体系,当下次再遇到正则表达式的时候可以不借助搜索引擎,自己解决。

正则表达式到底是什么

正则表达式(Regular Expression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于上个20世纪50年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。大家可以利用regexper.com这个工具很好地可视化自己写的正则表达式:
/\d\w+/这个正则生成的状态机图:

正则表达式(转载融合) - 图1
对于具体的算法实现,大家如果感兴趣可以阅读《算法导论》。

从字符出发

我们学习一个系统化的知识,一定要从其基础构成来了解。正则表达式的基本组成元素可以分为:字符和元字符。字符很好理解,就是基础的计算机字符编码,通常正则表达式里面使用的就是数字、英文字母。而元字符,也被称为特殊字符,是一些用来表示特殊语义的字符。如^表示非,|表示或等。利用这些元字符,才能构造出强大的表达式模式(pattern)。接下来,我们就来从这些基本单位出发,来学习一下如何构建正则表达式。

单个字符

最简单的正则表达式可以由简单的数字和字母组成,没有特殊的语义,纯粹就是一一对应的关系。如想在’apple’这个单词里找到‘a’这个字符,就直接用/a/这个正则就可以了。
但是如果想要匹配特殊字符的话,就得请出我们第一个元字符\, 它是转义字符字符,顾名思义,就是让其后续的字符失去其本来的含义。举个例子:
我想匹配*这个符号,由于*这个符号本身是个特殊字符,所以我要利用转义元字符\来让它失去其本来的含义:
复制代码

  1. /\*/

如果本来这个字符不是特殊字符,使用转义符号就会让它拥有特殊的含义。我们常常需要匹配一些特殊字符,比如空格,制表符,回车,换行等, 而这些就需要我们使用转义字符来匹配。为了便于记忆,我整理了下面这个表格,并附上记忆方式:

特殊字符 正则表达式 记忆方式
换行符 \n new line
换页符 \f form feed
回车符 \r return
空白符 \s space
制表符 \t tab
垂直制表符 \v vertical tab
回退符 [\b] backspace,之所以使用[]符号是避免和\b重复

多个字符

单个字符的映射关系是一对一的,即正则表达式的被用来筛选匹配的字符只有一个。而这显然是不够的,只要引入集合区间和通配符的方式就可以实现一对多的匹配了。
在正则表达式里,集合的定义方式是使用中括号[]。如/[123]/这个正则就能同时匹配1,2,3三个字符。那如果我想匹配所有的数字怎么办呢?从0写到9显然太过低效,所以元字符-就可以用来表示区间范围,利用/[0-9]/就能匹配所有的数字, /[a-z]/则可以匹配所有的英文小写字母。
即便有了集合和区间的定义方式,如果要同时匹配多个字符也还是要一一列举,这是低效的。所以在正则表达式里衍生了一批用来同时匹配多个字符的简便正则表达式:

匹配区间 正则表达式 记忆方式
除了换行符之外的任何字符 . 句号,除了句子结束符
单个数字, [0-9] \d digit
除了[0-9] \D not digit
包括下划线在内的单个字符,[A-Za-z0-9_] \w word
非单字字符 \W not word
匹配空白字符,包括空格、制表符、换页符和换行符 \s space
匹配非空白字符 \S not space

循环与重复

一对一和一对多的字符匹配都讲完了。接下来,就该介绍如何同时匹配多个字符。要实现多个字符的匹配我们只要多次循环,重复使用我们的之前的正则规则就可以了。那么根据循环次数的多与少,我们可以分为0次,1次,多次,特定次。

0 | 1

元字符?代表了匹配一个字符或0个字符。设想一下,如果你要匹配colorcolour这两个单词,就需要同时保证u这个字符是否出现都能被匹配到。所以你的正则表达式应该是这样的:/colou?r/

>= 0

元字符*用来表示匹配0个字符或无数个字符。通常用来过滤某些可有可无的字符串。

>= 1

元字符+适用于要匹配同个字符出现1次或多次的情况。

特定次数

在某些情况下,我们需要匹配特定的重复次数,元字符{}用来给重复匹配设置精确的区间范围。如’a’我想匹配3次,那么我就使用/a{3}/这个正则,或者说’a’我想匹配至少两次就是用/a{2,}/这个正则。
以下是完整的语法:
复制代码

  1. - {x}: x
  2. - {min, max}: 介于min次到max次之间
  3. - {min, }: 至少min
  4. - {0, max}: 至多max

由于这些元字符比较抽象,且容易混淆,所以我用了联想记忆的方式编了口诀能保证在用到的时候就能回忆起来。

匹配规则 元字符 联想方式
0次或1次 ? ,此事
0次或无数次 * 宇宙洪荒,辰宿列张:宇宙伊始,从无到有,最后星宿布满星空
1次或无数次 + 一加, +1
特定次数 {x}, {min, max} 可以想象成一个数轴,从一个点,到一个射线再到线段。min和max分别表示了左闭右闭区间的左界和右界

位置边界

上面我们把字符的匹配都介绍完了,接着我们还需要位置边界的匹配。在长文本字符串查找过程中,我们常常需要限制查询的位置。比如我只想在单词的开头结尾查找。

单词边界

单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:
复制代码

  1. The cat scattered his food all over the room.

我想找到cat这个单词,但是如果只是使用/cat/这个正则,就会同时匹配到catscattered这两处文本。这时候我们就需要使用边界正则表达式\b,其中b是boundary的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。
上面的例子改写成/\bcat\b/这样就能匹配到cat这个单词了。

字符串边界

匹配完单词,我们再来看一下一整个字符串的边界怎么匹配。元字符^用来匹配字符串的开头。而元字符$用来匹配字符串的末尾。注意的是在长文本里,如果要排除换行符的干扰,我们要使用多行模式。试着匹配I am scq000这个句子:
复制代码

  1. I am scq000.
  2. I am scq000.
  3. I am scq000.

我们可以使用/^I am scq000\.$/m这样的正则表达式,其实m是multiple line的首字母。正则里面的模式除了m外比较常用的还有i和g。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。
最后,总结一下:

边界和标志 正则表达式 记忆方式
单词边界 \b boundary
非单词边界 \B not boundary
字符串开头 ^ 头尖尖那么大个
字符串结尾 $ 终结者,美国科幻电影,美元符$
多行模式 m标志 multiple of lines
忽略大小写 i标志 ignore case, case-insensitive
全局模式 g标志 global

子表达式

字符匹配我们介绍的差不多了,更加高级的用法就得用到子表达式了。通过嵌套递归和自身引用可以让正则发挥更强大的功能。
从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式。

分组

其中分组体现在:所有以()元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。

回溯引用

所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1,\2,….,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。
假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?
复制代码

  1. Hello what what is the first thing, and I am am scq000.

利用回溯引用,我们可以很容易地写出\b(\w+)\s\1这样的正则。
回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2…来引用要被替换的字符串。下面以js代码作演示:
var’abc abc 123’/(ab)c/g’$1g’// 得到结果 ‘abg abg 123’复制代码

  1. str = ;
  2. str.replace(,);

如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。
var’scq000’/(scq00)(?:0)/‘$1,$2’// 返回scq00,$2// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2复制代码

  1. str = .
  2. str.replace(, )

有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。

前向查找

前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。例如happy happily这两个单词,我想获得以happ开头的副词,那么就可以使用happ(?=ily)来匹配。如果我想过滤所有以happ开头的副词,那么也可以采用负前向查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。

后向查找

介绍完前向查找,接着我们再来介绍一下它的反向操作:后向查找(lookbehind)。后向查找(lookbehind)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: applepeople都包含ple这个后缀,那么如果我只想找到appleple,该怎么做呢?我们可以通过限制app这个前缀,就能唯一确定ple这个单词了。
复制代码

  1. /(?<=app)ple/

其中(?<=regex)的语法就是我们这里要介绍的后向查找。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向查找。另外一种限制匹配是利用(?<!regex) 语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找appleple也可以这么写成/(?<!peo)ple
需要注意的,不是每种正则实现都支持后向查找。在javascript中是不支持的,所以如果有用到后向查找的情况,有一个思路是将字符串进行翻转,然后再使用前向查找,作完处理后再翻转回来。看一个简单的例子:
// 比如我想替换apple的ple为plyvar’apple people’’’’’/elp(?=pa)/‘ylp’’’’’复制代码

  1. str = ;
  2. str.split().reverse().join().replace(, ).split().reverse().join();

ps: 感谢评论区提醒,从es2018之后,chrome中的正则表达式也支持反向查找了。不过,在实际项目中还需要注意对旧浏览器的支持,以防线上出现Bug。详情请查看http://kangax.github.io/compat-table/es2016plus/#test-RegExp_Lookbehind_Assertions

最后回顾一下这部分内容:

回溯查找 正则 记忆方式
引用 \0,\1,\2 和 $0, $1, $2 转义+数字
非捕获组 (?:) 引用表达式(()), 本身不被消费(?),引用(:)
前向查找 (?=) 引用子表达式(()),本身不被消费(?), 正向的查找(=)
前向负查找 (?!) 引用子表达式(()),本身不被消费(?), 负向的查找(!)
后向查找 (?<=) 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),正的查找(=)
后向负查找 (?<!) 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),负的查找(!)

逻辑处理

计算机科学就是一门包含逻辑的科学。让我们回忆一下编程语言当中用到的三种逻辑关系,与或非。
在正则里面,默认的正则规则都是的关系所以这里不讨论。
关系,分为两种情况:一种是字符匹配,另一种是子表达式匹配。在字符匹配的时候,需要使用^这个元字符。在这里要着重记忆一下:只有在**[****]**内部使用的**^**才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)或后向负查找子表达式(?<!regex)
或关系,通常给子表达式进行归类使用。比如,我同时匹配a,b两种情况就可以使用(a|b)这样的子表达式。

逻辑关系 正则元字符
[^regex]和!
|

总结

对于正则来说,符号之抽象往往让很多程序员却步。针对不好记忆的特点,我通过分类和联想的方式努力让其变得有意义。我们先从一对一的单字符,再到多对多的子字符串介绍,然后通过分组、回溯引用和逻辑处理的方式来构建高级的正则表达式。
在最后,出个常用的正则面试题吧:请写出一个正则来处理数字千分位,如12345替换为12,345。请尝试自己推理演绎得出答案,而不是依靠搜索引擎:)。

常用表达式

一、校验数字的表达式

  • 数字:^[0-9]*$
  • n位的数字:^\d{n}$
  • 至少n位的数字:^\d{n,}$
  • m-n位的数字:^\d{m,n}$
  • 零和非零开头的数字:^(0|[1-9][0-9]*)$
  • 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
  • 带1-2位小数的正数或负数:^(-)?\d+(.\d{1,2})$
  • 正数、负数、和小数:^(-|+)?\d+(.\d+)?$
  • 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
  • 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
  • 非零的正整数:^[1-9]\d$ 或 ^([1-9][0-9]){1,3}$ 或 ^+?[1-9][0-9]*$
  • 非零的负整数:^-[1-9][]0-9”$ 或 ^-[1-9]\d$
  • 非负整数:^\d+$ 或 ^[1-9]\d*|0$
  • 非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
  • 非负浮点数:^\d+(.\d+)?$ 或 ^[1-9]\d.\d|0.\d[1-9]\d|0?.0+|0$
  • 非正浮点数:^((-\d+(.\d+)?)|(0+(.0+)?))$ 或 ^(-([1-9]\d.\d|0.\d[1-9]\d))|0?.0+|0$
  • 正浮点数:^[1-9]\d.\d|0.\d[1-9]\d$ 或 ^(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9]))$
  • 负浮点数:^-([1-9]\d.\d|0.\d[1-9]\d)$ 或 ^(-(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9])))$
  • 浮点数:^(-?\d+)(.\d+)?$ 或 ^-?([1-9]\d.\d|0.\d[1-9]\d|0?.0+|0)$

校验字符的表达式

  • 汉字:^[\u4e00-\u9fa5]{0,}$
  • 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
  • 长度为3-20的所有字符:^.{3,20}$
  • 由26个英文字母组成的字符串:^[A-Za-z]+$
  • 由26个大写英文字母组成的字符串:^[A-Z]+$
  • 由26个小写英文字母组成的字符串:^[a-z]+$
  • 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
  • 由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
  • 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
  • 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
  • 可以输入含有^%&’,;=?$\”等字符:[^%&’,;=?$\x22]+
  • 禁止输入含有~的字符:[^~\x22]+

三、特殊需求表达式

  • Email地址:^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*$
  • 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+.?
  • InternetURL:[a-zA-z]+://[^\s] 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=])?$
  • 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
  • 电话号码(“XXX-XXXXXXX”、”XXXX-XXXXXXXX”、”XXX-XXXXXXX”、”XXX-XXXXXXXX”、”XXXXXXX”和”XXXXXXXX):^((\d{3,4}-)|\d{3.4}-)?\d{7,8}$
  • 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
  • 电话号码正则表达式(支持手机号码,3-4位区号,7-8位直播号码,1-4位分机号): ((\d{11})|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)
  • 身份证号(15位、18位数字),最后一位是校验位,可能为数字或字符X:(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)
  • 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
  • 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
  • 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在 8-10 之间):^(?=.\d)(?=.[a-z])(?=.*[A-Z])[a-zA-Z0-9]{8,10}$
  • 强密码(必须包含大小写字母和数字的组合,可以使用特殊字符,长度在8-10之间):^(?=.\d)(?=.[a-z])(?=.*[A-Z]).{8,10}$
  • 日期格式:^\d{4}-\d{1,2}-\d{1,2}
  • 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
  • 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
  • 钱的输入格式:
    1. 有四种钱的表示形式我们可以接受:”10000.00” 和 “10,000.00”, 和没有 “分” 的 “10000” 和 “10,000”:^[1-9][0-9]*$
    2. 这表示任意一个不以0开头的数字,但是,这也意味着一个字符”0”不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$
    3. 一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
    4. 这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧。下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
    5. 必须说明的是,小数点后面至少应该有1位数,所以”10.”是不通过的,但是 “10” 和 “10.2” 是通过的:^[0-9]+(.[0-9]{2})?$
    6. 这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
    7. 这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
    8. 1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
    9. 备注:这就是最终结果了,别忘了”+”可以用”*”替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
  • xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\.[x|X][m|M][l|L]$
  • 中文字符的正则表达式:[\u4e00-\u9fa5]
  • 双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
  • 空白行的正则表达式:\n\s*\r (可以用来删除空白行)
  • HTML标记的正则表达式:<(\S?)[^>]>.?|<.? /> ( 首尾空白字符的正则表达式:^\s|\s$或(^\s)|(\s$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
  • 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
  • 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
  • IPv4地址:((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}

参考:
https://deerchao.cn/tutorials/regex/regex.htm 30分钟玩明白正则
https://juejin.cn/post/6844903845227659271 正则表达式不要背