正则

RegExp | 正则表达式

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来表达对字符串的一种过滤逻辑。

给定一个正则表达式和另一个字符串,我们可以达到如下的目的:

  • 给定的字符串是否符合正则表达式的过滤逻辑(称作"匹配");
  • 可以通过正则表达式,从字符串中获取我们想要的特定部分。

正则表达式的特点是:

  • 灵活性、逻辑性和功能性非常的强;
  • 可以迅速地用极简单的方式达到字符串的复杂控制。
  • 对于刚接触的人来说,比较晦涩难懂。

注意:正则表达式写好后,没有错对之分,返回结果只是 true 和 false。

  • Symbols

    • . —  匹配除了换行之外的任意字符
    • ^ —  匹配字符串的首部
    • $ —  匹配字符串的末尾
  • Number

    • * —  匹配前述的表达式零或多次
    • + —  匹配前述的表达式一或多次
    • ? —  匹配前述的表达式零或一次
    • a{3} - 匹配指定数目的 a
    • a{3,} - 匹配不少于 3 个的 a
    • a{3,6} - 匹配 3 到 6 个 a
  • Character groups

    • \d —  匹配任意的数值
    • \w —  匹配任意的字符
    • [XYZ] —  数组中任一值匹配,多范围混搭的话,可以使用 [A-Z][xyz]+ 来匹配集合中的任一字符
    • [^a-z] — ^ 用于进行反选,这里即表示匹配非 a-z 字符的其他值;([^B]*) 表示该位置是除了 B 之外的任意值。
  • Advanced

    • (x) —  捕获圆括号,匹配 x 并且记录捕获项
    • (x|y) - 匹配 x 或者 y
    • (?:x) —  非匹配圆括号,仅匹配而不记录
    • x(?=y) —  预测匹配,仅匹配那些 y 之前的 x
  • Flags

    • g —  全局搜索
    • i —  大小写敏感搜索

结构与操作符

元字符

元字符是构造正则表达式的一种基本元素。

元字符 说明 . 匹配除换行符以外的任意字符 \w 匹配字母或数字或下划线或汉字 \s 匹配任意的空白符 \d 匹配数字 \b 匹配单词的开始或结束 ^ 匹配字符串的开始 $ 匹配字符串结束

正则提供了转义的方式,也就是要把这些元字符、限定符或者关键字转义成普通的字符,做法很简答,就是在要转义的字符前面加个斜杠,也就是\即可。

重复限定符

正则表达式中一些重复限定符,把重复部分用合适的限定符替代,下面我们来看一些限定符:

语法 说明

  • 重复零次或更多次
  • 重复一次或更多次 ? 重复零次或一次 {n} 重复 n 次 {n,} 重复 n 次或更多次 {n,m} 重复 n 到 m 次

分组与条件

正则表达式中用小括号 () 来做分组,也就是括号中的内容作为一个整体。

正则用符号 | 来表示或,也叫做分支条件,当满足正则里的分支条件的任何一种条件时,都会当成是匹配成功。

^(130|131|132|155|156|185|186|145|176)\d{8}$

数量与匹配模式

在正则表达式中有这么三种模式:贪婪模式、懒惰模式、独占模式。在关于数量的匹配中,有 + ? * {min,max} 四种两次,如果只是单独使用,那么它们就是贪婪模式。

贪婪模式

懒惰模式

如果在他们之后加多一个 ? 符号,那么原先的贪婪模式就会变成懒惰模式,即尽可能少地匹配。但是懒惰模式还是会发生回溯现象的。TODO 例如下面这个例子:

text = "abbc";

regex = "ab{1,3}?c";

正则表达式的第一个操作符 a 与 字符串第一个字符 a 匹配,匹配成。于是正则表达式的第二个操作符 b{1,3}? 和 字符串第二个字符 b 匹配,匹配成功。因为最小匹配原则,所以拿正则表达式第三个操作符 c 与字符串第三个字符 b 匹配,发现不匹配。于是回溯回去,拿正则表达式第二个操作符 b{1,3}? 和字符串第三个字符 b 匹配,匹配成功。于是再拿正则表达式第三个操作符 c 与字符串第四个字符 c 匹配,匹配成功。于是结束。

独占模式

如果在他们之后加多一个 + 符号,那么原先的贪婪模式就会变成独占模式,即尽可能多地匹配,但是不回溯。

算法引擎

实现正则表达式引擎的有两种方式:DFA 自动机(Deterministic Final Automata 确定型有穷自动机)和 NFA 自动机(Non deterministic Finite Automaton 不确定型有穷自动机)。

DFA 自动机的时间复杂度是线性的,更加稳定,但是功能有限。而 NFA 的时间复杂度比较不稳定,有时候很好,有时候不怎么好,好不好取决于你写的正则表达式。但是胜在 NFA 的功能更加强大,所以包括 Java、.NET、Perl、Python、Ruby、PHP 等语言都使用了 NFA 去实现其正则表达式。Java 正则表达式使用的引擎实现是 NFA 自动机,这种正则表达式引擎在进行字符匹配时会发生回溯(backtracking)。而一旦发生回溯,那其消耗的时间就会变得很长,有可能是几分钟,也有可能是几个小时,时间长短取决于回溯的次数和复杂度。

NFA

那 NFA 自动加到底是怎么进行匹配的呢?我们以下面的字符和表达式来举例说明。

text="Today is a nice day."

regex="day"

要记住一个很重要的点,即:NFA 是以正则表达式为基准去匹配的。也就是说,NFA 自动机会读取正则表达式的一个一个字符,然后拿去和目标字符串匹配,匹配成功就换正则表达式的下一个字符,否则继续和目标字符串的下一个字符比较。或许你们听不太懂,没事,接下来我们以上面的例子一步步解析。

首先,拿到正则表达式的第一个匹配符:d。于是那去和字符串的字符进行比较,字符串的第一个字符是 T,不匹配,换下一个。第二个是 o,也不匹配,再换下一个。第三个是 d,匹配了,那么就读取正则表达式的第二个字符:a。 读取到正则表达式的第二个匹配符:a。那着继续和字符串的第四个字符 a 比较,又匹配了。那么接着读取正则表达式的第三个字符:y。 读取到正则表达式的第三个匹配符:y。那着继续和字符串的第五个字符 y 比较,又匹配了。尝试读取正则表达式的下一个字符,发现没有了,那么匹配结束。 上面这个匹配过程就是 NFA 自动机的匹配过程,但实际上的匹配过程会比这个复杂非常多,但其原理是不变的。

NFA 自动机的回溯

了解了 NFA 是如何进行字符串匹配的,接下来我们就可以讲讲这篇文章的重点了:回溯。为了更好地解释回溯,我们同样以下面的例子来讲解。

text="abbc"

regex="ab{1,3}c"

上面的这个例子的目的比较简单,匹配以 a 开头,以 c 结尾,中间有 1-3 个 b 字符的字符串。NFA 对其解析的过程是这样子的:

  • 首先,读取正则表达式第一个匹配符 a 和 字符串第一个字符 a 比较,匹配了。于是读取正则表达式第二个字符。
  • 读取正则表达式第二个匹配符 b{1,3} 和字符串的第二个字符 b 比较,匹配了。但因为 b{1,3} 表示 1-3 个 b 字符串,以及 NFA 自动机的贪婪特性(也就是说要尽可能多地匹配),所以此时并不会再去读取下一个正则表达式的匹配符,而是依旧使用 b{1,3} 和字符串的第三个字符 b 比较,发现还是匹配。于是继续使用 b{1,3} 和字符串的第四个字符 c 比较,发现不匹配了。此时就会发生回溯。
  • 发生回溯是怎么操作呢?发生回溯后,我们已经读取的字符串第四个字符 c 将被吐出去,指针回到第三个字符串的位置。之后,程序读取正则表达式的下一个操作符 c,读取当前指针的下一个字符 c 进行对比,发现匹配。于是读取下一个操作符,但这里已经结束了。

常用正则

VSCode 中匹配常用的中文汉字:

('[^\x00-\xff]+'|"[^\x00-\xff]+"|>[^\x00-\xff]+<|^\s*([^\x00-\xff]+)\s*$)

正则表达式是一种查找以及字符串替换操作。正则表达式在文本编辑器中广泛使用,比如正则表达式被用于:

  • 检查文本中是否含有指定的特征词
  • 找出文中匹配特征词的位置
  • 从文本中提取信息,比如:字符串的子串
  • 修改文本

与文本编辑器相似,几乎所有的高级编程语言都支持正则表达式。在这样的语境下,“文本”也就是一个字符串,可以执行的操作都是类似的。一些编程语言(比如 Perl,JavaScript)会检查正则表达式的语法。正则表达式只是一个字符串。没有长度限制,但是,这样的正则表达式长度往往较短。如下所示是一些正则表达式的例子:

  • I had a \S+ day today
  • [A-Za-z0-9\-_]{3,16}
  • \d\d\d\d-\d\d-\d\d
  • v(\d+)(\.\d+)*
  • TotalMessages="(.*?)"
  • <[^<>]>

这些字符串实际上都是微型计算机程序。正则表达式的语法,实际上是一种轻量级、简洁、适用于特定领域的编程语言。记住这一点,那么你就很容易理解下面的事情:

  • 每一个正则表达式,都可以分解为一个指令序列,比如“先找到这样的字符,再找到那样的字符,再从中找到一个字符。。。”
  • 每一个正则表达式都有输入(文本)和输出(匹配规则的输出,有时是修改后的文本)
  • 正则表达式有可能出现语法错误——不是所有的字符串都是正则表达式
  • 正则表达式语法很有个性,也可以说很恐怖
  • 有时可以通过编译,使得正则表达式执行更快

在实现中,正则表达式还有其他的特点。本文将重点讨论正则表达式的核心语法,在几乎所有的正则表达式中都可以见到这些规则。特别提示:正则表达式与文件通配语法无关,比如 *.xml。最后推荐几个常用的正则表达式在线编辑工具:

  • Debuggex:https://www.debuggex.com/
  • PyRegex:http://www.pyregex.com/
  • Regexper:http://www.regexper.com/

基础语法

字符

正则表达式中包含了一系列的字符,这些字符只能匹配它们本身。有一些被称为“元字符”的特殊字符,可以匹配一些特殊规则。如下所示的例子中,我用红色标出了元字符。大部分的字符,包括所有的字母和数字字符,是普通字符。也就意味着,它们只能匹配它们自己,如下所示的正则表达式:

cat

意味着,只能匹配一个字符串,以“c”开头,然后是字符“a”,紧跟着是字符“t”的字符串。到目前为止,正则表达式的功能类似于

  • 常规的 Find 功能
  • Java 中的 String.indexOf() 函数
  • PHP 中的 strpos()函数
  • 等等

注意:不做特殊说明,正则表达式中是区分大小写的。但是,几乎所有正则表达式的实现,都会提供一个 Flag 用来控制是否区分大小写。

. 匹配任意单个字符

我们第一个要讲解的元字符是“.”。这个符号意味着可以匹配任意一个字符。如下所示的正则表达式:

c.t

意味着匹配“以 c 开头,之后是任意一个字符,紧跟着是字母 t”的字符串。在一段文本中,这样的正则表达式可以用来找出 cat, cot, czt 这样的字符串,甚至可以找出 c.t 这样的组合,但是不能找到 ct 或者是 coot 这样的字符串。使用反斜杠“\”可以忽略元字符,使得元字符的功能与普通字符一样。所以,正则表达式

c\.t

表示“找到字母 c,然后是一个句号(“.”),紧跟着字母 t”。反斜杠本身也是一个元字符,这意味着反斜杠本身也可以通过相似的方法变回到普通字符的用途。因此,正则表达式

c\\t

表示匹配“以字符 c 开头,然后是一个反斜杠,紧跟着是字母 t”的字符串。注意!在正则表达式的实现中,.是不能用于匹配换行符的。”换行符“的表示方法在不同实现中也不同。实际编程时,请参考相关文档。在本文中,我认为.是可以匹配任意字符的。实现环境通常会提供一个 Flag 标志位,来控制这一点。

单次分隔符

在单词和非单词之间有单词分隔符。记住,一个单词\w 是[0-9A-Za-z_],而非单词字符是\W(大写),表示[^0-9a-za-z_].

在文本的开头和结尾通常也有单词分隔符。

在输入文本 it’s a cat 中,实际有八个单词分隔符。如果我们在 cat 之后在上一个空格,那就有九个单词分隔符。.

\b表示匹配一个单词分隔符
\b\w\w\w\b表示匹配一个三字母单词
a\ba表示匹配两个a中间有一个单词分隔符。这个正则表达式永远不会有匹配的字符,无论输入怎样的文本。

单词分隔符本身并不是字符。它们的宽度为 0。下列正则表达式的作用不同

(\bcat)\b
(\bcat\b)
\b(cat)\b
\b(cat\b)

换行符

一篇文本中可以有一行或多行,行与行之间由换行符分隔,比如:

Line一行文字
Line break换行符
Line一行文字
Line break换行符
…
Line break换行符
Line一行文字

注意,所有的文本都是以一行结束的,而不是以换行符结束。但是,任意一行都可能为空,包括最后一行。

行的起始位置,是在换行符和下一行首字符之间的空间。考虑到单词分隔符,文本的起始位置也可以当做是首行位置。

最后一行是最后一行的尾字符和换行符之间的空间。考虑到单词分隔符,文本的结束也可以认为是行的结束。

那么新的格式表示如下:

Start-of-line, line, end-of-line
Line break
Start-of-line, line, end-of-line
Line break
…
Line break
Start-of-line, line, end-of-line

基于上述概念:

^表示匹配行的开始位置
$表示匹配行的结束位置
^&表示一个空行
^.*& 表示匹配全文内容,因为行的开始符号也是一个字符,"."会匹配这个符号。找到单独的一行,可以使用 ^.*?$
\^\$表示匹配字符串“^$”
[$]表示匹配一个$。但是,[^]不是合法的正则表达式。记住在方括号中,字符有不同的特殊含义。要想在方括号内匹配^,必须用[\^]

与字符分隔符一样,换行符也不是字符。它们宽度为 0.如下所示的正则表达式作用不同:

(^cat)$
(^cat$)
^(cat)$
^(cat$)

字符类

字符类是一组在方括号内的字符,表示可以匹配其中的任何一个字符。

  • 正则表达式 c[aeiou]t,表示可以匹配的字符串是”以 c 开头,接着是 aeiou 中的任何一个字符,最后以 t 结尾”。在文本的实际应用中,这样的正则表达式可以匹配:cat,cet,cit,cot,cut 五种字符串。
  • 正则表达式[0123456789]表示匹配任意一个整数。
  • 正则表达式[a]表示匹配单字符 a。

包含忽略字符的例子

  • a 表示匹配字符串[a]
  • [[]ab]表示匹配的字符为”[“或者”]”或者”a”,或者”b”
  • [\[]]表示匹配的字符为”\”或者 “[”或者”]”

在字符类中,字符的重复和出现顺序并不重要。[dabaaabcc]与[abc]是相同的。重要提示:字符类中和字符类外的规则有时不同,一些字符在字符类中是元字符,在字符类外是普通字符。一些字符正好相反。还有一些字符在字符类中和字符类外都是元字符,这要视情况而定!比如,.表示匹配任意一个字符,而[.]表示匹配一个全角句号。这不是一回事!

字符类的范围

在字符集中,你可以通过使用短横线来表示匹配字母或数字的范围。

  • [b-f]与[b,c,d,e,f]相同,都是匹配一个字符”b”或”c”或”d”或”e”或”f”
  • [A-Z]与[ABCDEFGHIJKLMNOPQRSTUVWXYZ]相同,都是匹配任意一个大写字母。
  • [1-9]与[123456789]相同,都是匹配任意一个非零数字。

字符类可以多个混用,也可以与单个字符混合使用:

  • [0-9.,]表明匹配一个数字,或者一个全角句号,或者一个逗号
  • [0-9a-fA-F]意味着匹配一个十六进制数
  • [a-zA-Z0-9-]意味着匹配一个字母、数字或者一个短横线

字符类的反义

你可以在字符类的起始位放一个反义符。

  • [^a]表示匹配任何不是“a”的字符
  • [^a-za-z0-9]表示匹配任何不是字母也不是数字的字符
  • [^abc]匹配一个为“^”或者 a 或者 b 或者 c 的字符
  • [^^]表示匹配任何不为“^”的字符

譬如[^c]ei就表示,在 e 之前有 i,但是没有 c。

字符类的转义

  • \d 这个正则表达式与[0-9]作用相同,都是匹配任何一个数字。(要匹配\d,应该使用正则表达式\d)
  • \w 与[0-9A-Za-z]相同,都表示匹配一个数字或字母字符
  • \s 意味着匹配一个空字符(空格,制表符,回车或者换行)另外
  • \D 与[^0-9]相同,表示匹配一个非数字字符。
  • \W 与[^0-9a-za-z]相同,表示匹配一个非数字同时不是字母的字符。
  • \S 表示匹配一个非空字符。

这些是你必须掌握的字符。你可能已经注意到了,一个全角句号“.”也是一个字符类,可以匹配任意一个字符。很多正则表达式的实现中,提供了更多的字符类,或者是标志位在 ASCII 码的基础上,扩展现有的字符类。特别提示:统一字符集中包含除了 0 至 9 之外的更多数字字符,同样的,也包含更多的空字符和字母字符。实际使用正则表达式时,请仔细查看相关文档。

次数/重复

在字符或字符集之后,你可以使用{ }大括号来表示重复

  • 正则表达式 a{1}与 a 意思相同,都表示匹配字母 a

  • a{3}表示匹配字符串“aaa”

  • a{0}表示匹配空字符串。从这个正则表达式本身来看,它毫无意义。如果你对任何文本执行这样的正则表达式,你可以定位到搜索的起始位置,即使文本为空。

  • a{2}表示匹配字符串“a{2}” 在字符类中,大括号没有特殊含义。[{}]表示匹配一个左边的大括号,或者一个右边的大括号。简化下面的正则表达式

  • z.......z

  • \d\d\d\d-\d\d-\d\d

  • [aeiou][aeiou][aeiou][aeiou][aeiou][aeiou]

  • [bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz]

答案为:

  • z.{7}z
  • \d{4}-\d{2}-\d{2}
  • [aeiou]{6}
  • [bcdfghjklmnpqrstvwxyz]{10}

注意:重复字符是没有记忆性的,比如[abc]{2}表示先匹配”a 或者 b 或者 c”,再匹配”a 或者 b 或者 c”,与匹配”aa 或者 ab 或者 ac 或者 ba 或者 bb 或者 bc 或者 ca 或者 cb 或者 cc“一样。[abc]{2}并不能表示匹配”aa 或者 bb 或者 cc“

指定重复次数范围

重复次数是可以指定范围的

  • x{4,4}与 x{4}相同
  • colou{0,1}r 表示匹配 colour 或者 color
  • a{3,5}表示匹配 aaaaa 或者 aaaa 或者 aaa

注意这样的正则表达式会优先匹配最长字符串,比如输入 I had an aaaaawful day``会匹配单词aaaaawful中的aaaaa,而不会匹配其中的aaa

重复次数是可以有范围的,但是有时候这样的方法也不能找到最佳答案。如果你的输入文本是 I had an aaawful daaaaay 那么在第一次匹配时,只能找到 aaawful,只有再次执行匹配时才能找到 daaaaay 中的 aaaaa.

重复次数的范围可以是开区间

  • a{1,}表示匹配一个或一个以上的连续字符 a。依然是匹配最长字符串。当找到第一个 a 之后,正则表达式会尝试匹配尽量多个的连续字母 a。
  • .{0,}表示匹配任意内容。无论你输入的文本是什么,即使是一个空字符串,这个正则表达式都会成功匹配全文并返回结果。

重复的转义字符

-与{0,1}相同,比如,colou?r 表示匹配 colour 或者 color

  • *与{0,}相同。比如,.*表示匹配任意内容
  • +与{1,}相同。比如,\w+表示匹配一个词。其中”一个词”表示由一个或一个以上的字符组成的字符串,比如_var 或者 AccountName1.

这些是你必须知道的常用转义字符,除此之外还有:

  • ?*+ 表示匹配字符串”?*+”
  • [?*+]表示匹配一个问号,或者一个*号,或者一个加号

匹配策略

非贪婪匹配

正则表达式 “.*” 表示匹配双引号,之后是任意内容,之后再匹配一个双引号。注意,其中匹配任意内容也可以是双引号。通常情况下,这并不是很有用。通过在句尾加上一个问号,可以使得字符串重复不再匹配最长字符。

  • \d{4,5}?表示匹配\d\d\d\d 或者\d\d\d\d\d。也就是和\d{4}一样
  • colou??r 与 colou{0,1}r 相同,表示找到 color 或者 colour。这与 colou?r 一样。
  • “.*?”表示先匹配一个双引号,然后匹配最少的字符,然后是一个双引号,与上面两个例子不同,这很有用。

分组匹配

你可以使用括号表示分组:

通过使用 Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day 匹配一周中的某一天
(\w*)ility  与 \w*ility 相同。都是匹配一个由”ility”结尾的单词。稍后我们会讲解,为何第一种方法更加有用。
[()]表示匹配任意一个左括号或者一个右括号

分组可以包括空字符串:

(red|blue)表示匹配red或者blue或者是一个空字符串
abc()def与abcdef相同

你也可以在分组的基础上使用重复:

(red|blue)?与(red|blue|)相同
\w+(\s+\w+)表示匹配一个或多个由空格分隔的单词

常用正则表达式

图片

/http[s]?:\/\/(?:[a-z0-9\-]+\.)+[a-z]{2,6}(?:[^/#?]+)+\.(?:jpg|gif|png)/ig

正则之基本入门

规则 说明
/a/ 匹配字符 a
/?/ 匹配特殊字符?。特殊字符包括^, $, ?, ., /, , [, ], {, }, (, ), +, *.
. 匹配任意字符,例如/a./匹配 ab 和 ac
/[ab]c/ 匹配 ac 和 bc,[]之间代表范围,例如:/[a-z]/, /[a-zA-Z0-9]/,
/[^a-za-z0-9]/ 匹配不在该范围内的字符串
/[\d]/ 代表任意数字
/[\w]/ 代表任意字母,数字或者_
/[\s]/ 代表空白字符,包括空格,TAB 和换行
/[\D]/,/[\W]/,/[\S]/ 均为上述的否定情况
? 代表 0 或 1 个字符
* 代表 0 或多个字符
+ 代表 1 或多个字符
/d{3}/ 匹配 3 个数字
/d{1,10}/ 匹配 1-10 个数字
d{3,}/ 匹配 3 个数字以上
/([A-Z]\d){5}/ 匹配首位是大写字母,后面 4 个是数字的字符串