把一种格式的数据转换成另一种格式
具体来说,不管是文本格式还是二进制格式,都要对数据进行处理,直到得到所需的数据为止。
在过去的讲座中,我们已经看到一些基础数据的争论。几乎每次使用|
运算符时,都会执行某种数据争用。考虑像journalctl | grep-i intel
这样的命令。它查找所有提到Intel的系统日志条目(不区分大小写)。您可能不认为它是一个盘绕的数据,但它正在从一种格式(整个系统日志)转变为一种对您更有用的格式(只是英特尔日志条目)。大多数的数据争论都是关于知道你可以使用什么工具,以及如何组合它们。
让我们从头开始。要整理数据,我们需要两样东西:**要整理的数据,以及与之相关的东西。**日志通常是一个很好的用例,因为您经常想调查关于它们的事情,而阅读整个事情是不可行的。让我们通过查看我的服务器日志来确定谁试图登录到我的服务器:
ssh myserver journalctl
我们把它限制在ssh上:
ssh myserver journalctl | grep sshd
注意,我们正在使用管道在本地计算机上通过grep
传输远程文件!ssh
很神奇,我们将在下一节关于命令行环境的讲座中进一步讨论它。但这仍然比我们想要的要多得多。而且很难读。让我们做得更好:
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
编辑器之上。在它中,您基本上给出了如何修改文件的简短命令,而不是直接操作其内容(尽管您也可以这样做)。有很多命令,但最常见的命令之一是s
:substitution。例如,我们可以写:
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
匹配的东西
^
行首
$
行尾
sed
的正则表达式有些奇怪,需要在大多数正则表达式之前加上一个\
来赋予它们特殊的含义。或者你可以通过-E
。
所以,回头看/*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]
这可能不是我们想要的。在某些正则表达式实现中,您可以在*
或+
后面加上?
使他们不贪婪,但遗憾的是塞德不支持这一点。不过,我们可以切换到perl的命令行模式,它确实支持以下结构:
perl -pe 's/.*?Disconnected from //'
接下来我们将继续使用sed
,因为它是这些工作中更常见的工具。sed
还可以做其他一些方便的事情,比如在给定的匹配之后打印行、每次调用执行多个替换、搜索内容等等,但是我们在这里不会过多介绍这些内容。sed
本身基本上是一个完整的主题,但通常有更好的工具。
好的,我们还有一个后缀要去掉。我们该怎么做?只匹配用户名后面的文本有点棘手,特别是如果用户名可以有空格之类的!我们需要做的是将整个系列匹配起来:
| sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'
让我们看看regex调试器是怎么回事。好吧,开始还是和以前一样。然后,我们匹配任何“用户”变量(日志中有两个前缀)。然后我们在用户名所在的任何字符串上进行匹配。然后我们匹配任何单个单词([^]+
;任何非空的非空格字符序列)。然后是“端口”一词,后跟一系列数字。然后可能是后缀[preauth]
,然后是行的结尾。
注意,使用这种技术,因为“Disconnected from”的用户名不会再让我们感到困惑。你知道为什么吗?
但是有一个问题,那就是整个日志变空了。毕竟我们想保留用户名。为此,我们可以使用“捕获组”。由括号包围的正则表达式匹配的任何文本都存储在编号的捕获组中。这些在替换中是可用的(在某些引擎中,甚至在模式本身中!)如“\1
”、“\2
”、“\3
”等:
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
好吧,我们现在有了
ssh myserver journalctl
| grep sshd
| grep "Disconnected from"
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
sed
可以做其他各种有趣的事情,比如注入文本(使用i
命令)、显式打印行(使用p
命令)、按索引选择行,以及许多其他事情(使用man sed
进行查看)。
无论如何。我们现在提供了一个试图登录的所有用户名的列表。但这是毫无帮助的。我们来看看常见的:
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
将对输入进行排序。uniq -c
将 把相同的连续行折叠成一行,并以出现次数为前缀。我们可能也想对其进行排序,只保留最常见的登录名:
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
sort -n
将按数字(而不是字典)顺序排序。-k1,1
表示**“仅按第一个空格分隔的列排序”。,n
部分表示“排序到第n个字段,默认值是行的结尾**。在这个特别的例子中,按整行排序并不重要,但我们是来学习的!
如果我们想要最不常见的,我们可以用head
代替tail
。还有sort -r
,它按相反的顺序排序。
好吧,这很酷,但是我们只想给出用户名,也许每行不需要一个?
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
| awk '{print $2}' | paste -sd,
让我们从粘贴开始:它允许您使用给定的单字符分隔符(-d
)组合行(-s
)。但awk
是怎么回事?
awk
是一种非常擅长处理文本流的编程语言。
首先,{print$2}
做什么?好吧,awk
程序采用一个可选模式加上一个块的形式,说明如果模式匹配给定的行该怎么做。默认模式(我们在上面使用)匹配所有行。在块中,$0
被设置为整行的内容,$1
到$n
被设置为该行的第n个字段,用字段分隔符分隔(默认情况下,空格用
-F
改变)。在本例中,我们是说,对于每一行,打印第二个字段的内容,这恰好是用户名!
让我们看看能不能做些更新奇的事。让我们计算以c
开头以e
结尾的一次性用户名的数量:
| awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l
这里有很多东西要拆。首先,注意我们现在有一个模式(在{…}
之前的内容)。该模式表示行的第一个字段应该等于1(这是uniq -c
中的计数),第二个字段应该与给定的正则表达式匹配。块只是说要打印用户名。然后我们用wc -l
计算输出中的行数。
然而,awk
是一种编程语言,记得吗?
BEGIN {
rows = 0 }
$1 == 1 && $2 ~ /^c[^ ]*e$/ {
rows += $1 }
END {
print rows }
BEGIN
是一个模式,它匹配输入的开头(END
匹配结尾)。现在,每行块只添加第一个字段的计数(尽管在本例中始终是1),然后我们在最后打印出来。事实上,我们可以完全摆脱grep
和sed
,因为awk
可以做到这一切。
你可以进行计算!例如,将每行上的数字相加:
| paste -sd+ | bc -l
或产生更详细的表达:
echo "2*($(data | paste -sd+))" | bc -l
你可以通过多种方式获得数据。st
很整洁,但是如果你已经有R:
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从相机中捕获图像,将其转换为灰度,压缩它,通过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 -'