Skip to content

正则表达式

正则表达式基础

什么是正则表达式

让我们回忆一下自己是如何使用 Office 软件中的“查找”功能的。
该功能似乎很简单,比如说,想要在当前文档中找到 hello,只需要在查找选项中输入 hello 就可以了。
可能大家没有意识到,其实这就是一种形式最简单的“表达式”,查找工具会使用某种匹配方式进行全文搜索,其工作原理也非常简单,那就是先找到 h,然后看后面是不是 e,再看后面是不是 l,以此类推。如果全部符合,那就是匹配到了。
但这里可能也会出现一个问题,这种简单的查找其实也能匹配到 helloworld(注意中间没有空格)这样的文字,不过 Office 的查找中还提供了高级功能,选中“全字匹配”就只会匹配 hello 了。
再让我们想想数学中的方程组,它们实际上是一种表明变量关系的“表达式”,我们可以根据该表达式求出变量 x、y 的值,x 和 y 可能是唯一匹配,也可能有多个匹配。

在 Linux 文件模式中,没有类似于 Office 的图形化匹配工具,但可以使用“正则表达式”来做相同的匹配工作。
还是以精准匹配 hello 为例,在正则表达式中就可以用\<hello>\来表示,这里使用到了正则表达式的特殊符号。
正则表达式中还有更多复杂的符号可用来代表其他有意义的字符,这实际上是一种抽象的过程。
提到抽象,在现实生活中我们可以用“由内燃机驱动的、有轮子的工具”来代表所有的机动车,但是计算机并不懂这些自然语言,那么用什么来代表诸如手机号、IP 地址、一个网址等有一定格式和特征的字符串呢?答案就是使用正则表达式。

说到这里,再解释什么是正则表达式就显得简单明了了:
正则表达式就是能用某种模式去匹配一类字符串的公式,它是由一串字符和元字符构成的字符串。
所谓元字符,就是用以阐述字符表达式的内容、转换和描述各种操作信息的字符。

基础的正则表达式

我们第一次看到了\<hello>\,其中的\<\>就是正则表达式中的两个特殊字符,也叫做元字符,它代表的意思是一个单词开始或结束的位置。

"." 符号

点符号用于匹配除换行符之外的任意一个字符。
例如: r.t可以匹配rot、rut,但是不能匹配root,若使用r..t就可以匹配root、ruut等。
下面的例子是从/etc/passwd中搜索出“包含 r,紧跟着两个字符,后面再接 t”的行。

1
2
3
4
➜  ~ grep 'r..t' /etc/passwd
root:x:0:0:root:/root:/bin/zsh
operator:x:11:0:operator:/root:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin

"*"符号

"*"符号用于匹配前一个字符 0 次或任意多次,比如ab*,可以匹配a、ab、abb等。
*经常和.加在一起使用。比如.*代表任意长度的不包含换行的字符。
下面的例子是试图找到连续的 r 字母紧跟字母 t 的行。
由于在/etc/passwd中没有rr、rrt这样的匹配,所以该表达式实际上只找出了包含 t 的行(r 匹配了 0 次)

1
2
3
4
5
6
7
➜  ~ grep 'r*t' /etc/passwd
root:x:0:0:root:/root:/bin/zsh
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
operator:x:11:0:operator:/root:/sbin/nologin
...
tcpdump:x:72:72::/:/sbin/nologin

如果把上面的r*t换成r.*t,代表查找包含字母 r,后面紧跟任意长度的字符,再跟一个字母 t 的行。如下所示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
➜  ~ grep 'r.*t' /etc/passwd
root:x:0:0:root:/root:/bin/zsh
operator:x:11:0:operator:/root:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
systemd-bus-proxy:x:999:998:systemd Bus Proxy:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
polkitd:x:998:997:User for polkitd:/:/sbin/nologin
tss:x:59:59:Account used by the trousers package to sandbox the tcsd daemon:/dev/null:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin

\{n,m\}符号

虽然*可用于重复匹配前一个字符,但却不能精确地控制匹配的重复次数,使用\{n,m\}符号则能更加灵活地控制字符的重复次数,典型的有以下 3 种形式:

  • {n\}匹配前面的字符 n 次。下例匹配的是包含 root 的行(r 和 t 中包含两个 o)
1
2
3
➜  ~ grep 'ro\{2\}t' /etc/passwd
root:x:0:0:root:/root:/bin/zsh
operator:x:11:0:operator:/root:/sbin/nologin
  • \{n,\}匹配前面的字符至少 n 次以上(含 n 次)
1
2
3
➜  ~ grep 'ro\{0,\}t' /etc/passwd
root:x:0:0:root:/root:/bin/zsh
operator:x:11:0:operator:/root:/sbin/nologin
  • \{n,m\}匹配前面的字符 n 到 m 次

^符号

尖角号。这个符号用于匹配开头的字符。比如说^root匹配的是以字母 root 开始的行

1
2
➜  ~ grep '^root' /etc/passwd
root:x:0:0:root:/root:/bin/zsh

$符号

和上面的尖角号相对,$用于匹配尾部,比如说abc$代表的是以 abc 结尾的行。
如果是^$则代表该行为空,因为^$间什么都没有。
下例匹配的是以 r 开头,中间有一串任意字符,以 h 结尾的行。

1
2
➜  ~ grep '^r.*h$' /etc/passwd
root:x:0:0:root:/root:/bin/zsh

[]符号

这是一对方括号,用于匹配方括号内出现的任一字符。
比如说单项选择题的答案,可能是 A、B、C、D 选项中的任意一种,用正则表达式就是[ABCD]
如果遇到比较大的番位的匹配,比如说要匹配任意一个大写字母,就需要使用-号做范围限定,写成[A-Z],要匹配所有字母则写成[A-Za-z]
一定要注意,这里-的作用不是充当一个字符。

如果是要匹配不是大写字母 A、B、C、D 的字符又该怎么写呢?还记得上面的^号吗,如果这个符号出现在[]中,则代表取反,也就是“不是”的意思。
那这里的写法就是[^A-D],事情变得有点复杂了。

这里举个例子,看如何匹配手机号。手机号是 11 位连续的数字,第一位一定是 1, 所以表示为^1
第二位有可能是 3(移动)或 8(联通),表示为[38]
后面连续 9 个任意数字,表示为[0-9]\{9\};
所以整个表达式应该写为^1[38][0-9]\{9\}

\符号

假设有个固定电话号码 021-88888888,当染也可以写成 021 88888888(区号和电话号码之间用空格隔开),它们的不同之处就是区号和电话号码之间使用的符号不同,一个是-,一个是空格。
那么,对于这个电话号码要怎么匹配呢,很容易地想到应该使用[]来匹配。
但是这么些:[ -],对吗?答案是否定的,因为-放到[]中有特别的含义。
为了表示其作为一个字符的本意,就要使用\符了。
这个符号代表转义字符,我们可以对很多特殊的字符进行“转义”,让它只代表字符本身,因此这里的写法就应该是[\ \-]

1
2
3
4
5
6
7
➜  Desktop cat test.txt
hhh 2333
666
qifei-fei
➜  Desktop grep '.*[\ \-].*' test.txt
hhh 2333
qifei-fei

再举个例子,之前我们了解到.*代表的是任意长度的不包含换行的重复字符。
但是如果想要匹配任意长度的点号呢?这时使用转义字符就对了\.*
如果想要对\符号进行转义,就可以这样写: \\

\<符号和\>符号

这两个符号分别用于界定单词的左边界和右边界。
比如说\<hello用于匹配以hello开头的单词;而hello\>则用于匹配以hello结尾的单词。
还可以使用它们的组合\<\>用于精确匹配一个字符串。
所以\<hello\>可精确匹配单词 hello,而不是 helloworld 等。如下所示:

1
2
3
4
➜  Desktop cat test.txt | grep '\<hello\>'
hello
hello world
 nihao hello

以上是 8 种常见的元字符,还有些不太常用的字符,这些字符中有不少可以使用之前讲的 8 种基础的元字符来表示

\d符号

匹配一个数字,等价于[0-9],使用 grep 匹配这种正则表达式时可能会遇到无法匹配的问题

1
2
3
4
➜  Desktop echo 123 | grep '[0-9]'
123
➜  Desktop echo '123' | grep '[0-9]'
123

123 是一个数字,用[0-9]匹配成功

1
2
3
➜  ~ echo 123 | grep '\d'
➜  ~ echo 123 | grep -P '\d'
123

直接使用\d匹配不成功,这是因为\d是一种 Perl 兼容模式的表达式,又称作 PCRE,要想使用这种模式的匹配符,需要加上 -P 参数(在 mac 上不需要)

\b符号

匹配单词的边界,比如\bhello\b可精确匹配hello单词

1
2
3
4
➜  ~ echo "hello world" | grep '\bhello\b'
hello world
➜  ~ echo "helloworld" | grep '\bhello\b'
# 这里没有匹配

\B符号

匹配非单词的边界,比如hello\B可以匹配helloworld中的hello

1
2
➜  ~ echo "helloworld" | grep 'hello\B'
helloworld

\w符号

匹配字母、数字和下划线,等价于[A-Za-z0-9]

1
2
3
4
➜  ~ echo "a" | grep '\w'
a
➜  ~ echo "\\" | grep '\w'
# 这里没有匹配

\W符号

匹配非字母、非数字、非下划线,等价于[^A-Za-z0-9]

1
2
➜  ~ echo "\\" | grep '\W'
\

\n符号

匹配一个换行符

\r符号

匹配一个回车符

\t符号

匹配一个制表符

\f符号

匹配一个换页符

\s符号

匹配任何空白字符

\S符号

匹配任何非空白字符

扩展的正则表达式

顾名思义,扩展的正则表达式一定是针对基础正则表达式的一些补充。
实际上,扩展正则表达式比基础正则表达式多了几个重要的符号。
不过要注意的是,在使用这些扩展符号时,需要使用 egrep 命令。

?符号

用于匹配前一个字符 0 次或 1 次,所以ro?t仅能匹配rotrt

+符号

用于匹配前一个字符 1 次以上,所以ro+t就可以匹配rot、root

|符号

|符号是“或”的意思,即多种可能的罗列,彼此间是一种分支关系。
比如说有些地区固定电话的区号是 4 位数,有些地方却是 3 位数,这样针对不同的区号就有不同的固定电话的表示方式,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 区号是 3 位的固定电话的正则表达式方式   
^0[0-9]\{2\}\-[0-9]\{8\}
# 区号是 4 位的固定电话的正则表达式方式
^0[0-9]\{3\}\-[0-9]\{8\}
# 两种区号的固定电话可以如下写
^0[0-9]\{2,3\}\-[0-9]\{8\}
# 使用 | 符号也可以,但是比上面的方式麻烦些
^0[0-9]\{2\}\-[0-9]\{8\}\|^0[0-9]\{3\}\-[0-9]\{8\}

➜  ~ cat a.txt
021-88888888
0233-66666666
➜  ~ cat a.txt | grep '^0[0-9]\{2\}\-[0-9]\{8\}'
021-88888888
➜  ~ cat a.txt | grep '^0[0-9]\{3\}\-[0-9]\{8\}'
0233-66666666
➜  ~ cat a.txt | grep '^0[0-9]\{2,3\}\-[0-9]\{8\}'
021-88888888
0233-66666666
➜  ~ cat a.txt | grep '^0[0-9]\{2\}\-[0-9]\{8\}\|^0[0-9]\{3\}\-[0-9]\{8\}'
021-88888888
0233-66666666

()符号

()符号通常需要和|符号联合使用,用于枚举一系列可替换的字符。
比如说固定电话的区号和电话号码之间,可能用-符号或者用一个空格连接,用于匹配的正则表达式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
➜  ~ cat a.txt
021-88888888
0233-66666666
012 88888888
➜  ~ cat a.txt | grep '^0[0-9]\{2,3\}\(\-\|\ \)[0-9]\{8\}'
021-88888888
0233-66666666
012 88888888
➜  ~ cat a.txt | grep '^0[0-9]\{2,3\}\(-\| \)[0-9]\{8\}'
021-88888888
0233-66666666
012 88888888

这种写法可以换用[]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
➜  ~ cat a.txt | grep '^0[0-9]\{2,3\}[\ \-][0-9]\{8\}'
021-88888888
0233-66666666
012 88888888
但是下面的写法只能匹配到一个
➜  ~ cat a.txt | grep '^0[0-9]\{2,3\}[-\ ][0-9]\{8\}'
021-88888888
0233-66666666
012 88888888
➜  ~ cat a.txt | grep '^0[0-9]\{2,3\}[- ][0-9]\{8\}'
021-88888888
0233-66666666
012 88888888
➜  ~ cat a.txt | grep '^0[0-9]\{2,3\}[\-\ ][0-9]\{8\}'
012 88888888

虽然以上这两种写法没有本质上的不同,因为()|可以和[]相互混用,但是在某些场景下,()|可以做得更多,比如说像 hard、hold 或 hood 等这类开头和结尾的字符都一样的单词,要匹配这些就必须使用()|了,如下所示:

1
2
3
4
5
6
7
8
➜  ~ cat b.txt
hard
hold
hood
➜  ~ cat b.txt | grep 'h\(ar\|oo\|ol\)d'
hard
hold
hood

通配符

或许这是你第一次听说“通配符”,但实际上你一定用过它,只是你并没有意识到。
相信所有人都曾经用过 Windows 下的文件搜索功能。
你可能某一次想找个 .doc 文件,但是又一时想不起该文件名和放置的位置(确实没有养成归档的好习惯),所以你决定把计算机上所有的 .doc 文件全部找出来,然后再进行人工挑选,于是你用*号来代替该文件的名字,以.doc作为扩展名进行一次搜索。

实际上,通配符是一种特殊的语句,主要包含*?号(还有{}、^、!)。
主要用来模糊搜索文件,使用它替代一个或多个真正的字符,尤其是在不知道或者不确定完整的文件名时,用来匹配符合条件的文件。

*符号

这里的*就是提到的第一个通配符,代表 0 个或多个字符。
那么之前的*.doc就是值所有以 .doc 结尾的文件。
如果想要找的文档是以字母 A 开头,则可用A*.doc来查找。
在 Linux 中,列出当前目录中是否存在以 .doc 结尾的文件可以使用以下命令

1
ls -l *.doc

?符号

如果要列出以字母 A 开头、但是只有两个字母的文件名、以 .doc 结尾的文件,就需要使用?了。
当它作为通配符使用时,代表的是任意一个字符。其写法如下:

1
ls -l A?.doc

{}符号

{}可拥有匹配所有括号内包含的以逗号隔开的字符。
例如,下面列出了所有以字母 A、B、C 开头,以 .doc 结尾的文件

1
2
3
4
5
6
7
8
9
➜  ~ ls -l {A,B,C}.doc
-rw-r--r-- 1 root root 0 8月  28 23:18 A.doc
-rw-r--r-- 1 root root 0 8月  28 23:18 B.doc
-rw-r--r-- 1 root root 0 8月  28 23:18 C.doc

➜  ~ ls -l [A-C].doc
-rw-r--r-- 1 root root 0 8月  28 23:18 A.doc
-rw-r--r-- 1 root root 0 8月  28 23:18 B.doc
-rw-r--r-- 1 root root 0 8月  28 23:18 C.doc

以上两种方法都能满足题意,但是如果要列出以字母 AB 或者 CD

有意思的是,{}还支持嵌套的通配。以{x, y}为例,如果 x 和 y 各自本身也是通配符,则就变得更强大了

1
2
3
4
5
➜  ~ ls -l {[A-Z]*.doc,[0-9]??.txt}
-rw-r--r-- 1 root root 0 8月  28 23:28 233.txt
-rw-r--r-- 1 root root 0 8月  28 23:18 A.doc
-rw-r--r-- 1 root root 0 8月  28 23:18 B.doc
-rw-r--r-- 1 root root 0 8月  28 23:18 C.doc

^符号和!符号

这两个符号往往和[]一起使用,当出现在[]中的时候,代表取反。
所以[^A](或[!A])代表不是 A

通配符和正则表达式之间存在的一些差异,特别是有些相同的字符即用在正则表达式中又用在通配符中,极易造成混淆和干扰,只有通过多读多想才能加深理解和认识。
简要地说,正则表达式主要使用在对文件内容的匹配上,而通配符主要是用在文件名的匹配上,可以用这种方法来帮助区分二者。

正则表达式示例

我们了解了 grep 的一些基本用法,但是它的功能还远远不止这些。
grep 的英文是 Global search Regular Expression and print out the line,即全面搜索正则表达式并打印出匹配行。

grep 和正则表达式结合使用后产生了强大的搜索效果。
接下来通过更多的示例来介绍正则表达式和 grep 结合的用法,来理解和认识正则表达式和 grep。
由于正则表达式中含有较多特殊的字符,所以结合 grep 时,最好使用单引号将正则表达式括起来,以免造成错误

首先创建一个文件RegExp.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
good morning teacher
hello world is a script
gold sunshine looks beautiful
golden time files
god bless me
what a delicious food
they teast Good
you fell glad
verygood
wrong word gooood
wrong word gl0d
wrong word gl2d
wrogn world gl3d
www.helloworld.com
www@helloworld@com

Upper case
100% means pure

php have a gd module
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 搜素含有 good 单词的行,注意,默认是区分大小写的,所以这里只会打印出包含小写 good 的行
➜  ~ grep 'good' RegExp.txt
good morning teacher
verygood

# 搜索含有 good 单词的行,不区分大小写
➜  ~ grep -i 'good' RegExp.txt
good morning teacher
they teast Good
verygood

# 统计不含 good 单词的行的行数,不区分大小写
➜  ~ grep -ivc 'good' RegExp.txt
15

下面介绍正则表达式和 grep 结合的用法

(1) 使用^匹配行首

1
2
3
# 搜索以 good 开头的行
➜  ~ grep '^good' RegExp.txt
good morning teacher

(2) 使用$匹配行尾

1
2
3
# 搜索以 Good 结尾和行
➜  ~ grep 'Good$' RegExp.txt
they teast Good

(3) 使用^$组合,匹配空行,下面的命令可计算文件中共有多少行空行:

1
2
➜  ~ grep -c '^$' RegExp.txt
2

(4) 使用方括号匹配多种可能

1
2
3
4
5
# 搜索包含 good 和 Good 的行
➜  ~ grep '[Gg]ood' RegExp.txt
good morning teacher
they teast Good
verygood

(5) 在方括号中使用^做反选

1
2
3
4
# 搜索包含 ood 的行,但是不能是 Good 或 good
➜  ~ grep '[^Gg]ood' RegExp.txt
what a delicious food
wrong word gooood

(6) 使用.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 搜索包含一个词,该词以 g 开头、紧接着是两个任意字符、再接着是一个 d 的行
➜  ~ grep 'g..d' RegExp.txt
good morning teacher
gold sunshine looks beautiful
golden time files
you fell glad
verygood
wrong word gl0d
wrong word gl2d
wrogn world gl3d

# 搜索包含一个词,该词以 G 或 g 开头,紧接着是两个任意字符,再接着是一个 d 的行
➜  ~ grep '[Gg]..d' RegExp.txt
good morning teacher
gold sunshine looks beautiful
golden time files
they teast Good
you fell glad
verygood
wrong word gl0d
wrong word gl2d
wrogn world gl3d

# 搜索这样的一些行,该行包含某个单词,该词满足如下条件:第一个字符是 G 或 g,第二个字符是 l 或 o,第三个字符是换行符之外的任意字符,第四个字符是 d
➜  ~ grep '[Gg][lo].d' RegExp.txt
good morning teacher
gold sunshine looks beautiful
golden time files
they teast Good
you fell glad
verygood
wrong word gl0d
wrong word gl2d
wrogn world gl3d

(7) 使用精确匹配

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 搜索含有 gold 的行,从搜索结果中发现 golden 也被匹配出来了  
➜  ~ grep 'gold' RegExp.txt
gold sunshine looks beautiful
golden time files

# 精确匹配含有 gold 这个单词的行
➜  ~ grep '\<gold\>' RegExp.txt
gold sunshine looks beautiful

# 用`\b\b`的效果和`\<\>`一致
➜  ~ grep '\bgold\b' RegExp.txt
gold sunshine looks beautiful

(8) 使用*

1
2
3
4
5
6
7
# 搜索这样一些行,该行包含某个单词,该词满足如下条件:以 g 开头,g 后面跟 0 到无限个 o,再跟 d
➜  ~ grep 'go*d' RegExp.txt
good morning teacher
god bless me
verygood
wrong word gooood
php have a gd module

(9) 使用.*

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 搜索这样一些行,该行包含某个单词,该词满足条件:以 g 开头,最后是 d
➜  ~ grep 'g.*d' RegExp.txt
good morning teacher
gold sunshine looks beautiful
golden time files
god bless me
you fell glad
verygood
wrong word gooood
wrong word gl0d
wrong word gl2d
wrogn world gl3d
php have a gd module

(10) 使用-

1
2
3
4
5
# 文件中有一些拼写错误的单词,发现是把 glod 中的 o 字母写成数字 0 了
➜  ~ grep 'gl[0-9]d' RegExp.txt
wrong word gl0d
wrong word gl2d
wrogn world gl3d

(11) 使用\做字符转义

1
2
3
4
5
6
7
8
9
# 搜索文件中包含域名 www.helloworld.com 的行
# 这里的 . 号被解析成了除换行符外的任意字符
➜  ~ grep 'www.helloworld.com' RegExp.txt
www.helloworld.com
www@helloworld@com

# 将点做转义
➜  ~ grep 'www\.helloworld\.com' RegExp.txt
www.helloworld.com

(12) 使用\{\}

1
2
3
4
5
6
7
8
9
# 搜索以字母 g 开头包含两个以上 o 的单词
➜  ~ grep 'go\{2,\}' RegExp.txt
good morning teacher
verygood
wrong word gooood

# 搜索以字母 g 开头,中间正好包含 4 个 o 的单词
➜  ~ grep 'go\{4,\}' RegExp.txt
wrong word gooood

(13) 特殊的 POSIX 字符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
文字数字字符: [:alnum:]
文字字符: [:alpha:]
数字字符: [:digit:]
非空字符(非空格、控制字符): [:graph:]
小写字符: [:lower:]
控制字符: [:cntrl:]
非空字符(包括空格): [:print:]
标点符号: [:punct:]
所有空白字符(新行、空格、制表符): [:sapce:]
大写字符: [:upper:]
十六进制数字(0-9,a-f,A-F): [:xdigit:]
1
2
3
4
5
6
7
# 搜索以大写字母开头的行
➜  ~ grep '^[[:upper:]]' RegExp.txt
Upper case

# 搜索以数字开头的行
➜  ~ grep '^[[:digit:]]' RegExp.txt
100% means pure

(14) 使用扩展的正则表达式 egrep

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 搜索 g 和 d 之间至少有一个 o 的行
➜  ~ egrep 'go+d' RegExp.txt
good morning teacher
god bless me
verygood
wrong word gooood

# 搜索 g 和 d 之间只有 0 个或 1 个 o 的行
➜  ~ egrep 'go?d' RegExp.txt
god bless me
php have a gd module

# 搜索有 glad 或 gold 的行
➜  ~ egrep 'glad|gold' RegExp.txt
gold sunshine looks beautiful
golden time files
you fell glad

# 搜索有 glad 或 gold 的行的另一种写法  
➜  ~ egrep 'g(la|ol)d' RegExp.txt
gold sunshine looks beautiful
golden time files
you fell glad

文本处理工具 sed

sed(stream editor)是一种非交互式的流编辑器,通过多种转换修改流经它的文本。
但是请注意,默认情况下,sed 并不会改变原文件本身,而只有对流经 sed 命令的文本进行修改,并将修改后的结果打印到标准输出中(也就是屏幕)。

sed 处理文本时是以行为单位的,每处理完一行就立即打印出来,然后再处理下一行,直至全文处理结束。
sed 可做的编辑动作包括删除、查找替换、添加、插入、从其他文件中读入数据等。

注意:要想保存修改后的文件,必须使用重定向生成新的文件。如果想要直接修改源文件本身则需要使用-i参数

sed 命令使用的场景包括以下一些

  • 常规编辑器编辑困难的文本
  • 太过于庞大的文本,使用常规编辑器难以胜任(比如说 vi 一个几百兆的文件)
  • 有规律的文本修改,加快文本处理速度(比如说全文替换)

为了演示 sed 的用法,首先准备如下文件:

1
2
3
4
5
➜  ~ cat Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

使用 sed 修改文件流的方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
sed [options] 'command' file
# options 是 sed 可以接受的参数
# command 是 sed 的命令集(一共是 25 个)    
# 使用 -e 参数和分号连接多编辑器命令    
# 该参数本身只是 sed 的一个简单参数,表示将下一个字符串解析为 sed 编辑命令 
# 一般情况下可以忽略,但是当 sed 需要传递多个编辑命令时该参数就不能少了  

# 下面的例子就是演示了在将 this 改为 That 的同时,还要将 line 改为 LINE
# 两个编辑命令前都要使用 -e 参数,如果有更多的编辑需求,以此类推即可
➜  ~ sed -e 's/this/That/g' -e 's/line/LINE/g' Sed.txt
That is LINE 1, That is First LINE
That is LINE 2, the Second LINE, Empty LINE followed
That is LINE 4, That is Third LINE
That is LINE 5, That is Fifth LINE

# 使用分号连接两个编辑命令,上面的命令可以用分号改写为
➜  ~ sed 's/this/That/g;s/line/LINE/g' Sed.txt
That is LINE 1, That is First LINE
That is LINE 2, the Second LINE, Empty LINE followed
That is LINE 4, That is Third LINE
That is LINE 5, That is Fifth LINE

删除

使用 d 命令可删除指定的行,示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 将 file 的第一行删除后输出到屏幕
➜  ~ sed '1d' Sed.txt
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

# 由于 sed 默认不修改原文件,如果希望保存修改后的文件则需要用重定向   
➜  ~ sed '1d' Sed.txt > saved_file
➜  ~ cat saved_file
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

# 如果想直接修改文件,使用`-i`参数,这里不会有任何输出,而是直接修改了源文件,删除了第一行 
sed -i '1d' file

# 删除指定范围的行
➜  ~ sed '1,2d' Sed.txt
this is line 4, this is Third line
this is line 5, this is Fifth line

# 删除指定范围的行(第一行到最后行),清空文件
➜  ~ sed '1,$d' Sed.txt

# 删除最后一行
➜  ~ sed '$d' Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line

# 删除除指定范围以外的行
➜  ~ sed '2!d' Sed.txt
this is line 2, the Second line, Empty line followed

# 删除所有包含 Empty 的行
➜  ~ sed '/Empty/d' Sed.txt
this is line 1, this is First line
this is line 4, this is Third line
this is line 5, this is Fifth line

# 删除空行
➜  ~ sed '/^$/d' Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

查找替换

使用 s 命令可将查找到的匹配文本内容替换为新的文本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# s 命令用于替换为本,本例中使用 LINE 替换 line,请注意每一行只有第一个 line 被替换了,默认情况下只替换第一次匹配到的内容
➜  ~ sed 's/line/LINE/' Sed.txt
this is LINE 1, this is First line
this is LINE 2, the Second line, Empty line followed
this is LINE 4, this is Third line
this is LINE 5, this is Fifth line

# 要想每行最多匹配 2 个 line,并改为 LINE,可用如下方式,注意到第 2 行中有 3 个 line,只有前两个被替换了
➜  ~ sed 's/line/LINE/2' Sed.txt
this is line 1, this is First LINE
this is line 2, the Second LINE, Empty line followed
this is line 4, this is Third LINE
this is line 5, this is Fifth LINE

# 命令利用 g 选项,可以完成所有匹配值的替换   
➜  ~ sed 's/line/LINE/g' Sed.txt
this is LINE 1, this is First LINE
this is LINE 2, the Second LINE, Empty LINE followed
this is LINE 4, this is Third LINE
this is LINE 5, this is Fifth LINE

# 只替换开头的 this 为 that
➜  ~ sed 's/^this/that/' Sed.txt
that is line 1, this is First line
that is line 2, the Second line, Empty line followed
that is line 4, this is Third line
that is line 5, this is Fifth line

字符转换

使用 y 命令可进行字符转换,其作用为将一系列字符逐个地变换为另外一系列字符,基本用法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 该命令会将 file 中的 O 转换为 N、L 转换为 E、D 转换为 W,注意转换字符和被转换字符的长度要相等,否则 sed 无法执行
➜  ~ cat c.txt
O k Dang
➜  ~ sed 'y/OLD/NEW/' c.txt
N k Wang

# 下面的命令演示了将数字 1 转换为 A,2 转换为 B,4 转换为 C,5 转换成 D 的用法
➜  ~ sed 'y/1245/ABCD/' Sed.txt
this is line A, this is First line
this is line B, the Second line, Empty line followed
this is line C, this is Third line
this is line D, this is Fifth line

插入文本

使用 i 或 a 命令插入文本,其中 i 代表在匹配行之前插入,而 a 代表在匹配行之后插入,示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 使用 i 在第二行前插入文本
➜  ~ sed '2 i Insert' Sed.txt
this is line 1, this is First line
Insert
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

# 使用 a 在第二行后插入文本
➜  ~ sed '2 a Insert' Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed
Insert
this is line 4, this is Third line
this is line 5, this is Fifth line

# 在匹配行的上一行插入
➜  ~ sed '/Second/i\Insert' Sed.txt
this is line 1, this is First line
Insert
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

读入文本、打印、写文件

使用 r 命令可从其他文件中读取文件,并插入匹配行之后,示例如下:

可以给 Sed.txt 添加一个空行

1
2
3
4
5
6
7
➜  ~ cat Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

➜  ~
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 将 /etc/passwd 中的内容读出放到 Sed.txt 空行之后
➜  ~ sed '/^$/r /etc/passwd' Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

root:x:0:0:root:/root:/bin/zsh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
...

使用 p 命令可进行打印,这里使用 sed 命令时一定要加 -n 参数,表示不打印没关系的行。
从之前的例子中可以看出,由于 sed 的工作原理是基于行的,因此每次都有大量的输出。
可使这些输出有一些是我们并不需要看到的,而只需要输出匹配的行或者处理过的行就好了。
简单来说,打印操作是删除操作的“逆操作”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 打印出文件中指定的行
➜  ~ sed -n '1p' Sed.txt
this is line 1, this is First line

# 将 the 替换成 THE,sed 实际处理了第二行,其他几行由于没有匹配所以并未真正处理,但是 sed 的工作原理是基于流的,所以所有流过的行都打印出来
➜  ~ sed 's/the/THE/' Sed.txt
this is line 1, this is First line
this is line 2, THE Second line, Empty line followed
this is line 4, this is Third line
this is line 5, this is Fifth line

# 使用 p 命令,则只打印实际处理过的行,简化了输出(使用 -n 参数)
➜  ~ sed -n 's/the/THE/p' Sed.txt
this is line 2, THE Second line, Empty line followed

正如之前所说,sed 本身默认并不改写原文件,而只是对缓冲区的文本做了修改并输出到屏幕。
所以想保存文件,除了之前提到的两种方法(使用重定向或 -i 参数),还可以使用 w 命令将结果保存到外部指定文件。示例如下:

1
2
3
4
5
# 这里没有任何输出,因为输出被重定向到文件了
➜  ~ sed -n '1,2 w output' Sed.txt
➜  ~ cat output
this is line 1, this is First line
this is line 2, the Second line, Empty line followed

sed 脚本

在平日的工作中,我们可能需要定期对一些文件做分析操作,这种例行的工作往往有一定“标准化”的操作,比如说去除文件中所有的空行,然后再全部替换某些字符等,这种过程类似于生产线上程式化的流水作业。
事实上,可以把这些动作静态化地写到某个文件中,然后调用 sed 命令并使用 -f 参数指定该文件,这样就可以将一系列动作“装载”并应用于指定文件中,这无疑加快了工作效率,这种文件就是 sed 脚本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 该 sed 脚本的作用是将全文的 this 改为 THAT,并删除所有空行
➜  ~ cat Sed.rules
s/this/THAT/g
/^$/d
# 使用 -f 参数指定该脚本并应用于 Sed.txt
➜  ~ sed -f Sed.rules Sed.txt
THAT is line 1, THAT is First line
THAT is line 2, the Second line, Empty line followed
THAT is line 4, THAT is Third line
THAT is line 5, THAT is Fifth line

高级替换

替换匹配行的下一行

想要修改匹配行的下一行的文本,就需要使用 n 命令了。
该命令的作用在于读取匹配行的下一行,然后再用 n 命令后的编辑指令来处理该行。
在下面的 Sed.txt 文件中有一行空白行,现在想将该空格行的下一行中的 line 改为 LINE,而文本中其他部分保持不变,操作如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
➜  ~ cat Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed

this is line 4, this is Third line
this is line 5, this is Fifth line
➜  ~ sed '/^$/{n;s/line/LINE/g}' Sed.txt
this is line 1, this is First line
this is line 2, the Second line, Empty line followed

this is LINE 4, this is Third LINE
this is line 5, this is Fifth line

小结

sed 常用的命令

sed 命令 作用
a 在匹配行后面加入文本
c 字符转换
d 删除行
D 删除第一行
i 在匹配行的
h 复制模板块的内容到存储空间
H 追加模板块的内容到存储空间
g 将存储空间的内容复制到模式空间
G 将存储空间的内容追加到模式空间
n 读取下一个输入行,用下一个命令处理新的行
N 追加下一个输入行到模板后并在二者间插入新行
p 打印匹配的行
P 打印匹配的第一行
q 退出 sed
r 从外部文件中读取文本
w 追加写文件
! 匹配的逆
s/old/new 用 new 替换正则表达式 old
= 打印当前行号

sed 常用的参数

sed 参数 作用
-e 多条件编辑
-h 帮助信息
-n 不输出不匹配的行
-f 指定 sed 脚本
-V 版本信息
-i 直接修改原文件

sed 常用的正则表达式匹配

元字符 作用
^ 匹配行的开始,如/^cat/匹配所有以 cat 开头的行
$ 匹配行的结束,如/cat$/匹配所有以 cat 结尾的行
. 匹配任一非换行字符,如/c.t/匹配 c 后接一个任意字符,然后是 t
* 匹配零个或任意多个字符,如:/*cat匹配一串字符后紧跟 cat 的所有行
[] 匹配指定范围内的字符,如/[Cc]at/匹配 cat 和 Cat
[^] 匹配不在指定范围内的字符,如/[^A-Z]/匹配不是以大写字母开头的行
\(..\) 保存匹配的字符,如:s/\(love)able/\1rs,loveable 被替换成 lovers
& 保存搜索字符用来替换其他字符,如s/love/**&**/,love 变成 love
\< 锚定单词的开始。如/\<cat/匹配包含以 cat 开头的单词的行
\> 锚定单词的结束。如/cat\>/匹配包含以 cat 结尾的单词的行
x\{n\} 重复字符 x, m 次,如/o\{5\}/匹配包含 5 个 o 的行
x\{m,\} 重复字符 x,至少 m 次,如/o\{5,\}/匹配至少 5 个 o 的行
x\{n,m\} 重复字符 x,至少 n 次,不多于 m 次,如/o\{5,10\}/匹配 5 到 10 个 o 的行

文本处理工具 awk

sed 是以行为单位的文本处理工具,而 awk 则是基于列的文本处理工具,它的工作方式是按行读取文本并视为一条记录,每条记录以字段分割成若干字段,然后输出各字段的值。
事实上,awk 是一种编程语言,其语法异常复杂,所以 awk 也是一种较难掌握的文本处理工具。

awk 认为文件都是结构化的,也就是说都是由单词和各种空白字符组成的,这里的“空白字符”包括空格、Tab,以及连接的空格和 Tab 等。
每个非空白的部分叫做“域”,从左到右依次是第一个域、第二个域,等等。
$1、$2分别用于表示域,$0则表示全部域。

为了演示 awk 的用法,首先创建文件 Awk.txt,文件内容如下所示:

1
2
3
4
john.wang   Male    30  021-11111111
lucy.yang   Female  25  021-22222222
jack.chen   Male    35  021-33333333
lily.gong   Female  20  021-44444444    ShangHai

打印指定域

既然 awk 使用$1、$2代表不同的域,则可以打印指定域。
拿 Awk.txt 的第一行来说,第一个域为 john.wang,第二个域为 Male,第三个域为 30,第四个域为 021-11111111。
在下面的演示中,第一条命令打印了$1、$4这两个域,而第二条命令则打印了全部的域

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 只打印姓名和电话号码
➜  ~ awk '{print $1,$4}' Awk.txt 
john.wang 021-11111111
lucy.yang 021-22222222
jack.chen 021-33333333
lily.gong 021-44444444
# 打印全部内容
➜  ~ awk '{print $0}' Awk.txt 
john.wang   Male    30  021-11111111
lucy.yang   Female  25  021-22222222
jack.chen   Male    35  021-33333333
lily.gong   Female  20  021-44444444    ShangHai

指定打印分隔符

默认情况下 awk 是使用空白字符作为分隔符的,但是也可以通过 -F 参数指定分隔符,来区分不同的域(有点像 cut 命令)。示例如下:

1
2
3
4
5
➜  ~ awk -F . '{print $1, $2}' Awk.txt
john wang   Male    30  021-11111111
lucy yang   Female  25  021-22222222
jack chen   Male    35  021-33333333
lily gong   Female  20  021-44444444    ShangHai

内部变量 NF

文件 Awk.txt 所包含的内容并不多,所以我们很容易地知道它的前 3 行中每行都有 4 个域,而最后一行是 5 个域。
但是如果有时候文件很大,每行列数都不一样,靠观察就不现实了,必须通过特定的方式来获得文件的列数。
通过 awk 的内部变量 NF 可以简单地做到这点。当然,如果你指定了不同的分隔符,结果可能不一样。示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 使用默认分隔符
➜  ~ awk '{print NF}' Awk.txt 
4
4
4
5
# 指定分隔符
➜  ~ awk -F . '{print NF}' Awk.txt
2
2
2
2

打印固定域

通过内部变量可以简单地得到每行的列数,而如果在 NF 之前加上$符号,则代表“最后一列”,这样不管每行有多少列,只要使用$NF就能打印出最后一列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 打印最后一列
➜  ~ awk '{print $NF}' Awk.txt     
021-11111111
021-22222222
021-33333333
ShangHai
# 打印倒数第二行
➜  ~ awk '{print $(NF-1)}' Awk.txt
30
25
35
021-44444444

截取字符串

可以使用 substr() 函数对指定域截取字符串,该函数的基本使用方法如下:

1
substr(指定域第一个开始字符的位置,第二个结束的位置)其中第二个结束的位置可以为空,这样默认输出到该域的最后一个字符

下例中将输出 Awk.txt 文件第一个域的第六个字符到最后一个字符的内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
➜  ~ cat Awk.txt | awk '{print substr($1, 6)}'
wang
yang
chen
gong
➜  ~ cat Awk.txt | awk '{print substr($1, 1)}'
john.wang
lucy.yang
jack.chen
lily.gong
➜  ~ cat Awk.txt | awk '{print substr($1, 2)}'
ohn.wang
ucy.yang
ack.chen
ily.gong
➜  ~ cat Awk.txt | awk '{print substr($1, 2, 3)}'
ohn
ucy
ack
ily

确定字符串的长度

使用内部变量 length 可以确定字符串的长度,示例如下

1
2
3
4
5
➜  ~ cat Awk.txt | awk '{print length}'          
36
36
36
48

使用 awk 求列和

结构化的数据在系统中是随处可见的,比如ls -l命令得到的输出、各类系统日志等。
在日常工作中,经常有将其中的数据进行相加的需求。下面演示了对所有人的年龄进行的一些计算。
请注意,年龄字段是第三个域:

1
2
3
4
5
6
# 求年龄的和
➜  ~ cat Awk.txt | awk 'BEGIN{total=0}{total+=$3}END{print total}'
110
# 求平均年龄
➜  ~ cat Awk.txt | awk 'BEGIN{total=0}{total+=$3}END{print total/NR}'
27.5