第十九章 正则表达式
第十九章 正则表达式
文本型数据在所有的类UNIX系统(如 Linux)中会扮演着重要角色,在完全领会这些工具的全部特征之前,要先了解一下工具最为复杂的用法和相关技术:正则表达式。
什么是正则表达式
简单地说,正则表达式是一种用于识别文本模式的符号表示法,在某种程度上类似于匹配文件和路径名的Shell通配符,但用途更广。大多数工具和编程语言都支持正则表达式,以便于解决文本问题。正则表达式并非全都相同;不同的工具,不同的编程语言,其正则表达式实现略有差异。本章正则表达式限定在POSIX标准范围内,涵盖了大多数命令行工具,相较于许多编程语言,POSIX使用的符号写法要略微丰富一些。
grep
处理正则表达式的主要命令是grep。grep源于global regular expression print, 译为全局正则表达式输出。grep的基本功能实在文本文件搜索与指定的正则表达式匹配的文本,将包含匹配项的文本输出到标准输出。
grep命令用法如下,其中regex代表正则表达式:
grep [options] regex [file...]
常用grep选项
选项 | 描述 |
---|---|
-i, --ignore-case | 忽略字母大小写。不区分大写字母和小写字母 |
-v, --invert-match | 反向匹配。在正常情况下,grep会输出包含匹配项的文本行。该选项则使grep输出所有不包含匹配项的文本行 |
-c, --count | 输出匹配数量(如果同时制定了-v选项,则不输出匹配数量),不在输出文本行 |
-l, --files-with-matches | 输出包含匹配项的文件名,不在输出文本行 |
-L,–files-without-match | 和-l选项类似,但是只输出不包含匹配的文件名 |
-n,–line-number | 在包含匹配项的文本行之前加上行号 |
-h,–no-filename | 在多文件搜索中禁止输出文件名 |
例如:在以dirlist开头的txt文件内搜索bzip字符串
grep bzip dirlist*.txt
命令输出如下:
dirlist-bin.txt:bzip2
dirlist-bin.txt:bzip2recover
如果只需要包含匹配的项的文件,并不需要匹配项,可以指定-l选项:
grep -l bzip dirlist*.txt
输出结果如下:
dirlist-bin.txt
如果只需要不包含匹配项的文件,可以这样做:
grep -L bzip dirlist*.txt
输出结果如下:
dirlist-sbin.txt
dirlist-usr-bin.txt
dirlist-usr-sbin.txt
元字符与文字字符
通过grep搜索的时候其实已经正在使用正则表达式了,尽管是非常简单的那种。正则表达式bzip的意思是仅当文件中的某行至少包含4个字符且字符顺序为b、z、i、p的时候(之间没有任何其他字符)才匹配。字符串bzp中的字符全部都是文字字符(literal character),只能匹配自身。除了普通字符,正则表达式还包括元字符(metacharacter),用于指定更复杂的匹配。正则表达式元字符包括:
^ $ . [ ] - ? * + ( ) | \
其他所有字符均被视为普通字符,不过在少数情况下,反斜线字符可用于创建元序列(metasequenece),还能转义字符,使其成为普通字符。
注意
很多正则表达式元字符对Shell扩展具有特殊含义。当包含元字符的正则表达式出现在命令行上时,一定要记得将其放入引号中,避免Shell去扩展这些字符,这一点非常重要。
任意字符
用于匹配任意字符的元字符是点号“.”,它可用于匹配任意字符。如果我们将其放入正则表达式,它能够匹配该字符位置上的任意字符。
grep -h '.zip' dirlist*.txt
输出结果如下:
bunzip2
bzip2
bzip2recover
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipfx
在文件内搜索匹配正则表达式.zip的所有行。最终结果的有些地方值得注意。首先,在其中没有发现.zip程序。这是因为正则表达式中的点号元字符将需要匹配的字符串长度增加到了4个字符,又因为zip只包含3个字符,所以不匹配。如果文件列表中有扩展名为.zip的文件,也能够匹配,因为扩展中的点好也属于“任意字符”的范畴。
锚点
在正则表达式中,脱字符^和美元符号$被视为锚点(anchor),分别表示仅当正则表达式出现在行首或行尾的时候才匹配。
例如在以dirlist开头的txt文件中匹配行首为zip的行:
grep -h '^zip' dirlist*.txt
命令输出结果如下:
zip
zipcloak
zipgrep
zipinfo
zipnote
zipsplit
在以dirlist开头的txt文件中匹配行尾为zip的行:
grep -h 'zip$' dirlist*.txt
命令行输出结果如下:
gunzip
gzip
funzip
gpg-zip
preunzip
prezip
unzip
zip
在以dirlist开头的txt文件匹配行内容为zip文件:
grep -h '^zip$' dirlist*.txt
该命令输出结果如下:
zip
在文件列表中分别搜索于行首、位于行尾、单独作为一行的字符串zip。注意,正则表达式^$(表示行首和行尾之间什么都没有)可以匹配空行。
方括号表达式与字符类
除了匹配正则表达式中指定位置上的任意字符,还可以使用方括号表达式来匹配指定字符集合中的单个字符。借助方括号表达式,可以指定一组待匹配的字符(包括会被解释为元字符的字符)。使用两个字符组成的集合来匹配包含字符串bzip或gzip的行:
grep -h '[bg]zip' dirlist*.txt
集合中可以包含任意数量的字符,其中出现的元字符会丢失其特殊含义。但是,有两种特殊情况:脱字符用于表示否定;连字符表示字符范围。
排除
如果方括号表达式中的首个字符是脱字符,剩下的字符则被视为不该在指定字符位置出现的字符集合。将前一个例子修改如下:
grep -h '[^bg]zip' dirlist*.txt
输出结果如下:
bunzip2
gunzip
funzip
gpg-zip
preunzip
prezip
prezip-bin
unzip
unzipsfx
利用排除操作,得到了一份文件列表,其中的文件名均包含字符串zip,而该字符之前是除b或g之外的任意字符。注意,zip并不符合搜索文件。排除型字符集合仍需要指定位置上有一个字符存在,只不过这个字符不能使集合中的字符。
仅当脱字符是方括号表达式中的第一个字符的时候才表示排除含义;否则,它只代表一个普通字符。
传统的字符范围
构建一个正则表达式,查找文件列表中所有以大写字母开头的文件,可以这样做:
grep -h '^[ABCDEFGHIJKLMNOPQRSTUVWXYZ]' dirlist*.txt
把26个大写字母放进方括号表达式里就能搞定的事。但这种方法比较实在麻烦,另一种做法:
grep -h '^[A-Z]' dirlist*.txt
输出结果如下:
MAKEDEV
ControlPanel
GET
HEAD
POST
X
X11
Xorg
MAKEFLOPPIES
NetworkManager
NetworkManagerDisaptcher
通过使用3个字符表示的字符范围,直接实现了26个字母的缩写。不管哪种字符范围,都可以用这种方式表达,例如下面的正则表达式可以匹配以字母或数字开头的所有文件名:
grep -h '^[A-Za-z0-9]' dirlist*.txt
如何在方括号表达式中加入一个普通的连字符,这将匹配包含大写字母的文件名,以下文件将匹配每个包含破折号或大写A(Z)的文件名。
grep -h '[-AZ]' dirlist*.txt
POSIX字符类
传统的字符范围易于理解,能够有效地解决快速指定字符集合的问题。遗憾的是,这种方法未必总是管用。在使用grep时没有出现什么问题,很难说不会在其他程序那里遇到问题。
字符范围的用法几乎与正则表达式中的一致,但有个问题:
ls /usr/sbin/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]
该命令输出结果如下:
/usr/sbin/ModemManager
/usr/sbin/NetworkManager
取决于Linux发行版,得到的文件列表也不尽相同,也有可能时空列表。本例取自Ubuntu。该命令的结果符合预期——以大写字母开头的文件列表,但下面的命令得到的结果就完全不同了(只显示其中一部分):
ls /usr/sbin/[A-Z]*
该命令输出结果如下:
/usr/sbin/biosdecode
/usr/sbin/chat
/usr/sbin/chgpasswd
/usr/sbin/chpasswd
/usr/sbin/chroot
/usr/sbin/cleanup-info
/usr/sbin/complain
/usr/sbin/console-kit-daemon
该例子没有按照预期输出,这是因为UNIX开发支出只识别ASCCII字符,其排序规则如下(collation order):
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
这与正常的词典序不同,后者如下:
aAbBcCdDeEfFgGhHiIJkKlLmMnNoOpPQrRsStTuUvVwWxXyYzZ
随着UNIX的流行,支持美式英语字符以外的字符的需求也与日俱增。于是ASCII长度扩展到8bit,添加了编码值128~255字符,适应了更多的语言。因此POSIX标准引入了语言环境(locale)的概念,能够通过调整来选择特定区域所需要的字符集。可以使用以下命令查看系统的语言配置:
echo $LANG
en_US.UTF-8
随着UNIX的流行,兼容POSIX的应用就会使用词典排序,而不再是ASCII的顺序。这就解释了上述命令结果的不同。在词典序中,字符范围[A-Z]包含除a之外的所有字母,这也正是看到的结果。
为了部分解决这个问题,POSIX标准包含了许多字符类(character class),提供了各种有用的字符范围。
POSIX字符类
字符类 | 描述 |
---|---|
[:alnum:] | 字母和数字字符。在ASCII中,等价于[a-zA-Z0-9] |
[:word:] | 和[:alnum:]一样,另外加入了下画线字符_ |
[;alpha;] | 字母字符。在ASCII中,等价于[a-zA-Z] |
[:blank:] | 包括空格符和制表符 |
[:cntrl:] | ASCII控制字符。包括ASCII编码值0~31和127的字符 |
[:digit:] | 数字0~9 |
[:graph:] | 可见字符。在ASCII中,包括编码值为33~126的字符 |
[:lower:] | 小写字母 |
[:punct:] | 标点符号字符。在ASCII中,等价于[-!"#$%&()*+,./:;<=>?@[\]_`{ |
[:print:] | 可输出字符。包括[:graph:]中的所有字符加上空格符 |
[:space:] | 空白字符,包括空格符、制表符、回车符、换行符、垂直制表符、换页符 |
[:upper:] | 大写字符 |
[:xdigit;] | 用于表示十六进制数值的字符。在ASCII中,等价于[0-9A-Fa-f] |
即便有了POSIX字符类,还是没有便利的方法来表示部分范围,例如[A-M]。 |
这不是正则表达式的示例,而是Shell路径名扩展的示例。我们之所以在此演示,是因为POSIX字符类既可用于正则表达式,也可用于Shell扩展。
POSIX基本型正则表达式于扩展型正则表达式
POSIX把正则表达式的实现分为了两类:基本正则表达式(Basic Regular Expression, BRE)与扩展正则表达式(Extended Regular Expression, ERE)。所有兼容POSIX并实现了BRE应用程序都支持目前介绍的这些特征。grep程序就是这样的程序之一。
BRE和ERE的不同在于元字符。BRE识别下列元字符:
^ $ . [ ] *
除此之外的所有字符均被视为文字字符。ERE又加入了下列元字符(及其功能):
( ) { } ? + |
但是,如果使用了反斜线将(、)、{、}转义的话,BRE将其视为元字符;而BRE会将转义后的这些字符视为文字字符。
若要使用ERE,就得使用另一种grep。传统上,这要借助于egrep程序,但是GNU版本的grep程序可以使用-E选项来支持ERE。
POSIX:电气电子工程师学会(Institute of Electrical and Electronics Engineer, IEEE)出现了。IEEE制定了一套规范UNIX(以及类UNIX系统)
工作方式标准。这些标准官方名称是IEEE 1003,它定义了应用程序接口(Application Programming Interface, API)、Shell以及标准类UNIX系统中的实用工具。POSIX这个名字是由理查地·马修·斯托曼建议,结尾增加的X只是为了让名字更响亮,该叫法后被IEEE采纳。
多选结构
ERE的特征叫作多选结构(alternation),它允许匹配一组正则表达式中的某一个。就像方括号表达式允许匹配一组指定字符中的单个字符,多选结构可以从一组字符串或正则表达式中寻找匹配。
例如:查找包含AAA或BBB内容的行,其命令如下:
grep -E 'AAA | BBB'
其中-E选项指定采用ERE(扩展性正则表达式),将正则表达式放入引号中,避免Shell将其解释为管道。多选结构可不仅能二选一:
grep -E 'AAA | BBB | CCC'
将多选结构与其它正则表达式元素组合起来,可以实用()来分隔,例如:
grep -Eh '^(bz | gz | zip)' dirlist*.txt
改正则表达式可以匹配文件列表中以bz、gz或zip开头的文件名。如果去掉括号,正则表达式的含义就变成了匹配以bz开头,或者包含gz,或者包含zip的文件名:
grep -Eh '^bz | gz | zip' dirlist*.txt
量词
ERE支持多种方式指定匹配次数
?——匹配0次或1次
实际上,该量词(quantifier)表示“之前的元素是可选的”。假设我们要检查电话号码有效性。电话号码匹配下列两种形式之一(n为数字),则认为是有效的。
- (nnn)nnn-nnnn。
- nnn nnn-nnnn
根据据此构建下列正则表达式:
^(?[0-9] [0-9] [0-9] )? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9]$
其中,在括号后加上了问号,表示匹配括号的内容0次或1次。因为括号是元字符(在ERE中),所以在其之前加上反斜线,使其成为文字字符。
匹配符合(nnn)nnn-nnnn格式的电话号码:
echo "(555) 123-4567" | grep -E '^\(?[0-9] [0-9] [0-9]\)? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9]$'
命令输出如下:
(555) 123-4567
从结果可以看出能够匹配nnn nnn-nnnn格式的电话号码。
匹配符合nnn-nnnn格式的电话号码:
echo "555 123-4567" | grep -E '^\(? [0-9] [0-9] [0-9]\)? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9]$'
命令输出如下:
555 123-4567
可以看到能够匹配符合nnn nnn-nnnn格式的电话号码。
匹配不符合以上两种格式的情况:
echo "AAA 123-4567" | grep -E '^\(? [0-9] [0-9] [0-9]\)? [0-9] [0-9] [0-9] - [0-9] [0-9] [0-9] [0-9] $'
该命令无输出表示不符合以上两种格式,说明该正则表达式可以用于初步验证电话号码。
*——匹配0次或多次
和?一样,*也可用于表示可选项;但和?不同的是,*之前的可选项可以出现任意多次,而不仅仅出现一次。例如判断某个字符串是否是一句话;也就是说该字符以一个大写字母开头,然后是任意多个大/小写字母和空格符,最后以点好结尾。可以实用下列正则表达式:
[ [:upper:] ] [ [;upper;] [;lower;] ]* .
这个正则表达式由3项组成:包含字符类[:upper:]的方括号表达式,包含字符类[;upper;]、[;lower;]以及空格符的方括号表达式,经过反斜线转义的点好。第二项结尾处是*,所以在句子开头的大写字母之后,不管有多少个大/小写字母和空格符,都能够匹配。
匹配只有首字母大写的句子:
echo "This works." | grep -E '[ [:upper:] ] [ [;upper;] [;lower;] ]* \.'
命令输出如下:
This works
从结果可以匹配只有首字母大写的句子。
匹配首字母大写并且除此之外还有其他大写字母的句子:
echo "This Works" | grep -E '[ [:upper:] ] [ [;upper;] [;lower;] ]* \.'
输出结果如下:
This Works
从结果可以看出能够匹配首字母大写并且含有其他大写字母的句子。
匹配只有小写字母的句子:
echo "this works not" | grep -E '[ [:upper:] ] [ [;upper;] [;lower;] ]* \.'
没有输出结果,说明该正则表达式正确,因为首字母不是大写字母。
+——匹配1次或多次
+和*差不多,只不过要求之前的可选项至少匹配一次。下面的正则表达式所匹配的行只能包含由单个空格分隔的一个或多个字母:
^ ( [ [:alpha:] ] + ?) + $
匹配有两个单词和一个空格分隔符:
echo "This that" | grep -E '^([[:alpha:]]+ ?)+$'
输出结果如下:
This that
可以看出能够匹配两个单词一个空白分隔符。
匹配3个字母每两个字母间隔一个空格分隔符
echo "a b c" | grep -E '^([[:alpha:]]+ ?)+ $'
输出结果如下:
a b c
从结果可看出可以匹配由单个字母和空白分隔符组成的句子。
匹配由字母和数字匹配单个字母和空白分隔符组成的句子:
echo "a b 9" | grep -E '^([[;alpha;]]+ ?)+$'
没有输出结果,因为不能包含非字母字符。
匹配由单词和字母以及两个空白分隔符组成的句子:
echo "abc d" | grep -E '^([[:alpha:]]+ ?)+$'
没有输出结果,因为c和d之间不止有一个空白字符。
{} —— 匹配指定次数
{和}用于指定要求匹配的最小次数和最大次数,共有4种指定方式。
指定匹配次数
指定方式 | 含义 |
---|---|
{n} | 匹配之前的元素n次 |
{n,m} | 匹配之前的元素至少n次,至多m次 |
{n,} | 匹配之前的元素至少n次,最多不限 |
{,m} | 匹配之前的元素不超过m次 |
实用{}将前文的电话号码例子,简化为
‘^/( ?[0-9]{3} /)? [0-9] {3} - [0-9] {4}$’
匹配符合(nnn)nnn-nnnn的电话号码:
echo "(555) 123-4576" | grep -E '^/(?[0-9]{3}/)?[0-9]{3}-[0-9]{4}$'
输出结果如下:
(555) 123-4576
结果表明可以匹配此类格式的电话号码,与前一种写法结果相同,但是书写更方便。
匹配符合nnn nnn-nnnn的电话号码:
echo "555 123-4576" | grep -E '^/(?[0-9]{3}/)?[0-9]{3}-[0-9]{4}$'
输出结果如下:
555 123-4576
结果表明可以匹配此类格式的电话号码,与前一种写法结果相同,但是书写更方便。
匹配不符合的情况:
echo "AAA 123-4567" | grep -E '^/(?[0-9]{3}/)?[0-9]{3}-[0-9]{4}$'
没有输出结果,说明与预期相符合,可以替代原来的书写方式。
find使用正则表达式
find path -regex 'stirng' #在path目录查找匹配正则表达式string的文件
locate使用正则表达式
locate即支持BRE(–regexp选项)也支持ERE(–regex选项)。
locate --regex 'string' #在/目录查找匹配正则表达式string的文件
Less和Vim搜索文本
Less下的正则表达式:
/‘regex’
Less会高亮出匹配项,这样就很容易分辨出无效号码:
Vim只支持BRE,因此用于搜索的正则表达式得改写成这样
/([0-9]{3}) [0-9]{3}-[0-9]{4}
对于ERE支持的符号需要在其前面加上\。