首頁 > 軟體

何不 Ack?Grep, Ack, Ag的搜尋效率對比

2020-06-16 18:04:19

前言

我(@董偉明9 )經常看到很多程式設計師, 運維在程式碼搜尋上使用ack, 甚至ag(the_silver_searcher ), 而我工作中95%都是用grep,剩下的是ag。 我覺得很有必要聊一聊這個話題。

我以前也是一個運維, 我當時也希望找到最好的最快的工具用在工作的方方面面。 但是我很好奇為什麼ag和ack沒有作為linux發行版的內建部分。 內建的一直是grep。 我當初的理解是受各種開源協定的限制, 或者發行版的boss個人喜好。 後來我就做了實驗, 研究了下他們到底誰快。 當時的做法也無非跑幾個真實地線上log看看用時。 然後我也有了我的一個認識: 大部分時候用grep也無妨, 紀錄檔很大的時候用ag。

ack原來的域名是betterthangrep.com, 現在是beyondgrep.com。 好吧,其實我理解使用ack的同學, 也理解ack產生的原因。 這裡就有個故事。

最開始我做運維使用shell, 經常做一些分析紀錄檔的工作。 那時候經常寫比較複雜的shell程式碼實現一些特定的需求。 後來來了一位會perl的同學。 原來我寫shell做一個事情, 寫了20多行shell程式碼, 跑一次大概5分鐘, 這位同學來了用perl改寫, 4行, 一分鐘就能跑完。 亮瞎我們的眼, 從那時候開始, 我就覺得需要學perl,以至於後來的Python

perl是天生用來文字解析的語言, ack的效率確實很高。 我想著可能是大家認為ack要更快更合適的理由吧。 其實這件事要看場景。 我為什麼還用比較’土’的grep呢?

看一下這篇文章, 希望給大家點啟示。不耐煩看具體測試過程的同學,可以直接看結論:

  1. 在搜尋的總資料量較小的情況下, 使用grep, ack甚至ag在感官上區別不大
  2. 搜尋的總資料量較大時, grep效率下滑的很多, 完全不要選
  3. ack在某些場景下沒有grep效果高(比如使用-v搜尋中文的時候)
  4. 在不使用ag沒有實現的選項功能的前提下, ag完全可以替代ack/grep

實驗條件

PS: 嚴重宣告, 本實驗經個人實踐, 我盡量做到合理。 大家看完覺得有異議可以試著其他的角度來做。 並和我討論。

  • 我使用了公司的一台開發機(gentoo)

  • 我測試了純英文和漢語2種, 漢語使用了結巴分詞的字典, 英語使用了miscfiles中提供的詞典

  1. # 假如你是Ubuntu: sudo apt-get install miscfiles
  2. wget https://raw.githubusercontent.com/fxsjy/jieba/master/extra_dict/dict.txt.big

實驗前的準備

我會分成英語和漢語2種檔案, 檔案大小為1MB, 10MB, 100MB, 500MB, 1GB, 5GB。 沒有更多是我覺得在實際業務裡面不會單個紀錄檔檔案過大的。 也就沒有必要測試了(就算有, 可以看下面結果的趨勢)。用下列程式深入測試的檔案:

  1. cat make_words.py
  2. # coding=utf-8
  3. import os
  4. import random
  5. from cStringIO importStringIO
  6. EN_WORD_FILE ='/usr/share/dict/words'
  7. CN_WORD_FILE ='dict.txt.big'
  8. with open(EN_WORD_FILE)as f:
  9. EN_DATA = f.readlines()
  10. with open(CN_WORD_FILE)as f:
  11. CN_DATA = f.readlines()
  12. MB = pow(1024,2)
  13. SIZE_LIST =[1,10,100,500,1024,1024*5]
  14. EN_RESULT_FORMAT ='text_{0}_en_MB.txt'
  15. CN_RESULT_FORMAT ='text_{0}_cn_MB.txt'
  16. def write_data(f, size, data, cn=False):
  17. total_size =0
  18. while1:
  19. s =StringIO()
  20. for x in range(10000):
  21. cho = random.choice(data)
  22. cho = cho.split()[0]if cn else cho.strip()
  23. s.write(cho)
  24. s.seek(0, os.SEEK_END)
  25. total_size += s.tell()
  26. contents = s.getvalue()
  27. f.write(contents +'n')
  28. if total_size > size:
  29. break
  30. f.close()
  31. for index, size in enumerate([
  32. MB,
  33. MB *10,
  34. MB *100,
  35. MB *500,
  36. MB *1024,
  37. MB *1024*5]):
  38. size_name = SIZE_LIST[index]
  39. en_f = open(EN_RESULT_FORMAT.format(size_name),'a+')
  40. cn_f = open(CN_RESULT_FORMAT.format(size_name),'a+')
  41. write_data(en_f, size, EN_DATA)
  42. write_data(cn_f, size, CN_DATA,True)

好吧, 效率比較低是吧? 我自己沒有vps, 公司伺服器我不能沒事把全部核心的cpu都佔滿(不是運維好幾年了)。 假如你不介意htop的多核cpu飄紅, 可以這樣,耗時就是各檔案生成的時間短板。這是生成測試檔案的多進程版本:

  1. # coding=utf-8
  2. import os
  3. import random
  4. import multiprocessing
  5. from cStringIO importStringIO
  6. EN_WORD_FILE ='/usr/share/dict/words'
  7. CN_WORD_FILE ='dict.txt.big'
  8. with open(EN_WORD_FILE)as f:
  9. EN_DATA = f.readlines()
  10. with open(CN_WORD_FILE)as f:
  11. CN_DATA = f.readlines()
  12. MB = pow(1024,2)
  13. SIZE_LIST =[1,10,100,500,1024,1024*5]
  14. EN_RESULT_FORMAT ='text_{0}_en_MB.txt'
  15. CN_RESULT_FORMAT ='text_{0}_cn_MB.txt'
  16. inputs =[]
  17. def map_func(args):
  18. def write_data(f, size, data, cn=False):
  19. f = open(f,'a+')
  20. total_size =0
  21. while1:
  22. s =StringIO()
  23. for x in range(10000):
  24. cho = random.choice(data)
  25. cho = cho.split()[0]if cn else cho.strip()
  26. s.write(cho)
  27. s.seek(0, os.SEEK_END)
  28. total_size += s.tell()
  29. contents = s.getvalue()
  30. f.write(contents +'n')
  31. if total_size > size:
  32. break
  33. f.close()
  34. _f, size, data, cn = args
  35. write_data(_f, size, data, cn)
  36. for index, size in enumerate([
  37. MB,
  38. MB *10,
  39. MB *100,
  40. MB *500,
  41. MB *1024,
  42. MB *1024*5]):
  43. size_name = SIZE_LIST[index]
  44. inputs.append((EN_RESULT_FORMAT.format(size_name), size, EN_DATA,False))
  45. inputs.append((CN_RESULT_FORMAT.format(size_name), size, CN_DATA,True))
  46. pool = multiprocessing.Pool()
  47. pool.map(map_func, inputs, chunksize=1)

等待一段時間後,測試的檔案生成了。目錄下是這樣的:

  1. $ls -lh
  2. total 14G
  3. -rw-rw-r--1 vagrant vagrant 2.2KMar1405:25 benchmarks.ipynb
  4. -rw-rw-r--1 vagrant vagrant 8.2MMar1215:43 dict.txt.big
  5. -rw-rw-r--1 vagrant vagrant 1.2KMar1215:46 make_words.py
  6. -rw-rw-r--1 vagrant vagrant 101MMar1215:47 text_100_cn_MB.txt
  7. -rw-rw-r--1 vagrant vagrant 101MMar1215:47 text_100_en_MB.txt
  8. -rw-rw-r--1 vagrant vagrant 1.1GMar1215:54 text_1024_cn_MB.txt
  9. -rw-rw-r--1 vagrant vagrant 1.1GMar1215:51 text_1024_en_MB.txt
  10. -rw-rw-r--1 vagrant vagrant 11MMar1215:47 text_10_cn_MB.txt
  11. -rw-rw-r--1 vagrant vagrant 11MMar1215:47 text_10_en_MB.txt
  12. -rw-rw-r--1 vagrant vagrant 1.1MMar1215:47 text_1_cn_MB.txt
  13. -rw-rw-r--1 vagrant vagrant 1.1MMar1215:47 text_1_en_MB.txt
  14. -rw-rw-r--1 vagrant vagrant 501MMar1215:49 text_500_cn_MB.txt
  15. -rw-rw-r--1 vagrant vagrant 501MMar1215:48 text_500_en_MB.txt
  16. -rw-rw-r--1 vagrant vagrant 5.1GMar1216:16 text_5120_cn_MB.txt
  17. -rw-rw-r--1 vagrant vagrant 5.1GMar1216:04 text_5120_en_MB.txt

確認版本

  1. $ ack --version # ack在ubuntu下叫`ack-grep`
  2. ack 2.12
  3. Running under Perl5.16.3 at /usr/bin/perl
  4. Copyright2005-2013AndyLester.
  5. This program is free software.You may modify or distribute it
  6. under the terms of the ArtisticLicense v2.0.
  7. $ ag --version
  8. ag version 0.21.0
  9. $ grep --version
  10. grep (GNU grep)2.14
  11. Copyright(C)2012FreeSoftwareFoundation,Inc.
  12. LicenseGPLv3+: GNU GPL version 3or later <http://gnu.org/licenses/gpl.html>.
  13. Thisis free software: you are free to change and Redistribute it.
  14. Thereis NO WARRANTY, to the extent permitted by law.
  15. WrittenbyMikeHaerteland others, see <http://git.sv.gnu.org/cgit/grep.git/tree/AUTHORS>.

實驗設計

為了不產生並行執行的相互響應, 我還是選擇了效率很差的同步執行, 我使用了ipython提供的%timeit。 測試程式的程式碼如下:

  1. import re
  2. import glob
  3. import subprocess
  4. import cPickle as pickle
  5. from collections import defaultdict
  6. IMAP ={
  7. 'cn':('豆瓣','小明明'),
  8. 'en':('four','python')
  9. }
  10. OPTIONS =('','-i','-v')
  11. FILES = glob.glob('text_*_MB.txt')
  12. EN_RES = defaultdict(dict)
  13. CN_RES = defaultdict(dict)
  14. RES ={
  15. 'en': EN_RES,
  16. 'cn': CN_RES
  17. }
  18. REGEX = re.compile(r'text_(d+)_(w+)_MB.txt')
  19. CALL_STR ='{command} {option} {word} {filename} > /dev/null 2>&1'
  20. for filename in FILES:
  21. size, xn = REGEX.search(filename).groups()
  22. for word in IMAP[xn]:
  23. _r = defaultdict(dict)
  24. for command in['grep','ack','ag']:
  25. for option in OPTIONS:
  26. rs =%timeit -o -n10 subprocess.call(CALL_STR.format(command=command, option=option, word=word, filename=filename), shell=True)
  27. best = rs.best
  28. _r[command][option]= best
  29. RES[xn][word][size]= _r
  30. # 存起來
  31. data = pickle.dumps(RES)
  32. with open('result.db','w')as f:
  33. f.write(data)

溫馨提示, 這是一個灰常耗時的測試。 開始執行後 要喝很久的茶…

我來秦皇島辦事完畢(耗時超過1一天), 繼續我們的實驗。

我想要的效果

我想工作的時候一般都是用到不帶引數/帶-i(忽略大小寫)/-v(查詢不匹配項)這三種。 所以這裡測試了:

  1. 英文搜尋/中文搜尋
  2. 選擇了2個搜尋詞(效率太低, 否則可能選擇多個)
  3. 分別測試’’/’-i’/’-v’三種引數的執行
  4. 使用%timeit, 每種條件執行10遍, 選擇效率最好的一次的結果
  5. 每個圖程式碼一個搜尋詞, 3搜尋命令, 一個選項在搜尋不同大小檔案時的效率對比

我先說結論

  1. 在搜尋的總資料量較小的情況下, 使??grep, ack甚至ag在感官上區別不大
  2. 搜尋的總資料量較大時, grep效率下滑的很多, 完全不要選
  3. ack在某些場景下沒有grep效果高(比如使用-v搜尋中文的時候)
  4. 在不使用ag沒有實現的選項功能的前提下, ag完全可以替代ack/grep

渲染圖片的gist可以看這裡benchmarks.ipynb。 它的資料來自上面跑的結果在序列化之後存入的檔案。

 

grep使用簡明及正規表示式  http://www.linuxidc.com/Linux/2013-08/88534.htm

Linux下Shell程式設計——grep命令的基本運用 http://www.linuxidc.com/Linux/2013-06/85525.htm

grep 命令詳解及相關事例 http://www.linuxidc.com/Linux/2014-07/104041.htm

Linux基礎命令之grep詳解 http://www.linuxidc.com/Linux/2013-07/87919.htm

設定grep高亮顯示匹配項 http://www.linuxidc.com/Linux/2014-09/106871.htm

Linux grep命令學習與總結 http://www.linuxidc.com/Linux/2014-10/108112.htm

本文永久更新連結地址http://www.linuxidc.com/Linux/2015-03/115130.htm


IT145.com E-mail:sddin#qq.com