- 数据处理
Lecture 4 Data Wrangling
日志处理
less、★sed、★grep
您是否曾经有过这样的需求,将某种格式存储的数据转换成另外一种格式? 肯定有过,对吧! 这也正是我们这节课所要讲授的主要内容。具体来讲,我们需要不断地对数据进行处理,直到得到我们想要的最终结果。
在之前的课程中,其实我们已经接触到了一些数据整理的基本技术。可以这么说,每当您使用管道运算符的时候,其实就是在进行某种形式的数据整理。
例如这样一条命令
journalctl | grep -i intel
,它会找到所有包含intel(不区分大小写)的系统日志。您可能并不认为这是数据整理,但是它确实将某种形式的数据(全部系统日志)转换成了另外一种形式的数据(仅包含intel的日志)。大多数情况下,数据整理需要您能够明确哪些工具可以被用来达成特定数据整理的目的,并且明白如何组合使用这些工具。让我们从头讲起。既然是学习数据整理,那有两样东西自然是必不可少的:用来整理的数据以及相关的应用场景。日志处理通常是一个比较典型的使用场景,因为我们经常需要在日志中查找某些信息,这种情况下通读日志是不现实的。现在,让我们研究一下系统日志,看看哪些用户曾经尝试过登录我们的服务器:
1
ssh myserver journalctl
内容太多了。现在让我们把涉及 sshd 的信息过滤出来:
1
ssh myserver journalctl | grep sshd
注意,这里我们使用管道将一个远程服务器上的文件传递给本机的
grep
程序!ssh
太牛了,下一节课我们会讲授命令行环境,届时我们会详细讨论ssh
的相关内容。此时我们打印出的内容,仍然比我们需要的要多得多,读起来也非常费劲。我们来改进一下:1
ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' | less
多出来的引号是什么作用呢?这么说吧,我们的日志是一个非常大的文件,把这么大的文件流直接传输到我们本地的电脑上再进行过滤是对流量的一种浪费。因此我们采取另外一种方式,我们先在远端机器上过滤文本内容,然后再将结果传输到本机。
less
为我们创建来一个文件分页器,使我们可以通过翻页的方式浏览较长的文本。less
:less 命令的作用和 more 十分类似,都用来浏览文本文件中的内容,不同之处在于,使用 more 命令浏览文件内容时,只能不断向后翻看,而使用 less 命令浏览,既可以向后翻看,也可以向前翻看。less
命令的基本格式如下:less [参数] 文件
为了进一步节省流量,我们甚至可以将当前过滤出的日志保存到文件中,这样后续就不需要再次通过网络访问该文件了:
1
2ssh myserver 'journalctl | grep sshd | grep "Disconnected from"' > ssh.log
less ssh.log过滤结果中仍然包含不少没用的数据。我们有很多办法可以删除这些无用的数据,但是让我们先研究一下
sed
这个非常强大的工具。sed
是一个基于文本编辑器ed
构建的”流编辑器” 。在sed
中,您基本上是利用一些简短的命令来修改文件,而不是直接操作文件的内容(尽管您也可以选择这样做)。相关的命令行非常多,但是最常用的是s
,即替换命令,例如我们可以这样写:1
2
3
4ssh myserver journalctl
| grep sshd
| grep "Disconnected from"
| sed 's/.*Disconnected from //'上面这段命令中,我们使用了一段简单的正则表达式,对于过滤出来的信息,把一行中匹配正则表达式
“.*Disconnected from ”
的文本替换为空格。正则表达式是一种非常强大的工具,可以让我们基于某种模式来对字符串进行匹配。
sed
:sed 命令可依照脚本的指令来处理、编辑文本文件,可以对文本文件进行增、删、改、查等操作,支持按行、按字段、按正则匹配文本内容,灵活方便,特别适合于大文件的编辑,同时在脚本中运用。例如
sed -i 's/hello/nihao/' test.txt
是将test.txt
文件的所有 “hello” 字符串转换为 “nihao” 字符串。-i
是直接修改要处理的文件,默认为预览。
s
命令的语法如下:s/REGEX/SUBSTITUTION/
, 其中REGEX
部分是我们需要使用的正则表达式,而SUBSTITUTION
是用于替换匹配结果的文本。**
grep
**:很多时候,我们并不需要列出文件的全部内容,而是从文件中找到包含指定信息的那些行,要实现这个目的,可以使用 grep 命令。 -> (Globally search a Regular Expression and Print)grep 命令的由来可以追溯到 UNIX 诞生的早期,在 UNIX 系统中,搜索的模式(patterns)被称为正则表达式(regular expressions),为了要彻底搜索一个文件,有的用户在要搜索的字符串前加上前缀 global(全面的),一旦找到相匹配的内容,用户就像将其输出(print)到屏幕上,而将这一系列的操作整合到一起就是 global regular expressions print,而这也就是 grep 命令的全称。
grep命令能够在一个或多个文件中,搜索某一特定的字符模式(也就是正则表达式),此模式可以是单一的字符、字符串、单词或句子。
grep 命令的基本格式如下:
[root@localhost ~]# grep [选项] 模式(正则表达式) 文件名
-E
:支持使用扩展的正则表达式-i
:ignorecase,搜索时忽略字符的大小写-v
:–invert-match,显示不能被模式匹配到的行
★正则表达式
正则表达式非常常见也非常有用,值得您花些时间去理解它。让我们从这一句正则表达式开始学习:
/.*Disconnected from /
。正则表达式通常以(尽管并不总是)/
开始和结束。大多数的 ASCII 字符都表示它们本来的含义,但是有一些字符确实具有表示匹配行为的“特殊”含义。不同字符所表示的含义,根据正则表达式的实现方式不同,也会有所变化。常见的模式有:.
除换行符之外的”任意单个字符”*
匹配前面字符零次或多次+
匹配前面字符一次或多次[abc]
匹配a
,b
和c
中的任意一个(RX1|RX2)
任何能够匹配RX1
或RX2
的结果^
行首$
行尾
回过头我们再看
/.*Disconnected from /
,我们会发现这个正则表达式可以匹配任何以若干任意字符开头,并接着包含”Disconnected from “的字符串。这也正式我们所希望的。但是请注意,正则表达式并不容易写对。如果有人将 “Disconnected from” 作为自己的用户名会怎样呢?1
Jan 17 03:13:00 thesquareplanet.com sshd[2631]: Disconnected from invalid user Disconnected from 46.97.239.16 port 55920 [preauth]
正则表达式会如何匹配?
*
和+
在默认情况下是贪婪模式,也就是说,它们会尽可能多的匹配文本。因此对上述字符串的处理结果如下:1
46.97.239.16 port 55920 [preauth]
这可不是我们想要的结果。对于某些正则表达式的实现来说,您可以给
*
或+
增加一个?
后缀使其变成非贪婪模式,但是很可惜sed
并不支持该后缀。不过,我们可以切换到 perl 的命令行模式,该模式支持编写这样的正则表达式:1
perl -pe 's/.*?Disconnected from //'
让我们回到
sed
命令并使用它完成后续的任务,毕竟对于这一类任务,sed
是最常见的工具。sed
还可以非常方便的做一些事情,例如打印匹配后的内容,一次调用中进行多次替换搜索等。但是这些内容我们并不会在此进行介绍。sed
本身是一个非常全能的工具,但是在具体功能上往往能找到更好的工具作为替代品。好的,我们还需要去掉用户名后面的后缀,应该如何操作呢?
想要匹配用户名后面的文本,尤其是当这里的用户名可以包含空格时,这个问题变得非常棘手!这里我们需要做的是匹配一整行:
1
2
3
4ssh myserver journalctl
| grep sshd
| grep "Disconnected from"
| sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [0-9.]+ port [0-9]+( \[preauth\])?$//'让我们借助正则表达式在线调试工具 regex debugger 来理解这段表达式。OK,开始的部分和以前是一样的,随后,我们匹配两种类型的“user” (
(invalid | authenticating )?
,在日志中基于两种前缀区分)。再然后我们匹配属于用户名的所有字符 (.*
) 。接着,再匹配 ip([0-9.]+
)。紧接着后面匹配单词“port”
和它后面的一串数字,以及可能存在的后缀[preauth]
,最后再匹配行尾$
。问题还没有完全解决,日志的内容全部被替换成了空字符串,整个日志的内容因此都被删除了。我们实际上希望能够将用户名保留下来。对此,我们可以使用“捕获组(capture groups)”来完成。被圆括号内的正则表达式匹配到的文本,都会被存入一系列以编号区分的捕获组中。捕获组的内容可以在替换字符串时使用(有些正则表达式的引擎甚至支持替换表达式本身),例如
\1
、\2
、\3
等等,因此可以使用如下命令:1
2
3
4ssh myserver journalctl
| grep sshd
| grep "Disconnected from"
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [0-9.]+ port [0-9]+( \[preauth\])?$/\2/'相较于上面,这里把匹配用户名的
.*
用圆括号括起来了(.*)
,并且捕获后用\2
存储。\1
存储捕获的(invalid |authenticating )?
\2
存储捕获的(.*)
\3
存储捕获的( \[preauth\])?
回到数据整理
sort、uniq、paste、wc
OK,现在我们有如下表达式:
1
2
3
4ssh myserver journalctl
| grep sshd
| grep "Disconnected from"
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'现在,我们已经得到了一个包含用户名的列表,列表中的用户都曾经尝试过登录我们的系统。但这还不够,让我们过滤出那些最常出现的用户:
1
2
3
4
5ssh myserver journalctl
| grep sshd
| grep "Disconnected from"
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
| sort | uniq -csort
会对其输入数据进行排序。uniq -c
会把连续出现的行折叠为一行并使用出现次数作为前缀。我们希望按照出现次数排序,过滤出最常出现的用户名:1
2
3
4
5
6ssh 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 -n10sort -n
会按照数字顺序对输入进行排序(默认情况下是按照字典序排序),-k1,1
则表示“仅基于以空格分割的第一列进行排序”。,1
部分表示“仅排序到第1个部分”,默认情况是到行尾。就本例来说,针对整个行进行排序也没有任何问题,我们这里主要是为了学习这一用法!sort
:Linux sort 命令用于将文本文件内容加以排序,sort 可针对文本文件的内容,以行为单位来排序。sort
命令的基本格式如下:sort [选项] 文件
-n
:依照数值的大小排序。[-k field1[,field2]]
:按指定的列进行排序。
1
2sort testfile #以默认的方式将文本文件的第一列以 ASCII 码的次序排列,并将结果输出到标准输出
sort -k2 testfile #使用 -k 参数设置对第二列的值进行重排uniq
:用于检查及删除文本文件中重复出现的行列,一般与 sort 命令结合使用。 -> (unique)uniq
命令的基本格式如下:uniq [选项] 文件
-c
:在每列旁边显示该行重复出现的次数。
如果我们希望得到登录次数最少的用户,我们可以使用
head
来代替tail
。或者使用sort -r
来进行倒序排序。相当不错。但我们只想获取用户名,而且不要一行一个地显示。
1
2
3
4
5
6
7ssh 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,我们可以利用
paste
命令来合并行(-s
),并指定一个分隔符进行分割 (-d
),这里指定的分隔符是“,”
。**
paste
**:用于合并文件的列,paste
指令会把每个文件以列对列的方式,一列列地加以合并。paste
命令的基本格式如下:paste [选项] 文件
-s
:串列进行而非平行处理。-d<间隔字符>
:用指定的间隔字符取代跳格字符。
1
2paste file testfile testfile1 #合并指定文件的内容
paste -s file #合并指定文件的多行数据wc
:用于计算字数。利用wc
指令我们可以计算文件的 Byte 数、字数、或是行数,若不指定文件名称、或是所给予的文件名为”-“,则wc指令会从标准输入设备读取数据。 -> (word count)wc
命令的基本格式如下:wc [选项] 文件
-c
:显示Bytes数。-l
:显示行数。-w
:显示字数。
那
awk
的作用又是什么呢?
★awk – 另外一种编辑器
★awk
awk
其实是一种编程语言,只不过它碰巧非常善于处理文本。关于awk
可以介绍的内容太多了,限于篇幅,这里我们仅介绍一些基础知识。和 sed 命令类似,awk 命令也是逐行扫描文件(从第 1 行到最后一行),寻找含有目标文本的行,如果匹配成功,则会在该行上执行用户想要的操作;反之,则不对行做任何处理。
awk
命令的基本格式为:[root@localhost ~]# awk [选项] '匹配规则{执行命令}' 文件名
-F fs
:指定以 fs 作为输入行的分隔符,awk
命令默认分隔符为空格或制表符。
1
god@god-virtual-machine:~/桌面/missing-semester/data wrangling$ awk -F, '/nihao/{print "foo"}' 1.txt
在上面的例子中,以
","
为分隔符分割1.txt
文件的每一行,当awk
搜索到1.txt
文件中有匹配正则表达式"/nihao/"
的文本时,就输出打印“foo”
。
awk
非常擅长于处理列数据。首先,
{print $2}
的作用是什么?awk
程序接受一个模式串(可选),以及一个代码块,指定当模式匹配时应该做何种操作。默认当模式串即匹配所有行(上面命令中当用法)。 在代码块中,$0
表示整行的内容,$1
到$n
为一行中的 n 个区域,区域的分割基于awk
的域分隔符(默认是空格,可以通过-F
来修改)。在上面的例子中,我们的代码意思是:对于每一行文本,打印其第二个部分,也就是用户名。让我们康康,还有什么炫酷的操作可以做。让我们统计一下所有以
c
开头,以e
结尾,并且仅尝试过一次登录的用户。1
2
3
4
5
6
7ssh 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
| awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l让我们好好分析一下。首先,注意这次我们为
awk
指定了一个匹配模式串(也就是前面的那部分内容)。该匹配要求文本的第一部分需要等于1(这部分刚好是uniq -c
得到的计数值),然后其第二部分必须满足给定的一个正则表达式/^c[^ ]*e$/
。代码块中的内容则表示打印用户名。然后我们使用wc -l
统计输出结果的行数。不过,既然
awk
是一种编程语言,那么则可以这样:1
2
3
4
5
6
7ssh 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
| awk 'BEGIN { rows = 0 } $1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 } END { print rows }'BEGIN
也是一种模式,它会匹配输入的开头(END
则匹配结尾)。然后,对每一行第一个部分进行累加,最后将结果输出。