十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

grep、sed、awk三剑客实战:从日志分析到文本处理的完整方案

grep、sed、awk三剑客实战:从日志分析到文本处理的完整方案 开头想直接扔一段grep、sed、awk三兄弟的废话不行。我见过太多次这样的场景了开发同事线上排查问题日志文件几百兆他打开编辑器从头翻到尾小半天过去了还在骂骂咧咧找某个异常栈或者是报表脚本里的文本清洗逻辑写得又臭又长跑一次要几分钟明明用awk一行就能搞定的活非得写二十行shell循环。说白了问题就出在没把grep、sed、awk这三个命令当成真正趁手的工具来练。这篇不是走马观花的命令列表而是我这些年真刀真枪用下来的实操笔记。grep负责捞数据sed负责改数据awk负责算数据——三个命令单一拆开都能干活组合起来才是文本处理的完整流水线。适合刚接触Linux、被各种管道符绕晕的新手也适合那些明明用过很久但总觉得差点意思、想系统补一遍短板的老手。看完不需要记住所有参数但得知道什么时候该用哪个、遇到具体需求该往哪个方向查。1. 为什么非要死磕这三个命令先说个反直觉的事在Linux生态里处理文本最高效的方式不是写脚本而是组合命令行工具。你写Python或者Perl处理日志启动解释器就要几百毫秒处理大文件可能还要装第三方库。但grep、sed、awk是从Unix诞生起就存在的元老级工具C语言实现、单进程流式处理跑几百MB的文件也就是秒级的事。更关键的是这三个命令严格遵守Unix哲学每个工具只做一件事通过管道把它们串起来。管道这个东西好多新手理解不到位。比如grep ERROR app.log | sed s/ERROR/错误/g | awk {print $1}这条命令的意思是grep先筛出包含ERROR的行sed把ERROR替换成错误awk再取每行第一个字段。上一个命令的标准输出直接变成下一个命令的标准输入全程数据在内存里流动不产生中间文件。流的理念贯穿这三个命令的始终理解了这个后面所有操作都顺了。另外这三个命令在手几乎能涵盖日常90%的文本处理需求搜日志找关键信息grep搞定批量改配置文件sed搞定按列做统计、求和、格式化输出awk搞定再复杂的联动需求管道串起来搞定。好多人在图形界面里拖来拖去或者写一次性脚本其实时间成本远高于静下心来把这三个命令练熟。磨刀不误砍柴工说的就是这个。2. grep最快速度定位你想要的文本2.1 别只会grep 关键字grep全称是Global Regular Expression Print全局正则表达式打印。它最基本的用法确实就是grep 关键字 文件但实际工作里需求从来不会这么简单。最常见的场景是搜日志。应用日志里同一时间可能有几十个线程在输出你只关心某个交易号的记录grep T20240615A00321 merchant-service.log这样是能搜出来但如果日志被轮转过、打包成.gz了怎么办grep有个很有用的参数--include配合通配符再结合-r递归一次性搜索整个目录grep -r --include*.log --include*.log.gz T20240615A00321 /data/logs/注意.gz文件grep没法直接读这种场景建议用zgrep它是专门为压缩文件设计的zgrep T20240615A00321 /data/logs/*.log.gz我知道你大概率还不知道这个命令它跟grep的参数用法几乎完全一致但不需要解压就能搜gz包里的内容。记下来这能让你少掉一半头发。2.2 正则才是grep的灵魂grep搜固定字符串只能算入门配合正则表达式才是它的完全体。日常使用频率最高的是这几个参数功能常用场景-E使用扩展正则相当于egrep用-o只输出匹配到的部分从日志里提取IP、订单号-v反向匹配排除含有指定模式的行过滤掉DEBUG日志-c统计匹配行数不是匹配次数统计错误次数-n显示匹配行所在行号告诉同事bug在第几行-A/-B/-C显示匹配行的后文/前文/前后文看异常栈的上下文-i忽略大小写搜ID、id、Id都行--include/--exclude指定/排除文件类型排查代码里某函数的所有调用举例说明。假设你有一份Nginx的访问日志要统计今天的500错误有多少条grep -c HTTP/1.1 500 access.log要看看哪些接口在报错并且带上响应码和URIgrep -E HTTP/1.1 (500|502|503) access.log | awk {print $7, $9} | sort | uniq -c | sort -rn这行脚本已经出现awk了——你会发现一旦涉及统计grep就顶不住了得交给后面的命令。但grep至少帮你把范围缩小到了错误请求。2.3 grep的经典盲区伪装成正则的字符串我见过不少人写grep正则里带着一堆反斜杠转义最后发现压根匹配不到东西。比如要搜IP地址有人会写grep 192\.168\.1\.100 /var/log/secure这里有个细节.在正则里是匹配任意字符所以要匹配字面意义上的点必须转义。如果不加反斜杠192.168.1.100这个模式甚至能匹配到192x168y1z100这样的字符串。同理要匹配包含[error]标签的行方括号在正则里是字符集的含义也必须转义grep \[error\] /var/log/app.log有个实用技巧如果你要匹配的是纯文本、不带正则含义直接加-F参数把模式当固定字符串处理省去转义的麻烦性能也更好。比如搜一堆订单号列表建议把订单号放在文件里用-f指定模式文件批量匹配grep -f order_ids.txt pay.log顺便说一句日常写正则建议用单引号把模式包起来。双引号里$、、\这些会被shell先解释掉很容易产生诡异的结果。单引号内部是原始字符串正则里的特殊符号不会被shell伤害到。这是一个极小但极其影响排查效率的细节。3. sed精准手术刀文本增删改查样样行3.1 先建立流式处理的心智模型sed全称Stream Editor流编辑器。它不像vim那样打开整个文件让你人机交互而是逐行读取内容、按你给的规则处理、然后输出处理完一行丢一行内存里永远只保留当前行。这就是它能处理超大文件的底气。sed的基本语法结构是sed 寻址动作 文件理解寻址是关键你想让sed对哪些行执行操作动作你要对这些行做什么最常见的动作是替换ssed s/old/new/ file注意这条命令默认只替换每一行第一个匹配到的old。要替换一行内所有的old必须在命令尾部加g修饰符sed s/old/new/g file这个g是global的意思很多新手会漏掉这个后缀导致行内有多个匹配时只替换了第一个排查半天还以为是数据问题。老实说我基本每次都带g不带g的场景极其罕见。3.2 精准的寻址方式如果我们只想改特定范围的行需要在s///前面加寻址条件。按行号把第2行的foo改成barsed 2s/foo/bar/按行号范围第2到第5行sed 2,5s/foo/bar/按行号加步长从第1行开始每隔3行处理一次sed 1~3s/foo/bar/按正则寻址匹配到的行再执行替换sed /pattern/s/foo/bar/按正则范围从匹配到start的行开始到匹配到end的行结束闭区间sed /start/,/end/s/foo/bar/这个范围寻址在提取日志时间段的时候特别有用。比如日志里每个请求都有 BEGIN REQUEST 和 END REQUEST 标记你想把所有请求里某个字段的格式统一一条命令就完成不用手动画范围。3.3 删除、追加、插入不只是替换替换只是sed的一部分能力。删行用d在某行前插入用i在某行后追加用a。删掉所有空行sed /^$/d file删掉第3行sed 3d file删掉配置文件中所有注释行和空行——这个我常用来清理nginx.confsed -e /^#/d -e /^$/d nginx.conf在第5行后面追加一行worker_processes 4;sed 5a worker_processes 4; nginx.confa命令后面跟的内容中多个单词不需要加引号但如果要追加的内容本身就包含空格和特殊符号建议用双引号包住整个表达式再在内部用转义处理。实际使用中我更喜欢把a和i配合地址范围来做配置文件模板的批量生成这是sed最有生产力的地方后面综合实战会讲到。3.4 sed -i的杀伤力和正确姿势-i参数表示原地修改文件这是sed最危险也最常用的特性。危险在于一旦执行原文件直接被改写没有后悔药。我见过有人把生产环境的配置路径改错了导致服务起不来最后只能翻备份。正确姿势分两种。第一改之前先不写-i把输出结果另存为临时文件检查一遍sed s/old/new/g app.conf app.conf.tmp diff app.conf app.conf.tmp确认无误再真正改。第二用-i的时候带备份后缀让sed自动生成备份文件sed -i.bak s/old/new/g app.conf这样会生成一个app.conf.bak改错了还能立刻回滚。等运行一段时间确认稳定了再清理掉备份文件。我真的建议所有人都养成这个习惯特别是在生产环境批量修改配置的时候一条-i.bak能避免99%的后悔场景。3.5 sed的隐藏用法打印与退出除了改文件sed也常用来查看文件。配合-n参数sed不做默认的全文输出只打印你指定的行sed -n 20,30p app.log打印第20到30行这在排查日志时比head和tail方便得多。再配一个实用技巧打印某个时间点到结束的所有行sed -n /2024-06-15 10:00:00/,$p app.log$代表最后一行整个命令的意思是从匹配到指定时间戳的行开始打印到文件末尾。这个过程在任何文本编辑器里都得处理半天sed是瞬时的。还有q命令读到匹配的行立即退出。在扫描超大文件时找到目标就停能节省大量时间sed -n /FATAL ERROR/q app.log虽然head也能配合管道完成但sed这套逻辑更统一理解起来不费劲。4. awk你以为它在处理文本其实它是一门迷你语言4.1 awk的数据透视表本质很多新手学awk会被它的语法吓退说这跟C语言似的。但换个角度理解awk本质上是一个针对行和列的数据处理工具类似于你在Excel里对一张表做筛选、求和、透视只不过这张表可能有几百万行。awk的基本结构是awk 模式 {动作} 文件它逐行读取文件把每一行按分隔符拆分成N个字段默认按空白字符空格和Tab分隔。$1代表第一个字段$2代表第二个$0代表整行NF代表一行有多少个字段NR代表当前是第几行。比如要打印一个文件的第一列和第三列awk {print $1, $3} data.txt这行命令的思维模式跟SQL的SELECT很像print $1, $3就是选取列。如果想加个where条件在动作前加一个模式awk $3 100 {print $1, $3} data.txt第三列大于100的行才输出这不就是WHERE子句吗。4.2 定分隔符别让默认空白坑了你默认分隔符是空白但很多日志和配置文件的列是用逗号、冒号、竖线分隔的。这时候要用-F指定分隔符。awk -F, {print $1, $2} data.csv awk -F: {print $1, $3} /etc/passwd举一个真实例子。假设你有一份接口响应时间日志格式是接口名|耗时ms|状态码awk -F| $3200 {sum$2; count} END {print 平均耗时:, sum/count, ms} api.log这条命令的意思是以|为分隔符取出状态码为200的行累加耗时到sum行数累加到count最后在END块里算平均值。有SQL基础的人一眼就能看出这相当于SELECT AVG(耗时) FROM 日志 WHERE 状态码200。awk里有个执行顺序的概念BEGIN块在读取文件之前执行END块在所有行处理完之后执行中间的模式{动作}对每一行执行一次。这三个块是你组织awk逻辑的基本骨架。4.3 内建变量体系NR、NF、FS、OFSawk自带一套变量掌握了它们几乎就能解决80%的统计需求变量含义典型用法NR已经读过的记录数行号NR1判断表头NF当前行的字段数量过滤字段异常的脏数据FS输入字段分隔符同-FBEGIN{FS,}OFS输出字段分隔符控制print输出的分隔样式$0整行原样处理$n第n个字段核心操作对象举个例子如果想把csv格式转成tsv格式只需要设置OFSawk -F, BEGIN{OFS\t} {print $1, $2, $3} data.csv还有个高频需求跳过表头。用NR1作为模式第一行不处理awk -F, NR1 {print $2} data.csv或者不打印表头但用表头做列名映射——这个后面实战部分展开。4.4 printf让输出脱胎换骨很多人不知道awk里最值得认真学的是printf它能让你的输出对齐、格式化、完全可控。对比一下awk {print $1, $2} data.txt awk {printf %-20s %8.2f\n, $1, $2} data.txt第一列左对齐占20个字符宽第二列右对齐占8个字符、保留两位小数。这在生成报表的时候简直是神器输出不再是乱七八糟的挤在一起而是清爽的表格质感。printf的格式控制符沿用的是C语言那套%s字符串、%d整数、%f浮点数-表示左对齐数字表示宽度.2表示小数位数。刚开始不要求全记住遇到需求的时候查一下格式说明就够用但要记得awk里有printf这么个东西否则你会在拼字符串的路上越走越远。4.5 awk的数组与统计group by其实很简单awk支持关联数组索引可以是字符串。这是它做分组统计的核心能力。来一个经典需求统计访问日志里每个IP的访问次数按次数降序排列。awk {count[$1]} END {for (ip in count) print ip, count[ip]} access.log | sort -k2 -rn这个需求SQL里叫GROUP BYawk里就是用一个数组count以IP为键、以累加数值为值。END块里遍历数组打印结果再用sort做排序。再进阶一点统计每个接口的平均响应时间、最大响应时间、最小响应时间。awk -F| {sum[$1]$2; cnt[$1]; if($2max[$1]) max[$1]$2; if(min[$1] || $2min[$1]) min[$1]$2} END {for (api in sum) printf %s 调用%d次 平均%.2f 最大%d 最小%d\n, api, cnt[api], sum[api]/cnt[api], max[api], min[api]} api.log | sort -k3 -rn注意min[$1]这个判断因为awk变量默认值是空字符串/0如果不加这个判断第一个值进来跟空比较时会出错。这也是awk数组统计里最隐蔽的坑之一。4.6 awk处理日志的时间段统计再分享一个运营经常会问到的需求统计某个时间段内的请求量。日志格式的第一列是时间戳格式为2024-06-15 10:23:45。awk $12024-06-15 $210:00:00 $211:00:00 {count} END {print count} access.log更通用的做法是配合match函数提取时间字段做范围判断不过如果时间恰好是独立列直接字符串比较效率最高因为awk的字典序比较对固定格式的时间字符串是天然正确的。前提是时间格式必须统一如YYYY-MM-DD HH:MM:SS否则建议先把时间字段解析成时间戳再比。需要提醒的是字符串比较跟数字比较在awk里是有区别的。如果字段是纯数字建议先0强制转成数值再比否则可能因为字符串比较而出现100 20这种违背直觉的结果。这也是awk新手最容易踩的隐性坑。5. 综合实战三剑客组合拳实战才见真章。接下来我给几个我之前在项目中用过的、高频到几乎每周都用的组合场景你可以对照着敲一遍。5.1 场景一统计日志中错误码的Top 10假设Java服务日志格式里有ERROR_CODE500、ERROR_CODE404这样的片段我想看今天报得最多的前10个错误码。grep $(date %Y-%m-%d) app.log | grep -o ERROR_CODE[0-9]* | sort | uniq -c | sort -rn | head -10这条链路的逻辑grep按今天的日期筛出当天日志-o只提取ERROR_CODE503这样的片段不输出整行这样后续统计不受其他字段干扰sort对提取到的错误码排序sort是管道里一个不起眼但极其重要的中间步骤uniq只能合并相邻的重复行必须先sortuniq -c 统计每个错误码出现次数sort -rn 按次数降序head 10取前10。uniq这个命令虽然不是三剑客但它和grep组合频率极高请务必记住配套的sort使用。坑点就在这不sort直接uniq结果是错的。5.2 场景二批量修改多个配置文件并自动备份线上N台机器每个应用目录下有个config.yaml需要把数据库连接串从旧的改成新的同时保留备份。find /opt/app -name config.yaml -exec sed -i.bak s/jdbc:mysql://old-host:3306/jdbc:mysql://new-host:3306/g {} \;分解一下find找到所有目标文件-exec对每个文件执行后面的大括号{}被替换为文件名sed用-i.bak做原地替换并留备份末尾的\;告诉find命令结束。这个命令我实测在处理几十台机器、几百个配置文件的场景下执行时间在秒级。当然如果是管理大规模集群更推荐Ansible这类工具但一次性应急处理这行命令已经完全够用了。5.3 场景三日志清洗后生成运维报表有这样一个场景业务打点日志每行是用户ID|城市|渠道|页面|停留时长需要生成一份每个城市、每个渠道的PV量、平均停留时长的报表并保存为csv用于后续分析。awk -F| NR1 {pv[$2 FS $3]; time[$2 FS $3]$5} END {for (k in pv) {split(k, arr, FS); printf %s,%s,%d,%.2f\n, arr[1], arr[2], pv[k], time[k]/pv[k]}} business.log report.csv这条命令的关键点NR1是为了跳过第一行表头假设日志有表头用户ID|城市|渠道|页面|时长$2 FS $3是把城市和渠道拼成一个复合键FS在这里作为连接符保证key唯一split函数在END块里把复合键重新拆成两个字段用于输出printf直接输出csv格式逗号分隔。sound复杂但思路无非四步定义key、累加、累计时长、END里格式化输出。这是awk最经典的分组汇总套路建议反复练习到不用查文档也能写出来的程度。5.4 场景四批量压缩历史日志并只保留最近三天经常有日志目录快撑爆磁盘的情况在不影响最近排查的前提下需要把7天前的日志压缩3天前的压缩包删除。用grep配合find、xargs也能做但既然说三剑客组合我演示一个带awk的版本find /data/logs -name *.log -mtime 7 | awk {print gzip, $0} | bash find /data/logs -name *.log.gz -mtime 3 -delete这个例子不算正中三剑客的核心用法但演示了awk在管道里做命令字符串构造的灵活之处——awk {print gzip, $0}把find列出的文件名包装成gzip命令再用管道交给bash执行。这种构造命令再执行的模式应用很广远比手写for循环来得简洁。注意生产环境用这种写法前一定要先在测试目录验证因为awk构造出来什么命令你就执行什么一个字段拼接错误可能在线上批量压错文件。安全的做法是先不接| bash让命令打印出来检查一遍。5.5 awk带表头的报表优雅输出最后补充一个我经常被问到的需求awk统计结果想带表头怎么做才好看。awk -F| BEGIN{printf %-20s %-10s %10s\n, 接口, 状态码, 次数} {cnt[$1 FS $3]} END {for (k in cnt) {split(k, arr, FS); printf %-20s %-10s %10d\n, arr[1], arr[2], cnt[k]}} api.log这里用到了BEGIN块在读取任何数据之前先打印表头用printf的格式控制符%-20s控制各列宽度。表头和数据用了同样的格式说明列就能对齐。我见过很多人用print拼凑输出报表乱成一团每次都要在Excel里重新分列。printf才是awk输出的正确打开方式。还有一个很多人想知道的细节awk的END块输出顺序是随机的因为awk底层用哈希表存储数组如果希望结果有序就按我前面的做法在awk之后再接管道用sort -k指定排序字段。这就是命令组合的妙处awk负责算sort负责排各司其职谁也不越俎代庖。6. 省心提示哪些命令必须配管道最后聊一点我自己的习惯。你可能会发现前面所有复杂一点的示例几乎都离不开管道。原因在于三剑客各有所长但都不是全能的只有组合起来才是一个完整的工具链。我自己的固定搭配套路是第一步定位范围用find或grep把要处理的文件、行缩小。第二步提取内容用grep -o或awk把需要的字段抠出来。第三步统计变换用awk做求和、平均、分组。第四步整理展示用sort、uniq -c、head做排序和topN必要时用awk的printf对齐输出。这套流程适用于绝大多数的文本排查和报表生成需求。建议你找个真实一点的日志文件从这四步入手多练几次让肌肉记忆形成。等到哪一天你遇到问题第一反应不再是打开Notepad查找而是顺手敲出带管道符的命令三剑客就算真正上手了。敢直接在命令行里改生产配置、敢对几GB的日志做实时统计这份底气不是背参数背出来的是靠踩坑、试错、看输出结果慢慢磨出来的。工具永远是死的思路才是活的。
返回列表