一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

运维中搜索工具Grep, Ack, Ag的效率比较【附图】

时间:2015-06-19 编辑:简简单单 来源:一聚教程网

前言

我经常看到很多程序员, 运维在代码搜索上使用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呢? 看一下这篇文章, 希望给大家点启示


实验条件

PS: 严重声明, 本实验经个人实践, 我尽量做到合理. 大家看完觉得有异议可以试着其他的角度来做. 并和我讨论.

    我使用了公司的一台开发机(gentoo)

    我测试了纯英文和汉语2种, 汉语使用了结巴分词的字典, 英语使用了miscfiles中提供的词典

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

实验前的准备

我会分成英语和汉语2种文件, 文件大小为1MB, 10MB, 100MB, 500MB, 1GB, 5GB. 没有更多是我觉得在实际业务里面不会单个日志文件过大的. 也就没有必要测试了(就算有, 可以看下面结果的趋势)

cat make_words.py
# coding=utf-8

import os
import random
from cStringIO import StringIO

EN_WORD_FILE = '/usr/share/dict/words'
CN_WORD_FILE = 'dict.txt.big'
with open(EN_WORD_FILE) as f:
    EN_DATA = f.readlines()
with open(CN_WORD_FILE) as f:
    CN_DATA = f.readlines()
MB = pow(1024, 2)
SIZE_LIST = [1, 10, 100, 500, 1024, 1024 * 5]
EN_RESULT_FORMAT = 'text_{0}_en_MB.txt'
CN_RESULT_FORMAT = 'text_{0}_cn_MB.txt'


def write_data(f, size, data, cn=False):
    total_size = 0
    while 1:
        s = StringIO()
        for x in range(10000):
            cho = random.choice(data)
            cho = cho.split()[0] if cn else cho.strip()
            s.write(cho)
        s.seek(0, os.SEEK_END)
        total_size += s.tell()
        contents = s.getvalue()
        f.write(contents + '\n')
        if total_size > size:
            break
    f.close()


for index, size in enumerate([
        MB,
        MB * 10,
        MB * 100,
        MB * 500,
        MB * 1024,
        MB * 1024 * 5]):
    size_name = SIZE_LIST[index]
    en_f = open(EN_RESULT_FORMAT.format(size_name), 'a+')
    cn_f = open(CN_RESULT_FORMAT.format(size_name), 'a+')
    write_data(en_f, size, EN_DATA)
    write_data(cn_f, size, CN_DATA, True)

好吧, 效率比较低是吧? 我自己没有vps, 公司服务器我不能没事把全部内核的cpu都占满(不是运维好几年了). 假如你不介意htop的多核cpu飘红, 可以这样,耗时就是各文件生成的时间短板:

 # coding=utf-8

import os
import random
import multiprocessing
from cStringIO import StringIO

EN_WORD_FILE = '/usr/share/dict/words'
CN_WORD_FILE = 'dict.txt.big'
with open(EN_WORD_FILE) as f:
    EN_DATA = f.readlines()
with open(CN_WORD_FILE) as f:
    CN_DATA = f.readlines()
MB = pow(1024, 2)
SIZE_LIST = [1, 10, 100, 500, 1024, 1024 * 5]
EN_RESULT_FORMAT = 'text_{0}_en_MB.txt'
CN_RESULT_FORMAT = 'text_{0}_cn_MB.txt'

inputs = []

def map_func(args):
    def write_data(f, size, data, cn=False):
        f = open(f, 'a+')
        total_size = 0
        while 1:
            s = StringIO()
            for x in range(10000):
                cho = random.choice(data)
                cho = cho.split()[0] if cn else cho.strip()
                s.write(cho)
            s.seek(0, os.SEEK_END)
            total_size += s.tell()
            contents = s.getvalue()
            f.write(contents + '\n')
            if total_size > size:
                break
        f.close()

    _f, size, data, cn = args
    write_data(_f, size, data, cn)


for index, size in enumerate([
        MB,
        MB * 10,
        MB * 100,
        MB * 500,
        MB * 1024,
        MB * 1024 * 5]):
    size_name = SIZE_LIST[index]
    inputs.append((EN_RESULT_FORMAT.format(size_name), size, EN_DATA, False))
    inputs.append((CN_RESULT_FORMAT.format(size_name), size, CN_DATA, True))

pool = multiprocessing.Pool()
pool.map(map_func, inputs, chunksize=1)

等待一段时间后,目录下是这样的:

$ls -lh
total 14G
-rw-rw-r-- 1 vagrant vagrant 2.2K Mar 14 05:25 benchmarks.ipynb
-rw-rw-r-- 1 vagrant vagrant 8.2M Mar 12 15:43 dict.txt.big
-rw-rw-r-- 1 vagrant vagrant 1.2K Mar 12 15:46 make_words.py
-rw-rw-r-- 1 vagrant vagrant 101M Mar 12 15:47 text_100_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 101M Mar 12 15:47 text_100_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1G Mar 12 15:54 text_1024_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1G Mar 12 15:51 text_1024_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant  11M Mar 12 15:47 text_10_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant  11M Mar 12 15:47 text_10_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1M Mar 12 15:47 text_1_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 1.1M Mar 12 15:47 text_1_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 501M Mar 12 15:49 text_500_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 501M Mar 12 15:48 text_500_en_MB.txt
-rw-rw-r-- 1 vagrant vagrant 5.1G Mar 12 16:16 text_5120_cn_MB.txt
-rw-rw-r-- 1 vagrant vagrant 5.1G Mar 12 16:04 text_5120_en_MB.txt

确认版本

➜  test  ack --version # ack在ubuntu下叫`ack-grep`
ack 2.12
Running under Perl 5.16.3 at /usr/bin/perl

Copyright 2005-2013 Andy Lester.

This program is free software.  You may modify or distribute it
under the terms of the Artistic License v2.0.
➜  test  ag --version
ag version 0.21.0
➜  test  grep --version
grep (GNU grep) 2.14
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later .
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Mike Haertel and others, see .

实验设计

为了不产生并行执行的相互响应, 我还是选择了效率很差的同步执行, 我使用了ipython提供的%timeit. 上代码

import re
import glob
import subprocess
import cPickle as pickle
from collections import defaultdict

IMAP = {
    'cn': ('豆瓣', '小明明'),
    'en': ('four', 'python')
}
OPTIONS = ('', '-i', '-v')
FILES = glob.glob('text_*_MB.txt')
EN_RES = defaultdict(dict)
CN_RES = defaultdict(dict)
RES = {
        'en': EN_RES,
        'cn': CN_RES
}
REGEX = re.compile(r'text_(\d+)_(\w+)_MB.txt')
CALL_STR = '{command} {option} {word} {filename} > /dev/null 2>&1'

for filename in FILES:
    size, xn = REGEX.search(filename).groups()
    for word in IMAP[xn]:
        _r = defaultdict(dict)
        for command in ['grep', 'ack', 'ag']:
            for option in OPTIONS:
                rs = %timeit -o -n10 subprocess.call(CALL_STR.format(command=command, option=option, word=word, filename=filename), shell=True)
                best = rs.best
                _r[command][option] = best
        RES[xn][word][size] = _r

# 存起来

data = pickle.dumps(RES)

with open('result.db', 'w') as f:
    f.write(data)

温馨提示, 这是一个灰常耗时的测试. 开始执行后 要喝很久的茶…

我来秦皇岛办事完毕(耗时超过1一天), 继续我们的实验.


我想要的效果

我想工作的时候一般都是用到不带参数/带-i(忽略大小写)/-v(查找不匹配项)这三种. 所以这里测试了:

    英文搜索/中文搜索
    选择了2个搜索词(效率太低, 否则可能选择多个)
    分别测试’’/’-i’/’-v’三种参数的执行
    使用%timeit, 每种条件执行10遍, 选择效率最好的一次的结果
    每个图代码一个搜索词, 3搜索命令, 一个选项在搜索不同大小文件时的效率对比

多图预警, 我先说结论

    在搜索的总数据量较小的情况下, 使用grep, ack甚至ag在感官上区别不大
    搜索的总数据量较大时, grep效率下滑的很多, 完全不要选
    ack在某些场景下没有grep效果高(比如使用-v索索中文的时候)
    在不使用ag没有实现的选项功能的前提下, ag完全可以替代ack/grep

渲染图片的gist可以看这里benchmarks.ipynb. 他的数据来自上面跑的结果在序列化之后存入的文件

附图

chartchart-1chart-2chart-3chart-4chart-5chart-6chart-7chart-8chart-9chart-10chart-11

The silver search(ag)比ack-grep还快


今天用ag搜索android4.1的代码,发现总是被一个长得让崩溃的.json文件匹配(键入ag ::layout)搞得没法健康地查看ag.el的结果。

一看ag的代码才知道这长行打印压制还是个未完成功能

src/options.h:48:30:    int print_long_lines; /* TODO: support this in print.c */


好在作者的C代码简洁明了,使得我可以很快写出一个Patch,使用-M 参数来压制长行输出。patch邮件已经发送。我自己也backup一下:



diff --git a/src/options.c b/src/options.c  
index b08903f..2f4b18e 100644  
--- a/src/options.c  
+++ b/src/options.c  
@@ -77,6 +77,8 @@ Search options:\n\  
 -p --path-to-agignore STRING\n\  
                         Use .agignore file at STRING\n\  
 --print-long-lines      Print matches on very long lines (Default: >2k characters)\n\  
+-M --max-printable-line-length NUM \n\  
+                        Skip print the matching lines that have a length bigger than NUM\n      \  
 -Q --literal            Don't parse PATTERN as a regular expression\n\  
 -s --case-sensitive     Match case sensitively (Enabled by default)\n\  
 -S --smart-case         Match case insensitively unless PATTERN contains\n\  
@@ -115,6 +117,7 @@ void init_options() {  
     opts.color_path = ag_strdup(color_path);  
     opts.color_match = ag_strdup(color_match);  
     opts.color_line_number = ag_strdup(color_line_number);  
+    opts.max_printable_line_length = DEFAULT_MAX_PRINTABLE_LINE_LENGTH;  
 }  
   
 void cleanup_options() {  
@@ -221,6 +224,7 @@ void parse_options(int argc, char **argv, char **base_paths[], char **paths[]) {  
         { "version", no_argument, &version, 1 },  
         { "word-regexp", no_argument, NULL, 'w' },  
         { "workers", required_argument, NULL, 0 },  
+        { "max-printable-line-length", required_argument, NULL, 'M' },  
         { NULL, 0, NULL, 0 }  
     };  
   
@@ -253,7 +257,7 @@ void parse_options(int argc, char **argv, char **base_paths[], char **paths[]) {  
         opts.stdout_inode = statbuf.st_ino;  
     }  
   
-    while ((ch = getopt_long(argc, argv, "A:aB:C:DG:g:fhiLlm:np:QRrSsvVtuUwz", longopts, &opt_index)) != -1) {  
+    while ((ch = getopt_long(argc, argv, "A:aB:C:DG:g:fhiLlm:M:np:QRrSsvVtuUwz", longopts, &opt_index)) != -1) {  
         switch (ch) {  
             case 'A':  
                 opts.after = atoi(optarg);  
@@ -305,6 +309,9 @@ void parse_options(int argc, char **argv, char **base_paths[], char **paths[]) {  
             case 'm':  
                 opts.max_matches_per_file = atoi(optarg);  
                 break;  
+            case 'M':  
+                opts.max_printable_line_length = atoi(optarg);  
+                break;  
             case 'n':  
                 opts.recurse_dirs = 0;  
                 break;  
diff --git a/src/options.h b/src/options.h  
index 5049ab5..b4d2468 100644  
--- a/src/options.h  
+++ b/src/options.h  
@@ -7,6 +7,7 @@  
 #include  
   
 #define DEFAULT_CONTEXT_LEN 2  
+#define DEFAULT_MAX_PRINTABLE_LINE_LENGTH 2000  
   
 enum case_behavior {  
     CASE_SENSITIVE,  
@@ -45,6 +46,7 @@ typedef struct {  
     int print_heading;  
     int print_line_numbers;  
     int print_long_lines; /* TODO: support this in print.c */  
+    int max_printable_line_length;  
     pcre *re;  
     pcre_extra *re_extra;  
     int recurse_dirs;  
diff --git a/src/print.c b/src/print.c  
index dc11594..4be9874 100644  
--- a/src/print.c  
+++ b/src/print.c  
@@ -34,6 +34,15 @@ void print_binary_file_matches(const char* path) {  
     fprintf(out_fd, "Binary file %s matches.\n", path);  
 }  
   
+static void check_printable(int len, int *printable) {  
+    if (len > opts.max_printable_line_length) {  
+        *printable = FALSE;  
+        fprintf(out_fd, "+EVIL+MARK+VERY+LONG+LINES+HERE\n");  
+    } else {  
+        *printable = TRUE;  
+    }  
+}  
+  
 void print_file_matches(const char* path, const char* buf, const int buf_len, const match matches[], const int matches_len) {  
     int line = 1;  
     char **context_prev_lines = NULL;  
@@ -49,6 +58,7 @@ void print_file_matches(const char* path, const char* buf, const int buf_len, co  
     int i, j;  
     int in_a_match = FALSE;  
     int printing_a_match = FALSE;  
+    int printable = TRUE;  
   
     if (opts.ackmate) {  
         sep = ':';  
@@ -129,7 +139,8 @@ void print_file_matches(const char* path, const char* buf, const int buf_len, co  
                     }  
                     j = prev_line_offset;  
                     /* print up to current char */  
-                    for (; j <= i; j++) {  
+                    check_printable(i - prev_line_offset, &printable);  
+                    for (; j <= i && printable; j++) {  
                         fputc(buf[j], out_fd);  
                     }  
                 } else {  
@@ -141,7 +152,8 @@ void print_file_matches(const char* path, const char* buf, const int buf_len, co  
                     if (printing_a_match && opts.color) {  
                         fprintf(out_fd, "%s", opts.color_match);  
                     }  
-                    for (j = prev_line_offset; j <= i; j++) {  
+                    check_printable(i - prev_line_offset, &printable);  
+                    for (j = prev_line_offset; j <= i && printable; j++) {  
                         if (j == matches[last_printed_match].end && last_printed_match < matches_len) {  
                             if (opts.color) {  
                                 fprintf(out_fd, "%s", color_reset);  


在文件中搜索文本工具grep命令用法


grep(global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。

grep命令选项
-a 不要忽略二进制数据。
-A <显示列数> 除了显示符合范本样式的那一行之外,并显示该行之后的内容。
-b 在显示符合范本样式的那一行之外,并显示该行之前的内容。
-c 计算符合范本样式的列数。
-C <显示列数>或-<显示列数> 除了显示符合范本样式的那一列之外,并显示该列之前后的内容。
-d <进行动作> 当指定要查找的是目录而非文件时,必须使用这项参数,否则grep命令将回报信息并停止动作。
-e <范本样式> 指定字符串作为查找文件内容的范本样式。
-E 将范本样式为延伸的普通表示法来使用,意味着使用能使用扩展正则表达式
-f <范本文件> 指定范本文件,其内容有一个或多个范本样式,让grep查找符合范本条件的文件内容,格式为每一列的范本样式。
-F 将范本样式视为固定字符串的列表。
-G 将范本样式视为普通的表示法来使用。
-h 在显示符合范本样式的那一列之前,不标示该列所属的文件名称。
-H 在显示符合范本样式的那一列之前,标示该列的文件名称。
-i 胡列字符大小写的差别。
-l 列出文件内容符合指定的范本样式的文件名称。
-L 列出文件内容不符合指定的范本样式的文件名称。
-n 在显示符合范本样式的那一列之前,标示出该列的编号。
-q 不显示任何信息。
-R /-r 此参数的效果和指定“-d recurse”参数相同。
-s  不显示错误信息。
-v 反转查找。
-w 只显示全字符合的列。
-x 只显示全列符合的列。
-y 此参数效果跟“-i”相同。
-o 只输出文件中匹配到的部分。

grep命令常见用法
在文件中搜索一个单词,命令会返回一个包含“match_pattern”的文本行:
# grep match_pattern file_name
# grep "match_pattern" file_name

在多个文件中查找:
# grep "match_pattern" file_1 file_2 file_3 ...

输出除之外的所有行 -v 选项:
# grep -v "match_pattern" file_name

标记匹配颜色 ?color=auto 选项:
# grep "match_pattern" file_name --color=auto

使用正则表达式 -E 选项:
# grep -E "[1-9]+"

# egrep "[1-9]+"

只输出文件中匹配到的部分 -o 选项:
# echo this is a test line. | grep -o -E "[a-z]+\."
line.
# echo this is a test line. | egrep -o "[a-z]+\."
line.

统计文件或者文本中包含匹配字符串的行数 -c 选项:
# grep -c "text" file_name

输出包含匹配字符串的行数 -n 选项:
# grep "text" -n file_name

# cat file_name | grep "text" -n
#多个文件
# grep "text" -n file_1 file_2

打印样式匹配所位于的字符或字节偏移:
# echo gun is not unix | grep -b -o "not"
7:not
#一行中字符串的字符便宜是从该行的第一个字符开始计算,起始值为0。选项 -b -o 一般总是配合使用。

搜索多个文件并查找匹配文本在哪些文件中:
# grep -l "text" file1 file2 file3...

grep递归搜索文件
在多级目录中对文本进行递归搜索:
# grep "text" . -r -n
# .表示当前目录。

忽略匹配样式中的字符大小写:
# echo "hello world" | grep -i "HELLO"
hello

选项 -e 制动多个匹配样式:
# echo this is a text line | grep -e "is" -e "line" -o
is
line
#也可以使用-f选项来匹配多个样式,在样式文件中逐行写出需要匹配的字符。
# cat patfile
aaa
bbb
# echo aaa bbb ccc ddd eee | grep -f patfile -o

在grep搜索结果中包括或者排除指定文件:
#只在目录中所有的.php和.html文件中递归搜索字符"main()"
# grep "main()" . -r --include *.{php,html}
#在搜索结果中排除所有README文件
# grep "main()" . -r --exclude "README"
#在搜索结果中排除filelist文件列表里的文件
# grep "main()" . -r --exclude-from filelist

使用0值字节后缀的grep与xargs:
#测试文件:
# echo "aaa" > file1
# echo "bbb" > file2
# echo "aaa" > file3
# grep "aaa" file* -lZ | xargs -0 rm
#执行后会删除file1和file3,grep输出用-Z选项来指定以0值字节作为终结符文件名(\0),xargs -0 读取输入并用0值字节终结符分隔文件名,然后删除匹配文件,-Z通常和-l结合使用。

grep静默输出:
# grep -q "test" filename
#不会输出任何信息,如果命令运行成功返回0,失败则返回非0值。一般用于条件测试。

打印出匹配文本之前或者之后的行:
#显示匹配某个结果之后的3行,使用 -A 选项:
# seq 10 | grep "5" -A 3
5
6
7
8
 
#显示匹配某个结果之前的3行,使用 -B 选项:
# seq 10 | grep "5" -B 3
2
3
4
5
 
#显示匹配某个结果的前三行和后三行,使用 -C 选项:
# seq 10 | grep "5" -C 3
2
3
4
5
6
7
8
 
#如果匹配结果有多个,会用“--”作为各匹配结果之间的分隔符:
# echo -e "a\nb\nc\na\nb\nc" | grep a -A 1
a
b
--
a
b

热门栏目