匹配单个字符

匹配任意字符:.

. 元字符可以匹配任何单个的字符、字母、数字甚至是 . 字符本身

匹配特殊字符:\

\ 反斜杠是一个元字符( metacharacter,表示“这个字符有特殊含义,而不是字符本身含义"),反斜杠元字符可以对其它有特殊含义的元字符进行转义,来匹配该字符。

匹配一组字符

匹配多个字符中的某一个:[]

可以使用元字符 [] 来定义一个字符集合。这两个元字符之间的所有字符都是该集合的组成部分,字符集合的匹配结果是能够与该集合里的任意一个成员相匹配的文本。

利用字符集合区间:-

在使用正则表达式的时候,会频繁地用到一些字符区间( 0 ~ 9、A ~ Z 等)。为了简化字符区间的定义,正则表达式提供了一个特殊的元字符,字符区间可以用 - (连字符)来定义,连字符只能用在 [] 之间,其它地方只是普通字符,不需要转义。

字符区间的首、尾字符可以是 ASCII 字符表里的任意字符。以下都是合法的字符区间:

  • [0-9],匹配从 0 到 9 的任意数字,0 (ASCII 48) and 9 (ASCII 57),[0-9] 和 [0123456789] 等价。
  • [A-Z],匹配从 A 到 Z 的任意大写字母。
  • [a-z],匹配从 a 到 z 的任意小写字母。
  • [A-Za-z0-9],匹配字幕、数字中的任意一个。

取非匹配:^

除了那个字符集合里的字符,其他宇符都可以匹配。^ 的效果将作用于给定宇符集合里的所有字符或字符区间,而不是仅限于紧跟在 ^ 字符后面的那一个字符或字符区间。例如:[ ^A-Za-z0-9 ] 表示匹配任何不是数字或字母的字符。

使用元字符

匹配空白字符

元字符说明
\f换页符
\n换行符
\r回车符
\t制表符(Tab 键)
\v垂直制表符

\r\n 是 Windows 所使用的文本行结束标签。 Unix 和 Linux 系统只使用一个换行符来结束一个文本行,换句话说,在 Unix/Linux 系统上匹配空白行只使用 \n 即可,不需要加上 \r

匹配特定的字符类别

前面介绍的都是匹配特定的字符、匹配单个字符。一些常用的字符集合可以用特殊元字符来代替,这些元字符匹配的是某一类别的字符(术语称之为”字符类")。

匹配数字(或非数字)

元字符说明
\d任何一个数字字符(等价于 [ 0-9 ])
\D任何一个非数字字符(等价于 [ ^0-9 ])

匹配字母和数字(与非字母和数字)

元字符说明
\w任何一个字母、数字字符(大小写均可)或下划线字符(等价与 [a-zA-Z0-9_] )
\W任何一个非字母数字或非下划线字符(等价与 [ ^a-zA-Z0-9_ ])
  • 匹配空白字符(与非空白字符)

元字符说明
\s任何一个空白字符(等价千[ \f\n\r\t\v ])
\S任何一个非空白字符(等价于[ ^\f\n\r\t\v ])

重复匹配

匹配一个或多个字符:+

要想匹配同一个字符(或字符集合)的多次重复,只要简单地给这个字符(或字符集合)加上一个 + 字符作为后缀就行了。

匹配零个或多个字符:*

+ 最少匹配一个,* 匹配零个或多个字符(或字符集合)。

匹配零个或一个字符:?

? 只能匹配一个字符(或字符集合)的零次或一次出现,最多不超过一次。

匹配的重复次数:{}

前面的匹配无法控制匹配次数,为了解决这些问题并让程序员对重复性匹配有更多的控制,正则表达式语言提供了一个用来设定重复次数(interval)的语法。重复次数要用 {} 字符来给出,把数值写在它们之间。

  • 精确的匹配次数:{1},恰好匹配一次
  • 匹配次数范围:{0, 4},最少零次,最多四次
  • 至少重复多少次:{2, },至少重复两次

防止过多重复

因为 *+ 都是所谓的"贪婪型”元字符,它们在进行匹配时的行为模式是多多益善而不是适可而止的。它们会尽可能地从一段文本的开头一直匹配到这段文本的末尾,而不是从这段文本的开头匹配到碰到第一个匹配时为止。

在不需要这种“贪婪行为"的时候该怎么办?答案是使用这些元字符的“懒惰型”版本(“懒惰”在这里的含义是匹配尽可能少的字符,与“贪婪型”元字符的行为模式刚好相反)。懒惰型元字符的写法很简单,只要给贪婪型元字符加上一个 ? 后缀即可。

贪婪型(Greedy)元字符懒惰型(Reluctant)元字符
**?
++?
{n, }{n, }?

位置匹配

位置匹配用来解决在什么地方进行字符串匹配操作的问题。使用边界限定符,也就是在正则表达式里用一些特殊的元字符来表明我们想让匹配操作在什么位置(或边界)发生。边界限定符匹配的是某个位置,而不是具体的内容。

单词边界:\b

第一种边界(也是最常用的边界)是由限定符 \b 指定的单词边界。顾名思义,\b 用来匹配一个单词的开始或结尾。b 代表单词 boundary(边界)。

它匹配的是一个这样的位置,这个位置位于一个能够用来构成单词的字符(字母、数字和下划线,也就是与 \w 相匹配的字符)和一个不能用未构成单词的字符(也就是与 \W 相匹配的宇符)之间或者构成单词的 \w 的开始和结束位置,相当于如下正则的竖点线处:(^ \w|\w $|\W \w|\w \W)。

如果想匹配单词本身,只需在单词前后加上 \b 即可,例如:\bcap\b,将匹配下面前后都是空白的字符:

如果想不匹配一个单词边界,可以使用 \B,即字母数字下划线之间,或非字母数字下划线之间。例如,我们可以使用 \B-\B 来查找其下面句子前后都有多余空格的连字符:

字符串边界:^ $

单词边界可以用来进行与单词有关的位置匹配(单词的开头、单词的结束、整个单词等)。字符串边界有着类似的用途,只不过是用来进行与字符串有关的位置匹配而已(字符串的开头、字符串的结束、整个字符串等)。用来定义字符串边界的元字符有两个:一个是用来定义字符串开头的 ^ ,另一个是用来定义字符串结尾的 $

分行匹配模式

有许多正则表达式都支待使用一些特殊的元字符去改变另外一些元字符行为的做法,用来启用分行匹配模式(multiline mode)的 (?m) 记号就是一个能够改变其他元字符行为的元字符序列。分行匹配模式将使得正则表达式引擎把行分隔符当作一个字符串分隔符来对待。

Java 下使用 Pattern.MULTILINE 来启用,或者嵌入 (?m) 到正则最前面,例如匹配行注释:

Pattern.compile("(?m)^\\s*//.*$");
// or
Pattern.compile("^\\s*//.*$");

JavaScript 下可以通过字面量的形式定义在后面的 flag 中,或者使用对象的形式,例如同样匹配行注释:

/^\s*\/\/.*$/m
// or
new RegExp("^\s*\/\/.*$", "m")

在分行匹配模式下,^ 不仅匹配正常的字符串开头,还将匹配行分隔符(换行符)后面的开始位置(这个位置是不可见的); 类似地,$ 不仅匹配正常的字符串结尾,还将匹配行分隔符(换行符)后面的结束位置。

使用子表达式

子表达式:()

子表达式是一个更大的表达式的一部分;把一个表达式划分为一系列子表达式的目的是为了把那些子表达式当作一个独立元素来使用。子表达式必须用 () 括起来。

子表达式可以被当作整体和前面描述的重复匹配的元字符组合等。

子表达式嵌套

子表达式允许嵌套。事实上,子表达式允许多重嵌套,这种嵌套的层次在理论上没有限制,但在实际工作中还是应该遵循适可而止的原则。

例如:((A)(B(C))),A、B、C 都是独立的正则表达式。

回溯引用

回溯引用匹配 \n

假设你有一段文本,你想把这段文本里所有连续重复出现的单词(打字错误,其中有一个单词输了两遍)找出来。显然,在搜索某个单词的第二次出现时,这个单词必须是已知的。回溯引用(backreference)允许正则表达式模式引用前面的匹配结果(具体到这个例子,就是前面匹配到的单词)。

下面是一段包含着 3 组重复单词的文本,它们就是我们要找的东西:

文本

正则

[ ]+(\w+)[ ]+\1

结果

分析

这个模式的最后一部分是 \1;这是一个回溯引用,而它引用的正是前面划分出来的那个子表达式:当 (\w+) 匹配到单词 of 的时候,\1 也匹配单词 of;当 (\w+) 匹配到单词 and 的时候,\1 也匹配单词 and。

\1 到底代表着什么?它代表着模式里的第1 个子表达式,\2 代表着第 2 个子表达式、\3 代表着第 3 个;依次类推。可以把回溯引用想象成变量 。

Java 中需要对回溯引用的反斜杠转义,变为:[ ]+(\w+)[ ]+\\1

命名捕获:(?<name>X) \k<name>

正如看到的那样,子表达式是通过它们的相对位置未引用的:\1 对应着笫 1 个子表达式,\5 对应着笫 5 个子表达式,等等。虽然受到普遍的支持,但这种语法存在着一个严重的不足:如果子表达式的相对位置发生了变化,整个模式也许就不能再完成原来的工作,删除或添加子表达式的后果可能更为严重。

为了弥补这一不足,一些比较新的正则表达式实现还支持“命名捕获”(named capture): 给某个子表达式起一个唯一的名字,然后用这个名字(而不是相对位置)来引用这个子表达式。

在 Java、JavaScript 中,可以使用语法 (?<name>X) 为捕获组分配显式名称。其中 X 是通常的正则表达式。

Java 中可以通过 Matcher#group(String name) 来访问命名捕获组匹配的内容,JavaScript 中通过 match.groups.name 直接访问。

例如找到下面的以 , 结尾的单词:

文本

正则

[ ]+(?<stopWord>\\w+,)[ ]+

结果

如果要回溯引用命名捕获分组,可以使用 \k<name>

Java 中同样需要对这个反斜杠转义,变为:\\k<name>

之前通过位置引用的使用命名分组可以修改为:[ ]+(?<repeatWord>\\w+)[ ]+\\k<repeatWord>

回溯引用在替换中的应用

正则不仅可以用来搜索匹配,也可以用在复杂的替换操作中,尤其是需要使用回溯引用的场合。

例如你需要把原始文本里的电子邮件地址全都转换为 HTML 中可点击的链接:

文本

正则

(\w+[\w\.]*@[\w\.]+\.\w+)

替换

<a href="mailto:$1'>$1</a>

结果

替换操作需要用到两个正则表达式:一个用来给出搜索模式,另一个用来给出匹配文本的替换模式。回溯引用可以跨模式使用,在第一个模式里被匹配的子表达式可以用在第二个模式里,回溯引用可以被引用任意多次。

我们再来看一个例子。在一个用来保存用户信息的数据库里,电话号码被保存为 313-555-1234。现在,你需要把电话号码重新排版为 (313) 555-1234。下面就是这个例子:

文本

正则

(\d{3})(-)(\d{3})(-)(\d{4})

替换

($1) $3-$5

结果

正则将内容被划分为 5 个子表达式(5 个组成部分),这 5 个部分都可以单独拿出来使用,我们只使用了其中的 3 个。

前后查找

到目前为止,我们见过的正则表达式都是用来匹配文本的,但有时我们还需要用正则表达式标记要匹配的文本的位置(而不仅仅是文本本身)。这就引出了前后查找(lookaround,对某一位置的前、后内容进行查找)的概念,

向前查找:(?=X)

向前查找(lookahead)指定了一个必须匹配但不在结果中返回的模式。向前查找实际就是一个子表达式,而且从格式上看也确实如此。从语法上看,一个向前查找模式其实就是一个以 ?= 开头的子表达式,需要匹配的文本跟在 = 的后面。

例如,我们想把一些 URL 地址的协议提取出来:

文本

正则

.+(?=:)

结果

分析

模式 .+ 匹配任意文本(第 1 个匹配是 http),子表达式 (?=:) 匹配 :。注意,被匹配到的 : 并没有出现在最终的匹配结果里;我们用 ?= 向正则表达式引擎表明:从 : 位置处先前查找,但不要把它包括在最终的匹配结果里。

向前查找(和向后查找)匹配本身其实是有返回结果的,只是这个结果的字节长度永远是 0 而已。因此,前后查找操作有时也被称为零宽度(zero - width)匹配操作。

向后查找:(?<=X)

除了向前查找,许多正则表达式实现还支持向后查找,也就是从标记位置向后查找,向后查找操作符是 ?<=

向后查找的 < 可以看作左闭合处需满足条件,然后向右边开放处匹配

例如我们想把一份产品目录里面的产品价格提取出来:

文本

正则

{?<=\$)[0-9.]+

结果

(?<=\$) 只匹配 $,但不消费它:最终的匹配结果里只有价格数字(没有前缀的 $ 字符)。

向前查找 X 模式的长度是可变的,可以包含 . 或 + 之类的元字符。

向后查找模式只能是固定长度,所有正则实现都遵守该限制。

结合前后查找

向前查找和向后查找可以组合在一起使用。

例如我们想获取 title 标签内的内容:

文本

正则

(?<=\<[tT][iI][tT][lL][eE]>).*(?=\<\/[tT][iI][tT][lL][eE]>)

结果

对前后查找取非

向前查找和向后查找通常用来匹配文本,其目的是为了确定将被返回为匹配结果的文本的位置(通过指定匹配结果的前后必须是哪些文本)。 这种用法被称为正向前查找(positive lookahead)和正向后查找(positive lookbehind) 。术语“正”指的是寻找匹配的事实。

前后查找还有一种不太常见的用法叫作负前后查找(negative lookaround)。负向前查找(negative lookahead)将向前查找不与给定模式相匹配的文本,负向后查找(negative lookbehind)将向后查找不与给定模式相匹配的文本。

前后查找必须用 ! 来取非(它将替换掉 = )。下表列出了所有的前后查找操作符。

操作符说明
(?=)正向前查找
(?!)负向前查找
(?<=)正向后查找
(?<!)负向后查找

为了掩饰正向后查找和负向后查找的区别,我们来看一个例子,下面是一段既包含价格又包含数量的文本,我们先来查找且只查找价格:

文本

正则

(?<=\$)\d+

结果

接下来,我们再去查找且只查找数量:

正则

\b(?<!\$)\d+\b

结果

分析

\d+ 还是匹配数值,但这次只匹配数量,不匹配价格。表达式 (?<!\$) 是一个负向后查找,它使得最终的匹配结果只包含那些不以 $ 开头的数值。把操作符 ?<= 改为操作符 ?<! 使得整个模式从一个正向后查找变成了一个负向后查找 。

使用前后查找拆分字符串

正则有时也被用做字符串的拆分中,通过匹配分隔符的正则,将字符串一分为二拆分为自子串。但有时候我们想保留匹配的分隔符,则可以利用前后查找的零宽度只标记位置的特性来拆分字符串。

例如我们想把一段表达式根据逻辑运算符拆分开来,并保留逻辑运算符:

文本

正则

(?<=\s(and|or|&&|\|\|)\s)|(?=\s(and|or|&&|\|\|)\s)

结果

嵌入条件

正则表达式语言还有一种威力强大(但不经常被用到)的功能在表达式的内部嵌入条件处理功能。

Java 的 Pattern 不支持该带条件的表达式

嵌入条件语法也使用了 ? ,这并没有什么让人感到吃惊的地方,因为嵌入条件不外乎以下两种情况。

  • 根据一个回溯引用来进行条件处理。
  • 根据一个前后查找来进行条件处理。

回溯引用条件

回溯引用条件只在一个前面的子表达式搜索取得成功的情况下才允许使用一个表达式。

用来定义这种条件的语法是 (?(backreference)true-regex) ,其中 ? 表明这是一个条件,括号里的 backreference 是一个回溯引用,true-regex 是一个只在 backreference 存在时才会被执行的子表达式。

听起来很费解,我们还是用一个例子来说明好了:你需要把一段文本里的 <IMG> 标签全都找出来;不仅如此,如果某个 <IMG> 标签是一个链接(被括在 <A></A> 标签之间)的话,你还要把整个链接标签匹配出来。

文本

正则

(<[Aa]\s+[^>]+>\s*)?<[Ii][Mm][Gg]\s+[^>]+>(?(1)\s*</[Aa]>)

结果

分析

(?(1)\s*</[Aa]>) 是一个回溯引用条件,?(1) 的含义是:如果第一个回溯引用(具体到本例,就是 <A> 标签)存在,则使用 \s*</[Aa]> 继续进行匹配(换句话说,只有当前面的 <A> 标签匹配成功,才继续进行后面的匹配)。如果 (1) 存在,\s*</[Aa]> 将匹配结束标签 </A> 之后出现的任意空白字符。

条件还可以有否则表达式,否则表达式只在给定的回溯引用不存在(也就是条件没有得到满足)时才会被执行。用来定义这种条件的语法是 (?(backre­ference)true-regex|false-regex) 这个语法接受一个条件和两个将分别在这个条件得到满足和没有得到满足时执行的子表达式。

前后查找条件

前后查找条件只在一个向前查找或向后查找操作取得成功的情况下才允许一个表达式被使用。定义一个前后查找条件的语法与定义一个回溯引用条件的语法大同小异,只需把回溯引用(括号里的回溯引用编号)替换为一个完整的前后查找表达式就行了。

作为一个例子,请你思考一下怎样匹配美国的邮政编码(简称 ZIP 编码)。美国邮政编码有两种格式,一种是 12345 形式的 ZIP 格式,另一种是 12345-6789 形式的 ZIP+4 格式。只有 ZIP+4 格式才必须使用连字符来分隔前 5 位和后 4 位数字。

文本

正则

\d{5}(?(?=-)-\d{4})

结果

分析

\d{5} 匹配前 5 位数字。接下来是一个 (?(?=-)-\d{4}) 形式的向前查找条件。这个条件使用了 ?=- 来匹配(但不消费)一个连字符,如果条件得到满足(那个连字符存在),-\d{4} 将匹配那个连字符和随后的 4 位数。