概念
正则表达式可以进行文本的查找与替换,经常被简称为模式,它们其实是一些由字符构成的字符串。这些字符可以是普通字符(纯文本)或元字符(有特殊含义的特殊字符)。
BRE和ERE
POSIX 把正则表达式的实现分成了两类:基本正则表达式(BRE)和扩展的正则表达式(ERE)。BRE 和 ERE 之间有什么区别呢?这是关于元字符的问题。BRE 可以辨别以下元字符:
^ $ . [ ] *
其它的所有字符被认为是文本字符。ERE 添加了以下元字符(以及与其相关的功能):
( ) { } ? + |
然而,在 BRE 中,字符“(”,“)”,“{”,和“}”用反斜杠转义后, 被看作是元字符, 相反在 ERE 中,在任意元字符之前加上反斜杠会导致其被看作是一个文本字符。
注意:正如我们所见到的,当 shell 执行展开的时候,许多正则表达式元字符,也是对 shell 有特殊含义的字符。当我们在命令行中传递包含元字符的正则表达式的时候,把元字符用引号引起来至关重要,这样可以阻止 shell 试图展开它们。
句号(.)
. (英文句号)字符用来匹配任何一个单个的字符(有且仅有一个),甚至是 . 本身,因为 . 也是一个单个字符。
数据:
sales.xls
sales1.xls
orders3.xls
sales2.xls
sales3.xls
apac1.xls
europe2.xls
na1.xls
na2.xls
sa1.xls
# 把由字符串sales和另外一个字符构成的文件名查找出来
grep 'sales.' input.txt
注意:正则表达式可以用来匹配包含着字符串内容的模式。匹配的并不总是整个字符串,而是与某个模式相匹配的字符——即使它们只是整个字符串的一部分。在上面的例子里,我们使用的正则表达式并不能匹配整个文件名,它只匹配了文件名的一部分。如果你需要把某个正则表达式的匹配结果传递到其他代码或应用程序里做进一步处理,就必须记住这一细节差异。
匹配特殊字符
如果希望匹配字符本身而不是它在正则表达式中的特殊含义,就需要使用 \ 读对它进行转义。
# 把以 na或 sa开头的文件(不管它们后面跟着一个什么数字)找出来
grep '[ns]a[0-9]\.' input.txt
. 字符在正则表达式里有着特殊的含义,如果需要一个 . 本身,那就用 \ 对它进行转义。在正则表达式里, \ 字符永远出现在一个有着特殊含义的字符序列的开头,这个序列可以由一个或多个字符构成。
注意:如果需要搜索 \ 本身,就必须对 \ 字符进行转义;相应的转义序列是两个连续的反斜杠字符 \\。
匹配多个字符中的某一个
在正则表达式里,我们可以使用元字符"["和"]"来定义一个字符集合。在使用"["和"]"定义的字符集合里,这两个元字符之间的所有字符都是该集合的组成部分,字符集合的匹配结果是能够与该集合里的任意一个成员相匹配的文本。
数据:
The phrase 'regular expression' is often abbreviated as RegEx or regex.
# 可以匹配 RegEx, regex, Regex, regEx,但不匹配 REGEX。
grep '[Rr]eg[Ee]x' demo1.txt
匹配字符集合区间
在使用正则表达式的时候,会频繁地用到一些字符区间(0~9、A~Z,等等)。为了简化字符区间的定义,正则表达式提供了一个特殊的元字符——字符区间可以用 -(连字符)来定义。让我们回顾一下之前的例子:
grep '[ns]a[0-9]\.' input.txt
模式 [0-9]的功能与 [0123456789]完全等价,所以这次的匹配结果与刚才那个例子完全一样。字符区间并不仅限于数字,以下这些都是合法的字符区间:
- A-Z,匹配从 A到 Z的所有大写字母。
- a-z,匹配从 a到 z的所有小写字母。
- A-F,匹配从 A到 F的所有大写字母。
- A-z,匹配从ASCII字符 A到ASCII字符 z的所有字母。这个模式一般不常用,因为它还包含着 [和 ^等在ASCII字符表里排列在 Z和 a之间的字符。
字符区间的首、尾字符可以是ASCII字符表里的任意字符。但在实际工作中,最常用的字符区间还是数字字符区间和字母字符区间。
注意: -(连字符)是一个特殊的元字符,作为元字符它只能用在 [和 ]之间。在字符集合以外的地方, -只是一个普通字符,只能与 -本身相匹配。因此,在正则表达式里, -字符不需要被转义。
在字符区域中,我们看到这个连字符被特殊对待,所以我们怎样在一个正则表达式中包含 一个连字符呢?方法就是使连字符成为表达式中的第一个字符。
# 匹配包含任意大写字母的行
grep -h '[A-Z]' input.txt
# 匹配包含一个 -,A或Z的行
grep -h '[-AZ]' input.txt
取反匹配
字符集合通常用来指定一组必须匹配其中之一的字符。但在某些场合,我们需要反过来做,给出一组不需要得到的字符。换句话说,除了那个字符集合里的字符,其他字符都可以匹配。元字符"^"可以用来表示对一个字符集合取反匹配。
# 匹配包含na或sa,后跟一个字符不是数字的行,比如saf/nsx
grep '[ns]a[^0-9]' input.txt
注意:^必须出现在字符区间的第一个,且^的效果将作用于给定字符集合里的所有字符或字符区间,而不是仅限于紧跟在 ^字符后面的那一个字符或字符区间。
POSIX字符类
| 字符类 | 说明 |
|---|---|
| [:alnum:] | 任何一个字母或数字(等价于[a-zA-Z0-9]) |
| [:alpha:] | 任何一个字母(等价于[a-zA-Z]) |
| [:blank:] | 空格或制表符(等价于[\t ])# t后面有一个空格 |
| [:digit:] | 任何一个数字(等价于[0-9]) |
| [:lower:] | 任何一个小写字母(等价于[a-z]) |
| [:space:] | 任何一个空白字符,包括空格(等价于[\f\n\r\t\v ])# v后面有一个空格 |
| [:upper:] | 任何一个大写字母(等价于[A-Z]) |
| [:xdigit:] | 任何一个十六进制数字(等价于[a-fA-F0-9]) |
匹配一个或多个字符
要想匹配同一个字符(或字符集合)的多次重复,只要简单地给这个字符(或字符集合)加上一个+字符作为后缀就行了。+匹配一个或多个字符(至少一个;不匹配零个字符的情况)。比如,a匹配a本身,a+将匹配一个或多个连续出现的a。类似地, [0-9]匹配任意单个数字, [0-9]+将匹配一个或多个连续的数字。
注意:在给一个字符集合加上+后缀的时候,必须把+放在这个字符集合的外面。比如说,[0-9]+是正确的,[0-9+]则不是。[0-9+]其实也是一个合法的正则表达式,但它匹配的不是一个或多个数字;它定义了一个由数字0到9和+构成的字符集合,因而只能匹配一个单个的数字字符或加号。虽然合法,可它并不是我们需要的东西。
匹配零个或多个字符
+匹配一个或多个字符,但不匹配零个字符。+最少也要匹配一个字符。那么,如果你想匹配一个可有可无的字符,也就是该字符可以出现零次或多次的情况,这种匹配需要用*元字符来完成。*的用法与+完全一样——只要把它放在一个字符(或一个字符集合)的后面,就可以匹配该字符(或字符集合)连续出现零次或多次的情况。比如说,模式 B.* Forta将匹配 B Forta、B. Forta、Ben Forta和其他有类似规律的组合。
匹配零个或一个字符
?只能匹配一个字符(或字符集合)的零次或一次出现,最多不超过一次。如果需要在一段文本里匹配某个特定的字符(或字符集合)而该字符可能出现、也可能不出现, ?无疑是最佳的选择。
# 简单匹配网址
# ?在这里的含义是:我前面的字符( s)要么不出现,要么最多出现一次
# 换句话说, https?://既可以匹配 http://,也可以匹配 https://
grep 'https?://[[:alnum:]./]+'
# 'http[s]?://[[:alnum:]./]+'和'http[s]?://[[:alnum:]./]+完全等价,不过使用
# []元字符定义了一个字符的集合,一眼就能看出 ? 修饰的是 s
匹配的重复次数
正则表达式里的 +、 *和 ?解决了许多问题,但有些问题光靠它们还不够。请思考以下问题:
- +和 *匹配的字符个数没有上限。我们无法为它们将匹配的字符个数设定一个最大值。
- +、 *和 ?至少匹配零个或一个字符。我们无法为它们将匹配的字符个数另行设定一个最小值。
- 如果只使用 +和 *,我们无法把它们将匹配的字符个数设定为一个精确的数字。
元字符 {} 用来匹配特定个数的元素。{}元字符都被用来表达要求匹配的最小和最大数目。它们可以通过四种方法来指定:
| 限定符 | 意思 |
|---|---|
| n | 匹配前面的元素,如果它确切地出现了 n 次。 |
| n,m | 匹配前面的元素,如果它至少出现了 n 次,但是不多于 m 次。 |
| n, | 匹配前面的元素,如果它至少出现了 n 次。 |
| ,m | 匹配前面的元素,如果它出现的次数不多于 m 次。 |
# 连续出现3个数字
grep -E '[0-9]{3}'
# 至少出现3个数字
grep -E '[0-9]{3,}'
# 至多出现三个数字
grep -E '[0-9]{,3}
# 出现三个以上六个以下数字
grep -E '[0-9]{3,6}'
+ 类似于{1,}
* 类似于{0,}
? 类似于{0,1}
防止过度匹配
数据:
This offer is not available to customers living in <B>AK</B> and <B>HI</B>.
# 查找出文本中的B标签
grep '<Bb>.*</Bb>' html.txt
这个模式只找到了一个匹配而不是预期中的两个:第一个 <B>标签之后、最后一个 </B>标签之前的所有东西—— AK</B> and <B>HI——被 .*一网打尽。虽然没有漏掉我们想要匹配的文本,但问题是第2个 <B>标签不明不白地“失踪”了。
因为 *和 +都是所谓的“贪婪型”元字符,它们在进行匹配时的行为模式是多多益善而不是适可而止的。它们会尽可能地从一段文本的开头一直匹配到这段文本的末尾,而不是从这段文本的开头匹配到碰到第一个匹配时为止。
在不需要这种“贪婪行为”的时候该怎么办?答案是使用这些元字符的“懒惰型”版本(“懒惰”在这里的含义是匹配尽可能少的字符——与“贪婪型”元字符的行为模式刚好相反)。懒惰型元字符的写法很简单,只要给贪婪型元字符加上一个 ?后缀即可。
| 贪婪型元字符 | 懒惰型元字符 |
|---|---|
| * | *? |
| + | +? |
| {n,m} | {n.m}? |
# 使用懒惰的 *?,第一个匹配将仅限于 AK,原始文本里的 <B>HI</B>成为了第二个匹配。
# -P参数表示grep采用Perl的正则,可以支持懒惰匹配
# 测试过BRE和ERE均不支持
grep -P '<Bb>.*?</Bb>' html.txt
单词边界
限定符 \b指定单词边界,顾名思义, \b用来匹配一个单词的开始或结尾。
数据:
The cat scattered his food all over the room.
# 不使用单词边界
grep 'cat' cat.txt
# 使用单词边界
grep '\bcat\b' cat.txt
可以看到,没有边界修饰的时候,scattered也会被匹配出来。在有边界修饰的时候,scattered中的字符序列cat不能与这个模式相匹配,因为它的前一个字符是s、后一个字符是t(这两个字符都不能与\b相匹配)。如果想匹配一个完整的单词,就必须在想要匹配的文本的前后都加上 \b限定符:\bcat\b表示完整匹配cat这个单词,而\bcat表示以cat开头的单词。
注意:\b匹配的是一个这样的位置,这个位置位于一个能够用来构成单词的字符(字母、数字和下划线,也就是与 \w相匹配的字符)和一个不能用来构成单词的字符(也就是与 \W相匹配的字符)之间。
提示:\w相当于[a-zA-Z0-9_],\W相当于[^a-zA-Z0-9_]。
限定符\B表示不匹配一个单词边界。
数据:
Please enter the nine-digit id as it appears on your color - coded pass-key.
# \b匹配的位置一边属于\w,一边属于\W
# 连字符-已经属于\W了,因此只要连字符左右两边的字符属于\w就可以匹配
# 因此这里匹配出nigne-digit中间的连字符
grep '\b-\b' backslash-B.txt
# \B匹配的位置不属于单词边界,也就是说如果两个字符都属于\w或者\W,那就不是边界了
# 这里连字符属于\W,因此只要连字符左右两边的字符也属于\W就可以了
grep '\B-\B' backslash-B.txt
字符串边界
单词边界可以用来进行与单词有关的位置匹配(单词的开头、单词的结束、整个单词,等等)。字符串边界有着类似的用途,只不过是用来进行与字符串有关的位置匹配而已(字符串的开头、字符串的结束、整个字符串,等等)。用来定义字符串边界的元字符有两个:一个是用来定义字符串开头的 ^,另一个是用来定义字符串结尾的 $。
子表达式
用来表明重复次数的元字符(如?或*或{2},等等)只作用于紧挨着它的前一个字符或元字符,而子表达式是一个更大的表达式的一部分;把一个表达式划分为一系列子表达式的目的是为了把那些子表达式当作一个独立元素来使用。子表达式必须用 (和 )括起来。
数据:
Windows 10 is a new generation of OS from MicroSoft.
 是html中的不换行空格。
假如Windows和10之间有不少于两个 ,如何用正则匹配呢?
# 没有使用子表达式,导致 * 只作用于在它前面的 ;,也就是只匹配 ;;;;这种的
grep 'Windows {2,}10' win10.txt
# 使用子表达式,这样 就变成了一个整体
grep 'Windows( ){2,}10' win10.txt
Alternation
扩展表达式有一个特性叫做alternation(交替),其是一款允许从一系列表达式之间选择匹配项的实用程序。就像中括号表达式允许从一系列指定的字符之间匹配单个字符那样,alternation允许从一系列字符串或者是其它的正则表达式中选择匹配项。
# 没有使用alternation
echo "AAA" | grep -E 'AAA'
echo "BBB" | grep -E 'AAA' # 没有匹配的,因而没有输出
# 使用alternation以竖杠线元字符为标记:
echo "AAA" | egrep 'AAA|BBB'
echo "BBB" | egrep 'AAA|BBB'
这里我们看到正则表达式‘AAA|BBB’,这意味着“匹配字符串 AAA 或者是字符串 BBB”。 注意因为这是一个扩展的特性,我们给 grep 命令(虽然我们能以 egrep 程序来代替)添加了 -E 选项,并且我们把这个正则表达式用单引号引起来,为的是阻止 shell 把竖杠线元字符解释为一个 pipe 操作符。Alternation 并不局限于两种选择:
echo "AAA" | grep -E 'AAA|BBB|CCC'
为了把 alternation 和其它正则表达式元素结合起来,我们可以使用 () 来分离 alternation。
grep -Eh '^(bz|gz)' dirlist*.txt
这个表达式将会在我们的列表中匹配以“bz”或“gz”开头的文件名。如果我们删除了圆括号,这个表达式的意思:
grep -Eh '^bz|gz' dirlist*.txt
会变成匹配任意以“bz”开头或包含“gz”的文件名。
回溯引用——前后一致匹配
假设你有一段文本,你想把这段文本里所有连续重复出现的单词(其中有一个单词输了两遍)找出来。显然,在搜索某个单词的第二次出现时,这个单词必须是已知的。回溯引用允许正则表达式模式引用前面的匹配结果(具体到这个例子,就是前面匹配到的单词)。
数据:
This is a block of of text, several words here are are repeated, and and theyshould not be.
# 这里如果不用回溯引用是无法完成的
grep '[ ]+(\w+)[ ]+\1' backtrack.txt
这个模式找到了我们想要的东西,但它是如何做到这一点的呢? [ ]+匹配一个或多个空格, \w+匹配一个或多个字母数字字符, [ ]+匹配随后的空格。注意, \w+是括在括号里的,它是一个子表达式。这个子表达式不是用来进行重复匹配的,这里根本不涉及重复匹配的问题。这个子表达式只是把整个模式的一部分单独划分出来以便在后面引用。这个模式的最后一部分是 \1;这是一个回溯引用,而它引用的正是前面划分出来的那个子表达式:当 (\w+)匹配到单词 of的时候, \1也匹配单词 of;当 (\w+)匹配到单词 and的时候, \1也匹配单词 and。
\1到底代表着什么?它代表着模式里的第1个子表达式,\2代表着第2个子表达式、\3代表着第3个;依次类推。于是,在上面那个例子里, [ ]+(\w+)[ ]+\1将匹配同一个单词的连续两次重复出现。
注意:回溯引用指的是模式的后半部分引用在前半部分中定义的子表达式(如上例所示)。
向前查找
向前查找指定了一个必须匹配但不在结果中返回的模式。向前查找实际就是一个子表达式,而且从格式上看也确实如此。从语法上看,一个向前查找模式其实就是一个以 ?=开头的子表达式,需要匹配的文本跟在 =的后面。
从下列URL中提取出协议名:
http://linux.org
https://apple.com
ftp://vimgenuis.com
# 不使用向前查找
# 协议以 :作为分隔
grep '\w*:' uri.txt
可以看到,虽然我们查找到了协议名,但作为分隔符的 : 也出现在结果中,这不是我们想要的。
# 使用向前查找
grep -P '\w*(?=:)' uri.txt
向后查找
?=将向前查找(查找出现在被匹配文本之后的字符,但不消费那个字符)。因此,?=被称为向前查找操作符。除了向前查找,许多正则表达式实现还支持向后查找,也就是查找出现在被匹配文本之前的字符(但不消费它),向后查找操作符是?<=。?<=与 ?=的具体使用方法大同小异;它必须用在一个子表达式里,而且后跟要匹配的文本。
找出下列数据出现的金额:
ABC01: $23.45
HGG42: $5.31
CFMX1: $899.00
XTC99: $69.96
Total items found: 4
grep '\$([0-9]*)\.[0-9]*' money.txt
如果不想让 $出现在最终的匹配结果里,该怎么办?不能简单的只匹配数字,因为有可能匹配到不是金额的情况,比如ABC01里的01。
# 使用向后查找,匹配$但不把它列入最终的结果
grep -P '(?<=\$)[0-9.]+' money.txt