小編編程資質通常,剛出道的時候使用的是windows來作程序開發,平時linux命令的知識僅限於在學校裏玩ubuntu的時候學到的那丁點。在一次偶然看見項目的主程敲着複雜的shell單行命令來處理日誌的時候感到驚訝不已。後來本身自學了一點shell編程,剛看完一本書沒過多久就忘記了,由於工做中用到的實在太少,並且命令如此之多,學了一個忘了另外一個,始終摸不着門道在哪。html
直到某天靈感爆發,發現了一個竅門以後,才緊緊地把握住了shell指令的精髓。linux
寫SQL小編很是在行,畢業第一年的時候SQL就寫的行雲流水。常常別人寫了一個存儲過程來幹某件事的時候,哥用一條語句搞定。天然這樣的語句也是被很多人吐槽的,難以看懂。git
偶然一天我將一個數據表導入成一個CSV文件的時候發現了這個竅門。若是把這個CSV文件當作一個數據表,把各類shell指令當作SQL的查詢條件,這兩種數據處理方式在思惟模式上就沒有什麼區別了。github
而後就開始仔細研究了一番,又有了好多驚人的發現。原來shell指令除了查詢以外還能夠作修改,至關於SQL的DML操做。shell指令除了能作單表數據處理以外還能夠實現相似於SQL多表的JOIN操做。連排序和聚合功能也能輕鬆搞定。算法
首先下載本章用到的數據,該數據有20多M,建議耐心等待。shell
git clone https://github.com/pyloque/shellquery_ppt.git
複製代碼
第一個文件groups.txt表示小組,有三個字段,分別是小組ID、小組名稱和小組建立時間數據庫
第二個文件rank_items.txt表明行爲積分。字段分別是行爲惟一ID、行爲類型、行爲關聯資源ID、行爲時間和行爲積分。行爲類型包含group單詞的是和小組相關的積分行爲。其它行爲還有與帖子、用戶、問題、文章相關的。編程
數據表是有模式的數據,每一個列都有特定的含義。表的模式信息能夠在數據庫的元表裏找到。ubuntu
CSV文本文件也是有模式的數據,只不過它的列信息只存在於用戶的大腦裏。文件裏只有純粹的數據和數據分隔符。CSV文本文件的記錄之間使用換行符分割,列之間使用製表符或者逗號等符號進行分隔。windows
數據表的行記錄等價於CSV文本文件的一行數據。數據表一行的列數據可使用名稱指代,可是CSV行的列數據只能用位置索引,表達能力上相比要差一截。
在測試階段,咱們使用少許行的數據進行測試,這個時候可使用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
...
複製代碼
數據過濾通常會使用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"
複製代碼
去重的命令時uniq,可是跟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
# 很明顯這個值不是咱們指望的
複製代碼
有不少指令能夠接受一個文件名做爲參數,而後對這個文件進行文本處理。若是輸入不是文件而是由一串命令生成的動態文件怎麼辦呢?也許你會想到先將這一串命令輸出到臨時文件中再將這個臨時文件名做爲指令的輸入,處理完畢後再刪除這個臨時文件。
# 首先建立臨時文件
bash> mktemp
/var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp
# 輸出到臨時文件
bash> cat groups.txt | grep 技術 > /var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp
# 處理臨時文件,統計臨時文件的行數
bash> cat /var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp | wc -l
3
# 刪除臨時文件
bash> rm /var/folders/w3/4z1zbpdn6png5y3bl0pztph40000gn/T/tmp.LoWLFvJp
複製代碼
可是本文的主題是單行shell命令。你很難使用單行命令來實現上面提到的臨時文件法。這時咱們就須要藉助於一個高級語法:進程替換。
# 等價於上面的臨時文件法,進程替換符號<()
bash> cat <(cat groups.txt | grep 技術) | wc -l
3
複製代碼
進程替換的原理也是臨時文件法,只是這裏的文件路徑是/dev/fd/。
當兩個數據表有關聯時,可使用join操做進行連表查詢。一樣shell也有特殊的方法能夠關聯兩個文件的內容進行查詢,這個命令在shell裏面也是join。考慮到性能,join指令要求兩個輸入文件的join字段必須是排序的。
# rank_items表裏面的行爲類型字段有個值爲hot_group,它表示小組由於活躍而上了熱門小組
# 而後系統給這個小組累積了一個score,好比
# hot_group後面跟的是小組ID,最後的值1表示score積分
bash> cat rank_items.txt | grep hot_group | head -n 5
"5aa19d6a-3482-4a92-ae20-f26218d8debd";"hot_group";"96";"2013-06-03 21:43:58.62761+08";1
"6ae0f144-33af-432b-a9af-db51938e8faf";"hot_group";"48";"2013-06-03 21:44:05.050322+08";1
"55dcb43e-e2c0-43d2-8ed7-dbec6771e7b4";"hot_group";"185";"2013-06-05 18:14:08.406047+08";1
"98a54f24-fdef-4029-ad79-90055423f5c3";"hot_group";"31";"2013-06-03 21:47:28.476056+08";1
"4284d4d5-41b9-4dfd-ada9-537332c5cbd6";"hot_group";"63";"2013-06-01 10:07:18.58019+08";1
# 如今咱們來聚合一下全部小組的各自積分,而後排序取前5名
# 用grep過濾只保留包含hot_group的行
# 篩選字段,只保留小組ID和積分字段,由於小組ID先後有引號,因此得用substr去掉引號
# 用awk的聚合功能累積各小組的積分
# sort -n -r按積分數字倒排,再head -n 5取前5名展現出來
bash> cat rank_items.txt| grep hot_group | \
awk -F';' '{print substr($3, 2, length($3)-2)";"$5}' | \
awk -F';' '{scores[$1]+=$2} END{for(id in scores) print id";"scores[id]}' | \
sort -t';' -n -r -k2 | head -n 5
63;5806
30;4692
69;4605
73;3177
27;2801
# 接下來咱們將上面的結果和groups.txt文件join起來,以顯示小組ID對應的名稱
# -t指定分隔符,兩個輸入分隔符必須一致
# -1 1 -2 1表示取第一個輸入文件的第一個字段和第二個輸入文件的第一個字段來join
# -o1.1,1.2,2.2表示輸出第一個輸入文件的第一第二字段和第二個輸入文件的第二字段
bash> join -t';' -1 1 -2 1 -o1.1,1.2,2.2 \
<(sort -t';' -k1 groups.txt) \
<(cat rank_items.txt| grep hot_group | \
awk -F';' '{print substr($3, 2, length($3)-2)";"$5}' | \
awk -F';' '{scores[$1]+=$2} END{for(id in scores) print id";"scores[id]}' | \
sort -t';' -n -r -k2 | head -n 5)
63;"Geek笑點低";5806
69;"吃貨研究所";4605
73;"美麗也是技術活";3177
# 咱們看到結果只有3條,緣由是有30和27兩個ID在groups.txt裏面找不到。
複製代碼
《Unix Shell編程》
《The AWK programming language》
《Sed & Awk 101 Hacks》
GNU Parallel http://www.gnu.org/software/parallel/
複製代碼
閱讀相關文章,請關注公衆號【碼洞】