正则表达式

正则表达式

正则表达式是你所定义的模式模板 (pattern template),很多程序工具都能用它来过滤文本,在处理数据时使用正则表达式对数据进行模式匹配。如果数据匹配模式,它就会被接受并进一步处理;如果数据不匹配模式,它就会被滤掉。理解正则表达式的第一步在于弄清它们到底是什么,下面就主要介绍正则表达式再 Linux shell 工具中的使用。

什么是正则表达式

正则表达式模式利用通配符来描述数据流中的一个或多个字符。

Linux 中很多场景都可以使用通配符来描述不确定的数据。例如在 ls 命令中使用通配符列出文件和目录:ls -al da*ls 命令会读取目录中所有文件的信息,但只显示 da 开头的文件的信息。

正则表达式通配符模式的工作原理与之类似,使用不同的特殊字符来定义特定的数据过滤模式。

正则表达式的类型

使用正则表达式最大的问题在于有不止一种类型的正则表达式。Linux 中的不同应用程序可能会用不同类型的正则表达式。这其中包括编程语言(Java、Perl 和 Python)、Linux 实用工具(比如 sed 编辑器、gawk 程序和 grep 工具)以及主流应用(比如 MySQL 和 PostgreSQL 数据库服务器)。

正则表达式是通过正则表达式引擎(regular expression engine)实现的,在 Linux 中有两种流行的正则表达式引擎:

  1. POSIX 基础正则表达式(basic regular expression, BRE)引擎
  2. POSIX 扩展正则表达式(extended regular expression, ERE)引擎

大多数 Linux 工具都至少符合 POSIX BRE 引擎规范,遗憾的是,有些工具(比如 sed 编辑器)只符合了 BRE 引擎规范的子集,这是处于速度方面的考虑导致的。

定义 BRE 模式

最基本的 BRE 模式是匹配数据流中的文本字符。

纯文本

$ echo "This is a test" | sed -n '/test/p'
This is a test
echo "This is a test" | sed -n '/trial/p'
$

第一个模式定义了一个单词 test,由于 echo 语句在文本字符串中包含了单词 test,数据流文本能够匹配所定义的正则表达式模式,因此 sed 编辑器显示了该行。而第二个模式定义的单词 trial 没有包含在 echo 语句的字符串中,所以正则表达式模式没有匹配,因此 sed 编辑器没有显示该行。

你可能注意到了,正则表达式并不关心模式在数据流中的位置。它也不关心模式出现了多少 次。一旦正则表达式匹配了文本字符串中任意位置上的模式,它就会将该字符串传回 Linux 工具。

重要的是记住正则表达式对匹配的模式非常挑剔。第一条原则就是:正则表达式模式都区分大小写。这意味着它们只会匹配大小写都相符的模式。在正则表达式中,你不用写出整个单词。只要定义的文本出现在数据流中,正则表达式就能够匹配。你也不用局限于在正则表达式中只用单个文本单词,可以在正则表达式中使用空格和数字,在正则表达式中,空格和其他的字符并没有什么区别,甚至可以创建匹配多个连续空格的正则表达式模式。

特殊字符

正则表达式识别的特殊字符包括:.*[]^${}\+?|()。如果要用某个特殊字符作为文本字符,就必须转义。

尽管正斜线不是正则表达式的特殊字符,但如果它出现在sed编辑器的正则表达式中,你就会得到一个错误。要使用正斜线,也需要进行转义:\/

锚字符

默认情况下,当指定一个正则表达式模式时,只要模式出现在数据流中的任何地方,它就能匹配。有两个特殊字符可以用来将模式锁定在数据流中的行首或行尾。

  1. 锁定在行首

    脱字符(^)定义从数据流中文本行的行首开始的模式。如果模式出现在行首之外的位置, 正则表达式模式则无法匹配。脱字符会在每个由换行符决定的新数据行的行首检查模式。如果你将脱字符放到模式开头之外的其他位置,那么它就跟普通字符一样,不再是特殊字符了。

    $ echo "The book store" | sed -n '/^book/p'
    $
    $ echo "Books are great" | sed -n '/^Book/p'
    Books are great
    
    $ echo "This ^ is a test string" | sed -n '/s ^/p'
    This ^ is a test string
    
  2. 锁定在行尾

    特殊字符美元符($)定义了行尾锚点。将这个特殊字符放在文本模式之后来指明数据行必须以该文本模式结尾。

    $ echo "This is a good book" | sed -n '/book$/p'
    This is a good book
    $ echo "This book is good" | sed -n '/book$/p'
    $
    

    使用结尾文本模式的问题在于你必须要留意到底要查找什么。将行尾的单词 book 改成复数形式,就意味着它不再匹配正则表达式模式了,尽管 book 仍然在数据流中。

  3. 组合锚点

    可以在同一行中将行首锚点和行尾锚点组合在一起使用。假定你要查找只含有特定文本模式的数据行:

    $ cat test
    this is a test of using both anchors
    I said this is a test
    this is a test
    I am sure this is a test
    $ sed -n '/^this is a test$/p' test
    this is a test
    

    将两个锚点直接组合在一起,之间不加任何文本,这样过滤出数据流中的空白行:

    $ cat test
    This is one test line.
    
    This is another test line.
    $ sed '/^$/p' test
    This is one test line.
    This is another test line.
    

    定义的正则表达式模式会查找行首和行尾之间什么都没有的那些行。由于空白行在两个换行符之间没有文本,刚好匹配了正则表达式模式,这是从文档中删除空白行的有效方法。

点字符

特殊字符点号用来匹配除换行符之外的任意单个字符。它必须匹配一个字符,如果在点号字符的位置没有字符,那么模式就不成立。

$ cat test
This is a test of a line.
The cat is sleeping.
That is a very nice hat.
This test is at line four.
at ten o'clock we'll go home
$ sed -n '/.at/p' test

第四行中,在 at 前面并没有任何字符来匹配点号字符,但其实是有的,在正则表达式中, 空格也是字符,因此 at 前面的空格刚好匹配了该模式。第五行证明了这点,将 at 放在行首就不会匹配该模式了。

字符组

使用方括号来定义一个字符组。然后在模式中使用整个组,就跟其他单个通配符一样。在不太确定某个字符的大小写时,字符组会非常有用:

$ echo "Yes" | sed -n '/[Yy]es/p'
Yes
$ echo "yes" | sed -n '/[Yy]es/p'
yes

字符组不必只含有字母,也可以在其中使用数字。

$ sed -n '/[0123]/p' test

这个正则表达式模式匹配了任意含有数字 0、1、2 或 3 的行。含有其他数字以及不含有数字的行都会被忽略掉。

排除型字符组

在正则表达式模式中,也可以反转字符组的作用。可以寻找组中没有的字符,而不是去寻找组中含有的字符。要这么做的话,只要在字符组的开头加个脱字符:

$ sed -n '/[^ch]at/p' test

通过排除型字符组,正则表达式模式会匹配 c 或 h 之外的任何字符以及文本模式。

区间

可以用单破折线符号在字符组中表示字符区间。只需要指定区间的第一个字符、单破折线以及区间的最后一个字符就行了。

$ sed -n '/[0-9]/p' test
$ sed -n '/[c-h]at/p' test

还可以在单个字符组指定多个不连续的区间。

$ sed -n '/[a-ch-m]at/p' test

该字符组允许区间 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:]]匹配任意空白字符:空格、制表符、NL、FF、VT 和 CR
[[:upper:]]匹配任意大写字母字符 A~Z

星号

在字符后面放置星号表明该字符必须在匹配模式的文本中出现 0 次或多次。

$ echo "ik" | sed -n '/ie*k/p'
ik
$ echo "ieek" | sed -n '/ie*k/p'
ieek

这个模式符号广泛用于处理有常见拼写错误或在不同语言中有拼写变化的单词。

$ echo "I am getting a colour TV" | sed -n '/colou*r/p'
I am getting a colour TV

另一个方便的特性是将点号特殊字符和星号特殊字符组合起来。这个组合能够匹配任意数量的任意字符。它通常用在数据流中两个可能相邻或不相邻的文本字符串之间。可以使用这个模式轻松查找可能出现在数据流中文本行内任意位置的多个单词。

扩展正则表达式

POSIX ERE 模式包括了一些可供 Linux 应用和工具使用的额外符号。例如,gawk 程序就能识别 ERE 模式。

问号

问号类似于星号,不过有点细微的不同。问号表明前面的字符可以出现 0 次或 1 次,但只限于 此。它不会匹配多次出现的字符。与星号一样,你可以将问号和字符组一起使用。如果字符组中的字符出现了 0 次或 1 次,模式匹配就成立。但如果两个字符都出现了,或者其中一个字符出现了 2 次,模式匹配就不成立。

加号

加号是类似于星号的另一个模式符号,但跟问号也有不同。加号表明前面的字符可以出现 1 次或多次,但必须至少出现 1 次。如果该字符没有出现,那么模式就不会匹配。

$ echo "beet" | gawk '/be+t/{print $0}'
beet
$ echo "bt" | gawk '/be+t/{print $0}'
$

如果字符 e 没有出现,模式匹配就不成立。加号同样适用于字符组,与星号和问号的使用方式相同。

使用花括号

ERE 中的花括号允许你为可重复的正则表达式指定一个上限。这通常称为间隔 (interval)。可以用两种格式来指定区间:

  1. m:正则表达式准确出现m次
  2. m,n:正则表达式至少出现 m 次,至多 n 次

这个特性可以精确调整字符或字符集在模式中具体出现的次数。

$ echo "beet" | gawk --re-interval '/be{1,2}t{print $0}'
beet
$ echo "beeet" | gawk --re-interval '/be{1,2}t{print $0}'
$

管道符号

管道符号允许你在检查数据流时,用逻辑 OR 方式指定正则表达式引擎要用的两个或多个模 式。如果任何一个模式匹配了数据流文本,文本就通过测试。如果没有模式匹配,则数据流文本匹配失败。

$ 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 "Sat" | gawk '/Sat(urday)?/{print $0}'
Sat
$ echo "Saturday" | gawk '/Sat(urday)?/{print $0}'
Saturday

将分组和管道符号一起使用来创建可能的模式匹配组是很常见的做法。

实践

文件计数器

这个示例的目的是对 PATH 环境变量中定义的目录中的可执行文件进行计数。

首先需要将 PATH 变量解析成单独的目录名。由于 PATH 中的每个路径都是由冒号分隔的,要获取目录列表,就必须用空格来替换冒号,这由一条 sed 命令就可以解决:

echo $PATH | sed 's/:/ /g'

分离出目录后,就可以用 for 语句来遍历每个目录:

#!/usr/local/bin/bash

mypath=$(echo $PATH | sed 's/:/ /g')
count=0
for directory in $mypath
do
  path=$(ls $directory)
  for item in $path
  do
    count=$[ $count+1 ]
  done
  echo "$directory - $count"
  count=0
done

---
/usr/local/bin - 76
/usr/bin - 974
/bin - 36
/usr/sbin - 244
/sbin - 61

解析邮件地址

正则表达式通常用于验证数据,确保脚本中数据格式的正确性。一个常见的数据验证应用就是检查邮件地址。邮件地址的基本格式为:

username@hostname

username 可用字母数字以及点号、单破折号、加号和下划线。在有效的邮件用户名中,这些字符可能以任意组合形式出现。邮件地址的 hostname 部分由一个或多个域名和一个服务器名组成,可以包含字母数字以及点号及单破折号。服务器名和域名都用点分隔,先指定服务器名,紧接着指定子域名,最后是后面不带点号的顶级域名。

从左侧开始构建这个正则表达式,先是用户名:

^([a-zA-z0-9_\-\.\+]+)@

这个分组指定了用户名中允许的字符,加号表明必须有至少一个字符 (@)。

hostname 模式使用同样的方法来匹配服务器名和子域名。

([a-zA-Z0-9\-\.]+)

这个模式可以匹配文本:

  • server
  • server.subdomain
  • server.subdomain.subdomain

对于顶级域名,有一些特殊的规则。顶级域名只能是字母字符,必须不少于二个字符 (国家 或地区代码中使用),并且长度上不得超过五个字符。下面就是顶级域名用的正则表达式模式:

\.([a-zA-Z]{2,5})$

将整个模式放在一起会生成如下模式:

^([a-zA-z0-9_\-\.\+]+)@([a-zA-Z0-9\-\.]+)\.([a-zA-Z]{2,5})$
avatar

Frank Lin

Code learning...