注:

  1. 使用 grep -P 作为regex解释器
  2. 部分地方使用 -o 选项只输出匹配部分
  3. 部分地方使用 [] 表明里面的内容是匹配的部分

正则表达式的两种基本用途:「搜索」和「替换」。

第1章 略

第2章 匹配单个字符

最简单的是匹配纯文本。

全局搜索: 针对有多个匹配结果, 大多数正则表达式引擎的默认行为是只返回第1个匹配结果。不过很多实现都提供了能够把所有的匹配结果全部找出来的机制。常见的元字符是 g (表示global)

大小写问题: 正则表达式是区分字母大小写的, 大多数正则表达的式实现也支持不区分字母大小写的匹配操作。常见的远字符是 i (表示ignore)

匹配任意单个字符(除了newline字符, 除非设置了DOTALL模式): .

s (PCRE_DOTALL) If this modifier is set, a dot metacharacter in the pattern matches all characters, including newlines. Without it, newlines are excluded.

转义特殊字符: \ 。比如上面的 . , 如果要把这个当成普通字符匹配, 则需要写为 \. ; 同理, 因为 \ 是做转义用的特殊字符, 要匹配其本身, 则需要写为 \\ 。后续的一些元字符, 如果要匹配其自身, 都需要转义。

正则表达式经常被简称为模式, 它们其实是一些由字符构成的字符串。这些字符可以是 普通字符(纯文本) 或 元字符(有特殊含义的特殊字符)。

第3章 匹配一组字符

匹配多个字符中的某一个: []。里面定义多个字符的集合, 表示匹配该集合中任意一个字符。

如:

[rR]e[gG]ex  # 匹配 regex, Regex, reGex, ReGex

在集合里面, 还可以定义区间, 比如表示某个数字, 虽然可以使用[0123456789]但是不够简洁清晰, 可以:

[0-9]

甚至可以表示多个区间, 如表示所有大小写字母和数字:

[A-Za-z0-9]

注意 连字符- 只有在集合里面才表示一个特殊的元字符, 在集合外面只是一个普通字符。

在集合里, 还可以对集合做 取非操作, 使用^, 如表示除数字以外其它字符:

[^0-9]

注意 ^ 的效果将作用于给定字符集合里的所有字符或字符区间, 而不是仅限于紧跟在 ^ 字符后面的那一个字符或字符区间。

第4章 使用元字符

元字符大致可以分为两种: 一种是用来匹配文本的(比如 .), 另一种是正则表达式的语法所要求的(比如 [])。

匹配空白元字符:

元字符 说明
\n 换行符
\r 回车符
\t 制表符(Tab键)
\f 换页符
\v 垂直制表符
[\b] 回退(并删除)一个字符(Backspace键)

\r\n 匹配一个 「回车+换行」组合,有许多操作系统(比如Windows) 都把这个组合用作文本行的结束标签。Unix和Linux 系统只使用一个换行符 \n 来结束一个文本行;

即在 Unix/Linux系统上匹配空白行只使用 \n\n 即可,不需要加上 \r。 同时适用于Windows和Unix/Linux系统的正则表达式应该包含一个可选的 \r 和一个必须被匹配的 \n

字符集合(匹配多个字符中的某一个)是最常见的匹配形式, 而一些常用的字符集合可以用特殊元字符来代替:

元字符 说明
\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])

注意 用来匹配退格字符的 [\b] 元字符是一个特例: 它不在类元字符 \s 的覆盖范围内。

关于-w, 是一种比较常用的字符集合, 这些字符常见于各种名字里,如文件名、子目录名、变量名、数据库对象名等。

进制匹配:

元字符 说明
\x 匹配十六进制。如 \x0A 等价于 \n
\0 匹配八进制。如 \011 等价于 \t

POSIX字符类是许多正则表达式实现都支持的一种简写形式:

字符类 说 明
[:alnum:] 任何一个字母或数字 (等价于[a-zA-Z0-9])
[:alpha:] 任何一个字母 (等价于[a-zA-Z])
[:blank:] 空格或制表符 (等价于[\t])
[:cntrl:] ASCII控制字符 (ASCII 0到31,再加上ASCII 127)
[:digit:] 任何一个数字 (等价于[0-9])
[:graph:] 和[:print:]一样,但不包括空格
[:lower:] 任何一个小写字母 (等价于[a-z])
[:print:] 任何一个可打印字符
[:punct:] 既不属于[:alnum:]也不属于[:cntrl:]的任何一个字符
[:space:] 任何一个空白字符,包括空格 (等价于[^\f\n\r\t\v])
[:upper:] 任何一个大写字母 (等价于[A-Z])
[:xdigit:] 任何一个十六进制数字 (等价于[a-fA-F0-9])

例如下面匹配html中的rgb color:

#[[:xdigit:]]{6}

注意 这里使用的模式以 [[ 开头、以 ]] 结束 (两对方括号)。 这是使用POSIX字符类所必须的。POSIX字符类必须括在 [::] 之间,我们使用的POSIX字符类是 [:xdigit:](不是:xdigit:)。外层的 [] 字符用来定义一个字符集合, 内层的 [] 字符是POSIX字符类本身的组成部分。

第5章 重复匹配

针对重复次数的匹配元字符:

元字符 说明
+ 匹配一个或多个
* 匹配零个或多个
? 匹配零个或一个
{N} 指定具体的重复匹配次数, 如
{M, N} 指定重复匹配次数的区间, 如
{M, } 指定最少重复多少次, 如

如:

a+  # 匹配一个或多个连续的a
[0-9]+  # 匹配一个或多个连续的数字

注意 +等元字, 如果在集合中, 则表示普通字符。

另外,?, +, *, {m, n} 都是「贪婪匹配 (greedy match)」的元字符,也就是他们会尽可能多的去匹配。如:

$ cat test.txt
---text block 1---text block 2---end

$ grep -P -o -- '---.*---' test.txt
---text block 1---text block 2---

某些情况需要尽可能的少的匹配, 即匹配第一次符合的就停止继续匹配, 也就是「懒惰匹配 (non-greedy match 或 lazy match)」, 只需要在贪婪的元字符后面加上 ? 后缀:

$ grep -P -o -- '---.*?---' test.txt
---text block 1---

注解:

这里匹配三个减号符之间的内容, 因为 - 在命令行中有特殊含义, 表示命令行参数, 所以这里在所有参数选项后加上 -- (bare double dash), 参考; 之前报错还没想到是这里的问题。

另外, grep -G, grep -E 都不支持懒惰匹配, 所以使用了Perl型的解析: grep -P

最后, grep -o 只打印出匹配的内容, 这么看就比较直观了。

再比如:

$ cat test.txt
---abcdefg---1234567---xxxxxxx

$ grep -P -o -- '---.{3,5}' test.txt
---abcde
---12345
---xxxxx

$ grep -P -o -- '---.{3,5}?' test.txt
---abc
---123
---xxx

正则匹配YAML Front Matter 里也提到了懒惰匹配。

第6章 位置匹配

匹配位置的元字符:

元字符 说明
\b 匹配一个单词的开始或结尾
\B 匹配不是一个单词的边界
^ 匹配一个(行)字符串的开头
$ 匹配一个(行)字符串的结尾
\A 匹配一个字符串的开头。忽略multiple mode
\Z 匹配一个字符串的结尾。忽略multiple mode

注意: \A\Z 的作用基本等价于 ^$, 但是不会因为加上了(?m) 前缀而改变行为。即在跨行匹配模式下使用 \A\Z 的做法不会收到在分行匹配模式下使用 ^$ 的效果。

\b 用来匹配一个单词的开始或结尾:

$ grep -P  -- 'cat' test.txt
The [cat] s[cat]tered his food all over the room

$ grep -P  -- '\bcat\b' test.txt
The [cat] scattered his food all over the room

如上:

\B\b 相反, 用于匹配不是一个单词的边界:

$ grep -P  -- '\Bcat\B' test.txt
The cat s[cat]tered his food all over the room.

上面是针对单词的, 下面这两个是针对一行字符串的, 经常用到:

注意: 之前提到, ^ 出现在一个字符集合里(被放在[和]之间)并紧跟在左方括号[的后面时, 是表示「求非」的意思。

针对下面这个例子, 要匹配以<?xml xxx ?>开始的xml配置(前面可以有空格、空行):

<?xml version="1.0" encoding="UTF-8" ?>
<wsdl:definitions targetNamespace="http://tips.cf"
xmlns:impl="http://tips.cf" xmlns:intf="http://tips.cf"
xmlns:apachesoap="http://xml.apache.org/xml-soap"

正则 ^\s*<\?xml.*\?> 可以匹配, ^\s* 将匹配一个字符串的开头位置和随后的零个或多个空白字符(这解决了<?xml>标签前允许有空格、制表符、换行符等空白字符的问题)

但是如果开头还有其它字符, 则匹配失败, 如:

This is bad, real bad!
<?xml version="1.0" encoding="UTF-8" ?>
<wsdl:definitions targetNamespace="http://tips.cf"
xmlns:impl="http://tips.cf" xmlns:intf="http://tips.cf"
xmlns:apachesoap="http://xml.apache.org/xml-soap"

(grep 是针对行匹配的, 所以这块就算开头(上一行)有其它字符, 则可以匹配上; 不过, 使用\A 替代 ^ 可以解决这个问题, 参考我在StackOverflow上的提问)

多行模式(multiple mode): (?m), 使得正则表达式引擎把行分隔符当做一个字符串分隔符来对待。否则上面的^$会把整段来匹配。

正则匹配YAML Front Matter 里也提到了多行模式。

下面这个例子:

<SCRIPT>
function doSpellCheck(form, field) {
   // Make sure not empty
   if (field.value == ‘’) {
      return false;
   }
   // Init
  var windowName='spellWindow';
  var spellCheckURL='spell.cfm?formname=comment&fieldname='+field.name;
...
    // Done
    return false;
}
</SCRIPT>

正则 (?m)^\s*//.*$ 用来匹配注释行。

$ grep -P -o -- '(?m)^\s*//.*$' test.txt
   // Make sure not empty
   // Init
        // Done

第7章 子表达式

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

比如html中的&nbsp; (non-breaking space), 如果要匹配多个它, 则需要用子表达式括起来, 否则相当于匹配最后的分号:

(&nbsp;){2,}

为了提高可读性, 有不少人喜欢给表达式的每一个子表达式都加上括号。这种方式没什么问题, 也基本影响不到性能, 主要看习惯和规范了。

另外, 比如匹配几个数字中的某一个, 使用 | 做或判断, 就需要作为子表达式, 否则和前后混在一起很容易导致错误, 如匹配19XX年或20XX年

(19|20)\d{2}

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

多重嵌套的子表达式可以构造出功能极其强大的正则表达式来, 但那难免会让模式变得难以阅读和理解, 而这也正是很多人觉得正则表达式难以学习和掌握的原因之一。这种表面现象掩盖了这样一个事实: 绝大多数嵌套子表达式都没有它们看上去那么复杂。

第8章 回溯引用:前后一致匹配

子表达式的另一个重要用途——定义回溯引用(backreference)。

回溯引用指的是模式的后半部分引用在前半部分中定义的子表达式。

\1 代表着模式里的第1个子表达式, \2 代表着第2个子表达式, \3 代表着第3个, 依次类推。

比如下面这个是匹配html中的<h1>...</h1> ~ <h6>...</h6> 标签, 其中 ([1-6]) 就是为了回溯引用, 而括起来的子表达式:

<[hH]([1-6])>.*?</[hH]\1>

和本章关系不大的, 用来进行大小写转换的元字符:

元字符 说 明
\E 结束\L或\U转换
\l 把下一个字符转换为小写
\L 把\L到\E之间的字符全部转换为小写
\u 把下一个字符转换为大写
\U 把\U到\E之间的字符全部转换为大写

\l\u 只能把下一个字符(或子表达式)转换为小写或大写。

\L\U 将把它后面的所有字符转换为小写或大写, 直到遇上 \E 为止。

如下面, 将标签内的内容全部改为大写(sed里 () 需要转义):

$ cat test.txt
<h1>hello world</h1>
<h3>test regex</h3>

$ sed -n 's/<h\([1-6]\)>\(.*\)<\/h\1>/<h\1>\U\2\E<\/h\1>/gp' test.txt
<h1>HELLO WORLD</h1>
<h3>TEST REGEX</h3>

第9章 前后查找

前后查找 (lookaround, 对某一位置的前、后内容进行查找)

向前查找 (lookahead) 指定了一个必须匹配但不在结果中返回的模式。

向前查找模式是一个以 ?= 开头的子表达式, 需要匹配的文本跟在=的后面。

有些正则表达式文档使用术语 「消费」(consume)来表述「匹配和返回文本」的含义。在向前查找里, 被匹配的文本不包含在最终返回的匹配结果里, 这被称为“不消费”(查找出现在被匹配文本之后的字符,但不消费那个字符)。

向后查找 (lookbehind) 是查找出现在被匹配文本之前的字符(但不消费它), 向后查找操作符是 ?<=

$ cat test.txt
<HEAD>
<TITLE>Ben Fortas Homepage</TITLE>
</HEAD>

# 普通匹配
$ grep -Pzo -- '<TITLE>.*</TITLE>' test.txt
<TITLE>Ben Fortas Homepage</TITLE>

# 前向匹配
$ grep -Pzo -- '<TITLE>.*(?=</TITLE>)' test.txt
<TITLE>Ben Fortas Homepage

# 后向匹配
$ grep -Pzo -- '(?<=<TITLE>).*</TITLE>' test.txt
Ben Fortas Homepage</TITLE>

# 前后匹配
$ grep -Pzo -- '(?<=<TITLE>).*(?=</TITLE>)' test.txt
Ben Fortas Homepage

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

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

之前绍过一个用来对字符集合进行取非处理的操作符 ^, 但 ^ 不能用来对前后查找进行取非处理。这里必须使用另外一种语法: 前后查找必须用 ! 来取非(它将替换掉 =)

$ cat test.txt
I paid $30 for 100 apples, 50 oranges, and 60 pears. I saved $5 on this order.

# 正
$ grep -Pz -- '\b(?<=\$)\d+\b' test.txt
I paid $[30] for 100 apples, 50 oranges, and 60 pears. I saved $[5] on this order.

# 负
$ grep -Pz -- '\b(?<!\$)\d+\b' test.txt
I paid $30 for [100] apples, [50] oranges, and [60] pears. I saved $5 on this order.
操作符 说 明
(?=) 正向前查找
(?!) 负向前查找
(?<=) 正向后查找
(?<!) 负向后查找

第10章 嵌入条件

使用正则配合条件逻辑, 处理复杂的情况。

主要是两种情况:

回溯引用的条件表达式格式是:

(?(backreference)true-regex)

其中backreference是一个回溯引用, true-regex是一个只在backreference存在时才会被执行的子表达式。

更进一步, 还可以加上backreference不存在时的子表达式:

(?(backer- ference)true-regex|false-regex)

这点类似于C/C++中的三元运算符:

expr1? expr2 : expr3

例子, 想要匹配 要么以 xxx-xxx 开头, 要么以 (xxx)xxx的字符串 (x表示任意数字):

$ cat test.txt
123-456-7890
(123)456-7890
(123)-456-7890
(123-456-7890
1234567890
123 456 7890)

$ grep -Pzo -- '(\()?\d{3}(?(1)\)|-)\d{3}-\d{4}' test.txt
123-456-7890
(123)456-7890

其中, (?(1)\)|-) 就是回溯引用的条件表达式, (1) 引用前面的 (\(), 即如果有左圆括号, 才放上右圆括号, 否则是减号。

前后查找条件表达式, 和前后查找一个, 把括号里的引用编号改为一个前后查找表达式。

例子, 完整的一行要匹配 xxxxx, 或者 xxxxx-xxxx (x表示任意数字):

$ cat test.txt
11111
22222
33333-
44444-4444

$ grep -Pzo -- '\d{5}(-\d{4})?' test.txt
11111
22222
33333
44444-4444

$ grep -Pzo -- '\d{5}(?(?=-)-\d{4})' test.txt
11111
22222
44444-4444

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

这个就稍微复杂了。

另外, grep-x 直接支持整行匹配。

小结

实践出真知:

在使用正则表达式的时候, 你将发现几乎所有的问题都有不止一种解决方案。它们有的比较简单, 有的比较快速, 有的兼容性更好,有的功能更全。这么说吧, 在编写正则表达式的时候, 只有「对」、「错」两种选择的情况是相当少见的 —— 同一个问题往往会有多种解决方案。(第1章)

把必须匹配的情况考虑周全并写出一个匹配结果符合预期的正则表达式很容易,但把不需要匹配的情况也考虑周全并确保它们都将被排除在匹配结果以外往往要困难得多。(第7章)

看的这本书是图灵引进的中文版, 整体篇幅很小, 不过全部都实践一遍, 收获还是不少; 另外, 中文版下, 里面一些小错误还是不少……