正则表达式入门

强大的字符匹配工具

Posted by Xiaosheng on March 13, 2017

1 前言

正则表达式用来指定字符串的模式,其最常见的应用就是搜索字符串。作为一个工具,正则表达式非常有用,并且功能强大,因此经常可以看到整本书和整个网站专门来讨论这个话题。有时候,可能会创建非常复杂的正则表达式。但是多数时候,所需的正则表达式都比较简单和直观。因此在学习正则表达式时,只需学习一些简单的规则,然后不断的练习。本文的目标就是提供一个入门介绍。

下文中,将示范许多使用 grep 命令的例子。如果发现某些正则表达式的特性不适合自己的 grep,则可以试着使用 egrepgrep -E。除了 grep ,正则表达式还可以在许多 Unix 程序中使用,例如 vimlesssed 等。另外,正则表达式还可以用于许多编程语言中,例如 Awk、C、C++、C#、Java、Perl、PHP、Python、Ruby、Tcl 和 VB.NET。

下面将介绍典型的正则表达式通用概念,一旦掌握了这些基本规则,那么需要时只需学习少数几种变体即可。

2 简介

正则表达式(regular expression)通常简写为 regex 或 re,是一种指定字符串模式的简洁方式。例如,考虑下面由 3 个字符串组成的一组字符串:

harley1 harley2 harley3

作为正则表达式,可以使用 harley[123] 表示这组模式。如果希望描述一组包含大写字符“H”,后面跟任意数量的小写字母,最后是小写字母“y”的字符串,那么所使用的正则表达式是 H[[:lower:]]*y

可以看出,正则表达式的强大来自拥有特殊含义的元字符和缩写的使用。后面我们将对此进行详细的讨论。出于参考目的,下面列出了 3 张表,汇总了正则表达式的使用语法。

元字符 含义
. 除新行字符外,匹配任意的单个字符
^ 锚:匹配行的开头
$ 锚:匹配行的末尾
\< 锚:匹配单词的开头
\> 锚:匹配单词的末尾
[list] 字符类:匹配 list 中的任何字符
[^list] 字符类:匹配不在 list 中的任何字符
() 组:视为一个单独的单元
| 交变:匹配选择之一
\ 引用:从字面上解释元字符
运算符 含义
* 匹配 0 次或多次
+ 匹配 1 次或多次
? 匹配 0 次或 1 次
{n} 限定:匹配 n 次
{n,} 限定:最少匹配 n 次
{0,m} 限定:最多匹配 m 次
{n,m} 限定:最少匹配 n 次,最多匹配 m 次
含义 类似于
[:lower:] 小写字母 a-z
[:upper:] 大写字母 A-Z
[:alpha:] 大小写字母 A-Za-z
[:alnum:] 大小写字母、数字 A-Za-z0-9
[:digit:] 数字 0-9
[:punct:] 标点符号  
[:blank:] 空格或制表符(空白符)  

Unix 支持 2 种主要的正则表达式变体:一个现代版本,一个以前的废弃版本。现代版本是扩展正则表达式(extended regular expression),简称为 ERE,它是当前的标准,属于 IEEE 1003.2 标准。以前版本是基本正则表达式(basic regular expression),简称为 BRE,被 1003.2 标准所取代。

两条极有可能遇到问题的命令是 grepsed。如果系统使用的是 GNU 实用工具,例如 Linux 和 FreeBSD 系统,那么一些命令已经升级为提供 -E 选项,从而允许使用扩展正则表达式。因为扩展正则表达式更可取,所以应该养成使用 -E 的习惯。

3 开始匹配

3.1 匹配行和单词

假设有一个文件 data,包含 4 行内容:

Harley is smart
Harley
I like Harley
the dog likes the cat

我们可以使用 grep 查看所有包含有“Harley”的行:grep Harley dataHarley 实际上是一个正则表达式,它将选取第 1、2 和 3 行。我们还可以使用锚指定所查找模式的位置,例如为了搜索那些以“Harley”开头的行,可以使用:grep '^Harley' data,它将选取第 1 行和第 2 行。

命令行中,建议引用正则表达式(通过单引号),以确保 shell 不理会这些字符,将它们传递给程序。

为了搜索那些以“Harley”结尾的行,可以使用:grep 'Harley$' data,它将选取第 2 行和第 3 行。^$ 可以在一个正则表达式中组合使用,例如要搜索整行就是一个单词“Harley”的行,可以同时使用这两个锚:grep '^Harley$' data,这将选取第 2 行。通过使用这两个锚,但是之间不指定任何内容,就可以方便地查找空行。例如,grep '^$' data | wc -l 将统计文件 data 中的空行数量。

类似的,也可以使用锚来匹配单词的开头或末尾,或者两者都进行匹配。为了匹配一个单词的开头,可以使用 \<;为了匹配单词的末尾,可以使用 \>

例如,希望搜索文件 data 中所有包含字符串“kn”的行,但“kn”只能出现在单词的开头,可以使用:grep '\<kn' data。查找字符串“ow”,但是“ow”只能出现在单词的末尾,可以使用:grep 'ow\>' data。为了搜索完整的单词,可以同时使用 \<\>。例如,为了搜索“know”,并且“know”必须是一个完整的单词,可以使用:grep '\<know\>' data

在使用 GNU 实用工具的系统上,例如 Linux 和 FreeBSD,可以使用 \b 代替 \<\>。例如,grep '\<know\> data'grep '\bknow\b data' 等价。

在正则表达式中,单词就是一个自包含的,由字母、数字或下划线字符构成的连续字符序列。fussbudget Weedly 1952 error_code_5 这些都是单词。

3.2 匹配字符:字符类

假设希望搜索文件 data,查找所有包含下述模式的行:字符串“Har”,后面跟两个任意字符,再后跟一个字母“y”,可以使用:grep 'Har..y' data. 匹配的是任意字符,而有时我们希望匹配特定的字符。例如,希望搜索一个大写字母“H”,后面跟“a”或“A”,这时可以通过将字符放在方括号 [] 中来指定希望搜索的字符,这样的结构就称为一个字符类

例如在文件 data 中搜索所有包含“H”,后面跟“a”或者“A”的行,可以使用:grep 'H[aA]' data。在同一个正则表达式中可以多次使用字符类。例如要搜索包含“license”的行,且要求由于混用“c”和“s”而引起错误拼写的单词也可以搜索出来:grep 'li[cs]en[cs]e' data

下面示范一个更有用的命令,该命令使用 \<\> 或者 \b 只匹配整个单词:

grep '\<li[cs]en[cs]e\>' data
grep '\bli[cs]en[cs]e\b' data

这两条命令将匹配以下任一单词:licence license lisence lisense

3.3 预定义字符类

一些字符集比较常见,因此被冠以相应的名称,这些字符集称为预定义字符类

例如,要查找文件 data 中包含数字 21,后面跟一个小写字母或大写字母的所有行:grep '21[[:alpha:]]' data。查找包含两个连续大写字母,后面跟一个数字,再跟一个小写字母的所有行:grep '[[:upper:]][[:upper:]][[:digit:]][[:lower:]]' data

除了预定义字符类外,还有一种方式指定一组字母或数字,即使用字符范围,具体方法是将第一个字符和最后一个字符用连字符分开。例如为了搜索文件 data 中所有包含数字 3 至 7 的行,可以使用:grep '[3-7]' data。为了搜索包含大写字母“X”,后面跟任意两个数字的行,可以使用下面两条命令中的一个:

grep 'X[0-9][0-9]' data
grep 'X[[:digit:]][[:digit:]]' data

如果要匹配不在特定字符类之中的字符,只需在开头的左方括号之后放一个 ^ 即可。例如,要搜索文件 data,查找包含字母“X”,同时后面不跟有“a”或“o”的所有行:grep 'X[^ao]' data。下述两条命令搜索所有包含至少一个非字母字符的行:

grep '[^A-Za-z]' data
grep '[^[:alpha:]]' data

每个字符类,不管看上去多么复杂,实际上只表示一个单独的字符。

3.4 使用范围和预定义字符类

当希望匹配所有的大写或小写字母时,既可以使用预定义字符类,也可以使用范围。例如,搜索文件 data 中所有包含字母“H”,后跟任何从 a 到 z 的小写字母的行,例如“Ha”、“Hb”、“Hc”等:grep 'H[[:lower:]]' datagrep 'H[a-z]' data。再比如要搜索所有包含一个单独的大写或小写字母,后跟一个单独的数字,再后跟一个小写字母的行:grep '[A-Za-z][0-9][a-z]' datagrep '[[:alpha:]][[:digit:]][[:lower:]]' data

下面展示一个更复杂的例子——搜索加拿大邮政编码。加拿大邮政编码的格式为“字母 数字 字母 空格 数字 字母 数字”,其中所有的字母都是大写字母,例如 M5P 3G4。可以这样写:

grep '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]' data
grep '[[:upper:]][[:digit:]][[:upper:]] [[:digit:]][[:upper:]][[:digit:]]' data

范围比名称更容易键入,但是名称更加可读。并且不管使用哪一种区域设置或语言,名称总是正确的,因此它们的移植性更出色。

3.5 重复运算符

在正则表达式中,单个的字符(例如 A)或者字符类(例如 A-Z)只匹配一个字符。为了一次匹配多个字符,可以使用重复运算符(repetition operator)

最有用的重复运算符就是 *,一个 * 可以匹配前面字符的 0 次或多次出现。例如,假设希望搜索文件 data 中所有包含大写字母“H”,后面跟 0 个或多个小写字母的行:grep 'H[a-z]*' datagrep 'H[[:lower:]]*' data。这将匹配例如 H Har Harley 这样的模式。

最常见的组合就是使用一个 . 后跟一个 *。这将匹配任何字符的 0 次或多次出现。例如要搜索包含“error”,后跟 0 个或多个字符,再后跟“code”的行:grep 'error.*code' data。这将匹配例如 Don’t make an error while you are writing code. 这样的行。

有时候需要匹配 1 个或多个字符,这时可以使用 +。例如搜索文件 data 中包含字符串“variable”,后跟 1 个或多个数字的行:grep 'variable[0-9]+' datagrep 'variable[[:digit:]]+' data

下一个重复运算符是 ?,它允许匹配某个实例 0 次或 1 次(该实例是可选的)。例如,希望查找文件 data 中所有包含单词“color”(美国拼法)或“colour”(英国拼法)的行,可以使用:grep 'colou?r' data

最后一个重复运算符通过使用方括号创建所谓的限定(bound)来指定字符出现的次数。限定有 4 种不同的类型:

  • {n}:正好匹配 n 次
  • {n,}:至少匹配 n 次
  • {0,m}:最多匹配 m 次
  • {n,m}:至少匹配 n 次,最多匹配 m 次

例如要在文件 data 中查找所有包含 2 个数字或者 3 个数字的行:grep '\<[0-9]{2,3}\> data'

到目前为止,我们只对单个字符使用过重复运算符,如果将多个字符用圆括号括起来,也可以对多个字符使用重复运算符,这样的模式成为。通过创建组,可以将一串字符视为一个单元。例如,匹配字符串“xyz” 5 次,可以使用下述两种正则表达式之一:xyzxyzxyzxyzxyz(xyz){5}

最后一个重复运算符是 |,它允许使用交变,也就是说可以匹配这一个,也可以匹配另一个。例如,希望在文件中搜索包含 cat dog bird 中任意一个单词的行:grep '\<(cat|dog|bird)\>'

如果希望匹配元字符,例如匹配真实的 ***、.** 或 |。需要使用 \ 引用这些字符,把这些字符从元字符变成常规字符,从而从字面上解释这些字符。例如,为了搜索文件 data,查找包含“$”字符的行,可以使用:grep '\$' data。如果希望搜索反斜线本身,只需连续使用两个反斜线。

4 使用示例

在 Unix 系统中,自带了一个包含大量英语单词的字典文件,名称是 words。字典中,每个单词一行,各行以字母顺序排列。这个文件的常见位置有:

  • /usr/share/dict/words
  • /usr/dict/words
  • /usr/share/lib/dict/words

我们将使用这个文件来做一些非常有趣的查询。

查询以“qu”开头并以“y”结尾的单词:grep '^qu[a-z]+y$' words

查找一个包含所有 5 个元音字母 a、e、i、o、u(并以该顺序出现),且以 a 开头以 u 结尾的英语单词:

grep 'a[a-z]*e[a-z]*i[a-z]*o[a-z]*u' words

最后一个问题,希望查找 /bin 目录下所有以两个字母命名的程序:ls /bin | grep '^[a-z]2$'ls /bin | grep '^[a-z][a-z]$'

参考

Harley Hahn 《Unix & Linux 大学教程》