沒想到 Shell 命令竟然還能這么玩?
正文開始
在前面的這篇文章中 —— 優(yōu)秀的程序員是如何利用工具來提升工作效率的?,石頭介紹了可以提高程序猿工作效率的一些軟件和工具及相關配置。文中提到了, 程序猿應該了解一些常見的命令行工具來提高效率。
本文是一個命令行工具的綜合應用,將用一個具體的例子來闡述如何用 Shell 來進行高效地數(shù)據(jù)統(tǒng)計和分析。最近北京又開始了新一批積分落戶的填報工作,恰好這篇文章用 shell 來對首批北京積分落戶同學進行 "大數(shù)據(jù)"分析。
現(xiàn)如今到處都是各種"大數(shù)據(jù)",本文分析對象也就是首批積分落戶的6000多條數(shù)據(jù)而已,顯然不能算什么大數(shù)據(jù)。
印象中,我記得當初該官網(wǎng)的這6000多條數(shù)據(jù)也是一次性就能wget下來的(后端估計沒做限制,可能稍微調(diào)整下接口的分頁參數(shù)之類不需要嚴格按照各種分頁多次下載)。(注:本文舊文重新整理發(fā)送。)
問題描述
輸入是 json數(shù)據(jù),格式化之后的 json 數(shù)據(jù)主題結構如下所示,rows為數(shù)組,數(shù)組中元素所代表的 object 即描述了獲得北京戶口的同學的各種屬性:例如分數(shù)、排名、身份證號(后四位打碼了)、公司等等信息。為了方便大家練習對數(shù)據(jù)進行試驗,我將文中的數(shù)據(jù)附在這里(https://www.tanglei.name/resources/use-shell-to-analysis-the-first-people-of-getting-residence-of-beijing-by-score/jifenluohu.json.gz)。
- "rows": [
- {
- "id": 62981,
- "idCard": "32092219721222****",
- "idCardSHA": "9ef70bde894959a4e4a1d1b2b9592b470294f9e4012a8cf480319665d1a7c1c6",
- "insertTime": 1539518353000,
- "integralQualified": 1,
- "internetAnnual": {
- "annual": 2018,
- "id": 43,
- "insertTime": 1539518353000,
- "publicityEnd": 1540224000000,
- "publicityStart": 1539591600000,
- "publishResultEndDate": 1541679300000,
- "publishResultStartDate": 1539591600000,
- "publishResultStatus": 1,
- "score": 90.75,
- "status": 1
- },
- "md5Code": "54e9ff7ce0b004f7141b157f8afc66db",
- "name": "楊效豐",
- "pxid": 1,
- "ranking": 1,
- "s1": 51,
- "s10": 0,
- "s2": 12.59,
- "s3": 15,
- "s4": 0,
- "s5": 4,
- "s6": 0,
- "s7": 20,
- "s8": 20,
- "s9": 0,
- "score": 122.59,
- "unit": "北京利德華福電氣技術有限公司"
- },
拿到這個文件,比如希望你用最快的方法獲得以下信息,你將會怎么做?
- 獲取取得戶口名額最多的top10公司
- 獲取取得戶口名額的人中姓氏最多的
- 獲取戶口名字中叫啥名最流行
- 獲取年齡分布
- 獲取取得戶口的同學戶籍地top10
- 生肖/星座/生日...
當然,方法有很多,比如熟悉各種編程語言的,例如 python, php, java 等等寫個簡單的腳本程序,也能比較快獲取答案?;蛘甙严鄳臄?shù)據(jù)提取出來,放到 excel 中也可以。
如果你對 Shell 很熟悉,那真的是分分鐘,應該是秒秒鐘就能獲取答案。就算用 Shell 來實現(xiàn),不同的人可能也有不同的寫法,后面我就列舉其中的一種來解決這些問題。
本文不對 Shell 具體每個命令做過多的解釋,不熟悉的同學可以直接 man $cmd 或者 $cmd --help 等等查看。
之前我也寫過一篇名叫 Shell 助力開發(fā)效率提升 的文章,算是給常用的命令的常用參數(shù)做了一個解釋和示例,有興趣的同學可以前往查閱。
問題解答
獲取取得戶口名額最多的top10公司
看看想通過積分落戶,最好是進哪些公司,哈哈。
"unit": "北京利德華福電氣技術有限公司"
先通過 grep 得到包含公司名字的一行,然后通過 ":" 分割 cut 取第2列得到公司名字,對結果進行sort排序進行去重uniq統(tǒng)計得到重復次數(shù),次時結果為重復次數(shù) 公司名,再對第一列-k 1重復數(shù)字進行按照數(shù)字排序逆序-nr 即 sort -nr -k 1,最后取結果的前10行 head -n 10。
- ➜ 積分落戶 > grep 'unit' jifenluohu.json| cut -f2 -d: | sort | uniq -c | sort -nr -k 1 | head -n 10
- 137 "北京華為數(shù)字技術有限公司"
- 73 "中央電視臺"
- 57 "北京首鋼建設集團有限公司"
- 55 "百度在線網(wǎng)絡技術(北京)有限公司"
- 48 "聯(lián)想(北京)有限公司"
- 40 "北京外企人力資源服務有限公司"
- 40 "中國民生銀行股份有限公司"
- 39 "國際商業(yè)機器(中國)投資有限公司"
- 29 "中國國際技術智力合作有限公司"
- 27 "華為技術有限公司北京研究所"
獲取取得戶口名額的人中姓氏最多的
看看想通過積分落戶,最好是姓啥,哈哈。
"name": "楊效豐",
套路跟之前差不多的,我這邊就不特別指出了。
下面shell實際上是取到這行后,將真正表示名字之前的所有字符都刪除,就只剩下名字開頭了,取行首第一個字符cut -c 1即得到姓,再按照之前的套路就能拿到了。
其實用什么sed替換冗余的字符都是多余的,因為json的格式都是良好的,可以直接通過 cut -c ? 取姓這個字符即可。
也不用挨個去數(shù)到底是第幾個字符,直接 copy出來,然后 echo -n $paste | wc -c 就能數(shù)到第幾個字符了。
看結果還是姓 "張, 王" 之類的最有戲。??
- # 或者 grep '"name":' jifenluohu.json| sed 's|"name": "||g' | sed 's|[[:space:]]||g' | cut -c 1 | sort | uniq -c | sort -nr -k 1 | head -n 10
- ➜ 積分落戶 > grep '"name":' jifenluohu.json| sed 's|"name": "||g' | sed 's| ||g' | cut -c 1 | sort | uniq -c | sort -nr -k 1 | head -n 10
- 541 張
- 531 王
- 462 李
- 376 劉
- 205 陳
- 193 楊
- 166 趙
- 132 孫
- 95 郭
- 95 徐
獲取戶口名字中叫啥名最流行
套路差不多,不做過多解釋了。
- ➜ 積分落戶 > grep '"name":' jifenluohu.json| sed 's|"name": "||g' | sed 's|[[:space:]]||g' | cut -c 2-4 | sort | uniq -c | sort -nr -k 1 | head -n 10
- 51 偉",
- 39 靜",
- 38 濤",
- 36 勇",
- 36 軍",
- 32 敏",
- 31 穎",
- 30 鵬",
- 28 杰",
- 28 峰",
- # 取名字, 必須包含2個字
- ➜ 積分落戶 > grep '"name":' jifenluohu.json| sed 's|"name": "||g' | sed 's|[[:space:]]||g' | cut -c 2-3 | sed '/"/d' | sort | uniq -c | sort -nr -k 1 | head -n 10
- 19 海濤
- 19 曉東
- 12 志強
- 11 海燕
- 11 永強
- 11 建華
- 10 雪梅
- 9 海龍
- 9 麗娜
- 8 洪濤
作為碼農(nóng),必須得養(yǎng)成對自己得到結果進行自測的習慣,所以如果對自己的結果不夠自信,可以正向去計算一下最終的結果。
例如可以簡單grep一下進行驗證,叫 "海濤" 的是不是19個。
- ➜ 積分落戶 > grep '海濤' jifenluohu.json | wc -l
- 19
獲取年齡分布
思路是截取身份證中號碼中代表出生年的4位數(shù),然后拿當前年份2019減出生年得到年齡,后面的套路又一樣了。
bc 一個簡單的計算器程序,了解下?
- ➜ shell-train > echo "3+2-5/5" | bc
- 4
- ➜ shell-train > echo "3.141592*5-4" | bc
- 11.707960
- #思路1: `cut -c 9-12` 獲取出生年, 拼接表達式 `2019-出生年` 得到年齡.
- ➜ 積分落戶 > grep '"idCard":' jifenluohu.json| cut -f2 -d: | cut -c 9-12 | xargs -n1 echo 2019 -|bc | sort | uniq -c
- 3 34
- 13 35
- 39 36
- 109 37
- 162 38
- 302 39
- 507 40
- 773 41
- 799 42
- 813 43
- 757 44
- 586 45
- 507 46
- 378 47
- 238 48
- 4 49
- 9 50
- 1 51
- 4 52
- 3 53
- 2 54
- 5 55
- 1 56
- 1 58
- 1 59
- 1 60
- 1 61
awk 是個好東西, 多練練.
- # 拿到出生年后, 直接通過 awk 計算結果輸出
- ➜ 積分落戶 > grep '"idCard":' jifenluohu.json| cut -f2 -d: | cut -c 9-12 |awk '{print 2019-$1}' | sort | uniq -c
- 3 34
- 13 35
- 39 36
- 109 37
- 162 38
- 302 39
- 507 40
- 773 41
- 799 42
- 813 43
- 757 44
- 586 45
- 507 46
- 378 47
- 238 48
- 4 49
- 9 50
- 1 51
- 4 52
- 3 53
- 2 54
- 5 55
- 1 56
- 1 58
- 1 59
- 1 60
- 1 61
獲取取得戶口的同學戶籍地top10
有時候,我們在寫Shell的時候,為了debug方便,可能會將一些中間結果緩存到文件中,后續(xù)以該文件為基礎進行后續(xù)的計算。
比如先拿到top10的身份證中代表的戶籍地的四位編碼,這里需要借助另外的一個表示身份證戶籍地的編碼來進行對應。
借此機會解釋下 join 這個命令。
- # 身份證前4位為例, 拿到戶籍地
- grep '"idCard":' jifenluohu.json| cut -f2 -d: | cut -c 3-6 | sort | uniq -c | sort -nr -k 1 >topcity.code
- # 城市列表
- ➜ 積分落戶 > more city.csv
- 11,北京市
- 1101,北京市市轄區(qū)
- 110101,北京市東城區(qū)
- 110102,北京市西城區(qū)
- 110103,北京市崇文區(qū)
- 110104,北京市宣武區(qū)
- 110105,北京市朝陽區(qū)
- # grep -E '^[0-9]{4},' city.csv | sed 's|,| |g' > city.code4
- ➜ shell-train > head -n 2 city.code4
- 1101 北京市市轄區(qū)
- 1102 北京市市轄縣
- ➜ shell-train > head -n 2 topcity.code
- 197 1201
- 156 1302
- ➜ shell-train > join
- usage: join [-a fileno | -v fileno ] [-e string] [-1 field] [-2 field]
- [-o list] [-t char] file1 file2
其實,join 就類似sql中的 ...inner join ...on ..., -t 分隔符,默認為空格或tab。
- # 未排序, 所以沒有將所有的導出(join需要排序)
- ➜ shell-train > join -1 1 -2 2 city.code4 topcity.code
- 1201 天津市市轄區(qū) 197
- 1302 河北省唐山市 156
- 2301 黑龍江哈爾濱市 123
- 4201 湖北省武漢市 118
- 6101 陜西省西安市 100
- 6201 甘肅省蘭州市 59
- 6501 新疆烏魯木齊市 29
- 6523 新疆昌吉回族自治州 11
一定需要將結果輸出到文件,然后再進行嗎?
其實也不一定。用管道的方式 | 可以將上一個命令的輸出結果作為下一個命令的輸入,可以通過 <(command) 的方式,將command 的輸出作為一個文件輸入。
- # 需要排序
- ➜ shell-train > join -1 1 -2 2 city.code4 <(head -n 10 topcity.code | sort -k 2)
- 1201 天津市市轄區(qū) 197
- 1301 河北省石家莊市 114
- 1302 河北省唐山市 156
- 1324 河北省保定地區(qū) 103
- 1501 內(nèi)蒙古呼和浩特市 88
- 2101 遼寧省沈陽市 109
- 2201 吉林省長春市 113
- 2301 黑龍江哈爾濱市 123
- 4201 湖北省武漢市 118
- 6101 陜西省西安市 100
舉個例子paste用來將兩個文件按列合并在一起:
- ➜ shell-train > cat paste.f1
- hello, i am
- world, you are
- ➜ shell-train > cat paste.f2
- tanglei, wechat is: tangleithu
- ?, hahaha
- ➜ shell-train > paste paste.f1 paste.f2
- hello, i am tanglei, wechat is: tangleithu
- world, you are ?, hahaha
以上用paste將兩個文件合并在一起了,實際上通過 <(cmd)的方式,可以不借助外部文件也能做到。
方法如下:
- ➜ shell-train > paste <(echo "hello, i am \nworld, you are") <(echo "tanglei, wechat is: tangleithu\n?, hahaha")
- hello, i am tanglei, wechat is: tangleithu
- world, you are ?, hahaha
本文轉載自微信公眾號「程序猿石頭」,可以通過以下二維碼關注。轉載本文請聯(lián)系程序猿石頭公眾號。