Missing Semester Notes - Data Wrangling

2020-05-16

数据整理

原文连接:https://missing.csail.mit.edu/2020/data-wrangling/

数据整理

大家都会遇到需要把一种格式的数据转换到另一种格式的问题,这节课就是介绍这些技巧。

在预备节Shell中大家已经知道了|操作符。例如你使用journalctl | grep -i intel,这条指令可以找出所有提到了intel的日志。你可能不会觉得这是一种数据整理,但它确实是把一种格式的数据转换成了另一种格式。

让我们从简单点的东西开始。日志是很好的例子,因为你会经常研究他们。例如说想看看谁试着登录我的服务器

ssh myserver journalctl

输出的内容太多了,让我们只看ssh有关的内容

ssh myserver journalctl | grep sshd

注意到我们正在通过grep在本机上使用远程文件pipe流。关于ssh我们会在之后的命令行环境中详细介绍。这条命令的输出还是太多了太难读了,我们继续改进。

ssh myserver 'journalctl | grep ssh | 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/SUBSTITUTIONREGEX是你的匹配用的表达式,SUBSTITUTION是的要替换上去的文本。

正则表达式

正则表达式有点复杂但是足够牛逼到值得你花时间去学。我们就从刚刚我们已经用过的表达式/.*Disconnected from /开始。正则表达式常常(不是总是)被/所包围。大多数ASCII字符仅仅带有他们本身的意义,但是少数字符有着特别的用于匹配的作用。不过在不同的正则表达式实现中,不同字符表达不同含义这件事确实会令人沮丧。

一些常见的的模式:

  • . 表示除了换行符外 “任意单个字符”
  • * 0次或多次前述匹配
  • + 1次或多次前述匹配
  • [abc]a,b,c中任意的字符
  • (RX1|RX2) 匹配任意RX1RX2的东西
  • ^ 行首
  • $ 行尾

sed的正则表达式有点点奇怪,会要求你在上述的模式前加一个\,或者你传个参数-E

好了,让我们回到/.*Disconnected from /,它匹配了拥有任意数量字符前缀并跟着Disconnected from的字符串,这正是我们想要的。但是要小心哟,正则表达式是有些tricky的,如果有人用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 -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'

让我们在regex debugger中看看发生了什么。开头与之前的表达式一样,之后我们匹配任意user,然后我们匹配任意username,然后我们匹配任意单词([^ ]+任意非空格字符的非空序列),然后匹配单词port后跟一串数字,然后匹配可能的后缀[preauth],然后结束整行。

所以你现在知道了为啥这样就不会被Disconnected from这个用户名所影响了吗?

但这个表达式还有个问题,就是整个log会变空。毕竟我们希望保持用户名,为了这点我们可以用到“抓取组”的功能。任意被小括号包围的表达式匹配上的文本将被放入按数字编号的抓取组中。这些组直接可以被替换(甚至在一些引擎中,连表达式都可以被替换)用\1,\2,\3

| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'

正如你所想的,你可以写出一个非常复杂的表达式。例如这篇文章讨论了你如何匹配一个电子邮件地址,这其实并不简单,已经有非常多的讨论了。这里有别人写好的test评价。还有个例子是写一个表达式判断给出的数是否是素数。

回到数据整理

好了,我们现在有了

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看看吧。

虽然我们现在可以打印出所有每次尝试登陆的用户名,但好像并没有什么卵用。来看看更通用的版本:

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,

我们从新加入的paste开始:它让你通过给定的单个分隔符(,)-d与行-s结合。但awk又是什么东西呢?

awk 另一个编辑器

awk本来是个编程语言,碰巧在流式文本处理上非常牛逼。它也有很多东西可以说,但是这里只介绍一下基础。

首先,{print $2}是什么意思呢?awk程序采用一个可选模式加上一个块的形式,表示如果匹配上了应该做什么。默认模式(也就是这个命令中用到的)是匹配所有行。在块内,$0被设置成整个行的内容,$1$n被设置成行上的第n号域(分割依据是awk域分隔符,默认为空格,可以用-F指定)。在上述例子中,整个意思是“对每一行,输出这一行的第二个域” 恰好是对应用户名的域。

让我们看看我们还可以做什么好玩的事情。比如计算只用过一次的以c开头以e结尾的用户名:

 | awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }' | wc -l

很多参数全堆在一起了。首先,注意到我们现在有了模式串。这个模式串表示这一行的第一个域应该等于1,第二个域应该能被正则表达式匹配上,在块中表示只输出第二个域。然后我们用wc -l统计输出的行数。

还记得awk是个编程语言吗?

BEGIN { rows = 0 }
$1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
END { print rows }

BEGIN是一个模式串表示匹配输入的起点(END匹配终点)。然后每行都会在末尾加上他的计数。其实我们前面可以完全不用grepsed因为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

整理二进制数据

至今我们都只整理过文本类数据,但pipes也是可以搞二进制数据了。我们可以用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 -'
Missing Semesternotesshell

Missing Semester Notes - Command-line Environment

Missing Semester Notes - Editor(Vim)