Linux文本三件套实战:grep、sed、gawk核心技巧与避坑指南
简介这份PDF文档面向具备一定Linux基础、希望提升文本处理效率的技术人员系统梳理了grep、sed、gawk三大命令行工具的使用方法。内容从正则表达式元字符集切入覆盖grep的基本集与扩展集、POSIX字符类及常用命令选项sed部分讲解定址功能、调用格式与删除、替换、多点编辑等流编辑操作gawk部分则展开字段处理、模式与动作、内部函数及控制结构并结合日志分析、配置文件批量修改、CSV与结构化数据统计等场景给出大量实例。资源包为1个PDF文件大小约342KB目录按工具分章、层级清晰便于按需检索与对照练习。目前已有121人学习。读者可借此掌握正则表达式在查找、替换与数据解析中的实际用法积累可直接复用的命令示例与排错思路适合作为日常运维与自动化脚本编写的案头参考。1. 三件套到底谁管哪一段从一次日志清洗翻车说起线上排查时拿到一份 2.3GB 的 Nginx 访问日志要统计某个接口在 14 点到 15 点之间的 P99 耗时同时把状态码非 200 的请求单独抽出来。第一反应是写个 Python 脚本读文件、切分、过滤、聚合跑一遍两分半。同事看了一眼甩过来一行grep加一行awk三秒出结果。这件事让我重新审视 Linux 文本三件套的定位grep负责「找行」sed负责「改行」gawk负责「算行」。三者串起来就是一条不需要编译、不需要依赖、在任何一台 Linux 机器上都能跑的数据流水线。这篇文章面向的是每天要和日志、配置、CSV、批量文本打交道的运维和开发。不管你是刚接触linux常用命令的新手还是已经能写grep在shell脚本中的常见用法的老手下面会把命令选项、正则表达式语法、sed 保持空间 模式空间这类容易混淆的概念以及真实场景下的应用实例逐层拆开。目标很明确读完能直接在自己的服务器上复现遇到翻车场景知道去哪查。2. grep把「找行」这件事做到极致2.1 为什么 grep 比你想象的快grep的全称是 Global Regular Expression Print它的工作模型非常朴素逐行读取输入对每一行尝试匹配给定的模式匹配成功就输出整行。听起来简单但它的实现用了 Boyer-Moore 和 Aho-Corasick 等字符串搜索算法在纯文本匹配场景下比大多数脚本语言的手写循环快一个数量级。关键选项决定了它的行为边界选项作用典型场景-i忽略大小写搜索日志中的 ERROR/error/Error-v反向匹配排除注释行和空行-c只输出匹配行数快速统计出现次数-n显示行号定位配置文件中的问题行-r递归搜索目录在源码树中查找函数调用-E使用扩展正则需要、?、-o只输出匹配部分提取 IP、时间戳等字段-A/-B/-C显示上下文行看异常前后的日志很多人不知道grep -o配合正则可以直接做字段提取。比如从日志里抽出所有 IP 地址# -o 只输出匹配到的部分-E 启用扩展正则 # [0-9]{1,3} 匹配 1 到 3 位数字\. 匹配字面量点号 grep -oE ([0-9]{1,3}\.){3}[0-9]{1,3} access.log | sort | uniq -c | sort -rn | head -20这行命令的逻辑是grep -oE从每行中提取所有匹配 IP 格式的子串sort排序后uniq -c统计每个 IP 出现次数再按次数倒序排列取前 20。参数上{1,3}限定了每段数字的位数范围\.中的反斜杠是必须的否则.在正则里表示「任意字符」会把192x168x1x1这种也匹配进来。2.2 正则表达式在 grep 里的三个层次grep支持三种正则模式用错了就是白费功夫基本正则BRE默认模式、?、|、()、{}这些元字符需要加反斜杠才有特殊含义。写grep a\才能匹配一个或多个 a。扩展正则ERE加-E选项元字符直接使用不需要反斜杠。grep -E a效果同上。日常使用建议一律加-E可读性好得多。Perl 正则PCRE加-P选项支持\d、\w、\s、lookahead、lookbehind 等高级语法。比如提取#符号后面的字符串# -P 启用 Perl 正则\K 表示丢弃前面匹配的内容 # (?#) 是 lookbehind要求前面有 # 但不消耗它 grep -oP (?#)\w config.txt这里(?#)是零宽断言意思是「位置前面必须是 #」但 # 本身不出现在结果里。\w匹配一个或多个字母数字下划线。如果你的 grep 版本不支持-P某些精简系统确实没有可以用sed替代后面会讲。注意grep -P依赖 libpcre 库在 Alpine 等精简镜像中可能不可用。部署脚本前先用echo test | grep -P test验证一下。2.3 递归搜索与文件过滤的实战组合在大型项目中查找代码grep -r是标配但直接递归会扫到.git、node_modules、二进制文件又慢又吵。正确的做法是配合--include和--exclude-dir# 在 src 目录下搜索所有 .py 文件中的 TODO 注释 # --include 限定文件类型--exclude-dir 排除无关目录 # -n 显示行号-C 2 显示前后各两行上下文 grep -rn --include*.py --exclude-dir{.git,__pycache__,venv} -C 2 TODO src/参数说明--include*.py只搜索 Python 文件--exclude-dir接受花括号展开的目录列表-C 2是 context 的缩写等价于-A 2 -B 2。这个组合在排查「某个配置项在哪里被引用」时特别高效比 IDE 的全局搜索还快因为不建索引。还有一个容易被忽略的选项是-l只输出文件名不输出内容。当你只想知道「哪些文件包含这个关键词」时grep -rl比grep -r快很多因为匹配到第一个就停止读该文件。3. sed流编辑器的不传之秘3.1 模式空间与保持空间到底怎么理解sed的核心概念是两个缓冲区模式空间pattern space和保持空间hold space。网上讲sed 保持空间 模式空间的文章很多但大多绕来绕去。用一句话说清楚模式空间是「当前正在处理的那一行」保持空间是「一个可以跨行存东西的草稿纸」。sed默认逐行读取每读一行放入模式空间执行完所有命令后输出然后清空模式空间读下一行。保持空间在默认情况下是空的只有用h、H、g、G、x这些命令才会和它交互h把模式空间内容复制到保持空间覆盖H把模式空间内容追加到保持空间g把保持空间内容复制回模式空间覆盖G把保持空间内容追加到模式空间x交换模式空间和保持空间理解了这个模型就能看懂那些「一行 sed 解决复杂文本处理」的玄学命令。比如把文件倒序输出# 1!G 表示除了第一行之外都把保持空间追加到模式空间 # h 把当前行存入保持空间$p 在最后一行打印 sed -n 1!G;h;$p file.txt逐行拆解读第一行时1!G不执行因为是第一行h把第一行存入保持空间不打印。读第二行时G把保持空间第一行追加到模式空间末尾此时模式空间是「第二行\n第一行」然后h存入保持空间。以此类推到最后一行时模式空间已经是倒序的全部内容$p打印出来。这就是sed的图灵完备之处。3.2 替换命令 s 的完整语法与边界s命令是sed使用频率最高的功能完整语法是s/正则/替换/标志。标志位决定了替换行为标志含义示例无只替换每行第一个匹配s/foo/bar/g替换所有匹配s/foo/bar/gi忽略大小写s/foo/bar/gip打印替换后的行配合-n使用数字只替换第 N 个匹配s/foo/bar/2替换部分支持反向引用\1、\2对应正则中的捕获组。一个典型场景是把2024-01-15格式的日期改成15/01/2024# \(...\) 在 BRE 中定义捕获组\1 \2 \3 引用它们 # 这里用 -E 启用扩展正则捕获组直接用 () sed -E s/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/g dates.txt逻辑说明三个捕获组分别匹配年、月、日替换部分按日/月/年的顺序重新排列。注意替换部分中的/需要转义为\/否则会被当成分隔符。如果觉得转义麻烦可以换分隔符比如sed -E s|([0-9]{4})-([0-9]{2})-([0-9]{2})|\3/\2/\1|g用|当分隔符就不需要转义斜杠了。注意sed的-i选项直接修改原文件在 macOS 上需要写成-i Linux 上直接-i。跨平台脚本里建议先输出到临时文件再mv避免兼容性问题。3.3 用 sed 做多行处理和条件分支sed不止能处理单行。通过N、D、P命令可以操作多行模式空间。比如把 JSON 文件中跨行的key:\nvalue合并成一行# N 读取下一行追加到模式空间 # s/\n/ / 把换行替换为空格 # 匹配 key: 开头的行时执行上述操作 sed /key:/{N;s/\n/ /} data.json更复杂的场景可以用分支命令b和测试命令t。t命令在前一个s成功替换时跳转到指定标签这实现了条件逻辑。比如只替换包含ERROR的行中的时间戳# /ERROR/ 是地址匹配只对包含 ERROR 的行执行后续命令 # s/.../.../ 替换时间戳如果成功则 t 跳转到 end # b end 是无条件跳转用于跳过不需要处理的逻辑 sed /ERROR/{s/[0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\}/XX:XX:XX/;t end};b end;:end app.log这个例子展示了sed的脚本能力。虽然可读性不如awk但在只需要简单条件替换的场景下sed启动更快内存占用更小。嵌入式 Linux 环境中sed几乎是必装的而gawk不一定有这就是sed的生态位。4. gawk当文本处理需要「算」的时候4.1 awk 的字段模型与内置变量gawk是 GNU awk在大多数 Linux 发行版中是awk的默认实现。它的核心模型是「记录-字段」默认按行读取记录每行按空格或制表符切分成字段用$1、$2、$3引用。$0表示整行NF表示当前行的字段数NR表示当前行号FNR表示当前文件内的行号。这些内置变量让awk天然适合处理结构化文本。比如统计 CSV 文件中第三列的总和# -F, 指定逗号作为字段分隔符 # NR1 跳过表头sum$3 累加第三列 # END 块在处理完所有行后执行 awk -F, NR1{sum$3} END{printf Total: %.2f\n, sum} data.csv参数说明-F,把分隔符设为逗号NR1是条件表达式sum$3是动作END{}是结束块。printf的%.2f控制输出两位小数。这比用cut取列再paste再bc求和简洁得多而且一次遍历完成。awk还支持关联数组这是它比sed强大的关键。统计日志中每个 IP 的请求次数# $1 是 IP 列count[$1] 对每个 IP 计数 # END 块遍历数组输出| sort -rn 把结果管道给 sort 排序 awk {count[$1]} END{for(ip in count) print count[ip], ip} access.log | sort -rn | head -10逻辑说明count[$1]利用关联数组自动创建键值对for(ip in count)遍历所有键。注意awk的数组遍历顺序是不确定的所以需要外部sort来排序。这个模式在linux常用命令大全运维类文章里出现频率极高因为它确实是日志分析的基本功。4.2 模式-动作对与条件过滤awk的完整语法是pattern { action }。pattern 可以是正则、比较表达式、逻辑组合甚至范围模式。这比grep的单一匹配灵活得多# 范围模式从包含 START 的行到包含 END 的行 awk /START/,/END/ file.txt # 比较表达式第三列大于 100 的行 awk $3 100 data.txt # 逻辑组合第二列等于 error 且第五列大于 1000 awk $2 error $5 1000 app.log # 正则匹配第一列匹配 IP 格式 awk $1 ~ /^[0-9]\.[0-9]\.[0-9]\.[0-9]$/ access.log~是匹配操作符!~是不匹配。这些模式可以组合使用比如「状态码是 5xx 且响应时间超过 3 秒」# $9 是状态码$NF 是最后一个字段响应时间 # 5xx 用正则匹配响应时间用数值比较 awk $9 ~ /^5[0-9]{2}$/ $NF 3 {print $1, $7, $9, $NF} access.log这里$NF比写死列号更稳健因为日志格式可能变化但响应时间通常在最后。^5[0-9]{2}$确保匹配的是完整的 5xx 状态码不会把1500这种误匹配进来。4.3 用 awk 做多文件关联与格式化输出awk可以同时处理多个文件用NR和FNR区分。一个实际场景有两个文件一个是用户 ID 到用户名的映射另一个是用户 ID 到订单金额的记录需要合并输出用户名和订单金额。# 第一个文件读入映射关系FNRNR 表示正在读第一个文件 # 第二个文件读入时用 $1 作为键查映射表 awk FNRNR{name[$1]$2; next} {print name[$1], $2} users.txt orders.txt逻辑说明FNRNR是awk处理多文件的经典技巧因为NR是全局行号FNR是当前文件行号只有读第一个文件时两者相等。next跳过后续动作只建立映射。读第二个文件时name[$1]取出对应的用户名$2是订单金额。这个模式在数据清洗中非常常见相当于一次内存中的 JOIN。awk的printf支持完整的格式化输出可以生成对齐的报表# %-20s 左对齐字符串占 20 字符%10.2f 右对齐浮点数占 10 字符 # 表头和分隔线手动构造 awk BEGIN{printf %-20s%10s\n, User, Amount; printf %-20s%10s\n, ----, ------} {printf %-20s%10.2f\n, $1, $2} data.txt这种输出在生成日报、周报时直接可用不需要再经过column命令格式化。BEGIN块在处理任何输入之前执行适合放表头和初始化逻辑。5. 三件套串联时的避坑与排查5.1 现象grep 匹配到了不该匹配的行原因正则中的.没有转义匹配了任意字符。比如grep 192.168.1.1会把192x168y1z1也匹配进来。解决用grep -F做固定字符串匹配或者手动转义grep 192\.168\.1\.1。如果模式来自变量用grep -F $pattern最安全。5.2 现象sed 替换后文件内容全乱了原因sed -i直接修改原文件如果正则写错原文件被破坏且无法恢复。另外sed的-i在不同平台行为不一致macOS 上-i后面必须跟一个参数通常是空字符串否则会把下一个参数当成备份后缀。解决永远先不加-i跑一遍看输出确认无误再加。或者用sed s/foo/bar/ file file.tmp mv file.tmp file。跨平台脚本用sed -i.bak生成备份文件出问题还能回滚。5.3 现象awk 统计结果比预期少原因字段分隔符不对。默认awk按空格和制表符切分但 CSV 文件用逗号日志可能用|或\t。如果分隔符不对$1可能是整行$2为空。解决用-F明确指定分隔符。对于连续空格-F[ ]或-F\t。如果分隔符是正则元字符如|需要写成-F\\|或在 shell 中加引号-F|。5.4 现象管道串联时结果为空原因grep的退出码影响了管道。当grep没有匹配到任何行时退出码是 1如果脚本用了set -e整个管道会中断。另外grep在管道中可能因为缓冲区问题导致输出延迟。解决在管道中grep后面加|| true避免退出码影响。对于缓冲区问题grep --line-buffered强制行缓冲适合实时日志监控。5.5 现象正则表达式在 grep 和 awk 中行为不一致原因grep默认用 BREawk用 ERE。同一个在grep中需要转义在awk中不需要。\d在grep -P中可用在awk中不可用要用[0-9]。解决统一用grep -E和awk的 ERE 语法避免混用。需要 PCRE 特性时用grep -P但要知道awk不支持 lookahead 等高级断言。写复杂正则前先在regex101.com上验证选择对应的 flavorBRE/ERE/PCRE。6. 把三件套写进脚本一个日志分析函数的演进前面讲的都是单条命令实际工作中更常见的是把它们封装成可复用的函数。我拿一个真实需求来演示从 Nginx 日志中提取指定时间范围内、指定接口的 P95 响应时间。第一版直接用管道拼# 提取 14:00 到 15:00 之间 /api/order 的响应时间 # 假设日志格式IP - - [时间] METHOD /path HTTP/1.1 状态码 响应时间 grep /api/order access.log | grep 14: | awk {print $NF} | sort -n | awk {a[NR]$1} END{print a[int(NR*0.95)]}这行能跑但问题很多时间过滤用grep 14:太粗糙会把14:出现在其他字段的行也匹配进来P95 的计算用int(NR*0.95)在数据量小时会取到第 0 个元素。第二版用awk统一处理# 用 awk 一次性完成过滤、提取、排序、百分位计算 # 时间匹配用正则限定在方括号内的字段 awk -v start14:00 -v end15:00 -v api/api/order $0 ~ api match($0, /\[[^]]\]/) { ts substr($0, RSTART1, RLENGTH-2) if (ts start ts end) { times[n] $NF } } END { if (n 0) { print no data; exit } asort(times) idx int(n * 0.95) if (idx 1) idx 1 printf P95: %s (samples: %d)\n, times[idx], n } access.log逻辑说明-v定义外部变量match($0, /\[[^]]\]/)定位方括号内的内容RSTART和RLENGTH是match设置的内置变量。asort是gawk特有的数组排序函数标准awk没有需要自己实现排序。idx做了边界保护避免取到无效索引。参数上start和end是字符串比较要求时间格式统一为HH:MM。如果日志用的是HH:MM:SS需要调整比较逻辑。api变量支持正则可以传/api/order也可以传/api/(order|pay)。这个函数的演进说明了三件套的边界grep适合快速筛选sed适合格式转换awk适合需要计算和聚合的场景。三者不是替代关系而是流水线上的不同工位。我现在的习惯是能用grep一行搞定的绝不上awk需要跨行处理或数值计算时才请出gawksed则专门负责「把 A 格式变成 B 格式」这类脏活。最后一个血泪教训任何要写入生产脚本的三件套命令先在测试环境用head -1000限制数据量跑一遍确认输出符合预期再放开。文本处理命令的破坏力在于它们太快了快到出错时你来不及按 CtrlC。希望帮到你。本文还有配套的精品资源点击获取