本文主要介绍了正则表达式的一些基本用法。

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

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

与文本编辑器相似,几乎所有的高级编程语言都支持正则表达式。在这样的语境下,“文本"也就是一个字符串,可以执行的操作都是类似的。一些编程语言(比如 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

正则表达式的基础语法

字符

正则表达式中包含了一系列的字符,这些字符只能匹配它们本身。有一些被称为"元字符"的特殊字符,可以匹配一些特殊规则。

大部分的字符,包括所有的字母和数字字符,是普通字符。也就意味着,它们只能匹配它们自己,如下所示的正则表达式:

cat

意味着,只能匹配一个字符串,以"c"开头,然后是字符"a",紧跟着是字符"t"的字符串。

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

点"."

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

c.t

意味着匹配"以 c 开头,之后是任意一个字符,紧跟着是字母 t"的字符串。

在一段文本中,这样的正则表达式可以用来找出 cat, cot, czt 这样的字符串,甚至可以找出 c.t 这样的组合,但是不能找到 ct 或者是 coot 这样的字符串。

使用反斜杠""可以忽略元字符,使得元字符的功能与普通字符一样。所以,正则表达式

c\.t

表示"找到字母 c,然后是一个句号("."),紧跟着字母 t"

反斜杠本身也是一个元字符,这意味着反斜杠本身也可以通过相似的方法变回到普通字符的用途。因此,正则表达式

c\\t

表示匹配"以字符 c 开头,然后是一个反斜杠,紧跟着是字母 t"的字符串。

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

字符类

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

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

在字符类中,字符的重复和出现顺序并不重要。[dabaaabcc][abc] 是相同的

重要提示:字符类中和字符类外的规则有时不同,一些字符在字符类中是元字符,在字符类外是普通字符。一些字符正好相反。还有一些字符在字符类中和字符类外都是元字符,这要视情况而定!

比如,. 表示匹配任意一个字符,而 [.] 表示匹配一个全角句号。这不是一回事!

字符类的范围

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

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

字符类的反义

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

  • [^a] 表示匹配任何不是"a"的字符
  • [^a-za-z0-9] 表示匹配任何不是字母也不是数字的字符
  • [\^abc] 匹配一个为"^“或者 a 或者 b 或者 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}"
  • 在字符类中,大括号没有特殊含义。[{}] 表示匹配一个左边的大括号,或者一个右边的大括号

注意:重复字符是没有记忆性的,比如 [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??rcolou{0,1}r 相同,表示找到 color 或者 colour。这与 colou?r 一样。
  • ".\*?" 表示先匹配一个双引号,然后匹配最少的字符,然后是一个双引号,与上面两个例子不同,这很有用。

选择匹配

你可以使用|来分隔可以匹配的不同选择:

  • cat|dog 表示匹配"cat"或者"dog"
  • red|blue| 以及 red||blue 以及 |red|blue 都表示匹配 red 或者 blue 或者一个空字符串
  • a|b|c[abc] 相同
  • cat|dog|\| 表示匹配"cat"或者"dog"或者一个分隔符"|"
  • [cat|dog] 表示匹配 a 或者 c 或者 d 或者 g 或者 o 或者 t 或者一个分隔符"|"

分组

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

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

单词分隔符

在单词和非单词之间有单词分隔符。记住,一个单词 \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)

换行符

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

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

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

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

那么新的格式表示如下:

那么新的格式表示如下:

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$)

文本分界

在很多的正则表达式实现中,将 ^$ 作为文本的开始符号和结束符号。

还有一些实现中,用 \A\z 作为文本的开始和结束符号。

捕捉和替换

从这里开始,正则表达式真正体现出了它的强大。

捕获组

你已经知道了使用括号可以匹配一组符号。使用括号也可以捕获子串。假设正则表达式是一个小型计算机程序,那么捕获子串就是它输出的一部分。

正则表达式 (\w*)ility 表示匹配以 ility 结尾的词。第一个被捕获的部分是由 \w* 控制的。比如,输入的文本内容中有单词 accessibility,那么首先被捕获的部分是 accessib。如果输入的文本中有单独的 ility,则首先被捕获的是一个空字符串。

你可能会有很多的捕获字符串,它们可能靠得很近。捕获组从左向右编号。也就是只需要对左括号计数。

假设有这样的正则表达式:(\w+) had a ((\w+) \w+)

输入的内容是:I had a nice day

  • 捕获组 1:I
  • 捕获组 2:nice day
  • 捕获组 3:nice 在一些正则表达式的实现中,你可以从零开始编号,编号零表示匹配整句话:I had a nice day.

在其他的实现中,如果没有制定捕获组,那么捕获组 1 会自动地填入捕获组 0 的信息。

是的,这也意味着会有很多的括号。有一些正则表达式的实现中,提供了"非捕获组"的语法,但是这样的语法并不是标准语法,因此我们不会介绍。

从一个成功的匹配中返回的捕获组个数,与使用原来的正则表达式获得的捕获组个数相同。记住这一点,你可以解释一些奇怪的现象。.

正则表达式 ((cat)|dog) 表示匹配 cat 或者 dog。这里有两个捕获组,如果输入文本是 dog,那么捕获组 1 是 dog,捕获组 2 为空。

正则表达式 a(\w)\* 表示匹配一个以 a 开头的单词。这里只有一个捕获组

  • 如果输入文本为 a ,捕获组 1 为空。
  • 如果输入文本为 ad,捕获组为 d
  • 如果输入文本为 avocado,捕获组 1 为 v。但是捕获组 0 表示整个单词 avocado.

替换

假如你使用了一个正则表达式去匹配字符串,你可以描述另外一个字符串来替换其中的匹配字符。用来替换的字符串称为替换表达式。它的功能类似于

  • 常规的 Replace 会话
  • Java 中的 String.replace() 函数
  • PHP 的 str_replace() 函数
  • 等等

反向引用

在一个正则表达式中,你也可以引用捕获组。这称作:反向引用

比如,[abc]{2} 表示匹配 aa 或者 ab 或者 ac 或者 ba 或者 bb 或者 bc 或者 ca 或者 cb 或者 cc。但是 {[abc]}\1 表示只匹配 aa 或者 bb 或者 cc。

使用正则表达式编程

特别提醒:

过度使用的反斜杠

在一些编程语言,比如 Java 中,对于包含正则表达式的字符串没有特殊标记。字符串有着自己的过滤规则,这是优先于正则表达式规则的,这是频繁使用反斜杠的原因。

比如在 Java 中

匹配一个数字,使用的正则表达式从 \d 变为代码中的 String re= "\\d"

在其他的编程语言中,正则表达式是由特殊标明的,比如使用 /。下面是 JavaScript 的例子:

  • 匹配一个数字,\d 会简单写成 var regExp = /\d/;
  • 匹配一个反斜杠或者一个左边的方括号或者一个右边的方括号, var regExp = /[\\\[\]]/;
  • var regExp = /\s/;var regExp = /[ \t\r\n]/; 是等价的
  • 当然,这意味着在使用 / 时必须重复两次。比如找到 URL 必须使用 var regExp = /https?:\/\//;.

我希望现在你能明白,我为什么让你特别注意反斜杠。

动态正则表达式

当你动态创建一个正则表达式的时候请特别小心。如果你使用的字符串不够完善的话,可能会有意想不到的匹配结果。这可能导致语法错误,更糟糕的是,你的正则表达式语法正确,但是结果无法预料。

错误的 Java 代码:

String sep = System.getProperty("file.separator");
String[] directories = filePath.split(sep);

Bug: String.split() 认为 sep 是一个正则表达式。但是,在 Windows 中,Sep 是表示匹配一个反斜杠,也就是与正则表达式"\“相同。这个正则表达式是正确的,但是会返回一个异常:PatternSyntaxException。

任何好的编程语言都会提供一种良好的机制来跳过字符串中所有的元字符。在 Java 中,你可以这样实现:

String sep = System.getProperty("file.separator");
String[] directories = filePath.split(Pattern.quote(sep));

循环中的正则表达式

将正则表达式字符串加入反复运行的程序中,是一种开销很大的操作。如果你可以在循环中避免使用正则表达式,你可以大大提高效率。

总结

  • 字符: a b c d 1 2 3 4 etc.
  • 字符类: . [abc] [a-z] \d \w \s
    • . 代表任何字符
    • \d 表示"数字”
    • \w 表示"字母", [0-9A-Za-z_]
    • \s 表示 “空格, 制表符,回车或换行符”
    • 否定字符类: [^abc] \D \W \S
  • 重复: {4} {3,16} {1,} ? * +
    • ? 表示 “零次或一次”
    • * 表示 “大于零次”
    • + 表示 “一次或一次以上”
    • 如果不加上 ?,所有的重复都是最长匹配的(贪婪)
  • 分组: (Septem|Octo|Novem|Decem)ber
  • 词,行以及文本的分隔: \b ^ $ \A \z
  • 转义字符: \1 \2 \3 etc. (在匹配表达式和替换表达式中都可用)
  • 元字符: . \ [ ] { } ? * + | ( ) ^ $
  • 在字符类中使用元字符: [ ] \ - ^
  • 使用反斜杠可以忽略元字符: \

参考链接:

  1. 30 分钟学习正则表达式 - Github