小编编程资质一般,刚出道的时候使用的是windows来做程序开发,平时linux命令的知识仅限于在学校里玩ubuntu的时候学到的那丁点。在一次偶然看见项目的主程敲着复杂的shell单行命令来处理日志的时候感到惊讶不已。后来自己自学了一点shell编程,刚看完一本书没过多久就忘记了,因为工作中用到的实在太少,而且命令如此之多,学了一个忘了另一个,始终摸不着门道在哪。
直到某天灵感爆发,发现了一个窍门之后,才牢牢地把握住了shell指令的精髓。
用写SQL查询的思维写shell命令
写SQL小编非常在行,毕业第一年的时候SQL就写的行云流水。经常别人写了一个存储过程来干某件事的时候,哥用一条语句搞定。自然这样的语句也是被不少人吐槽的,难以看懂。
偶然一天我将一个数据表导入成一个CSV文件的时候发现了这个窍门。如果把这个CSV文件看成一个数据表arm linux,把各种shell指令看成SQL的查询条件,这两种数据处理方式在思维模式上就没有什么区别了。
然后就开始仔细研究了一番,又有了好多惊人的发现。原来shell指令除了查询之外还可以做修改,相当于SQL的DML操作。shell指令除了能做单表数据处理之外还可以实现类似于SQL多表的JOIN操作。连排序和聚合功能也能轻松搞定。
首先下载本章用到的数据,该数据有20多M,建议耐心等待。
git clone https://github.com/pyloque/shellquery_ppt.git
第一个文件groups.txt表示小组,有三个字段,分别是小组ID、小组名称和小组创建时间
第二个文件rank_items.txt代表行为积分。字段分别是行为唯一ID、行为类型、行为关联资源ID、行为时间和行为积分。行为类型包含group单词的是和小组相关的积分行为。其它行为还有与帖子、用户、问题、文章相关的。
文本文件等价于数据表table
数据表是有模式的数据,每个列都有特定的含义。表的模式信息可以在数据库的元表里找到。
CSV文本文件也是有模式的数据,只不过它的列信息只存在于用户的大脑里。文件里只有纯粹的数据和数据分隔符。CSV文本文件的记录之间使用换行符分割,列之间使用制表符或者逗号等符号进行分隔。
数据表的行记录等价于CSV文本文件的一行数据。数据表一行的列数据可以使用名称指代,但是CSV行的列数据只能用位置索引linux统计文件行数,表达能力上相比要差一截。
在测试阶段,我们使用少量行的数据进行测试linux命令chm,这个时候可以使用head指令只吐出CSV文本文件的前N行数据,它相当于SQL的limit条件。同样也可以使用tail指令吐出文件的倒数前N行数据。使用cat指令吐出所有。
# 看前5行
bash> head -n 5 groups.txt
205;"真要瘦不瘦不罢休";"2012-11-23 13:42:38+08"
28;"健康朝九晚五";"2010-10-20 16:20:43+08"
280;"核谐家园";"2013-04-17 17:11:49.545351+08"
38;"创意科技";"2010-10-20 16:20:44+08"
39;"死理性派";"2010-10-20 16:20:44+08"
# 看倒数5行
bash> tail -n 5 groups.txt
69;"吃货研究所";"2010-11-10 14:35:34+08"
27;"DIY";"2010-10-20 16:20:43+08"
33;"心事鉴定组";"2010-10-20 16:20:44+08"
275;"盗梦空间";"2013-03-21 23:35:39.249583+08"
197;"万有青年养成计划";"2012-11-14 11:39:50+08"
# 显示所有
bash> cat groups.txt
...
数据过滤等价于查询条件where
数据过滤一般会使用grep或者awk指令。grep用来将整个行作为文本来进行搜索,保留满足指定文本条件的行,或者是保留不满足匹配条件的行。awk可以用来对指定列内容进行文本匹配或者是数字匹配。
# 显示包含‘技术’单词的行
bash> cat groups.txt | grep 技术
73;"美丽也是技术活";"2010-11-10 15:08:59+08"
279;"灰机与航空技术";"2013-04-12 13:30:31.617491+08"
243;"科学技术史";"2013-01-24 12:48:44.06041+08"
# 显示即包含单词‘技术’又包含‘灰机’的行
bash> cat groups.txt | grep 技术 | grep 灰机
279;"灰机与航空技术";"2013-04-12 13:30:31.617491+08"
# 显示小组ID小于30的行 -F限定分隔符 后面是一个awk脚本
# awk一门简单的编程语言,它处理的对象是以行为单位
# $0表示整行内容 $1代表第一列内容
# awk分4段,选择端|起始段|处理段|结束段
# filter BEGIN{} {} END{}
# 选择端起到过滤行的作用,选择成功的行进入处理段
# 起始端在第一个行处理之前进行,结束段在最后一个行处理完成之后进行,只进行依次
# 处理段就是对选择成功的行依次处理,依次处理一行
# 这些段都是可选的
# 参考awk简明教程 https://coolshell.cn/articles/9070.html
bash> cat groups.txt | awk -F';' '$1<30 {print $0}'
28;"健康朝九晚五";"2010-10-20 16:20:43+08"
29;"爱宠";"2010-10-20 16:20:44+08"
27;"DIY";"2010-10-20 16:20:43+08"
限定字段输出
我们经常使用列名称来限定SQL的输出对象。
SQL> select id, user from group
同样对于文本文件,我们可以使用cut指令或者awk来完成。
# 只显示前3行的第一列和第二列,保留分隔符 -d指明分隔符
bash> cat groups.txt | head -n 3 | cut -d';' -f1 -f2
205;"真要瘦不瘦不罢休"
28;"健康朝九晚五"
280;"核谐家园"
# 只显示前3行的第一列和第二列,用空格作为分隔符
bash> cat groups.txt | head -n 3 | awk -F';' '{print $1" "$2}'
205 "真要瘦不瘦不罢休"
28 "健康朝九晚五"
280 "核谐家园"
组合命令的效率
一个复杂的单行命令可以有非常多的单条指令组成,每个指令都会对应着一个进程。进程和进程之间使用管道将输入输出串接起来,形如人体蜈蚣。
第一个进程处理了一行数据后从输出吐了出来,成了第二个进程的输入,在第二个进程对第一行数据进行处理的过程中,第一个进程又可以继续处理后面的行。
如此就形成了一个流水线结构,每个进程都在并行的进行数据处理。整个组合命令的效率将取决于所有命令中最慢的一条。
排序操作又不同于其它操作,它需要等待所有的数据都接受完成才能决定第一个输出。所以排序是一个即占用内存又耗费时间的操作,它会导致后续进程的饥饿感。
聚合
数据聚合也是shell里经常使用到的命令,最常用的可能就是用wl来统计行数,其实也可以使用awk来完成更加复杂的统计功能。
# 总共多少行
bash> cat groups.txt | wc -l
216
# 用awk实现,遇到一行对变量l加1,最后输出l变量的值,也即行数
bash> cat groups.txt | awk '{l+=1} END{print l}'
awk还可以完成类似于group by的功能,这个脚本就要复杂一点
# 因为命令太长,下面用了shell命令续行符""
# 统计每行的名称长度[去掉前后两个引号],将相同长度的进行聚合统计数量
# awk不识别unicode,所以长度都是按字节算的,可以使用gawk工具来取代
# awk支持字典数据结构和循环控制语句,所以可以干聚合的事
bash> cat groups.txt | awk -F';' '{print length($2)-2}' |
awk '{g[$1]+=1} END{for (l in g) print l,"=",g[l]}'
22 = 1
3 = 2
4 = 1
24 = 9
6 = 6
...
排序和去重
排序命令是一种消耗内存的运算,它需要将全部的内容放置到内存的数组里,然后使用排序算法进行内容排序后输出。shell的排序就是sort命令,sort可以按字符排序也可以按数字排序。
# 以分号作为分隔符,排序第一列小组的ID
# 默认按字符进行排序
bash> cat groups.txt | sort -t';' -k1 | head -n 5
102;"说文解字";"2012-03-19 18:10:47+08"
103;"广告研发局";"2012-03-21 17:50:02+08"
104;"掀起你的内幕来";"2012-03-26 17:23:11+08"
105;"一分钟学堂";"2012-03-28 17:06:37+08"
106;"泥瓦匠";"2012-04-11 21:30:34+08"
# 加上-n选项按数字进行排序
bash> cat groups.txt | sort -t';' -n -k1 | head -n 5
27;"DIY";"2010-10-20 16:20:43+08"
28;"健康朝九晚五";"2010-10-20 16:20:43+08"
29;"爱宠";"2010-10-20 16:20:44+08"
30;"性 情";"2010-10-20 16:20:44+08"
31;"谋杀 现场 法医";"2010-10-20 16:20:44+08"
# 加上-r选项倒排
bash> cat groups.txt | sort -t';' -n -r -k1 | head -n 5
303;"怎么玩小组";"2013-06-05 13:18:06.079734+08"
302;"**精选";"2013-06-05 13:15:52.187787+08"
301;"土木建筑之家";"2013-06-05 13:14:58.968257+08"
300;"NBA那些事儿";"2013-06-03 15:50:14.415515+08"
299;"数据江湖";"2013-05-30 17:27:10.514241+08"
去重的命令时uniqlinux统计文件行数,但是跟SQL的distinct不一样,uniq一般和sort配合使用,它要求去重的对象必须是排过序的,否则就不能起到去重的效果。distinct一般是在内存里记录一个Set放入所有的值,然后查询新值是否在Set中。uniq只记录一个值,就是上一行的值,然后看新行的值是否和上一行的值一样。
# 打印第二列小组名称的长度的所有可能的值的个数
# awk打印长度,sort -n按长度数字排序, uniq去重,wc -l统计个数
bash> cat groups.txt | awk -F';' '{print length($2)-2}' | sort -n | uniq | wc -l
21
# 我们再看看,如果不排序会怎样
bash> cat groups.txt | awk -F';' '{print length($2)-2}' | uniq | wc -l
166
# 很明显这个值不是我们期望的
进程替换操作符