The Missing Semester of Your CS Education(第四课,文本处理和数据处理)笔记
NOTE:这堂课有一句话我很受用:好用的工具通常由别人告诉你,而使用这个工具的技巧则通常可以通过查手册学会
1. journalctl 命令
systemd用来启动系统并管理进程。systemd包含了一个叫做journalctl的辅助组件,其主要作用是管理系统的事件日志记录。
journalctl可以查看所有的系统日志文件,由于日志信息量很大,journalctl还提供了各种参数帮助用户更快速的定位到日志信息。
默认情况下,用户都可以访问自己的日志。对于系统主日志和其他用户的日志,仅限于有权限的用户访问,比如root用户,wheel组和systemd组的用户。
来源:https://blog.csdn.net/qq_36595013/article/details/107318025
2. systemd:暂时理解为linux下一个用于管理系统服务和进程的软件
.
除换行符之外的”任意单个字符”*
匹配前面字符零次或多次+
匹配前面字符一次或多次[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]
这可不是我们想要的结果。对于某些正则表达式的实现来说,您可以给 *
或 +
增加一个?
后缀使其变成非贪婪模式,但是很可惜 sed
并不支持该后缀。不过,我们可以切换到 perl 的命令行模式,该模式支持编写这样的正则表达式:
perl -pe 's/.*?Disconnected from //'
让我们回到 sed
命令并使用它完成后续的任务,毕竟对于这一类任务,sed
是最常见的工具。sed
还可以非常方便的做一些事情,例如打印匹配后的内容,一次调用中进行多次替换搜索等。但是这些内容我们并不会在此进行介绍。sed
本身是一个非常全能的工具,但是在具体功能上往往能找到更好的工具作为替代品。
正则表达式在线调试工具 regex debugger https://regex101.com/r/qqbZqh/2
好的,我们还需要去掉用户名后面的后缀,应该如何操作呢?
想要匹配用户名后面的文本,尤其是当这里的用户名可以包含空格时,这个问题变得非常棘手!这里我们需要做的是匹配一整行:
| sed -E 's/.*Disconnected from (invalid |authenticating )?user .* [^ ]+ port [0-9]+( \[preauth\])?$//'
让我们借助正则表达式在线调试工具regex debugger 来理解这段表达式。OK,开始的部分和以前是一样的,随后,我们匹配两种类型的“user”(在日志中基于两种前缀区分)。再然后我们匹配属于用户名的所有字符。接着,再匹配任意一个单词([^ ]+
会匹配任意非空且不包含空格的序列)。紧接着后面匹配单“port”和它后面的一串数字,以及可能存在的后缀[preauth]
,最后再匹配行尾。
注意,这样做的话,即使用户名是“Disconnected from”,对匹配结果也不会有任何影响,您知道这是为什么吗?(TODO: so...为什么呢?) (TODO:其实我想知道IP地址是在哪里匹配的)(回答:我大概明白了,IP地址是在[^ ]+匹配的,而至于带空格的用户名则由(.*)匹配,至于其中原因,我们可以回忆一下编译原理里学到的东西:用于匹配正则表达式的状态机可不会只扫描一次语句,当它发现用之前的逻辑无法匹配时,它就会退回,换另一种逻辑来匹配)
问题还没有完全解决,日志的内容全部被替换成了空字符串,整个日志的内容因此都被删除了。我们实际上希望能够将用户名保留下来。对此,我们可以使用“捕获组(capture groups)”来完成。被圆括号内的正则表达式匹配到的文本,都会被存入一系列以编号区分的捕获组中。捕获组的内容可以在替换字符串时使用(有些正则表达式的引擎甚至支持替换表达式本身),例如\1
、 \2
、\3
等等,因此可以使用如下命令:
| sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'
想必您已经意识到了,为了完成某种匹配,我们最终可能会写出非常复杂的正则表达式。例如,这里有一篇关于如何匹配电子邮箱地址的文章e-mail address,匹配电子邮箱可一点也不简单。网络上还有很多关于如何匹配电子邮箱地址的讨论。人们还为其编写了测试用例及 测试矩阵。您甚至可以编写一个用于判断一个数是否为质数的正则表达式。
正则表达式是出了名的难以写对,但是它仍然会是您强大的常备工具之一。
===========总结一下sed能干的事情================
1. 替换
2. 文本注入
3. 打印特定行(比如基于索引选择特定行)
========================================
==========一些关于sed使用技巧的小补充=============
1. 可以在正则表达式中使用小括号“()”来表示匹配字符串,比如(ab)表示匹配字符串“ab”,但是由于sed是个old tool,需要加上-E前缀来使用这些新特性。
2. 如果要匹配特殊字符,比如"()"或者".",则在特殊字符前面加上反斜杠"\"
========================================
回到数据整理
OK,现在我们有如下表达式:
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,
如果您使用的是 MacOS:注意这个命令并不能配合 MacOS 系统默认的 BSD paste
使用。参考课程概览与 shell的习题内容获取更多相关信息。
我们可以利用 paste
命令来合并行(-s
),并指定一个分隔符进行分割 (-d
),那awk
的作用又是什么呢?
awk – 另外一种编辑器
awk
其实是一种编程语言,只不过它碰巧非常善于处理文本。关于 awk
可以介绍的内容太多了,限于篇幅,这里我们仅介绍一些基础知识。
首先, {print $2}
的作用是什么? 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
统计输出结果的行数。
不过,既然 awk
是一种编程语言,那么则可以这样:
BEGIN { rows = 0 }
$1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
END { print rows }
BEGIN
也是一种模式,它会匹配输入的开头( END
则匹配结尾)。然后,对每一行第一个部分进行累加,最后将结果输出。事实上,我们完全可以抛弃 grep
和 sed
,因为 awk
就可以解决所有问题。至于怎么做,就留给读者们做课后练习吧。
分析数据
bc - An arbitrary precision calculator language
想做数学计算也是可以的!例如这样,您可以将每行的数字加起来:
| 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 从相机中捕获一张图片,将其转换成灰度图后通过SSH将压缩后的文件发送到远端服务器(NOTE:可以使用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 -'
除了ssh外,还可以使用scp来传输文件,但目标服务器要开启写入权限。
=========================第四课习题===============================
1. 学习正则表达式tutorial
在正则表达式中,\w 等同于 [A-Za-z0-9_] (注意,还有个下划线)
2. 可以使用花括号{}来表明前面的字符应该被匹配多少次(注意,这种语法在某些正则表达式实现中不被支持)
3. \d 代表数字 \s代表空白字符(包括\t\n\r还有空格等等)
4. 左右括号() 除了用来表示字符串外,还可以作为“子正则表达式”。此外捕捉组还有个用途,设想你要匹配一个文件夹下的所有图像,你可以使用^(IMG\d+\.png)$ 来匹配图像,但这样的字符串会带有.png后缀,如果你只想捕捉图像名称而不希望带后缀,则可以使用^(IMG\d+)\.png$ ,这样一来后缀名.png就不会被捕捉了。
来源: https://regexone.com/lesson/capturing_groups?
5. 捕捉组也可以嵌套使用,例如 ^(IMG(\d+))\.png$,\1会包含整个文件名(包括数字,但不包括.png),而\2则只包含文件名中的数字,这个顺序是根据左括号( 被定义的顺序来决定的。
来源:https://regexone.com/lesson/nested_groups
6. W 表示不匹配 [A-Za-z0-9_], \S表示不匹配空白字符, \D表示不匹配数字 (注意,如果使用[\W\S\D] 并不是不匹配字母数字空白字符,相反,是所有字符都匹配,因为[]内的不同字符是暗含了“或”('|')的)
7. \b 用于匹配一个单词和一个非单词字符之间的边界 (注意!\b只匹配边界,不匹配字符!)
8. 一些正则表达式的实现允许你使用back referencing,也就是把被捕捉的字符串重新用到正则表达式上(使用\0,\1,\2等)。\0表示整个被匹配的文本,而不是被捕捉的字符串。
===
9. tr命令可以把一个文件中的字符全部转成大写,也可以把一个文件中的空白字符都去掉
10.
diff --unchanged-group-format='' <(cat occurance.txt) <(cat all.txt) | wc -l
--unchanged-group-format=''
用于将两个文件中相同的内容设置为空字符串,剩下的内容就是差异的部分。
12.
sed s/REGEX/SUBSTITUTION/ input.txt > input.txt
表达式中后一个 input.txt
会首先被清空,而且是发生在前的。所以前面一个input.txt
在还没有被 sed
处理时已经为空了。在使用正则处理文件前最好是首先备份文件。
sed -i.bak s/REGEX/SUBSTITUTION/ input.txt > input.txt
可以自动创建一个后缀为 .bak
的备份文件。
13. 打印一个文件中指定行内容(也可以范围打印)
# sed 打印第 6 行内容 sed -n '6p' file.txt # awk 打印第 6 行内容 awk 'NR==6' file.txt # tail 配合 head,打印指定行内容 tail -n +6 file.txt | head -1
来源:https://www.cnblogs.com/sharesdk/p/14133293.html
====
1. 第二题,哪个组合从未出现过?
cat /usr/share/dict/words | grep -E -v -f <(echo ".*($(cat /usr/share/dict/words | grep ".*a.*a.*a.*[^'s]$" | sed -E "s/.*(.{2})$/\1/" | sort | uniq -c | sort -nk1,1 | awk '{print $2}' | paste -sd'|'))$") | sed -E "s/.*(.{2})$/\1/" | sort | uniq -c | sort -nk1,1 | awk '{print $2}'
2. 第四题(如果会R语言,这题很简单,或者使用shell script也很简单)
获取最长时间 journalctl | grep -E "\[1\]:\sStartup finished in" | sed -E "s/.*= (([0-9]*)min)?(.*)s\./\2\3/" | awk '{if ($0 ~ /^[0-9]+\s[0-9\.]+/) print $1*60+$2; else print $0}' | sort -n | tail -n1
获取最短时间 journalctl | grep -E "\[1\]:\sStartup finished in" | sed -E "s/.*= (([0-9]*)min)?(.*)s\./\2\3/" | awk '{if ($0 ~ /^[0-9]+\s[0-9\.]+/) print $1*60+$2; else print $0}' | sort -nr | tail -n1
获取平均数 journalctl | grep -E "\[1\]:\sStartup finished in" | sed -E "s/.*= (([0-9]*)min)?(.*)s\./\2\3/" | awk '{if ($0 ~ /^[0-9]+\s[0-9\.]+/) print $1*60+$2; else print $0}' > time.txt; echo "($(paste -sd+ time.txt))/$(wc -l time.txt | awk '{print $1}')" | bc -l
获取中位数(建议还是使用正确的工具做正确的事情。。。)
journalctl | grep -E "\[1\]:\sStartup finished in" | sed -E "s/.*= (([0-9]*)min)?(.*)s\./\2\3/" | awk '{if ($0 ~ /^[0-9]+\s[0-9\.]+/) print $1*60+$2; else print $0}' | sort -n > time.txt; cat time.txt | wc -l | awk '{print int($0/2)+1}' | xargs -I {} tail -n {} time.txt | tail -n1
3. 第五题
cat <(journalctl -b 1) <(journalctl -b 2) <(journalctl -b 3) | sed 's/.*chenyinhua-VirtualBox //' | sort | uniq -c | sort -nk1,1 | awk '$1!=3 {print $0}'
4. 第六题
TODO:这里skip吧,但是记住,工具jq适合用于处理json数据,工具pup适合用于处理html数据
来源: https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions/data-wrangling-solution/