0%

一点黑魔法:Linux 中对纯文本文件的列操作

如果你经常和数据打交道,那么你肯定会经常需要对列进行操作。在 Linux 中,对纯文本文件的列操作有两个十分有用的命令:cutpaste。其中 cut 主要用于从纯文本文件中取出某些列,paste 则可以用于按列合并。

cut 命令

假设有这样一个测试文件 cut.txt

1
2
3
4
5
6
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0
1|2|3|4|5|6|7|8|九|0

我们将用这个测试文件来做一些实验。

cut 基础与字节模式

前面说过,cut 命令的本职工作就是取出某些列。实际上,更准确地说法,是 cut 命令逐行地处理输入,并从中取出某些列。这里说的「列」有三种模式:

1
2
3
-b      # 以字节作为标准取出列
-c # 以字符作为标准取出列
-f # 以域 (field) 作为标准取出列

首先我们看看字节模式。比如我们可以取出每一行的第三个字节中的内容。我们知道,英文字符都是以 ASCII 编码用一个字符保存的。这样,我们预期会输出 6 个 2。我们来看下实际的输出。

1
2
3
4
5
6
7
$ cut -b 3 cut.txt
2
2
2
2
2
2

完美,完全符合预期!

我们来看一下 cut 命令的样式

1
cut -[b,c,f] <columns> <filename>

在刚才的例子中,我们选择了字节模式(-b),并指定了第三列。值得一提的是,cut 命令的列指定风格非常的灵活。

1
2
3
4
5
3       # 第三列
3,5,8 # 第三列、第五列、第八列
3-5,8 # 第三列至第五列、第八列
-3,8 # 第一列至第三列、第八列
1,3- # 第一列、第三列至最后一列

字节模式在某些情况会遇到问题。比如,遇到非 ASCII 编码的字符时(特别是多字节字符),就会遇到问题。我们试着看看用 -b 模式输出第 17 列会怎样。

1
2
3
4
5
6
7
8
$ cut -b 17 cut.txt







实际上,-b 模式的第 17 列,会输出「九」的第一个字节。具体输出的内容取决于系统使用的编码。如果我们想输出字符「九」就需要使用字符模式了。

字符模式与域模式

-c 是字符模式。为了输出一列汉字「九」,我们可以这样

1
2
3
4
5
6
7
$ cut -c 17 cut.txt






除了解析列的方式不一样之外,-c-b 完全一样。

类似的,还有域模式。与字节模式以及字符模式最大的不同是,域模式可以指定单个字符作为分隔符,逐行地将文件分成若干列。比如,这里我们可以用 | 作为分隔符,输出第三列至第五列以及第九列。注意,在列模式下,分隔符也会按需输出。

1
2
3
4
5
6
7
$ cut -d '|' -f 3-5,9 cut.txt
3|4|5|九
3|4|5|九
3|4|5|九
3|4|5|九
3|4|5|九
3|4|5|九

补集

cut 命令还支持 --complement 参数,意思是取补集。比如在我们刚才的例子中,取补集就意味着取出第一列、第二列、第六列至第八列以及第十列。

1
2
3
4
5
6
7
$ cut -d '|' -f 3-5,9 --complement cut.txt
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0
1|2|6|7|8|0

使用 --complement 参数,我们可以很容易地从纯文本中删除某一列。比如我们想删除第四列

1
2
3
4
5
6
7
$ cut -d '|' -f 4 --complement cut.txt
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0
1|2|3|5|6|7|8|九|0

轻而易举~

一点黑魔法:处理连续空格分割的情况

cut 在处理连续空格分割列的时候,结果就会变得一团糟。不过,好在我们有 tr 命令。使用 -s 参数,可以逐行地将连续的字符 unique 成单独的一个字符。

1
2
3
4
5
6
7
8
$ who
Liam :0 2016-11-08 00:07
Liam pts/0 2016-11-08 00:23 (:0.0)
Liam pts/1 2016-11-08 00:15 (:0.0)
$ who | tr -s ' '
Liam :0 2016-11-08 00:07
Liam pts/0 2016-11-08 00:23 (:0.0)
Liam pts/1 2016-11-08 00:15 (:0.0)

这样,我们就能轻易地获得各个用户的登录时间了

1
2
3
4
$ who | tr -s ' ' | cut -d ' ' -f 1,3,4
Liam 2016-11-08 00:07
Liam 2016-11-08 00:23
Liam 2016-11-08 00:15

paste 命令

基本用法

相比 cut 命令,paste 命令的用法就简单粗暴许多了。

假设我们有三个文件

1
2
3
4
$ cat paste1.txt  | $ cat paste2.txt | $ cat paste3.txt
1 | a | A
2 | b | B
3 | c | C

现在我们用 paste 试试看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ paste paste1.txt paste2.txt
1 a
2 b
3 c
$ paste paste2.txt paste1.txt
a 1
b 2
c 3
$ paste paste2.txt paste1.txt paste3.txt
a 1 A
b 2 B
c 3 C
$ paste paste2.txt paste1.txt paste3.txt | sed -n l
a\t1\tA
b\t2\tB
c\t3\tC

不难发现,paste 命令支持输入多个文件,并按顺序将他们用制表符粘在一起。如果你想用其他的分隔符将他们粘在一起,也可以像 cut 命令那样使用 -d 参数指定。

1
2
3
4
$ paste -d '|' paste2.txt paste1.txt paste3.txt
a|1|A
b|2|B
c|3|C

一点黑魔法:避免使用临时文件

如果我们需要将几个程序的即时输出(标准输出)按列粘在一起的话,可能不得不将这些输出先写入临时文件当中,然后再调用 paste 命令。不过,也有不用这样麻烦的办法——使用 Bash Process Substituation 来解决这个问题。

简单来说,就是使用 <(command) 来「伪装成一个文件」的样子,作为 paste 命令的输入。比如

1
2
3
4
$ paste -d '|' <(cat paste2.txt) <(cat paste1.txt) <(cat paste3.txt)
a|1|A
b|2|B
c|3|C
俗话说,投资效率是最好的投资。 如果您感觉我的文章质量不错,读后收获很大,预计能为您提高 10% 的工作效率,不妨小额捐助我一下,让我有动力继续写出更多好文章。