简介:这份PDF文档面向具备一定Linux基础、希望提升文本处理效率的技术人员,系统梳理grep、sed、gawk三大命令行工具的使用方法。内容从grep的正则表达式元字符集、扩展集与POSIX字符类讲起,结合命令选项与实例演示精准查找;sed部分覆盖定址功能、调用格式、元字符集,以及删除、替换、选定行范围、多点编辑、从文件读入等流编辑操作;gawk部分则讲解执行方式、文件与字段处理、模式与动作、内部函数及控制结构,展示结构化数据解析与统计汇总的灵活用法。资源包为1个PDF文件,大小约342KB,目录按工具分章、层级清晰,便于按需检索。目前已有121人学习。读者可借助大量可直接套用的命令示例,掌握日志分析、配置文件批量修改、CSV与日志数据统计等场景的实战思路,并通过正则表达式的强化练习加深理解。
1. grep、sed、gawk:三条命令撑起 Linux 文本处理的日常
日志排查、配置批量改写、数据清洗,这三件事几乎占了 Linux 运维和开发日常的一半时间。很多人第一反应是写 Python 脚本,但真到线上环境,往往连解释器都不一定顺手,反而是 grep、sed、gawk 这三条命令随取随用。grep 负责“找”,sed 负责“改”,gawk 负责“算和格式化”,三者组合起来就是一套完整的文本流水线。这篇笔记面向的是需要处理日志、配置文件、CSV 导出的从业者,从命令选项讲到正则表达式,再落到能直接抄的实例。新手可以按步骤复现,熟手可以重点看参数边界和踩坑记录。全文不堆概念,每个命令都配可运行的代码块和参数说明。
2. grep:从日志里精准捞出你要的那几行
2.1 grep 的匹配模式和三个必调选项
grep 的核心行为是逐行匹配,默认使用基本正则表达式(BRE)。它的选项很多,但日常真正高频的只有三个:-i忽略大小写、-n显示行号、-r递归目录。先看一个最小可运行示例,从 Nginx 访问日志里找出所有 500 错误的行:
# -n 显示行号,-i 忽略大小写,'500' 是基本正则 grep -ni '500' /var/log/nginx/access.log # 递归搜索整个配置目录,只列出匹配的文件名 grep -rl 'server_name' /etc/nginx/ # 反向匹配:排除包含 debug 的行 grep -v 'debug' app.log逻辑说明:-n让输出带行号,方便后续用 sed 定位;-r递归时默认会跟随符号链接,如果目录里有循环链接需要加-R或--no-follow。参数说明:-i在日志场景要慎用,因为 HTTP 方法 GET 和 get 在日志里通常不会混用,开了反而可能误匹配。-v是反向选择,常用来过滤掉噪音行。
真正拉开差距的是-E扩展正则和-PPerl 正则。-E让+、?、|、()不用转义,写起来干净很多:
# 用扩展正则匹配 4xx 或 5xx 状态码 grep -nE ' (4[0-9]{2}|5[0-9]{2}) ' access.log # -o 只输出匹配到的部分,适合提取 IP grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | sort | uniq -c | sort -rn | head逻辑说明:第一条用-E把分组和或运算写得更直观;第二条用-o只保留匹配文本,再配合sort | uniq -c做频次统计,这是排查恶意 IP 的常见套路。参数说明:-P支持\d、\b等 Perl 语法,但并非所有发行版默认编译进去,生产环境用之前先grep -P '' /dev/null测一下。
2.2 用 grep 做上下文提取和文件筛选
只看匹配行往往不够,报错前后几行才是关键。-A、-B、-C分别控制后、前、上下文的行数:
# 显示匹配行及其后 3 行 grep -A 3 'Exception' app.log # 显示匹配行及其前后各 2 行 grep -C 2 'timeout' app.log # 结合 --include 只在 .conf 文件里搜 grep -rn --include='*.conf' 'listen' /etc/逻辑说明:-A/-B/-C在排查堆栈时特别有用,因为异常信息通常跨多行。--include和--exclude用来限定文件范围,避免在二进制文件或日志归档里浪费时间。参数说明:-C等价于-A n -B n,数字可以按需调整,但不要设太大,否则输出会淹没终端。
一个容易被忽略的点是 grep 的退出码:匹配到返回 0,没匹配到返回 1,出错返回 2。在 shell 脚本里用if grep -q ...判断时,-q静默模式能避免输出干扰:
if grep -q 'ERROR' app.log; then echo "发现错误,触发告警" fi逻辑说明:-q只返回状态码不输出内容,适合条件判断。参数说明:如果文件不存在,grep 返回 2,脚本里最好区分 1 和 2,否则会把“文件缺失”误判为“没有错误”。
3. sed:流式编辑器的模式空间与保持空间
3.1 sed 的工作模型:一行一行读,模式空间里改
sed 是流编辑器,默认逐行读取到模式空间(pattern space),执行脚本后再输出。它不会直接改原文件,除非加-i。理解模式空间和保持空间(hold space)是用好 sed 的分水岭。先看最常用的替换:
# 把文件里的 old 替换成 new,只替换每行第一处 sed 's/old/new/' file.txt # 全局替换,加 g sed 's/old/new/g' file.txt # 直接改原文件,建议先备份 sed -i.bak 's/old/new/g' file.txt逻辑说明:s是替换命令,分隔符默认是/,如果替换内容含/,可以换成#或|。-i.bak会在修改前生成.bak备份,这是血泪经验——线上直接-i改配置,改错了没有后悔药。参数说明:g只影响单行内的多次匹配,跨行替换需要N或-z。
sed 的地址定位决定了它改哪几行。地址可以是行号、正则、范围:
# 只替换第 5 行 sed '5s/old/new/' file.txt # 替换第 3 到第 8 行 sed '3,8s/old/new/g' file.txt # 从匹配 start 的行到匹配 end 的行之间替换 sed '/start/,/end/s/old/new/g' file.txt # 删除空行 sed '/^$/d' file.txt逻辑说明:地址和命令之间不加空格,5s表示第 5 行执行替换。范围地址是“第一次匹配 start 到第一次匹配 end”,如果 end 没出现会一直匹配到文件末尾。参数说明:d是删除,p是打印(常配合-n只输出匹配行),a是追加,i是插入。
3.2 保持空间:sed 处理跨行数据的黑匣子
模式空间是当前行的工作区,保持空间是一块临时缓存。h把模式空间复制到保持空间,H追加,g反向复制,G追加。用它可以实现倒序、合并行等操作:
# 用保持空间把文件行倒序输出 sed -n '1!G;h;$p' file.txt # 把多行合并成一行,用逗号分隔 sed ':a;N;$!ba;s/\n/,/g' file.txt逻辑说明:第一条1!G表示第一行不追加保持空间,h每行覆盖保存,$p最后打印,效果是倒序。第二条用标签:a和N把下一行读入模式空间,$!ba表示不是最后一行就跳回a,最后把换行替换成逗号。参数说明:N会消耗下一行,处理大文件时内存占用会上升,几百 MB 的日志要谨慎。
-i的跨平台差异是个经典坑。GNU sed 支持-i不带参数,BSD/macOS 的 sed 要求-i ''。写脚本时可以用变量判断,或者统一用-i.bak规避:
# 兼容写法:先备份再替换 sed -i.bak 's/foo/bar/g' file.txt && rm -f file.txt.bak逻辑说明:-i.bak在 GNU 和 BSD 上都能工作,生成备份后手动删除,既安全又兼容。参数说明:如果确实不需要备份,GNU 下用-i,BSD 下用-i '',不要混用。
4. gawk:把文本当表格来算和格式化
4.1 gawk 的记录、字段和内置变量
gawk 是 awk 的 GNU 实现,核心模型是“记录(record)”和“字段(field)”。默认按行读记录,按空白拆字段,$1是第一列,$0是整行。内置变量NR是当前行号,NF是字段数,FS是输入分隔符,OFS是输出分隔符:
# 打印第一列和第三列 awk '{print $1, $3}' data.txt # 指定逗号为分隔符,打印第二列 awk -F',' '{print $2}' data.csv # 打印行号和字段数 awk '{print NR, NF, $0}' data.txt # 输出时用制表符分隔 awk -F',' 'BEGIN{OFS="\t"} {print $1, $2}' data.csv逻辑说明:-F','等价于BEGIN{FS=","},但写在命令行更简洁。BEGIN块在读取前执行,适合初始化OFS。参数说明:$NF是最后一个字段,$(NF-1)是倒数第二个,处理列数不固定的数据时很有用。
gawk 的条件和循环让它能直接做统计:
# 统计访问日志里每个 IP 的出现次数 awk '{count[$1]++} END{for (ip in count) print count[ip], ip}' access.log | sort -rn | head # 按状态码分组求和 awk '{sum[$9]++} END{for (code in sum) print code, sum[code]}' access.log # 过滤出第 5 列大于 100 的行 awk '$5 > 100 {print $0}' data.txt逻辑说明:count[$1]++用第一列做数组下标,END块在全部读完后遍历输出。$5 > 100是模式匹配,条件为真才执行动作。参数说明:gawk 的数组是关联数组,遍历顺序不保证,需要排序时管道接sort。
4.2 gawk 的格式化输出和字符串函数
gawk 的printf和 C 语言类似,适合生成对齐的报表。字符串函数gsub、substr、split、match在清洗数据时很实用:
# 格式化输出:左对齐 20 字符,右对齐 10 字符 awk '{printf "%-20s %10s\n", $1, $2}' data.txt # 把第一列的逗号替换成下划线 awk '{gsub(/,/, "_", $1); print $1}' data.csv # 按冒号拆分第一列,取第二部分 awk -F':' '{n=split($1, arr, ":"); print arr[2]}' data.txt # 截取子串:从第 2 个字符开始取 5 个 awk '{print substr($0, 2, 5)}' data.txt逻辑说明:printf的%-20s表示左对齐占 20 宽,%10s右对齐占 10 宽。gsub直接修改字段内容,返回替换次数。split把字符串拆成数组,返回元素个数。参数说明:substr的索引从 1 开始,不是 0,这点和很多语言不同。
gawk 还支持if-else、while、for和自定义函数,复杂逻辑不必硬塞进一行:
# 根据状态码分类输出 awk '{ if ($9 ~ /^2/) status = "成功"; else if ($9 ~ /^3/) status = "重定向"; else if ($9 ~ /^4/) status = "客户端错误"; else status = "服务端错误"; print status, $9 }' access.log | sort | uniq -c逻辑说明:~是正则匹配运算符,/^2/表示以 2 开头。if-else链让分类逻辑清晰。参数说明:gawk 的~和!~分别表示匹配和不匹配,右侧可以是正则字面量或字符串变量。
5. 避坑与排查:三条命令最容易翻车的地方
5.1 正则表达式在三种工具里的语法差异
现象:同一条正则,grep 能匹配,sed 报错,gawk 结果不对。原因:grep 默认 BRE,sed 默认 BRE,gawk 默认 ERE。+、?、|、()在 BRE 里需要转义,在 ERE 里不需要。解决:统一用grep -E、sed -E、awk默认 ERE,或者记住 BRE 的转义规则。写脚本时优先用-E,可读性更好。
5.2 sed -i 在 macOS 和 Linux 上行为不一致
现象:脚本在本地 macOS 跑得好,到 Linux 服务器上sed -i报错或生成奇怪文件。原因:BSD sed 的-i必须跟一个参数作为备份后缀,GNU sed 的-i后缀可选。解决:统一写sed -i.bak,然后rm -f *.bak;或者用perl -pi -e替代,跨平台更一致。
5.3 gawk 处理大文件时内存暴涨
现象:用awk '{arr[$1]++} END{...}'统计几个 GB 的日志,进程被 OOM kill。原因:关联数组把所有 key 存在内存里,key 数量大时内存线性增长。解决:先用sort | uniq -c做外部排序统计,或者用awk分片处理,再合并结果。如果必须用 gawk,考虑--profile观察内存,或者换用mawk等更轻量的实现。
5.4 grep 递归搜索时误入二进制文件和符号链接
现象:grep -r 'pattern' /卡死或输出乱码。原因:递归进入/proc、/sys或二进制文件,匹配到大量无意义内容。解决:加--include='*.log'限定文件类型,加-I忽略二进制文件,加--exclude-dir排除目录。生产环境搜索前先cd到目标目录,不要从根目录开始。
5.5 正则表达式中的贪婪匹配导致提取错误
现象:用grep -oE '".*"'提取引号内容,结果把整行都匹配了。原因:*是贪婪匹配,尽可能吃更多字符。解决:用非贪婪写法,但 grep 的 ERE 不支持非贪婪,需要改用-P的.*?,或者用[^"]*限定字符集。gawk 里可以用match($0, /"[^"]*"/)配合substr精确提取。
6. 把三条命令串成流水线:一个日志分析的完整实例
单条命令再熟,真正省时间的是把它们串起来。下面这个例子从 Nginx 访问日志里找出访问量最高的 10 个 IP,并统计它们的状态码分布。先看完整命令:
# 第一步:提取 IP 和状态码,输出 "IP 状态码" awk '{print $1, $9}' access.log > /tmp/ip_status.txt # 第二步:统计每个 IP 的总请求数,取前 10 awk '{count[$1]++} END{for (ip in count) print count[ip], ip}' /tmp/ip_status.txt \ | sort -rn | head -10 > /tmp/top_ips.txt # 第三步:对前 10 个 IP,统计各自的状态码分布 while read -r cnt ip; do echo "=== $ip (总请求 $cnt) ===" grep "^$ip " /tmp/ip_status.txt | awk '{code[$2]++} END{for (c in code) print c, code[c]}' | sort done < /tmp/top_ips.txt逻辑说明:第一步用 gawk 把日志压成两列,减少后续处理量;第二步用关联数组计数并排序取头部;第三步用while read逐行读取,grep精确匹配 IP 后交给 gawk 做状态码分组。参数说明:grep "^$ip "里的^和空格确保不会匹配到 IP 前缀相同的其他地址,比如10.0.0.1和10.0.0.10。
如果想进一步看每个 IP 的请求时间分布,可以在第一步多取一列时间戳,然后用 gawk 做分桶:
# 按小时统计请求量 awk -F'[:[]' '{print $3}' access.log | sort | uniq -c | sort -rn逻辑说明:Nginx 日志的时间格式是[dd/Mon/yyyy:HH:MM:SS,用-F'[:[]'把方括号和冒号都当分隔符,$3就是小时。参数说明:字段索引取决于日志格式,先用head -1 access.log | awk -F'[:[]' '{for(i=1;i<=NF;i++) print i, $i}'确认每一列是什么。
验证方法:拿一份已知结果的日志,手动数几行,对比脚本输出。我一般会先在小样本上跑,确认字段索引和正则没问题,再放到全量日志上。另一个习惯是每个中间结果都落盘到/tmp,出问题时能回看是哪一步偏了,而不是从头再跑一遍。
这套流水线的价值在于:grep 做精确过滤,sed 做格式清洗,gawk 做聚合计算,三者各司其职。真到线上排查,往往不需要写完整脚本,几条命令拼起来就能定位问题。希望帮到你。
本文还有配套的精品资源,点击获取