Linux
中大部分时间都是在处理文本内容,而为了更加快速和自动化的处理文本,我们就需要正则表达式。正则表达式提供了功能强大、灵活而又高效的方式来处理文本。正则表达式的模式匹配可以使你在大量的文本内容中快速找到特定的字符序列。验证文本来确保所匹配的模式符合某种规则:如电子邮件地址,还可以提起,编辑,删除文本字符串。对于快速处理大量的文本内容,正则表达式是不可或缺的工具。
正则表达式是你所定义的模式模版(pattern template
),Linux
工具可以使用它来过滤和处理文本。如grep
,sed,
awk`等工具都支持正则表达式。如果数据匹配模式,它就会被接受并进行下一步处理,如果数据不匹配模式则它会被丢弃。下图描述下这个过程:
文本数据 -----> 正则表达式 -------> 匹配的数据
|
|
|
↓
丢弃的数据
1. 正则表达式的类型
正则表达式是通过正则表达式引擎(regular expression engine)实现的。正则表达式引擎是一套底层软件,负责解释正则表达式模式并使用这些模式进行文本匹配。
在Linux
中,有两种流行的正则表达式引擎:
- BRE: POSIX基础正则表达式引擎(basic regular expression)
- ERE: POSIX扩展正则表达式引擎(extended regular expression)
2. BRE模式
最基本的BRE
模式就是匹配数据流中的文本字符。
2.1 纯文本
下面使用sed
程序使用正则表达式过滤数据.
$ echo "This is a test" | sed -n '/test/p'
This is a test
$ echo "This is a test" | sed -n '/tset/p'
$
$ echo "This is a test" | sed -n '/this/p'
$
$ echo "This is a test" | sed -n '/This/p'
This is a test
$
第一个例子使用test
来匹配数据流中的文本。由于echo
语句中包含了单词test
,所以sed
能匹配到该文本模式,然后使用p
命令输出匹配到的文本行。第二个例子使用tset
,然后文本流中并没有这个单词,所以sed
并没有匹配到这个模式,就没有输出任何数据。第三个例子使用的this
,这次也没有匹配成功,应该文本流中并没有小写的this
,所以也没有匹配成功。第四次文本流中出现了This
,所以匹配成功。
正则表达式并不关心模式匹配出现在数据流的位置,它也不关心模式出现了多少次,一旦匹配到该模式,则会把数据流中的文本行传回给Linux
命令。但是正则表达式会区分大小写,它们只匹配字符序列和模式一样的文本行。匹配到的文本行中必须出现模式,而且文本流中匹配到的字符序列必须和模式中的字符顺序一样。
$ echo "There are many dog." | sed -n '/dogs/p'
$
$ echo "There are many dogs." | sed -n '/dogs/p'
There are many dogs.
$
上面的第一个例子匹配dogs
,但是文本流中只有dog
,所以匹配失败。第二个例子则成功的匹配到了dogs
。所以模式中的字符串必须安装顺序全部出现在文本流中才能匹配成功。
如果只使用上面这张字符串匹配则并不能一次处理大量的文本。如果想一次匹配很多字符串则必须把它们都写出来,这并不能发挥正则表达式的强大能力,下面会介绍正则表达式中非常有用的一些特殊字符来构造复杂的模式。
2.2 特殊字符
有些字符在正则表达式中有特别的含义,结合这些特殊字符可以构造复杂的模式匹配。这些特殊字符包括:
.*[]^$\+?|()
因为这些特殊字符在正则表达式中具有特殊意义,如果只是想把它们作为文本字符来匹配则需要使用\
来转义。而且如果想匹配反斜线也必须使用\
来转义。
$ echo "\ is a special character" | sed -n '///p'
$
$ echo "\ is a special character" | sed -n '/\//p'
\ is a special character
$
2.3 锚字符
默认情况下,当匹配模式时,只要模式在数据流的任何位置出现,模式匹配都会成功。有两个字符可以现在模式在数据流中出现的位置。
2.3.1 行首
^
字符定义从数据流中文本行的行首开始的模式。如果模式出现在行首则匹配成功,否则无法匹配。要使用^
,必须将它放到正则表达式中模式的前面。
$ echo "Make American great again." | sed -n '/^make/p'
$
$ echo "Make American great again." | sed -n '/^Make/p'
Make American great again.
2.3.2 行尾
和^
一样,$
表示模式必须出现在行尾。将这个特殊字符放在文本模式后面来指明数据必须以该文本模式结尾。
$ echo "MAKE AMERICAN GREAT AGAIN." | sed -n '/again$/p'
$
$ echo "MAKE AMERICAN GREAT AGAIN." | sed -n '/AGAIN$/p'
MAKE AMERICAN GREAT AGAIN.
2.3.2 组合锚点
在某些情况下可以在同一行中把行首锚点和行尾锚点组合起来使用。一种情况是为了查找只含有特定文本的数据行。
$ cat data1.txt
make american great again.
Make american great again.
MAKE AMERICAN GREAT AGAIN.
$ sed -n '/^MAKE AMERICAN GREAT AGAIN.$/p' data1.txt
MAKE AMERICAN GREAT AGAIN.
$
还有一个情况就是在行首锚点和行尾锚点之间不加任何文本,这样就会过滤出数据流中的空白行。
$ cat data2.txt
make american great again.
Make american great again.
MAKE AMERICAN GREAT AGAIN.
$ sed -n '/^$/d' data2.txt
make american great again.
Make american great again.
MAKE AMERICAN GREAT AGAIN.
$
上面定义的正则表达式模式会查找行首和行尾之间什么都没有的数据行。然后使用d
命令来删除匹配到的空白行,因此输出的文本流中就没有空白行了。这是从文档中删除空白行的很有效的方法。
2.4 点字符
特殊字符点号用来匹配除换行符之外的任意一个字符。在点号出现的位置必须有且只有一个字符,才能匹配成功
$ cat zip.txt
bzip2
bunzip
gzip
gunzip
unzip
zip
$ sed -n '/.zip/p' zip.txt
bzip2
bunzip
gzip
gunzip
unzip
$
上面的例子中.zip
表示最少得匹配4个字符,其中第一个可以是任意字符,后面三个必须是zip
。所以最后一行没有匹配成功,其他则都匹配成功。
2.5 字符组
特殊字符点号在匹配某个位置上的任意字符时很有效,但是如果想限制字符的范围的时候就没有那么给力了。但是正则表达式中的字符组(character class)
可以用来限定待匹配的字符是一组字符中任意一个。如果字符组中的某个字符出现在数据流中则匹配成功。
使用方括号来定义一个字符组。方括号中的字符是你希望匹配的字符,然后你就可以使用整个组来匹配字符。
$ sed -n '[bg]zip' zip.txt
bzip2
gzip
$
上面的模式匹配中的方括号就是字符组,只要匹配到其中的任何一个字符都会匹配成功。这个匹配模式可以匹配bzip
和gzip
。字符组中的位置必须要有其中的一个字符出现才能匹配成功。字符组中不仅能使用字符,也可以使用数字。
$ cat data3.txt
This is test line0.
This is test line1.
This is test line2.
This is test line3.
This is a end line.
$ sed -n '/[12345]/p' data3.txt
This is test line1.
This is test line2.
This is test line3.
$
这个正则表达式模式匹配数据行中有数字0
,1
,2
,3
,4
,5
的行。其他数字以及没有数字的行都会被过滤。
可以讲字符组组合在一起以检查特定的字符序列是否符合某种格式:比如电话号码和邮编。
cat data4.txt
606333
462018
2230011
4353
51900
$ sed -n '
> /[0123456789][0123456789][0123456789][0123456789][0123456789][0123456789]/
> ' data4.txt
606333
462018
2230011
上面的例子成功的过滤了6
位数以下的邮编,但是它也让7
位数的邮编通过了模式匹配。正则表达式只有在文本流中匹配到了最小的模式字符串就匹配成功,上面的例子中7
位数字中的前6
位已经匹配成功,则整行文本都会匹配成功。如果想只匹配长度为6个数字的邮编则可以使用行首行尾限定符:
$ sed -n '
> /^[0123456789][0123456789][0123456789][0123456789][0123456789][0123456789]$/
> ' data4.txt
606333
462018
$
2.6 区间
上面的例子只是匹配6个数字的邮编,如果我们想匹配11位的移动手机号码,如果按照上面的方式...:( 。好在有一种可以方式可以避免写那么多字母和数字。这就是区间,只要在方括号的开始和结束指定一个字符,中间使用破折号分隔这两个字符就可以了。现在可以优化下上面的邮编的例子:
$ sed -n '
> /^[0-9][0-9][0-9][0-9][0-9][0-9]$/
> ' data4.txt
606333
462018
$
这个模式和上面的是一样的,只能匹配6位数字。同样的方法也使用于字母字符:
$ sed -n '/[c-h]at/p' data5.txt
The cat is sleeping.
That is a very nice hat.
$
上面的例子中[c-h]
匹配c
和h
之间的所有字母。还可以在单个字符组中指定多个字符区间:
$ sed -n '/[c-hx-z]at/p' data5.txt
The cat is sleeping.
That is a very nice hat.
$
2.7 排除型字符组
在正则表达式中我们也可以反转字符组的作用。就是匹配不在字符组中的字符。在字符组的前面加上^
,就可以匹配字符组之外的字符了。
$ sed -n '/[^c-h]at/p' data5.txt
$
通过排除型字符组,可以匹配除了c
和h
之间的所有字符。
2.8 特殊的字符组
除了自己定义的字符组之外,BRE
还包含了一些特殊的字符组,可以用来匹配特定类型的字符。
组 | 描述 |
---|---|
[[:alpha:]] | 匹配任意字母字符,不管是大写还是小写 |
[[:alnum:]] | 匹配任意字母数字字符09、AZ或a~z |
[[:blank:]] | 匹配空格或制表符 |
[[:digit:]] | 匹配0~9之间的数字 |
[[:lower:]] | 匹配小写字母字符a~z |
[[:print:]] | 匹配任意可打印字符 |
[[:punct:]] | 匹配标点符号 |
[[:space:]] | 匹配任意空白字符:空格、制表符、NL、FF、VT和CR |
[[:upper:]] | 匹配任意大写字母字符A~Z |
可以在正则表达式中将特殊表达式当作普通字符组使用:
$ echo "abc" | sed -n '/[[:digit:]]/p'
$
$ echo "abc" | sed -n '/[[:alpha:]]/p'
abc
$ echo "abc123" | sed -n '/[[:digit:]]/p'
abc123
$ echo "This is, a test" | sed -n '/[[:punct:]]/p'
This is, a test
$ echo "This is a test" | sed -n '/[[:punct:]]/p'
$
2.9 星号
在字符后面放置星号表面前面的字符必须在模式匹配中出现0次或多次。
$ echo "k" | sed -n '/o*k/p'
k
$ echo "ok" | sed -n '/o*k/p'
ok
$ echo "oooook" | sed -n '/o*k/p'
oooook
$
o*
表示模式中可以有0个o
,1个o
,或者多个o
。
3. 扩展正则表达式
POSIX ERE
模式包括了BRE
而且还提供了一些额外的特殊符号。下面来介绍ERE
模式中剩下的特殊符号。
3.1 问号
问号类似于星号,但是问号和星号又不同。问号表示前面的字符只能出现0
次或1
次,不能是其他次数。就是说问号前面的字符要么不出现,要么只出现一次,其他情况都会匹配失败。
$ echo "ct" | gawk '/ca?t/{print $0}'
ct
$ echo "cat" | gawk '/ca?t/{print $0}'
cat
$ echo "caat" | gawk '/ca?t/{print $0}'
$
$ echo "caaat" | gawk '/ca?t/{print $0}'
$
?
表示前面的字符只能出现0
次或1
次,其他情况都不会匹配成功。?
也可以和字符组一起使用:
$ echo "ct" | gawk '/c[au]?t/{print $0}'
ct
$ echo "cat" | gawk '/c[au]?t/{print $0}'
cat
$ echo "cut" | gawk '/c[au]?t/{print $0}'
cut
$ echo "cot" | gawk '/c[au]?t/{print $0}'
$
$ echo "caut" | gawk '/c[au]?t/{print $0}'
$
$ echo "caet" | gawk '/c[au]?t/{print $0}'
$
3.2 加号
+
和*
、?
类似,加号表示前面的字符最少出现1
次。如果加号前面的字符没有出现则匹配失败。
$ echo "ct" | gawk '/ca+t/{print $0}'
$
$ echo "cat" | gawk '/ca+t/{print $0}'
cat
$ echo "cut" | gawk '/ca+t/{print $0}'
$
$ echo "caat" | gawk '/ca+t/{print $0}'
caat
$ echo "caet" | gawk '/ca+t/{print $0}'
$
$ echo "caaat" | gawk '/ca+t/{print $0}'
caaat
$
如果a
字符没有出现则匹配失败。+
也可以作用于字符组:
$ echo "ct" | gawk '/c[au]+t/{print $0}'
$
$ echo "cat" | gawk '/c[au]+t/{print $0}'
cat
$ echo "cut" | gawk '/c[au]+t/{print $0}'
cut
$ echo "caat" | gawk '/c[au]+t/{print $0}'
caat
$ echo "caut" | gawk '/c[au]+t/{print $0}'
caut
$ echo "caaaaut" | gawk '/c[au]+t/{print $0}'
caaaaut
$
3.3 花括号
ERE
中的花括号允许前面的模式可以重复出现的次数。这通过称为间隔(interval
)。可以用四种方式来指定:
限定符 | 描述 |
---|---|
{n} | 匹配前面的元素,如果它确切地出现了 n 次。 |
{n,m} | 匹配前面的元素,如果它至少出现了 n 次,但是不多于 m 次。 |
{n,} | 匹配前面的元素,如果它出现了 n 次或多于 n 次。 |
{,m} | 匹配前面的元素,如果它出现的次数不多于 m 次。 |
$ echo "ct" | grep -E 'ca{1}t'
$
$ echo "cat" | grep -E 'ca{1}t'
cat
$ echo "caat" | grep -E 'ca{1}t'
$
设置间隔为1
则表示a
必须且只能出现一次,其他情况都不会匹配成功。grep -E
可以识别ERE
模式下的正则表达式。也可以单独设置重复出现的最少次数和最大次数,也可以设置一起设置最小和最大次数。
3.4 Alternation(交替)
管道符号允许你在检查正则表达式时,用逻辑OR
方式指定多个模式。如果匹配了其中任何一个单独的模式则整个模式匹配成功,如果没有模式匹配上,则数据流匹配失败。使用管道符号的格式如下:
expr1 | expr2 | ...
$ echo "There is a dog." | gawk '/cat|dog/{print $0}'
There is a dog.
$ echo "There is a cat." | gawk '/cat|dog/{print $0}'
There is a cat.
$ echo "He has a cat" | gawk '/[ch]at|dog/{print $0}'
He has a cat
$
管道符号两侧的正则表达式可以采用任何形式的正则表达式来定义文本(包括字符组)。
3.5 表达式分组
可以使用圆括号对正则表达式分组,当你将正则表达式分组时,该组会被视为一个标准字符。可以像普通字符一样给改组使用特殊字符。
$ echo "Sat" | gawk '/Sat(urday)?/{print $0}'
Sat
$ echo "Saturday" | gawk '/Sat(urday)?/{print $0}'
Saturday
$
结尾的urday
分组就像一个普通字符,后面的问号表示urday
可以不出现或者只出现一次。将分组和管道一起使用是很常见的做法。
$ echo "cat" | gawk '/(c|b)a(b|t)/{print $0}'
cat
$ echo "cab" | gawk '/(c|b)a(b|t)/{print $0}'
cab
$ echo "bat" | gawk '/(c|b)a(b|t)/{print $0}'
bat
$ echo "bab" | gawk '/(c|b)a(b|t)/{print $0}'
bab
$ echo "tab" | gawk '/(c|b)a(b|t)/{print $0}'
$
$ echo "tac" | gawk '/(c|b)a(b|t)/{print $0}'
$
上面就是 POSIX
的正则表达式。每种编程语言都会在 POSIX
基础上提供更加丰富的符号集。上面只是最基本的正则表达式,其他更加复杂的就需要你们自己去学习了。加油 :)