The Missing Semester - 第四讲 学习笔记

第四讲 数据整理

课程视频地址:https://www.bilibili.com/video/BV1ym4y197iZ

课程讲义地址:https://missing-semester-cn.github.io/2020/data-wrangling/

本机学习使用平台:虚拟机ubuntu18.04.6

需要用到的东西

  1. 最基本的:grep
  2. 流编辑器:sedawk
  3. 配对用的:正则表达式语法

正则表达式

正则表达式的语法比较复杂,但它非常强大。有很多前辈也总结过这方面的内容了,就放个易于理解的菜鸟教程链接在下面:

https://www.runoob.com/regexp/regexp-tutorial.html

菜鸟教程自带了正则表达式的在线编写调试,当然也可以用课程讲义给的一系列网址:

正则表达式在线调试工具regex debugger

为了完成某种匹配,我们最终可能会写出非常复杂的正则表达式。例如,这里有一篇关于如何匹配电子邮箱地址的文章e-mail address,匹配电子邮箱可一点也不简单。网络上还有很多关于如何匹配电子邮箱地址的讨论。人们还为其编写了测试用例测试矩阵。您甚至可以编写一个用于判断一个数是否为质数的正则表达式。

记住,正则表达式很强大,需要多加练习和阅读才能掌握。

SED 流编辑器

sed是一个基于文本编辑器ed构建的”流编辑器” 。在 sed 中,您基本上是利用一些简短的命令来修改文件,而不是直接操作文件的内容。tldr给出了sed常用的命令选项:

 - Replace all 
   apple
 (basic regex) occurrences with 
   mango
 (basic regex) in all input lines and print the result to 
   stdout
:
   {{command}} | sed 's/apple/mango/g'

 - Execute a specific script [f]ile and print the result to 
   stdout
:
   {{command}} | sed -f {{path/to/script.sed}}

 - Print just a first line to 
   stdout
:
   {{command}} | sed -n '1p'

sed常见的用途有,呃,像课程上那样看运行日志,并把它进行筛选替换后格式化输出。“除了搜索替换以外,sed的语法挺烂的,别用它统计数据啥的..” 教授给了统计数据的方案,见下一小节。sed 还可以做很多各种各样有趣的事情,例如文本注入:(使用 i 命令),打印特定的行 (使用 p命令),基于索引选择特定行等等。详情请见man sed!😄

SORT 和 UNIQ

sort 会对其输入数据进行排序。uniq -c 会把连续出现的行折叠为一行并使用出现次数作为前缀。两个命令用管道符接起来很有用,看下面这条命令,将100条压缩后就好看很多:

# 利用sort + uniq 一下就能统计出相同的行为有多少次
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c
      1  Anacron 2.3 started on 2023-01-26
      1  Anacron 2.3 started on 2023-01-27
      7  Anacron 2.3 started on 2023-01-28
      7  Anacron 2.3 started on 2023-01-31
      3  Job `cron.daily' started
      3  Job `cron.daily' terminated
      3  Jobs will be executed sequentially
     14  Normal exit (0 jobs run)
      3  Normal exit (1 job run)
     10  (root) CMD (   cd / && run-parts --report /etc/cron.hourly)
      2  (root) CMD (   test -x /etc/cron.daily/popularity-contest && /etc/cron.daily/popularity-contest --crond)
     12  session closed for user root
     12  session opened for user root by (uid=0)
     16  Started Run anacron jobs.
      1  Updated timestamp for job `cron.daily' to 2023-01-27
      1  Updated timestamp for job `cron.daily' to 2023-01-28
      1  Updated timestamp for job `cron.daily' to 2023-01-31
      3  Will run job `cron.daily' in 5 min.

我们希望按照出现次数排序,过滤出最常出现的行为:

# 再次使用sort和tail
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | sort -nk1,1 | tail -5
      9  (root) CMD (   cd / && run-parts --report /etc/cron.hourly)
     11  session opened for user root by (uid=0)
     12  session closed for user root
     14  Normal exit (0 jobs run)
     17  Started Run anacron jobs.
# 可见最常见的行为出现了17次
# 顺便一提, wc -l 命令可以看输出一共有多少行

sort -n 会按照数字顺序对输入进行排序(默认情况下是按照字典序排序 -k1,1 则表示“仅基于以空格分割的第一列进行排序”。,n 部分表示“仅排序到第n个部分”,默认情况是到行尾。

AWK 基于列的流编辑器

我们来看看按照课程上的awkpaste命令来写一个:

# oops, 因为原本的流输入太烂了,所以输出也挺抽象的
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | sort -nk1,1 | tail -5 | awk '{print $2}'
(root)
session
session
Normal
Started
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | sort -nk1,1 | tail -5 | awk '{print $2}' | paste -s
(root)	session	session	Normal	Started
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | sort -nk1,1 | tail -5 | awk '{print $2}' | paste -sd!
(root)!session!session!Normal!Started
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | sort -nk1,1 | tail -5 | awk '{print $2}' | paste -sd,
(root),session,session,Normal,Started

如果您使用的是 MacOS:注意这个命令并不能配合 MacOS 系统默认的 BSD paste使用。参考课程概览与 shell的习题内容获取更多相关信息。

我们可以利用 paste命令来合并行(-s),并指定一个分隔符进行分割 (-d),那awk的作用又是什么呢?

awk 其实是一种编程语言,只不过它碰巧非常善于处理文本。关于 awk 可以介绍的内容太多了,限于篇幅,这里我们仅介绍一些基础知识。

{print $2} 中,$0 表示整行的内容,$1$n 为一行中的 n 个区域(即 n 列),区域的分割基于 awk 的域分隔符(即分列)(默认是空格,可以通过-F来修改)。在这个例子中,我们的代码意思是:对于每一行文本,打印其第二列,也就是用户名。

我们统计一下所有以j 开头,以 `b 结尾,并且有 3 次行为的第二列:

grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | awk '$1 == 3 && $2 ~ /J.*b$/'
      3  Job `cron.daily' started
      3  Job `cron.daily' terminated
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | awk '$1 == 3 && $2 ~ /J.*b$/ {print $2}'
Job
Job

还不够好..emm..既然awk是编程语言,那么看教授摸的表达式:

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 'BEGIN { rows = 0 } $1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 } END { print rows }'
###########
BEGIN { rows = 0 }
$1 == 1 && $2 ~ /^c[^ ]*e$/ { rows += $1 }
END { print rows }
########### 很酷不说话!

除了可以计数,awk还有条件判断,格式化输出等,看mantldr

事实上,我们完全可以抛弃 grepsed ,因为 awk 就可以解决所有问题。👈详细看这篇文章。

分析数据

  • 我们可以利用bc(伯克利计算器?)来计算表达式。
# 保留行为第一行:执行次数
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | awk '{print $1}'
1
7
8
3
3
3
14
3
10
2
12
12
16
1
1
1
3

# 用paste生成加法式子
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | awk '{print $1}' | paste -sd+
1+7+8+3+3+3+14+3+10+2+12+12+16+1+1+1+3

# 用bc计算表达式,加 -l 是因为这些工具默认设置都很笨(在这里可以不加,结果一样)
grapefruitcat@grapefruitcat:~$ journalctl | grep -i cron | tail -100 | sed -E 's/.*?\://' | sort | uniq -c | awk '{print $1}' | paste -sd+ | bc -l
100
  • 如果安装了R语言,可以用st。R 也是一种编程语言,它非常适合被用来进行数据分析和绘制图表。(还没去了解,要专门去学,就算啦)
  • 如果您希望绘制一些简单的图表, gnuplot 可以帮助到您。

这些工具能熟练用上就能成为命令行高手!(羡慕😭)

下面两小节我将直接把讲义copy过来,因为我的机器上没有这样的环境可以show出来..💤

利用数据整理来确定参数

有时候您要利用数据整理技术从一长串列表里找出你所需要安装或移除的东西。我们之前讨论的相关技术配合 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 -'

课后练习

  1. 学习一下这篇简短的 交互式正则表达式教程.

    这篇教程被墙了,建议去菜鸟教程学。

  2. 统计words文件 (/usr/share/dict/words) 中包含至少三个a 且不以's 结尾的单词个数。这些单词中,出现频率前三的末尾两个字母是什么? sedy命令,或者 tr 程序也许可以帮你解决大小写的问题。共存在多少种词尾两字母组合?还有一个很 有挑战性的问题:哪个组合从未出现过?

    # 至少三个a,即至少三个含a的匹配词
    ([^a]*a){3}
    # 不以 's 结尾, 需要用到反向否定预查
    (?<!'s)
    
    # 看这两篇文章更好地理解正向否定预查:https://www.jianshu.com/p/8bf162425d83
    # https://www.jb51.net/article/52491.htm
    # 正向否定预查使用例子: https://regex101.com/r/x9HMBO/1
    # 反向否定预查:https://www.jianshu.com/p/2ea1385e60e8
    # 最终结果:https://regex101.com/r/SJDpZx/1
    

    生成的正则表达式为:^([^a]*a){3,}.*(?<!'s)$。但grep似乎不支持否定预查..(md害我试了那么久,awk也是不行),所以直接分两步来:

    # -E 是正则表达式扩展, -v是反选
    grapefruitcat@grapefruitcat:~$ cat /usr/share/dict/words | grep -E "^([^a]*a){3,}.*$" | wc -l
    1214
    grapefruitcat@grapefruitcat:~$ cat /usr/share/dict/words | grep -E "^([^a]*a){3,}.*$" | grep -v "'s" | wc -l
    764
    
    # 用上大小写转换
    grapefruitcat@grapefruitcat:~$ cat /usr/share/dict/words | grep -E "^([^a]*a){3,}.*$" | grep -v "'s" | tr "[:upper:]" "[:lower:]" | sort | wc -l
    764
    

    获得了规定的单词后,就开始进行统计:

    # 这个是把后面两个字母筛选出来
    grapefruitcat@grapefruitcat:~$ cat /usr/share/dict/words | grep -E "^([^a]*a){3,}.*$" | grep -v "'s" | tr "[:upper:]" "[:lower:]" | sort | sed -E 's/.*([a-z]{2})/\1/'
    764
    
    # 按照课上说的 sort 和 uniq
    grapefruitcat@grapefruitcat:~$ cat /usr/share/dict/words | grep -E "^([^a]*a){3,}.*$" | grep -v "'s" | tr "[:upper:]" "[:lower:]" | sort | sed -E 's/.*([a-z]{2})/\1/' | sort | uniq -c | wc -l
    105
    
    # okay,有105种词尾两字母组合。再来..排列一下
    grapefruitcat@grapefruitcat:~$ cat /usr/share/dict/words | grep -E "^([^a]*a){3,}.*$" | grep -v "'s" | tr "[:upper:]" "[:lower:]" | sort | sed -E 's/.*([a-z]{2})/\1/' | sort | uniq -c | sort -nk1,1 | tail -3
         52 as
         57 ns
         85 an
    # okay,频率前三出来了
    

    对于思考题,我们不能把思路局限在这节课内容,想想上节课!利用脚本生成文件进行比较。

    # 写个脚本
    grapefruitcat@grapefruitcat:~$ cat lesson4.sh
    #!/usr/bin/env bash
    for i in {a..z};
    do
    	for j in {a..z};
    	do
    		echo "$i$j"
    	done
    done
    
    # 权限不够先改好
    grapefruitcat@grapefruitcat:~$ chmod 775 lesson4.sh
    grapefruitcat@grapefruitcat:~$ ./lesson4.sh > lesson4.txt
    grapefruitcat@grapefruitcat:~$ cat /usr/share/dict/words | grep -E "^([^a]*a){3,}.*$" | grep -v "'s" | tr "[:upper:]" "[:lower:]" | sort | sed -E 's/.*([a-z]{2})/\1/' | sort | uniq > lesson4cmp.txt
    grapefruitcat@grapefruitcat:~$ diff <(lesson4.txt) <(lesson4cmp.txt)
    lesson4.txt:未找到命令
    lesson4cmp.txt:未找到命令
    
    # 可以看到详细的不同
    grapefruitcat@grapefruitcat:~$ diff <(cat lesson4.txt) <(cat lesson4cmp.txt)
    
    # 忽略详细信息,直接看有多少个
    grapefruitcat@grapefruitcat:~$ diff --unchanged-group-format='' <(cat lesson4.txt) <(cat lesson4cmp.txt) | wc -l
    571
    
  3. 进行原地替换听上去很有诱惑力,例如: sed s/REGEX/SUBSTITUTION/ input.txt > input.txt。但是这并不是一个明智的做法,为什么呢?还是说只有 sed是这样的? 查看 man sed 来完成这个问题。

    # 可以看一下例子
    grapefruitcat@grapefruitcat:~$ cat kksk.txt
    aa
    aa
    aa
    grapefruitcat@grapefruitcat:~$ sed -E 's/aa/bb/' kksk.txt
    bb
    bb
    bb
    grapefruitcat@grapefruitcat:~$ sed -E 's/aa/bb/' kksk.txt > kksk.txt
    grapefruitcat@grapefruitcat:~$ cat kksk.txt
    # 什么也没有了...
    

    文件内容全部不见了!!怎么会是呢!😭

    查查查,找到了一篇关于sed重定向的文章:https://www.jianshu.com/p/5d098768e5cf

    这应该不单是sed的问题,而是shell的 I/O重定向的问题。重定向的时候要先准备好输入输出然后才进行读写操作,所以在sed读取kksk.txt中的内容之前,已经由于>操作将这个文件中的内容都清空了。

    再查了下man sed,发现要改文件内容的话需要加一个-i参数:

    -i [SUFFIX], --in-place [=SUFFIX] edit files in place (makes backup if SUFFIX supplied)

    grapefruitcat@grapefruitcat:~$ sed -i -E 's/aa/bb/' kksk.txt
    grapefruitcat@grapefruitcat:~$ cat kksk.txt
    bb
    bb
    bb
    

    或者不重定向到自身,这样就可以了🥳

  4. 找出您最近十次开机的开机时间平均数、中位数和最长时间。在Linux上需要用到journalctl,而在 macOS 上使用log show找到每次起到开始和结束时的时间戳。在Linux上类似这样操作:Logs begin at ...systemd[577]: Startup finished in ..., 在 macOS 上, 查找:=== system boot:Previous shutdown cause: 5.

    一共看到有33次的开机记录:

    grapefruitcat@grapefruitcat:~$ journalctl --list-boots | wc -l
    33
    # 最后十次开机记录
    grapefruitcat@grapefruitcat:~$ journalctl --list-boots | tail -n10
     -9 5593639f1a984b058c68621b41fa5523 Fri 2022-11-04 09:33:04 CST—Fri 2022-11-04 11:50:11 CST
     -8 d47d5e24cef84eccac1823d382c9f135 Fri 2022-11-18 21:26:25 CST—Fri 2022-11-18 21:53:24 CST
     -7 a3c5e65d03c74a06bd71774e2958dbb6 Mon 2022-11-21 15:37:00 CST—Tue 2022-11-22 00:07:32 CST
     -6 d11561420d504b69858086938ec415c1 Tue 2022-11-22 20:58:41 CST—Wed 2022-11-23 00:00:05 CST
     -5 18afd5af9eee46efa97a3e4998f17508 Wed 2022-11-23 14:26:47 CST—Wed 2022-11-23 14:31:27 CST
     -4 79591260448648968580f5877829a8cc Wed 2022-11-23 14:40:51 CST—Wed 2022-11-23 14:59:03 CST
     -3 d48b6aeb768e410cafef3e60cd181529 Wed 2022-11-23 23:03:40 CST—Fri 2022-12-16 13:13:40 CST
     -2 3a92289757e147f6aed9bff8174c20a1 Fri 2022-12-16 13:13:46 CST—Fri 2022-12-16 13:13:59 CST
     -1 6e8661f5b88147b8957d816739c62d38 Fri 2022-12-16 13:14:17 CST—Fri 2022-12-16 13:20:29 CST
      0 4bb41aa2370647849194529f3df1c865 Wed 2023-01-11 11:22:37 CST—Wed 2023-02-01 00:24:19 CST
    

    我们来用Startup finished in做一下筛选:

    # 写一个脚本,输出开机信息到 boottest.txt
    grapefruitcat@grapefruitcat:~$ cat boottest.sh
    #!/usr/bin/env bash
    for i in {0..9};do
    	journalctl -b-$i | grep "Startup finished in " | grep "systemd\[1\]"  >> boottest.txt
    done
    
    # 只有 systemed[1] 的数据是我们要的
    grapefruitcat@grapefruitcat:~$ cat boottest.txt
    1月 11 11:22:52 grapefruitcat systemd[1]: Startup finished in 3.599s (kernel) + 15.937s (userspace) = 19.537s.
    12月 16 13:14:22 grapefruitcat systemd[1]: Startup finished in 3.386s (kernel) + 5.240s (userspace) = 8.626s.
    12月 16 13:13:52 grapefruitcat systemd[1]: Startup finished in 2.987s (kernel) + 6.033s (userspace) = 9.020s.
    11月 23 23:03:47 grapefruitcat systemd[1]: Startup finished in 3.622s (kernel) + 6.735s (userspace) = 10.358s.
    11月 23 14:40:56 grapefruitcat systemd[1]: Startup finished in 3.380s (kernel) + 5.286s (userspace) = 8.666s.
    11月 23 14:27:06 grapefruitcat systemd[1]: Startup finished in 3.261s (kernel) + 19.173s (userspace) = 22.434s.
    11月 22 20:59:03 grapefruitcat systemd[1]: Startup finished in 3.712s (kernel) + 20.767s (userspace) = 24.480s.
    11月 21 15:37:19 grapefruitcat systemd[1]: Startup finished in 3.183s (kernel) + 17.539s (userspace) = 20.722s.
    11月 18 21:26:45 grapefruitcat systemd[1]: Startup finished in 4.557s (kernel) + 21.224s (userspace) = 25.782s.
    11月 04 09:33:26 grapefruitcat systemd[1]: Startup finished in 3.139s (kernel) + 22.429s (userspace) = 25.569s.
    
    # 处理后剩下时间
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | sed 's/.*in //' | awk '{print $1}' | sed -E 's/([0-9.]*)s/\1/'
    3.599
    3.386
    2.987
    3.622
    3.380
    3.261
    3.712
    3.183
    4.557
    3.139
    
    # 平均数
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | sed 's/.*in //' | awk '{print $1}' | sed -E 's/([0-9.]*)s/\1/' | paste -sd+ | bc -l
    34.826
    grapefruitcat@grapefruitcat:~$ echo "($(cat boottest.txt | sed 's/.*in //' | awk '{print $1}' | sed -E 's/([0-9.]*)s/\1/' | paste -sd+))/10" | bc -l
    3.48260000000000000000
    
    # 中位数,因为每个开机时间都不同,取中间两个除以二
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | sed 's/.*in //' | awk '{print $1}' | sed -E 's/([0-9.]*)s/\1/' | sort | paste -sd" " | awk '{print ($5+$6)/2}'
    3.383
    
    # 最长时间(最短时间就将sort加上参数 -r)
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | sed 's/.*in //' | awk '{print $1}' | sort | tail -n1
    4.557s
    
  5. 查看之前三次重启启动信息中不同的部分(参见 journalctl-b 选项)。将这一任务分为几个步骤,首先获取之前三次启动的启动日志,也许获取启动日志的命令就有合适的选项可以帮助您提取前三次启动的日志,亦或者您可以使用sed '0,/STRING/d' 来删除STRING匹配到的字符串前面的全部内容。然后,过滤掉每次都不相同的部分,例如时间戳。下一步,重复记录输入行并对其计数(可以使用uniq )。最后,删除所有出现过3次的内容(因为这些内容是三次启动日志中的重复部分)。

    # 修改一下脚本
    grapefruitcat@grapefruitcat:~$ cat boottest.sh
    #!/usr/bin/env bash
    >boottest.txt
    for i in {0..2};do
    	journalctl -b-$i | grep "systemd\[1\]"  >> boottest.txt
    done
    # 可以见到有两千多条启动日志
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | wc -l
    2202
    # 第一步过滤
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | sed -E 's/.*systemd\[1\]: //' | sort | wc -l
    2202
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | sed -E 's/.*systemd\[1\]: //' | sort | uniq -c | wc -l
    474
    # 第二次过滤,删除出现3次的内容
    grapefruitcat@grapefruitcat:~$ cat boottest.txt | sed -E 's/.*systemd\[1\]: //' | sort | uniq -c | sort -nk1,1 | awk '$1 != 3 {print $0}'
    
  6. 在网上找一个类似 这个 或者这个国内被墙)的数据集。或者从这里找一些。使用 curl 获取数据集并提取其中两列数据,如果您想要获取的是HTML数据,那么pup可能会更有帮助。对于JSON类型的数据,可以试试jq。请使用一条指令来找出其中一列的最大值和最小值,用另外一条指令计算两列之间差的总和。

    看到一篇好文章,可能有所帮助:https://blog.51cto.com/quguanhai/1825537

🌈标准解答在:https://missing-semester-cn.github.io/missing-notes-and-solutions/2020/solutions//data-wrangling-solution

学习愉快~!😘

posted @ 2023-02-01 14:35  GrapefruitCat  阅读(142)  评论(0编辑  收藏  举报