引言:对于文本操作来说,除了剪切和粘贴外还有更多的操作,尤其是不使用GUI的时候,这更明显。在本文中,作者讲解了使用GUN文本工具包来进行文本处理。学完本文,你将会像专家一样处理文本。
概述:本文介绍了“过滤器”,可以使用过滤器构建复杂的管道来处理文本。你将学会如何显示文本、排序、单词和行统计、转换等多种操作技术。你还会学习使用sed编辑器。具体入下:
- 通过向文本工具包过滤器发送文本文件或者输出流来改变输出;
- 使用GNU文本工具包里的命令行工具;
- 使用sed脚本来处理复杂的文本改变
本文针对的是LPIC-1的103.2目标,其权重为3.
文本过滤
文本过滤是这样一个过程,接受输入文本流,执行一些改变,然后转发给输出流。尽管输入输出流可以来自文件,在Linux和Unix环境中,过滤通常是通过构建一个命令行管道来实完成的。一个命令的输出被重定向给下一个命令作为输入。管道和重定向在另一篇文章中有详细讲解,现在我们看看管道和重定向的操作符 | 和 >。
流
流就是一个可以使用库函数读写的字节序列,读写它的库函数对上层应用程序屏蔽了底层设备的细节。同一个程序可以使用流可以做到设备无关的方式来读取和写入一个终端,一个文件,或者网络套接字。现代编程环境和shell都使用三个标准输入输出流:
- stdin 标准输入流,为命令提供输入;
- stdout 标准输出流,用来显示命令的输出;
- stderr 标准错误流,用来显示命令的错误输出
管道
命令的输入来自你提供的命令行参数,输出显示到你的终端设备上。很多文本处理命令可以接收来自标准输入流或者文件的输入。为了把一个命令的输出作为另一个命令的输入,你可以使用管道符号|。下面显示了利用管道把echo 的输出作为sort的输入。
[root@localhost ~]# echo -e "apple\npear\nbanana" | sort
apple
banana
pear
[root@localhost ~]#
每个命令都可能有选项或者参数。你同样可以使用 | 来把第二个命令的输出作为第三个命令的输入,以此类推。通过构建长的命令管道(每个命令能力有限)来完成任务是Linux/Unix中常见的方法。 有时候,你可能会看到一个命令的参数是 - 而不是一个文件名,这个-的含义是此命令的输入来自标准输入流而不是文件。
输出重定向
通过管道构建一个多个命令组成的流水线可以把结果输出到终端上。但是有时候你会需要把结果保存到文件中。这可以通过输出重定向操作符>来完成。
接下来我们将会使用一些小文件,所以我们创建一个叫做 lpi103-2的文件夹,然后cd到这个目录下。然后我们使用>重定向echo命令的输出到一个叫做text1的文件中。
[root@localhost ~]# mkdir lpi103-2
[root@localhost ~]# cd lpi103-2/
[root@localhost lpi103-2]# echo -e "1 apple\n2 pear\n3 banana" > text1
既然我们已经熟悉了管道和重定向这两个基本的工具,接下来我们看看一些常用的UNIX和LINUX文本处理命令和过滤器。本节只简要介绍这些命令,具体的用法参考man手册页。
Cat, od, split
前面我们创建了text1文件,现在我们来看看文件里是什么。使用cat(concatenate的缩写)命令来显示一个文本文件的内容到标准输出。
[root@localhost lpi103-2]# cat text1
1 apple
2 pear
3 banana
如果不给cat命令提供参数,那么它将会把标准输入流作为其输入。这中特性和输出重定向配合就可以创建另一个文件,如下:
cat >text2
9 plum
3 banana
10 apple
cat会持续地读取标准输入的内容直到到达文件结尾。使用Ctrl + d来告诉cat标准输入的结束。bash也是使用Ctrl+d来退出。本例使用Tan键来分割编号和水果名。
注意,cat是concatenate的缩写,是连接的意思。因为你可以使用cat来连接多个文件一起输出。下面,我们把text1,text2一起输出显示。
[root@localhost lpi103-2]# cat text*
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
注意,上面两个文件输出对齐的不同。要弄清楚其中的原因,你需要查看文件里的控制字符。控制字符用来控制输出效果,本身并不显示自身,所以我们需要以一种格式dump文件来找出和解释这些特殊的字符。GUN工具包里的od命令可以完成这个任务。-A 选项指定字符位置的计数法,可以是x(十六进制),d(十进制),o(八进制)或者n(不显示位置)。-t 用来指定显示的方式,c是转义字符的方式,a则是名字方式。
[root@localhost lpi103-2]# od text2
0000000 004471 066160 066565 031412 061011 067141 067141 005141
0000020 030061 060411 070160 062554 000012
0000031
[root@localhost lpi103-2]# od -A d -t c text2
0000000 9 \t p l u m \n 3 \t b a n a n a \n
0000016 1 0 \t a p p l e \n
0000025
[root@localhost lpi103-2]# od -A n -t a text2
9 ht p l u m nl 3 ht b a n a n a nl
1 0 ht a p p l e nl
我们的示例文件都很小,当遇到大文件时就需要把它分割成多个小的分片。例如,你可能需要把一个大文件分割成CD-大小的分片,这样才能把它刻录成CD盘。split命令用来完成分割任务,并且cat可以很容易地把分割的分片重新连接成原来的大文件。默认情况下,split分割出的小文件的文件名格式为: xaa, xab,....。当然你可以通过命令行选项来改变默认值,也可以改变小文件的大小以及是否按行分割或者按字符分割。
看下面实例:
[root@localhost lpi103-2]# split -l 2 text1 # 按行分割,每2行一个文件片
[root@localhost lpi103-2]# split -b 17 text2 y # 按字符数分割,每个文件片17个字符
[root@localhost lpi103-2]# cat yaa
9 plum
3 banana
1[root@localhost lpi103-2]# cat y* x*
9 plum
3 banana
10 apple
1 apple
2 pear
3 banana
注意yaa文件片没有以换行结束,所以输出它以后,我们的bash提示符偏移了。
wc, head, tail
cat可以显示文件全部内容,这对于小文件是合适的。但是驾驶你有一个大文件,首先你可能需要使用wc(word count)来看看这个文件有多大。wc命令用来显示一个文件有多少行、多少字符、多少字节。ls -l命令也可以获得文件的字节数。如下例:
[root@localhost lpi103-2]# ls -l text*
-rw-r--r-- 1 root root 24 May 7 14:29 text1
-rw-r--r-- 1 root root 25 May 7 14:48 text2
[root@localhost lpi103-2]# wc text*
3 6 24 text1
3 6 25 text2
6 12 49 total
wc提供了其他的选项来控制输出格式或者输出其他的信息,如最大行的长度,具体参看man手册页。
head和tail命令用来显示一个文件的首部和尾部。他们可以用作过滤器,也可以使用一个文件名作为参数。默认情况下,head和tail都是显示10行的内容。下面是个综合的例子:
root@localhost lpi103-2]# dmesg | tail -n15 | head -n 6 # 先从倒数15行开始到结束,然后是取前6行
tg3 0000:02:00.0: eth0: Link is up at 100 Mbps, full duplex
tg3 0000:02:00.0: eth0: Flow control is on for TX and on for RX
tg3 0000:02:00.0: eth0: Link is down
tg3 0000:02:00.0: eth0: Link is up at 100 Mbps, full duplex
tg3 0000:02:00.0: eth0: Flow control is off for TX and off for RX
Bluetooth: Core ver 2.15
tail的另外一个常用的方式是 tail -f 文件名。 当一个后台程序修改文件时,可以通过这个命令跟踪文件的变化,常用于日志文件。
Expand, unexpand, tr
当我们创建text1和text2文件时,我们在text2中使用了Tab字符。有时候你需要把tab换成空格,或者反之。expand和unexpand命令用来完成这个任务。两个命令都提供了-t选项来设置Tab宽度。
[root@localhost lpi103-2]# expand -t 1 text2 # 把一个Tab替换成一个空格
9 plum
3 banana
10 apple
[root@localhost lpi103-2]# expand -t 8 text2 | unexpand -a -t2 | expand -t3 # 把一个Tab替换成8个空格,再把每2个空格替换成一个Tab, 在把每个Tab替换成3个空格
9 plum
3 banana
10 apple
不幸的是,你无法使用unxpand把text1中的空格替换成Tab。因为unexpand至少需要2个空格才能换成Tab。不过,你可以使用tr命令来完成这种替换。因为tr就是一个单纯的过滤器,你可以使用cat命令为其提供输入。
[root@localhost lpi103-2]# cat text1 | tr ' ' '\t' | cat - text2
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
如果想知道到底发生了怎样的替换,可以使用od -ta来查看。
Pr, nl, fmt
pr命令(print)被用来为打印而格式化文件。默认输出的头部包含文件名、创建日期和时间、以及页码,尾部则是两个空行。当操作多个文件或者是标准输入时,日期时间是现在的时间而不是那一个文件的创建时间。
nl命令(number lines)用来为行编号。cat -n 也可以用来编号。
[root@localhost lpi103-2]# nl text2 | pr -m - text1 | head
2013-05-07 16:04 Page 1
1 9 plum 1 apple
2 3 banana 2 pear
3 10 apple 3 banana
另外一个用用的格式化文本的命令是fmt。它用来使用合适的边距格式文本,可以用来连接多个短行为一行,也可以分割一个长行为多行。例子如下:
[ian@echidna lpi103-2]$ echo "This is a sentence. " !#:* !#:1->text3
echo "This is a sentence. " "This is a sentence. " "This is a sentence. ">text3
[ian@echidna lpi103-2]$ echo -e "This\nis\nanother\nsentence.">text4
[ian@echidna lpi103-2]$ cat -et text3 text4
This is a sentence. This is a sentence. This is a sentence. $
This$
is$
another$
sentence.$
[ian@echidna lpi103-2]$ fmt -w 60 text3 text4
This is a sentence. This is a sentence. This is a
sentence.
This is another sentence.
sort, uniq
sort命令对输入的文本按照当前系统的排序规则进行排序后输出。sort也可以用来合并已经排序好的文件,以及检查一个文件是否已经排序。
下面例子中,先把text1中的空格替换成Tab,然后再排序这两个文件。
[root@localhost lpi103-2]# cat text1 | tr ' ' '\t' | sort - text2
10 apple
1 apple
2 pear
3 banana
3 banana
9 plum
[root@localhost lpi103-2]# cat text1 | tr ' ' '\t' | sort -u -k1n -k2 - text2
1 apple
2 pear
3 banana
9 plum
10 apple
请注意,10排到了最前面,因为默认是按照字母顺序排序的。庆幸的是,sort支持数字和字母两种排序方式,而且每一个列排序的方式还可以不同。默认各个列是根据Tab或者空格分隔的。
第二个例子中,第一列按照数字排序,第二列按照字母排序,-u则去除重复行。注意,我们仍然还有两个apple行,这是因为唯一性测试是按照整行进行的。想想如何修改或者增加步骤来消除第二个apple行。
另外一个命令uniq提供了另外一种控制重复行删除的机制。uniq通常在排序好的基础上进行操作,删除连续的重复行。uniq还可以忽略某些字段。如下:
[root@localhost lpi103-2]# cat text1 | tr ' ' '\t' | sort -k2 - text2 | uniq -f1 # 忽略第一个字段
10 apple
3 banana
2 pear
9 plum
cut, past, join
现在我们来学习处理文本数据中字段的三个命令。这三个命令在处理表格式数据的时候特别好用。第一个是cut, 它抽取文件中的字段。默认的字段分割符是Tab。
[root@localhost lpi103-2]# cut -f1-2 --output-delimiter=' ' text2 # 抽取两个字段,并在输出时字段之间使用空格分开
9 plum
3 banana
10 apple
paste命令用来把多个文件中的相同行号的行对应连接起来,有点像pr命令的-m选项。
[root@localhost lpi103-2]# paste text1 text2
1 apple 9 plum
2 pear 3 banana
3 banana 10 apple
这只是paste最简单的用法,更多复杂的用法,请参考man手册页。
最后一个字段操作命令是join。它通过一个匹配字段来连接文件(译者注:类似于数据库中表的等值连接)。连接的字段必须是排序好的。
[root@localhost lpi103-2]# sort -k2 text2 | join -1 2 -2 2 text5 -
apple 1 10
banana 3 3
sed
Sed是stream editor。关于sed有很多文章和书籍来介绍。Sed超级强大,用它可以完成任何文本编辑工作。本文只是简单介绍sed以吊起读者的胃口,但是不会详细介绍。
与前面的所有文本处理命令一样,sed也可以从文件或者管道获得输入。其输出则是标准输出流。Sed读取行到模式空间,对模式空间里的内容进行编辑操作,然后把模式空间的内容写到标准输出。Sed可能会在模式空间组合多个行,也可能写入文件,只写选择的输出,或者根部不写。
Sed使用正则表达式来选择操作的行,以及完成查找和替换。一个保持缓冲区提供文本存储的临时空间。这个缓冲区可以替换模式空间,也可以被加入到模式空间,或者与模式空间进行交换。sed拥有一个有限的命令集合,这些命令与正则表达式以及保持缓冲区相结合将会带来巨大的能力。一组sed命令的集合通常叫做sed脚本。
下面展示了三个简单的sed脚本。
[root@localhost lpi103-2]# sed 's/a/A/' text1
1 Apple
2 peAr
3 bAnana
[root@localhost lpi103-2]# sed 's/a/A/g' text1
1 Apple
2 peAr
3 bAnAnA
[root@localhost lpi103-2]# sed '2d;$s/a/A/g' text1
1 apple
3 bAnAnA
第一个脚本中,我们使用s(substitute)命令来把每一行的第一个a替换为A。
第二个脚本中,完成同样的替换,只是会把每一行中所有的a都替换为A。
第三个脚本中,我们增加了d(删除)命令,删除第二行,然后把最后一行的a替换为A,两个命令之间用;分割。
sed默认是真多所有行进行操作,但也提供操作部分行的选项。这可以通过行号、正则表达式等表示。其中$表示最后一行。
例子如下:
[root@localhost lpi103-2]# sed -e '2,${' -e 's/a/A/g' -e '}' text1
1 apple
2 peAr
3 bAnAnA
[root@localhost lpi103-2]# sed -e '/pear/,/bana/{' -e 's/a/A/g' -e '}' text1
1 apple
2 peAr
3 bAnAnA
[root@localhost lpi103-2]# sed -e '/pear/,/bana/{s/a/A/g}' text1
1 apple
2 peAr
3 bAnAnA
上面例子的作用是,把text1文件中的最后两行中的a替换成A。同时显示了-e的作用是把多个命令连接起来。
sed脚本哈可以存放到文件中。实际上你可能把经常用到的脚本都存入文件中。前面,我们使用tr命令把text1文件中的空格替换为Tab。现在我们使用sed完成同样的任务,并且把sed脚本存入文件中。
[root@localhost lpi103-2]# echo -e "s/ /\t/g">sedtab
[root@localhost lpi103-2]# cat sedtab
s/ / /g
[root@localhost lpi103-2]# sed -f sedtab text1
1 apple
2 pear
3 banana
下面是最后一个Sed脚本的例子:
[root@localhost lpi103-2]# sed '=' text2
1
9 plum
2
3 banana
3
10 apple
[root@localhost lpi103-2]# sed '=' text2 | sed 'N;s/\n//'
19 plum
23 banana
310 apple
本例中,我们使用了=命令,然后把输出再交给sed来处理,来模拟nl来给行进行编号。
例子中,先使用=来打印行号,然后使用N命令把文件下一行读入模式空间,最后在模式空间中删除两行之间的换行符。
看起来不太好,因为行号和行内容之间没有分割开。下面是改进版本:
[root@localhost lpi103-2]# cat text1 text2 text1 text2 >text6
[root@localhost lpi103-2]# cat text6
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple
[root@localhost lpi103-2]# ht=$(echo -en "\t")
[root@localhost lpi103-2]# echo $ht
[root@localhost lpi103-2]# sed '=' text6 | sed "N
> s/^/ /
> s/^.*\(......\)\n/\1$ht/"
1 1 apple
2 2 pear
3 3 banana
4 9 plum
5 3 banana
6 10 apple
7 1 apple
8 2 pear
9 3 banana
10 9 plum
11 3 banana
12 10 apple
下面来具体分析各个步骤:
- 使用cat创建一个多行的文件,备用
- 因为Tab键在bash中是自动补全快捷键,不能直接输入,所以创建一个bash变量保存备用
- 使用=命令增加行号
- 读入下一行到模式空间中
- 在行号前面增加6个空格
- 把换行符前面的内容替换为最后六个字符(为了保证序号6位以内都能对齐),然后加上Tab。
Sed的第四个版本包含了Info格式的文档,其中有大量精彩的例子,这在3.02版本中是没有的。GNU sed可以使用--version选项来显示当前的版本信息。
[root@localhost lpi103-2]# sed --version
GNU sed version 4.2.1
Copyright (C) 2009 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE,
to the extent permitted by law.
GNU sed home page: <http://www.gnu.org/software/sed/>.
General help using GNU software: <http://www.gnu.org/gethelp/>.
E-mail bug reports to: <bug-gnu-utils@gnu.org>.
Be sure to include the word ``sed'' somewhere in the ``Subject:'' field.