ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

精通grep:从日志筛选到shell脚本高效编程的核心技能

2026/9/7 23:57:49 拓冰建站 浏览量
精通grep:从日志筛选到shell脚本高效编程的核心技能 1. 我为什么劝你先学会grep再谈shell脚本1.1 一次线上日志排查让我重新认识了grep刚入职那会儿我以为grep不过是“在文件里找词”。直到有次线上服务半夜报警日志文件很大我只会用编辑器打开日志再CtrlF搜索。文件几百MB编辑器直接卡死。旁边同事在终端里敲了一串命令grep 2026-03-21 14:2[0-9] app.log | grep -A 20 ERROR几秒钟就把故障时间段内的错误上下文全部捞了出来。那一刻我才意识到grep不是“找词”它是文本处理的筛子是所有shell管道里的第一道关卡。从那以后我养成了一个习惯任何人学shell编程我都建议先别急着写for循环、case结构先把grep用熟。因为shell编程里大量任务本质就是对文本做筛选、抽取、统计和改造而grep解决的是最前面的筛选问题。你要找谁、排除谁、只看哪一行这几个问题想清楚了后面管道里的awk、sed才有发挥空间。1.2 grep在文本处理链路中的定位在Linux文本处理三剑客grep、sed、awk里面grep负责“按模式找人”sed负责“按规则改人”awk负责“把人分类再算账”。三者分工明确但grep是最入口、最高频的工具。你可以不会awk的复杂语法但不能不会grep因为日志分析、配置查询、进程过滤、代码搜索都在用它。grep的全名是global regular expression print意思就是全局正则搜索并打印。它的输入可以是文件也可以是标准输入。也就是说只要你在管道里用|把上一条命令的输出接进来grep就能帮你继续筛选。这个设计让它在shell脚本里的地位像筛子之于厨房东西再多先过一遍筛子。举个例子你想查系统里哪个进程占用了8080端口常见做法是lsof -i :8080 | grep LISTEN这里lsof输出所有网络连接信息内容很多但grep LISTEN能直接帮你把处于监听状态的那一行捞出来。这种“先输出全部再grep筛一行”的用法在shell里每天都会出现。1.3 什么基础的人可以从grep开始不管你是运维、后端开发、数据工程师还是刚学Linux的学生grep都是性价比最高的一课。它不要求你懂复杂的数据结构只需要记住几个参数和正则片段就能解决大量实际问题。比如查配置项、统计错误关键字、找某个函数出现在哪些文件里这些都是grep的主场。如果你已经会ls和cat就可以开始学grep了。我见过不少同学一上来就啃《Linux命令行与shell脚本编程大全》里的循环、函数结果真到写脚本时第一步卡在“怎么从日志里把报错行抓出来”。与其这样不如先把grep练成肌肉记忆再去接触其他命令。因为越底层的工具越值得反复打磨。2. grep的三种工作模式选错模式等于事倍功半2.1 基础正则、扩展正则与固定字符串很多人以为grep就是grep 关键字 file直到被正则表达式坑了才回头补课。grep其实有三种工作模式对应-G默认、-E、-F三个参数默认情况下用的是基础正则表达式BRE。基础正则最让人头疼的一点是、?、|、{}这些字符在BRE里不是“特殊字符”而是普通字符。如果你想表达“一次或多次”“可选”“或”这些意思必须给它们加上反斜杠。比如匹配abc后面跟一个或多个数字grep abc[0-9]\ file这里[0-9]\如果不加反斜杠\在BRE里就是一个普通的加号匹配结果完全不对。用grep -E切换到扩展正则ERE之后就舒服多了grep -E abc[0-9] file直接表示重复一次或多次。同理?、|、{}都不需要转义。还有一种情况你只想做纯文本匹配比如在日志里找包含a.b的字符串但点号在正则里表示“任意一个字符”于是a.b也会匹配axb、a1b。如果这不是你想要的就用grep -F把模式变成固定字符串grep -F a.b file-F模式下所有正则字符都失效grep只做普通字符串查找速度也更快。处理大规模日志时能用-F就不用正则这是性能优化的第一原则。2.2 行首、行尾、字符组和量词最常用的正则片段正则表达式本身是门大课但grep场景下你常用的其实就十几个符号。我按使用频率排个序^匹配行首。比如^ERROR只匹配以ERROR开头的行。$匹配行尾。比如error$只匹配以error结尾的行。.匹配任意单个字符但不包括换行符。比如app.log里的点号若不加转义就是模糊匹配。[abc]字符组匹配a、b、c中的任意一个。[0-9]表示任意数字[a-z]表示任意小写字母。[^abc]逻辑取反匹配除了a、b、c之外的任意一个字符。*重复前一个字符零次或多次。\BRE或ERE重复前一个字符一次或多次。\?BRE或?ERE前一个字符出现零次或一次。\{n,m\}BRE或{n,m}ERE前一个字符出现n到m次。举个例子匹配一个简单的IP地址片段grep -E 192\.168\.[0-9]{1,3}\.[0-9]{1,3} file这里点号要转义否则会匹配任何字符。[0-9]{1,3}表示1到3位数字正好对应IP地址的取值习惯。2.3 正则和shell通配符的区别一个最常见的误区很多新手会把grep里的*和shell通配符混淆。在shell里*.log表示匹配所有以.log结尾的文件名这是路径扩展而在grep里*表示前一个字符重复任意次。所以如果你写grep *.log fileshell会先把*.log展开成当前目录下的文件名列表grep收到的参数已经完全变了。要避免这个问题正则表达式要用单引号包起来grep *.log file单引号的作用是阻止shell对内部内容做任何扩展。我见过太多人因为忘记加引号导致grep报错“No such file or directory”或者匹配结果完全对不上。所以我的习惯是grep的正则模式永远用单引号包裹。3. 高频参数组合少背选项多记场景3.1 输出控制-n、-o、-H、-hgrep默认输出整行但很多时候你只需要一部分信息。这时候-o就非常有用它只输出匹配到的部分而不是整行。比如从一行日志里提取订单号grep -o order_id[0-9]* app.log就能得到一串order_id12345这样的结果方便后续继续处理。-n输出行号排查代码时特别常用grep -n TODO *.py结果会带着文件名和行号比如utils.py:42: TODO: need to refactor。这个输出格式刚好能被很多编辑器或脚本解析。-H和-h控制是否显示文件名。默认情况下只搜一个文件时不显示文件名搜多个文件时显示文件名。如果你想明确看每个匹配来自哪个文件加-H如果不想被文件名干扰加-h。比如统计多个日志文件里的错误总数用-h把文件名去掉后面接wc -l更干净。3.2 上下文控制-A、-B、-C日志分析里只看匹配行往往不够。比如某一行出现ERROR异常栈可能还要往下好几行。这时-A num表示显示匹配行以及后面num行-B num显示前面num行-C num前后都显示。-C等价于-A num -B num适合调试时快速看现场。grep -C 5 NullPointerException app.log这条命令直接把异常前后各5行都打出来省去你手工翻日志的功夫。在管道里它也照样工作比如先按时间过滤再取上下文grep 2026-03-21 14: app.log | grep -C 3 ERROR这里的第二个grep -C 3只对管道中的数据生效相当于在“14点这个时间段”的内部再做一次带上下文的错误匹配。3.3 反转与文件集合控制-v、-l、-L、-r、--include、--exclude-v是反向匹配也就是“不包含模式的行”。清理调试日志时很常用grep -v ^# config.conf把配置里的注释行全部排除掉。-v还可以配合-e实现“排除多个关键词”比如grep -v -e DEBUG -e TRACE app.log这样只保留不含DEBUG也不含TRACE的行。-l和-L用于文件列表-l只显示包含匹配内容的文件名不显示具体行-L显示不包含匹配内容的文件名。在代码库里搜索某个函数被哪些文件引用用-l最舒服grep -rl getUserById src/-r表示递归搜索目录-R会跟随符号链接。为了排查方便我通常直接用-r。--include和--exclude可以限制搜索的文件类型。比如只搜索.py文件跳过.pyc文件grep -rn --include*.py --exclude*.pyc def main .这套组合在大型项目里非常好用不用一个个目录进去搜。3.4 匹配模式控制-i、-w、-x、-e、-f-i忽略大小写比如搜error时能匹配ERROR、Error。-w强制单词边界匹配只匹配整个单词。例如grep -w cat file不会匹配concatenate因为cat后面跟着e不是独立的单词。-x要求整行完全匹配适合搜索空行或精确配置项grep -x export PATH/usr/bin file-e用来指定多个模式每个-e后面的模式都会被OR起来。正则里的|也行但多个-e更直白grep -e ERROR -e FATAL app.log-f从文件读取模式每个模式占一行。这在维护黑名单、白名单时很有用。把需要过滤的IP列表写进blocked_ips.txt然后grep -f blocked_ips.txt access.log就能快速筛出这些IP的访问记录。3.5 一个“日志必用”的组合样例把这些参数串起来一个很实用的日志分析命令是grep -E 2026-03-21 14:2[0-9] app.log \ | grep -E ERROR|FATAL \ | grep -v healthcheck \ | grep -o request_id:[0-9]* \ | sort | uniq -c | sort -nr这条命令能统计出14点20到14点29之间所有ERROR和FATAL日志里哪些request_id出现次数最多。先是时间过滤再是级别过滤然后排除健康检查噪音最后提取request_id并排序计数。整个链路完全依赖grep的各个参数没有任何多余的工具。4. 实战从几百MB日志里定位一次故障4.1 先按时间窗口过滤别一开始就全盘grep拿到一个大日志第一件事不是马上搜ERROR而是先确认时间范围。比如你知道故障发生在下午2点多先用grep把时间窗口切窄grep -E 2026-03-21 14:1[5-9]|2026-03-21 14:2[0-9] app.log window.log这里的正则匹配14:15到14:29之间的每一行。把结果输出到临时文件后后续操作都在window.log上做速度快很多也不会反复重度扫描整个大文件。如果日志里每行开头是标准时间戳这个方法几乎零成本。如果你不确定时间段也可以先扫出所有ERROR再看它们的分布grep -c ERROR app.log-c直接统计匹配行数不输出具体内容。很多新人不知道这个参数会用grep ERROR app.log | wc -l其实grep -c更简洁。4.2 用行号和上下文还原现场定位到具体时间点后下一步是看异常现场。命令还是那套grep -n -C 10 NullPointerException window.log | head -100-n给行号-C 10把异常前后各10行打出来。你可能会看到类似这样的结构14520: 2026-03-21 14:21:03 user request_id9a2f... 14521: 2026-03-21 14:21:03 Session timeout after 120s 14525: 2026-03-21 14:21:04 ERROR NullPointerException前面几行是业务日志说明这次请求之前发生了什么后面几行往往是堆栈信息能帮你判断是哪个方法抛的异常。这里的关键是别只盯着ERROR行上下文才完整还原现场。4.3 统计错误类型grep -o提取字段再用sort/uniq计数一旦发现错误很多手动看肯定不现实。这时候提取错误关键字并统计频率是最高效的方法。假设错误信息里都包含异常类型比如java.lang.NullPointerException可以直接提取grep -o java\.[a-zA-Z.]*Exception window.log | sort | uniq -c | sort -nrgrep -o只输出匹配部分sort把相同的异常类型排到一起uniq -c给它计数最后sort -nr按数量从大到小排。输出类似321 java.lang.NullPointerException 87 java.lang.IllegalArgumentException 12 java.net.SocketTimeoutException通过这个结果你能快速判断主要矛盾是什么而不是在几千条日志里一条条看。4.4 把可疑ID取出来顺着链路继续grep定位到具体错误后通常会想知道这个request_id在整个系统里经过了哪些服务。最简单的办法是先把ID提取出来再逐段搜索grep -o request_id[a-f0-9-]* window.log | head -1拿到一个ID后再在完整日志里搜grep 9a2f0b-37e3-4c91 app.log这样能看到同一个请求在多个模块里留下的痕迹。如果ID比较多还可以结合管道和循环grep -o request_id[a-f0-9-]* window.log \ | sort -u \ | while read id; do echo $id grep $id app.log | grep -c ERROR done这段小循环会让你看到每个请求ID对应的ERROR数量从而定位出最异常的请求。这里的while read id是shell里的标准读行循环grep的输出被逐行读进来每个ID再回到原日志里做二次匹配。5. 和awk、sed、xargs联手grep不是只能筛行5.1 为什么“能grep就不awk”不完全对网上有句话叫“能grep就不要awk”意思是如果你只是筛选行grep完全够用不要杀鸡用牛刀。但反过来也成立如果筛选完还需要对字段做计算grep就不够用了。比如日志行是2026-03-21 14:21:03 ERROR request_id9a2f duration230ms你想统计所有ERROR请求的平均耗时grep只能把行筛出来没法对duration字段求和。这时候要么用awk要么先grep再配合其他命令。最自然的做法是grep ERROR app.log | awk -Fduration {sum $2} END {print sum/NR}这里-Fduration把行按duration切分$2就是后面的耗时值。awk负责算术grep负责筛选各司其职。5.2 经典管道组合过滤、取列、去重、排序我实际分析日志时最常用的一条管道是grep ERROR app.log | awk {print $5} | sort | uniq -c | sort -nr | head -10这条命令做了四件事先用grep筛出关键行再用awk取第五列比如用户ID或请求路径然后sort | uniq -c统计出现次数最后sort -nr按次数降序排列取前10名。它完美展示了shell“一个命令做一件事然后通过管道组合”的设计哲学。如果第五列里有参数干扰比如路径后面带问号你还可以在grep阶段就用正则把它们去掉grep -E ^2026-03-21 14: app.log \ | grep ERROR \ | sed s/?.*// \ | awk {print $7} \ | sort | uniq -c | sort -nr这里sed用于截断问号后面的内容。没有sedawk处理起来会更麻烦。5.3 用xargs把grep结果变成下一个命令的参数grep配合xargs也很常见。搜索代码里所有包含TODO的文件然后统计每个文件的行数grep -l TODO src/*.py | xargs wc -lgrep -l输出文件名列表xargs把这些文件名作为参数传给wc -l。如果文件名里有空格直接这么写会出问题因为xargs默认按空白分割。稳妥的写法是grep -lZ TODO src/*.py | xargs -0 wc -l-Z让grep用空字符分隔输出xargs -0也按空字符读取这样带空格的特殊文件名也不会断。这个细节99%的情况下用不到但一旦遇到就省事很多。5.4 grep与sed、awk的分工边界我经常看到有人问“为什么不能用grep直接替换文本”因为grep本质是“匹配后打印”不负责修改文件内容。要修改用sedgrep ERROR app.log | sed s/ERROR/WARNING/这条命令只能把输出流里的ERROR改成WARNING不会影响原文件。如果你真的想原地替换还要加-i参数。但这不是grep的活别指望一个工具解决所有问题。同样grep对行内字段的定位能力很弱。它不知道“第几列”是什么只能按模式匹配。awk才是按列处理的行家。很多复杂的文本分析用“grep筛行、sed改字符串、awk算字段、sort/uniq做统计”这套组合基本能覆盖九成需求。6. 性能、二进制、中文和返回值grep容易翻车的四个点6.1 大文件为什么会慢locale与正则回溯处理几百MB甚至GB级别的日志时grep可能变得很慢。慢的原因主要有两个。第一个是locale设置。Linux下grep会把输入按当前语言环境做多字节编码识别如果LANG是en_US.UTF-8grep会花额外精力处理UTF-8字符边界。如果日志就是纯ASCII可以临时切到C locale大幅提速LC_ALLC grep ERROR big.log我实测过某些机器上能快一倍以上。但如果日志里有中文LC_ALLC可能导致中文匹配行为怪异所以要按文件内容取舍。第二个原因是正则回溯。模式越复杂grep内部状态机需要走的路径就越多。避免方法是能用-F固定字符串就用-F能用字符组[0-9]就少用.*尽量不要写大范围的.*加多个可选分支。比如匹配ERROR|FATAL没问题但如果有几十个分支考虑分多次grep再合并。6.2 二进制文件与grep -a默认情况下grep会检测文件类型如果发现二进制内容会输出类似“Binary file app.log matches”的一行而不是具体匹配内容。这本身是保护机制但如果你想在一个文件里搜索字符串而文件恰好包含一部分二进制数据就会很抓狂。解决办法是加-a参数让grep把二进制文件当成文本处理grep -a error dump.bin也可以直接用--text效果一样。处理网络抓包、内存转储、编码奇怪的配置文件时这个参数经常救命。6.3 中文匹配与编码问题grep匹配中文本身没问题但前提是文件编码和当前locale一致。最常见的情况是文件是GBK编码终端是UTF-8直接grep 中文大概率匹配不到。这时候要么转码iconv -f GBK -t UTF-8 file | grep 中文要么把文件统一转成UTF-8后再处理。如果你的环境是UTF-8直接匹配没问题。另外正则匹配中文时可以用Perl正则模式-P配合十六进制字符范围但普通场景直接写中文关键字更快。6.4 模式以“-”开头或包含特殊字符的坑要搜索以-开头的字符串时grep会把-当成参数选项。比如你想匹配文本-errorgrep -- -error file双横线--告诉grep后面的内容都是模式不再解析选项。同理如果搜索的字符串里包含$、*、.等正则特殊字符要么加-F要么用反斜杠转义。我建议优先用-F因为更直观、更安全。6.5 grep返回值在脚本里的正确用法很多人在shell脚本里忽略了grep的返回值。grep找到匹配返回0没找到返回1出错返回2。这个返回值可以直接作为if判断条件if grep -q FATAL app.log; then echo 发现致命错误 exit 1 fi-q让grep不输出任何内容只设置返回值适合只需要判断是否匹配的场景。在监控脚本里这种写法比grep ... /dev/null更专业也更省资源。不要忘了如果没有匹配项grep返回1这在set -e的脚本里会让脚本直接退出。如果你不希望这样记得在命令后加|| true或者把grep放到if条件里。7. 从shell编程岗位需求反推grep要学到什么程度7.1 面试里常见的grep管道题很多公司在考察shell编程时不会让你背参数而是给一道实际场景题。比如统计access.log里访问次数最多的前5个IP。找出/var/log下所有包含“OutOfMemory”的日志文件。过滤出配置文件里所有非注释、非空行的内容。这些题本质上都在考你能不能把grep和其他命令串起来。比如“访问次数最多的前5个IP”参考解法是用awk取IP列但前提是你知道怎么用grep排除不需要的请求grep -v 127.0.0.1 access.log | awk {print $1} | sort | uniq -c | sort -nr | head -5第一步就把本机访问排掉后面工作才更干净。这不仅仅是“会用grep”而是知道在哪个环节用grep。7.2 从常见100例脚本里看grep的高频角色你去看网上那些“shell脚本100例”会发现grep出现频率极高但很少单独出现。它的角色通常是三件套一是做检查比如检查配置里是否包含某项配置检查日志里是否出现异常关键字检查进程是否存在。 二是做筛选从命令输出中提取关键行比如从ps -ef里筛出某个服务进程从df -h里筛出磁盘使用率超过阈值的行。 三是做统计配合wc -l、sort、uniq实现各种计数排名。如果你能把grep用成“条件反射”写脚本时就不会卡在第一步。很多复杂脚本其实就是把一个个grep管道拼起来中间再加点变量和循环。7.3 学习路线怎么把grep练成肌肉记忆我建议的学习路径分三步。第一步每天用grep处理一个真实文件。比如把系统日志、nginx日志、项目代码当成练习材料强迫自己不用编辑器搜索只用grep。可以试试找出错误日志里出现最多的异常类型找出代码里所有TODO统计某个用户今天登录了几次。第二步把所有常用参数抄成速查表贴在自己终端旁。不需要刻意背用多了自然记得。重点记忆的只有十几个-n、-o、-v、-c、-l、-L、-r、-i、-w、-x、-A/-B/-C、-E、-F、-f、-q。这个数量级已经完全足够应付日常工作。第三步多读别人写的脚本。尤其注意他们为什么在某个环节用grep而不是sed或awk。看多了你会形成一种“管道思维”先想最终要什么再想每条命令能贡献什么最后用|把它们连起来。这个过程比记任何命令参数都重要。我个人还有一个习惯每周挑一天把当天所有手动操作写成一个小脚本哪怕只是三行命令也要放进文件里跑一遍。久而久之grep和管道就不再是需要想的命令而是像呼吸一样自然。