二十、正则表达式
-
正则表达式是一种可供 Linux 工具过滤文本的自定义模板。 Linux 工具(比如 sed 或 gawk) 会在读取数据时使用正则表达式对数据进行模式匹配。如果数据匹配模式,它就会被接受并进行处理。如果数据不匹配模式,它就会被弃用。
-
正则表达式模式使用元字符来描述数据流中的一个或多个字符。
- 正则表达式的工作方式与通配符类似。正则表达式包含文本和/或特殊字符(这些特殊字符在正则表达式中称作元字符),定义了 sed 和 gawk 匹配数据时使用的模板。你可以在正则表达式中使用不同的特殊字符来定义特定的数据过滤模式。
- 准确地说,通配符和正则表达式并不是一回事,虽然正则表达式中也有*和?,但作用和通配符完全不一样。
- 正则表达式是由正则表达式引擎实现的。这是一种底层软件,负责解释正则表达式并用这些模式进行文本匹配。
-
尽管在 Linux 世界中有很多不同的正则表达式引擎,但最流行的是以下两种。
- POSIX 基础正则表达式(basic regular expression,BRE)引擎。
- POSIX 扩展正则表达式(extended regular expression,ERE)引擎。
-
大多数 Linux 工具至少符合 POSIX BRE 引擎规范,能够识别该规范定义的所有模式符号。有些工具(比如 sed)仅符合 BRE 引擎规范的一个子集。这是出于速度方面的考虑导致的,因为 sed 希望尽可能快地处理数据流中的文本。
-
POSIX ERE 引擎多见于依赖正则表达式过滤文本的编程语言中。它为常见模式(比如数字、单词以及字母数字字符)提供了高级模式符号和特殊符号。 gawk 使用 ERE 引擎来处理正则表达式。
-
最基本的 BRE 模式是匹配数据流中的文本字符。
-
正则表达式并不关心模式在数据流中出现的位置,也不在意模式出现了多少次。只要能匹配文本字符串中任意位置的模式,正则表达式就会将该字符串传回 Linux 工具。
-
正则表达式区分大小写。
-
在正则表达式中,无须写出整个单词。只要定义的文本出现在数据流中,正则表达式就能够匹配。
-
也无须局限于在正则表达式中只使用单个文本单词,空格和数字也是可以的。
$ echo "This is line number 1" | sed -n '/ber 1/p' This is line number 1 $ $ echo "This is line number1" | sed -n '/ber 1/p' $
- 在正则表达式中,空格和其他的字符没有什么区别。如果在正则表达式中定义了空格, 那么它必须出现在数据流中。
- 甚至可以创建匹配多个连续空格的正则表达式:
$ cat data1 This is a normal line of text. This is a line with too many spaces. $ sed -n '/ /p' data1 This is a line with too many spaces. $
- 单词间有两个空格的行匹配了正则表达式模式。这是查找文本文件中空格的好办法。
- 正则表达式能识别的特殊字符如下所示:
.*[]^${}\+?|()
- 不能在匹配普通文本的模式中单独使用这些字符即可。
- 如果要将某个特殊字符视为普通字符,则必须使用反斜线(\)将其转义。
- 尽管正斜线(/)不属于正则表达式的特殊字符,但如果它出现在 sed 或 gawk 的正则表达式中,就会出现错误。因此,使用正斜线也需要进行转义。
- 在默认情况下,当指定一个正则表达式模式时,只要模式出现在数据流中的任何地方,它就能匹配。有两个特殊字符可以用来将模式锁定在数据流中的行首或行尾。
-
锚定行首
- 脱字符(^)可以指定位于数据流中文本行行首的模式。如果模式出现在行首之外的位置,则正则表达式无法匹配。
- 要使用脱字符,就必须将其置于正则表达式之前:
$ echo "The book store" | sed -n '/^book/p' $ $ echo "Books are great" | sed -n '/^Book/p' Books are great $
- 脱字符使得正则表达式引擎在每行(由换行符界定)的行首检查模式。
- 只要模式出现在行首,脱字符就能将其锚定。
- 如果将脱字符放到正则表达式开头之外的位置, 那么它就跟普通字符一样, 没有什么特殊含义了,sed 会将其视为普通字符来匹配。
- 如果正则表达式模式中只有脱字符,就不必用反斜线来转义。但如果在正则表达式中先指定脱字符,随后还有其他文本,那就必须在脱字符前用转义字符。即,如果只是匹配脱字符,可以不用转义,比如 echo “This ^ is a test” | sed -n ‘/^/p’。 但如果要匹配脱字符以及其他文本,则需要转义, 比如 echo “I love ^regex” | sed -n ‘/^regex/p’。
-
锚定行尾
- 特殊字符美元符号($)定义了行尾锚点。将这个特殊字符放在正则表达式之后则表示数据行必须以该模式结尾:
$ echo "This is a good book" | sed -n '/book$/p' This is a good book $ echo "This book is good" | sed -n '/book$/p' $ $ echo "There are a lot of good books" | sed -n '/book$/p' $
- 要想匹配,文本模式必须是行的最后一部分。
- 特殊字符美元符号($)定义了行尾锚点。将这个特殊字符放在正则表达式之后则表示数据行必须以该模式结尾:
-
组合锚点
- 可以在同一行中组合使用行首锚点和行尾锚点。
- 第一种情况是,假定要查找只含有特定文本模式的数据行:
$ cat data4 this is a test of using both anchors I said this is a test this is a test I'm sure this is a test. $ sed -n '/^this is a test$/p' data4 this is a test $
- sed 忽略了那些不单单包含指定模式的行。
- 第二种情况,将这两个锚点直接组合在一起,之间不加任何文本,可以过滤出数据流中的空行:
$ cat data5 This is one test line. This is another test line. $ sed '/^$/d' data5 This is one test line. This is another test line. $
- 指定的正则表达式会查找行首和行尾之间什么都没有的那些行。由于空行在两个换行符之间没有文本,因此刚好匹配正则表达式。
- sed 用删除命令来删除与该正则表达式匹配的行,因此也就删除了文本中的所有空行。这是从文档中删除空行的一种行之有效的方法。
- 点号字符
- 点号字符可以匹配除换行符之外的任意单个字符。点号字符必须匹配一个字符,如果在点号字符的位置没有可匹配的字符,那么模式就不成立。
- 字符组
- 可以在正则表达式中定义用来匹配某个位置的一组字符。如果字符组中的某个字符出现在了数据流中,那就能匹配该模式。
- 方括号用于定义字符组。在方括号中加入你希望出现在该字符组中的所有字符,就可以在正则表达式中像其他特殊字符一样使用字符组了。
- 一个创建字符组的例子:
$ sed -n '/[ch]at/p' data6 The cat is sleeping. That is a very nice hat. $
- 字符组中必须有个字符来匹配相应的位置。
- 在不太确定某个字符的大小写时非常适合使用字符组:
$ echo "Yes" | sed -n '/[Yy]es/p'
- 在单个正则表达式中可以使用多个字符组:
$ echo "Yes" | sed -n '/[Yy][Ee][Ss]/p'
- 字符组并非只能含有字母,也可以在其中使用数字:
$ sed -n '/[0123]/p' data7
- 这个正则表达式模式匹配任意含有数字 0 、1、2 或 3 的行。含有其他数字以及不含有数字的行都会被忽略。
- 匹配邮政编码出错的例子:
$ cat data8 60633 46201 223001 4353 22203 $ sed -n ' >/[0123456789][0123456789][0123456789][0123456789][0123456789]/p >' data8 60633 46201 223001 22203 $
- 它成功过滤掉了不可能是邮政编码的那些过短的数字,因为最后一个字符组没有字符可匹配。但其中有一个 6 位数也被正则表达式保留了下来,尽管我们只定义了 5 个字符组。
- 正则表达式可以匹配数据流中任何位置的文本。
- 匹配模式之外经常会有其他字符。如果要确保只匹配 5 位数,就必须将其与其他字符分开,要么用空格,要么像下面例子中那样,指明要匹配数字的起止位置:
$ sed -n ' > /^[0123456789][0123456789][0123456789][0123456789][0123456789]$/p > ' data8 60633 46201 22203 $
- 排除型字符组
- 在正则表达式中,你也可以反转字符组的作用:匹配字符组中没有的字符。为此,只需在字符组的开头添加脱字符即可:
$ sed -n '/[^ch]at/p' data6 This test is at line four. $
- 通过排除型字符组, 正则表达式会匹配除 c 或 h 之外的任何字符以及文本模式。由于空格字符属于这个范围,因此通过了模式匹配。但即使是排除型,字符组仍必须匹配一个字符,以 at 为起始的行还是不能匹配模式。
- 区间
- 可以用单连字符在字符组中表示字符区间。只需指定区间的第一个字符、连字符以及区间的最后一个字符即可。根据 Linux 系统使用的字符集(参见第 2 章),字符组会包括在此区间内的任意字符。
- 可以通过指定数字区间来简化邮政编码的例子:
$ sed -n '/^[0-9][0-9][0-9][0-9][0-9]$/p' data8 60633 46201 45902 $
- 每个字符组都会匹配 0~9 的任意数字。如果字母出现在数据中的任何位置,则这个模式都不成立。
- 同样的方法也适用于字母。
- 还可以在单个字符组内指定多个不连续的区间:
$ sed -n '/[a-ch-m]at/p' data6 The cat is sleeping. That is a very nice hat. $ $ echo "I'm getting too fat." | sed -n '/[a-ch-m]at/p' $
- 该字符组允许区间 a~c 和 h~m 中的字母出现在 at 文本前,但不允许出现区间 d~g 中的字母。
- 特殊的字符组
-
除了定义自己的字符组, BRE 还提供了一些特殊的字符组,以用来匹配特定类型的字符。
字符组 描述 [[:alpha:]] 匹配任意字母字符,无论是大写还是小写 [[:alnum:]] 匹配任意字母数字字符, 0~9 、A~Z 或 a~z [[:blank:]] 匹配空格或制表符 [[:digit:]] 匹配 0~9 中的数字 [[:lower:]] 匹配小写字母字符 a~z [[:print:]] 匹配任意可打印字符 [[:punct:]] 匹配标点符号 [[:space:]] 匹配任意空白字符:空格、制表符、换行符、分页符(formfeed)、垂直制表符和回车符 [[: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' $
- 使用特殊字符组定义区间更方便,可以用[[:digit:]]来代替区间 [0-9]。
- 星号
- 在字符后面放置星号表明该字符必须在匹配模式的文本中出现 0 次或多次。
- 如果需要写一个可在美式英语或英式英语中使用的脚本,可以这么做:
$ echo "I'm getting a color TV" | sed -n '/colou*r/p' I'm getting a color TV $ echo "I'm getting a colour TV" | sed -n '/colou*r/p' I'm getting a colour TV $
- 模式中的 u*表示字母 u 可以出现,也可以不出现。
- 另一个方便的特性是将点号字符和星号字符组合起来。这个组合能够匹配任意数量的任意字符,通常用在数据流中两个可能相邻或不相邻的字符串之间:
$ echo "this is a regular pattern expression" | sed -n ' > /regular.*expression/p' this is a regular pattern expression $
- 通过这种模式可以轻松查找可能出现在文本行内任意位置的多个单词。
- 星号还能用于字符组,指定可能在文本中出现 0 次或多次的字符组或字符区间:
$ echo "bt" | sed -n '/b[ae]*t/p' bt $ echo "bat" | sed -n '/b[ae]*t/p' bat $ echo "bet" | sed -n '/b[ae]*t/p' bet $ echo "btt" | sed -n '/b[ae]*t/p' btt $ echo "baat" | sed -n '/b[ae]*t/p' baat $ echo "baaeeet" | sed -n '/b[ae]*t/p' baaeeet $ echo "baeeaeeat" | sed -n '/b[ae]*t/p' baeeaeeat $ echo "baakeeet" | sed -n '/b[ae]*t/p' $
- 只要 a 和 e 字符以任何组合形式出现在 b和 t 字符之间(完全不出现也行),模式就能够匹配。如果出现了字符组之外的其他字符,那么模式就不能匹配。
- 扩展正则表达式
- POSIX ERE 模式提供了一些可供 Linux 应用程序和工具使用的额外符号。 gawk 支持 ERE 模式,但 sed 不支持。
- sed 和 gawk 的正则表达式引擎之间是有区别的。 gawk 可以使用大多数扩展的正则表达式符号, 并且能够提供了一些 sed 所不具备的额外过滤功能。但正因如此, gawk 在处理数据时往往比较慢。
- gawk 脚本中的常见 ERE 模式符号:
- 问号
- 加号
- 花括号
- 竖线符号
- 表达式分组
- 问号
- 问号表明前面的字符可以出现 0 次或 1 次,它不会匹配多次出现的该字符。
- 跟星号一样, 可以将问号和字符组一起使用:
$ echo "bt" | gawk '/b[ae]?t/{print $0}' bt
- 如果字符组中的字符出现了 0 次或 1 次,那么模式匹配就成立。但如果两个字符都出现了,或者其中一个字符出现了两次,那么模式匹配就不成立。
- 加号
- 加号表明前面的字符可以出现 1 次或多次, 但必须至少出现 1 次。如果该字符没有出现,那么模式就不会匹配:
$ echo "beeet" | gawk '/be+t/{print $0}' beeet
- 如果字符 e 没有出现,那么模式匹配就不成立。
- 加号同样适用于字符组, 跟星号和问号的使用方式相同:
$ echo "bt" | gawk '/b[ae]+t/{print $0}' $
- 如果出现了字符组中定义的任一字符,那么文本就会匹配指定的模式。
- 花括号
- ERE 中的花括号允许为正则表达式指定具体的可重复次数,这通常称为区间。可以用两种格式来指定区间:
- m:正则表达式恰好出现 m 次。
- m, n:正则表达式至少出现 m 次,至多出现 n 次。
- 这个特性可以精确指定字符(或字符组)在模式中具体出现的次数。
- 在默认情况下,gawk 不识别正则表达式区间,必须指定 gawk 的命令行选项–re-interval才行。
- 例子:
$ echo "bt" | gawk --re-interval '/be{1}t/{print $0}' $ $ echo "beet" | gawk --re-interval '/be{1,2}t/{print $0}' beet
- 通过指定区间为 1,限定了该字符应该出现的次数。如果该字符出现多次,那么模式匹配就不成立。
- 字符 e 出现一次或两次,模式都能匹配;否则,模式无法匹配。
- 区间也适用于字符组:
$ echo "bat" | gawk --re-interval '/b[ae]{1,2}t/{print $0}' bat
- 如果字母 a 或 e 在文本模式中只出现了 1~2 次,则正则表达式模式匹配;否则,模式匹配失败。
- 竖线符号
- 竖线符号允许在检查数据流时, 以逻辑 OR 方式指定正则表达式引擎要使用的两个或多个模式。如果其中任何一个模式匹配了数据流文本,就视为匹配。如果没有模式匹配,则匹配失败。
- 竖线符号的使用格式如下:
expr1 |expr2|...
- 例子:
$ echo "The cat is asleep" | gawk '/cat|dog/{print $0}' The cat is asleep $ echo "The dog is asleep" | gawk '/cat|dog/{print $0}' The dog is asleep $ echo "The sheep is asleep" | gawk '/cat|dog/{print $0}' $
- 这个例子会在数据流中查找正则表达式 cat 或 dog。
- 正则表达式和竖线符号之间不能有空格,否则竖线符号会被认为是正则表达式模式的一部分。
- 竖线符号两侧的子表达式可以采用正则表达式可用的任何模式符号(包括字符组):
$ echo "He has a hat." | gawk '/[ch]at|dog/{print $0}' He has a hat. $
- 这个例子会匹配数据流文本中的 cat、hat 或 dog。
- 表达式分组
- 也可以用圆括号对正则表达式进行分组。分组之后,每一组会被视为一个整体,可以像对普通字符一样对该组应用特殊字符。例如:
$ echo "Sat" | gawk '/Sat(urday)?/{print $0}' Sat $ echo "Saturday" | gawk '/Sat(urday)?/{print $0}' Saturday $
- 结尾的 urday 分组和问号使得该模式能够匹配 Saturday 的全写或 Sat 缩写。
- 将分组和竖线符号结合起来创建可选的模式匹配组是很常见的做法:
$ echo "cat" | gawk '/(c|b)a(b|t)/{print $0}' cat
- 正则表达式(c|b)a(b|t)匹配的模式是第一组中任意字母、a 以及第二组中任意字母的各种组合。
- 实战演练1——目录文件计数
- 对 PATH 环境变量中各个目录所包含的文件数量进行统计。
- PATH 中的各个路径由冒号分隔。要获取可在脚本中使用的目录列表,必须用空格替换冒号。
- 对于单个目录,可以用 ls 命令列出其中的文件,再用另一个 for 语句来遍历每个文件,对文件计数器增值。
- 这个脚本的最终版本如下:
$ cat countfiles #!/bin/bash # count number of files in your PATH mypath=$(echo $PATH | sed 's/:/ /g') count=0 for directory in $mypath do check=$(ls $directory) for item in $check do count=$[ $count + 1 ] done echo "$directory - $count" count=0 done $ ./countfiles /usr/local/sbin - 0 /usr/local/bin - 2 /usr/sbin - 213 /usr/bin - 1427 /sbin - 186 /bin - 152 /usr/games - 5 /usr/local/games – 0 $
- 实战演练2——验证电话号码
- 在美国, 电话号码的几种常见形式如下所示:
(123)456-7890 (123) 456-7890 123-456-7890 123.456.7890
- 完整的正则表达式如下:
^\(?[2-9][0-9]{2}\)?(| |-|\.)[0-9]{3}( |-|\.)[0-9]{4}$ 拆解看:^\(? [2-9] [0-9]{2} \)? (| |-|\.) [0-9]{3} ( |-|\.) [0-9]{4}$
- 可以在 gawk 中用这个正则表达式过滤掉格式不符的电话号码。现在只需创建一个使用该正则表达式的 gawk 脚本,然后用这个脚本来过滤你的电话簿。记住,在 gawk 中使用正则表达式区间时,必须加入–re-interval命令行选项,否则无法得到正确的结果。脚本如下:
$ cat isphone #!/bin/bash # script to filter out bad phone numbers gawk --re-interval '/^\(?[2-9][0-9]{2}\)?(| |-|\.) [0-9]{3}( |-|\.)[0-9]{4}/{print $0}' $ $ echo "317-555-1234" | ./isphone 317-555-1234 $ cat phonelist 000-000-0000 123-456-7890 ... $ cat phonelist | ./isphone
- 也可以将含有电话号码的整个文件通过管道传给脚本,过滤掉无效的号码。只有匹配该正则表达式模式的有效电话号码才会出现。
- 实战演练3——解析 email 地址
- email 地址的基本格式。
username@hostname
- username 可以包含字母数字字符以及下列特殊字符:
- 点号
- 连字符
- 加号
- 下划线
- email 地址的hostname 部分由一个或多个域名和一个服务器名组成。服务器名和域名也必须遵照严格的命名规则,允许包含字母数字字符以及下列特殊字符:
- 点号
- 下划线
- username 可以包含字母数字字符以及下列特殊字符:
- 将各部分组合在一起,得到下列正则表达式:
^([a-zA-Z0-9_\-\.\+]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$ 拆解看:^([a-zA-Z0-9_\-\.\+]+) @ ([a-zA-Z0-9_\-\.]+) \. ([a-zA-Z]{2,5})$