您是否曾经有过这样的需求,将某种格式存储的数据转换成另外一种格式? 肯定有过,对吧! 这也正是我们这节课所要讲授的主要内容。具体来讲,我们需要不断地对数据进行处理,直到得到我们想要的最终结果。
在之前的课程中,其实我们已经接触到了一些数据整理的基本技术。可以这么说,每当您使用管道运算符的时候,其实就是在进行某种形式的数据整理。
例如这样一条命令 journalctl | grep -i intel,它会找到所有包含intel(不区分大小写)的系统日志。您可能并不认为这是数据整理,但是它确实将某种形式的数据(全部系统日志)转换成了另外一种形式的数据(仅包含intel的日志)。大多数情况下,数据整理需要您能够明确哪些工具可以被用来达成特定数据整理的目的,并且明白如何组合使用这些工具。
让我们从头讲起。既然是学习数据整理,那有两样东西自然是必不可少的:用来整理的数据以及相关的应用场景。日志处理通常是一个比较典型的使用场景,因为我们经常需要在日志中查找某些信息,这种情况下通读日志是不现实的。现在,让我们研究一下系统日志,看看哪些用户曾经尝试过登录我们的服务器:
ssh myserver journalctl
内容太多了。现在让我们把涉及 sshd 的信息过滤出来:
ssh myserver journalctl | grep sshd
注意,这里我们使用管道将一个远程服务器上的文件传递给本机的 grep 程序!此时我们打印出的内容,仍然比我们需要的要多得多,读起来也非常费劲。我们来改进一下:
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less
多出来的引号是什么作用呢?这么说吧,我们的日志是一个非常大的文件,把这么大的文件流直接传输到我们本地的电脑上再进行过滤是对流量的一种浪费。因此我们采取另外一种方式,我们先在远端机器上过滤文本内容,然后再将结果传输到本机。 less 为我们创建来一个文件分页器,使我们可以通过翻页的方式浏览较长的文本。为了进一步节省流量,我们甚至可以将当前过滤出的日志保存到文件中,这样后续就不需要再次通过网络访问该文件了:
- $ ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
- $ less ssh.log
过滤结果中仍然包含不少没用的数据。我们有很多办法可以删除这些无用的数据,但是让我们先研究一下 sed 这个非常强大的工具。
sed 是一个基于文本编辑器ed构建的”流编辑器” 。在 sed 中,您基本上是利用一些简短的命令来修改文件,而不是直接操作文件的内容(尽管您也可以选择这样做)。相关的命令行非常多,但是最常用的是 s,即替换命令,例如我们可以这样写:
- ssh myserver journalctl
- | grep sshd
- | grep "Disconnected from"
- | sed 's/.*Disconnected from //'
上面这段命令中,我们使用了一段简单的正则表达式。正则表达式是一种非常强大的工具,可以让我们基于某种模式来对字符串进行匹配。s 命令的语法如下:s/REGEX/SUBSTITUTION/, 其中 REGEX 部分是我们需要使用的正则表达式,而 SUBSTITUTION 是用于替换匹配结果的文本。
正则表达式非常常见也非常有用,值得您花些时间去理解它。让我们从这一句正则表达式开始学习: /.*Disconnected from /。正则表达式通常以(尽管并不总是) /开始和结束。大多数的 ASCII 字符都表示它们本来的含义,但是有一些字符确实具有表示匹配行为的“特殊”含义。不同字符所表示的含义,根据正则表达式的实现方式不同,也会有所变化,这一点确实令人沮丧。常见的模式有:
. 除换行符之外的”任意单个字符”* 匹配前面字符零次或多次+ 匹配前面字符一次或多次[abc] 匹配 a, b 和 c 中的任意一个(RX1|RX2) 任何能够匹配RX1 或 RX2的结果^ 行首$ 行尾
这里我们执行的操作是把任何"a"或"b"字符替换为空。
但是这里我们即使把第一个字符换成"b",输出还会是"ba",这是因为正则表达式,还是在默认的情况下,每行只会匹配一次,替换一次,这就是"sed"通常会做的事情。

我们可以使用"g"修饰符,它表示尽可能多的匹配,这种情况下会删除整行,因为每个字符都是"a"或"b"。
同样,我们可以给这个模式添加修饰符:
例如这行代码,表示我们想要零个或多个"ab"字符串,并且用空替换它们。这代表我有一个单独的"a"或者"b"都不会被删除,但是如果是"ab",那么它将被删除。
sed 的正则表达式有些时候是比较奇怪的,它需要你在这些模式前添加\(表示转义)才能使其具有特殊含义。或者,您也可以添加-E选项来支持这些匹配。
删除任何匹配"ab"或者"bc"的内容:

回过头我们再看/.*Disconnected from /,我们会发现这个正则表达式可以匹配任何以若干任意字符开头,并接着包含”Disconnected from “的字符串。这也正式我们所希望的。但是请注意,正则表达式并不容易写对。如果有人将 “Disconnected from” 作为自己的用户名会怎样呢?
Jan 17 03:13:00 thesquareplanet.com sshd[2631]: Disconnected from invalid user Disconnected from 46.97.239.16 port 55920 [preauth]
正则表达式会如何匹配?* 和 + 在默认情况下是贪婪模式,也就是说,它们会尽可能多的匹配文本。因此对上述字符串的匹配结果如下:
46.97.239.16 port 55920 [preauth]
这可不是我们想要的结果。对于某些正则表达式的实现来说,您可以给 * 或 + 增加一个? 后缀使其变成非贪婪模式,但是很可惜 sed 并不支持该后缀。不过,我们可以切换到 perl 的命令行模式,该模式支持编写这样的正则表达式:
perl -pe 's/.*?Disconnected from //'
让我们回到 sed 命令并使用它完成后续的任务,毕竟对于这一类任务,sed是最常见的工具。sed 还可以非常方便的做一些事情,例如打印匹配后的内容,一次调用中进行多次替换搜索等。但是这些内容我们并不会在此进行介绍。sed 本身是一个非常全能的工具,但是在具体功能上往往能找到更好的工具作为替代品。
好的,我们还需要去掉用户名后面的后缀,应该如何操作呢?
想要匹配用户名后面的文本,尤其是当这里的用户名可以包含空格时,这个问题变得非常棘手!这里我们需要做的是匹配一整行:
| sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'
正则表达式中有两个特殊字符:①"^":匹配行的开头。②"$":匹配行的结尾。
在正则表达式中,我们可以使用捕获组(capture group),来捕获我们想要的输出的内容,捕获组可以指定我们关心的值,并在以后重复使用它。任何带圆括号"()"的表达式都算一个捕获组。而捕获组之所以有用,就是我们可以在替换时引用它们。
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) .* [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
上面代码中末尾的"\2"就是引用捕获组的方式,这里我们先匹配了整行,然后在替换中放入你在第二个捕获组"(.*)"匹配的值。
让我们借助正则表达式在线调试工具regex debugger 来理解这段表达式。
开始的部分和以前是一样的,随后,我们匹配两种类型的“user”(在日志中基于两种前缀区分)。再然后我们匹配属于用户名的所有字符。接着,再匹配任意一个单词([^ ]+ 会匹配任意非空且不包含空格的序列)。紧接着后面匹配单“port”和它后面的一串数字,以及可能存在的后缀[preauth],最后再匹配行尾。
这里我们将用户设置成了一个捕获组,整个字符串都是蓝色的代表它匹配成功了。绿色是第一个捕获组,红色是第二个捕获组,紫色是第三个捕获组(因为[preauth]也被放在括号中)。

现在我们做一些改动,将第一行的红圈部分变成第二行的用户名会怎么样?它尝试匹配第二个出现的"invaild",因为这是贪婪匹配,我们可以通过在"+"或者"*"后面加一个"?",这就变成了一个非贪婪匹配。这意味着它将不会匹配尽可能多的字符。
这时,这个表达式被正确解析了,因为".*"会停在第一个"Disconnected from",这个"Disconnected from"是由ssh生成的。
"wc"(word count):单词计数程序,"-l":计算行数。
"sort":可以接受多行输入,并将它们排序,之后将排好序的行输出。
"uniq -c":查看一个排序后的行列表,然后它会去除重复的行,即如果有多个相同的行,则仅打印一次。"-c":将计算任何重复行的重复次数并消除它们。
"sort -nk1,1":"-n":数字排序,"-k":选择输入中以空格为分隔符的列来执行排列,"1,1":从第一列开始并在第一列停止排序,或者说依据所有的列进行排序(在此情况下只按第一列排序)。默认情况下"sort"会按照升序输出。
"awk":基于列的流处理器,默认将输入解析为以空格为分隔符的列,然后再单独操作这些列。
"awk '{print $2}'":仅打印第二列。
"paste":可以把多个行合并在一次,使其成为一行,"-s":可以分割输入。
"paste -sd":获得一个以","分割的用户排名列表。
awk 程序接受一个模式串(可选),以及一个代码块,指定当模式匹配时应该做何种操作。默认当模式串即匹配所有行(上面命令中当用法)。 在代码块中,$0 表示整行的内容,$1 到 $n 为一行中的 n 个区域,区域的分割基于 awk 的域分隔符(默认是空格,可以通过-F来修改)。
让我们统计一下所有以c 开头,以 e 结尾,并且仅尝试过一次登录的用户。
| awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l
让我们好好分析一下。首先,注意这次我们为 awk指定了一个匹配模式串(也就是{...}前面的那部分内容)。该匹配要求文本的第一部分需要等于1(这部分刚好是uniq -c得到的计数值),然后其第二部分必须满足给定的一个正则表达式。代码块中的内容则表示打印用户名。然后我们使用 wc -l 统计输出结果的行数。
- BEGIN { rows = 0 }
- $1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
- END { print rows }
BEGIN 也是一种模式,它会匹配输入的开头( END 则匹配结尾)。然后,对每一行第一个部分进行累加,最后将结果输出。事实上,我们完全可以抛弃 grep 和 sed ,因为 awk 就可以解决所有问题。

想做数学计算也是可以的!例如这样,您可以将每行的数字加起来:
| paste -sd+ | bc -l
下面这种更加复杂的表达式也可以:
echo "2*($(data | paste -sd+))" | bc -l
您可以通过多种方式获取统计数据。如果已经安装了R语言,st是个不错的选择:
- ssh myserver journalctl
- | grep sshd
- | grep "Disconnected from"
- | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
- | sort | uniq -c
- | awk '{print $1}' | R --slave -e 'x <- scan(file="stdin", quiet=TRUE); summary(x)'
R 也是一种编程语言,它非常适合被用来进行数据分析和绘制图表。这里我们不会讲的特别详细, 您只需要知道summary 可以打印某个向量的统计结果。我们将输入的一系列数据存放在一个向量后,利用R语言就可以得到我们想要的统计数据。
如果您希望绘制一些简单的图表, gnuplot 可以帮助到您:
- ssh myserver journalctl
- | grep sshd
- | grep "Disconnected from"
- | sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
- | sort | uniq -c
- | sort -nk1,1 | tail -n10
- | gnuplot -p -e 'set boxwidth 0.5; plot "-" using 1:xtic(2) with boxes'
"xargs":将输入的每行都转换成参数(从一个形式的数据转换到另一种)。
从一长串列表里找出你所需要安装或移除的东西。
rustup toolchain list | grep nightly | grep -vE "nightly-x86" | sed 's/-x86.*//' | xargs rustup toolchain uninstall
"ffmpeg":用于编码和解码视频,某种程度上也可以处理图像。
"-":通常是告诉程序使用标准输入/输出而不用文件。
用 ffmpeg 从相机中捕获一张图片,将其转换成灰度图后通过SSH将压缩后的文件发送到远端服务器,并在那里解压、存档并显示。
- ffmpeg -loglevel panic -i /dev/video0 -frames 1 -f image2 -
- | convert - -colorspace gray -
- | gzip
- | ssh mymachine 'gzip -d | tee copy.jpg | env DISPLAY=:0 feh -'