【金丹·89】sed/awk文本三剑客日志分析和批量处理码农·修仙传 · 金丹期 · 第89篇我是玄芯散人带你从炼气修到大乘。境界标识╔══════════════════════════════════╗ ║ 金丹期 · 第89篇 ║ ║ sed/awk文本三剑客 ║ ║ grep找行 / sed改行 / awk切列 ║ ║ 正则匹配 日志统计 ║ ║ 预计阅读35 分钟 ║ ╚══════════════════════════════════╝修仙引入088 那一篇把 Makefile 聊清楚了编译这条流水线算是半自动。修真路不止编译这一条。还有一类更常见的脏活服务跑了一晚上access.log 多出 800MB老板扔过来一句「看看哪个 IP 在刷接口」运维给了 200 个配置文件要批量改一行参数CTO 想看这个季度哪个接口最常被调用。这种活写 C 程序太重写 Python 脚本又得先开个解释器。修真界里 Unix 祖师爷给弟子留了三把短刀grep、sed、awk。grep负责找出含关键字的行sed负责按条件替换或删行awk负责切字段做统计。这三把刀 1977 年传下来比修真界里任何新潮工具都能扛。本篇把后两把讲透正则打配合最后拿 nginx 日志当样本走一遍统计流程。修真比喻修真界里 grep 是寻矿弟子站在山头扫一眼就能锁定矿脉位置。sed 是凿器锤矿石被敲成器胚。awk 是点金炉按材料属性分拣熔铸。三把刀合起来就是一条完整的矿石加工线。硬核主体为什么是 sed/awk修真界的文本三剑客Unix 哲学第一条「一个程序只做一件事但做到极致」。修真界里 grep 找出行sed 改字删行awk 切字段统计——三者各管一摊靠|管道串起来干大事工具角色典型场景grep找出行在日志里捞包含某个关键字的行sed编辑行把所有old替换成new或按条件删行awk切列统计按分隔符切开每行对某列做聚合三把刀叠加起来的套路# 第一刀grep 捞出关心的行grep404access.log|\# 第二刀sed 把行改成想要的格式seds/.*: //|\# 第三刀awk 按 URL 聚合计数awk{ count[$1] } END { for (u in count) print count[u], u }|\# 第四步sort 排序sort-rn|head修真界里这一段五行命令顶得上一段 50 行的 Python 脚本。弟子会问那 Python 不是更快吗修真路里不是所有活都值得开脚本。像日志分析这种一次性任务三把刀配合管道几分钟搞定。Python 一启动解释器就花一秒、加载 pandas 又得十秒。修真界里三把刀是「一次性刀法」Python 是「开炉重铸」各有各的适用场景。修真比喻修真界里 grep 像寻矿弟子的灵识探针站在山头一指弟子就知道哪片山有矿。sed 是凿器锤凿完一块矿石就放下走人。awk 是点金炉按材料属性把器胚熔成法器。整条流水线串起来就是「原矿入、成品出」的完整加工线。sed 基本用法流编辑器一锤定形sed的全称是 Stream Editor流编辑器。它一次读一行处理完输出原文件不动除非加-i。这把凿器锤的设计哲学凿一下改一个字凿完就丢从不回头。命令格式sed [选项] 脚本 输入文件下面几个最常用的命令字符命令含义示例s/old/new/替换每行只替换第一个匹配sed s/foo/bar/ files/old/new/g全行替换globalsed s/foo/bar/g filed删除匹配的行sed /pattern/d filep打印匹配的行sed -n /pattern/p filei\text在行前插入sed i\插入的行 filea\text在行后追加sed a\追加的行 filec\text替换整行sed c\新行 filep命令默认会把所有行再输出一遍所以必须配-n安静模式-n告诉 sed「没叫我打印的别出声」。替换命令 sS是 sed 最常用的命令。它有四种变体# 只替换每行第一个匹配seds/foo/bar/file.txt# 全局替换每行所有匹配都换seds/foo/bar/gfile.txt# 忽略大小写seds/foo/bar/gifile.txt# 替换第 N 个匹配从 1 开始seds/foo/bar/2file.txt这条命令等价于 Python 的re.sub count 参数。区别是 sed 在流里逐行处理不把全文读进内存。1GB 的日志也能凿完。修真界里替换分隔符不一定是/。如果原文里有/可以换|或#当分隔符# 路径里有 /用 | 当分隔符seds|/usr/local|/opt|gfile.txt# 或用 #seds#/usr/local#/opt#gfile.txt这条细节常被忽略工程里改路径命令用得上。删除命令 d修真界里d命令删除匹配的行常配正则地址# 删掉所有空行sed/^$/dfile.txt# 删掉包含 DEBUG 的行sed/DEBUG/dapp.log# 删掉第 5 行sed5dfile.txt# 删掉 2-5 行sed2,5dfile.txt# 删掉从匹配 start 到文件末尾sed/start/,$dfile.txt这里^$是正则的「行首紧跟行尾」等价于「这一行没东西」。这条写日志清理的时候常用调试日志太多先grep DEBUG出来看再sed /DEBUG/d把 DEBUG 那一行剔掉。修真路上有个坑-i选项之前要带扩展名macOS 跟 Linux 不一样。修真界里 macOS 的 BSD sed 要求-i # Linux GNU sedsed-is/foo/bar/gfile.txt# macOS BSD sedsed-is/foo/bar/gfile.txt这条差异在跨平台脚本里一定要处理。地址范围修真界里 sed 的地址不一定是「匹配某行」可以用行号定位也可以用行号范围圈一段# 10 到 20 行sed10,20dfile.txt# 50 到结尾sed50,$dfile.txt# 从 #BEGIN 到 #END 的范围sed/#BEGIN/,/#END/dconfig.conf# 只处理奇数行步长sed1~2dfile.txt这条步长语法是 GNU sed 扩展macOS 的 BSD sed 不支持。跨平台要删奇数行改用 awk 处理awk NR%20 file.txt只保留偶数行等价于删除奇数行。修真比喻修真界里 sed 是流编辑器像弟子手持一把凿器锤站在传送带旁。修真界里传送带送来一块矿石弟子看一眼符纹地址符合的凿一下s/d/p/i/a不符合的直接传走。修真界里这条流水线永不回头每块矿石只凿一次。awk 基本用法点金炉切分字段awk名字取自三位创始人 Alfred Aho、Peter Weinberger、Brian Kernighan 三人姓氏首字母的组合A、W、K。修真界里它是一种「模式驱动的字段处理语言」。修真界里它的最小单位是「输入行 → 切字段 → 模式匹配 → 执行动作」。命令格式awk BEGIN{初始化} 模式{动作} END{收尾} 文件awk 处理文件时分三段走BEGIN 在读文件前跑一次正文对每行做模式匹配匹配就跑动作END 在文件读完跑一次。这三个段可以全省略只写中间的动作也行。一行里最常见的写法# 打印第 1 和第 3 列默认分隔符是空白awk{ print $1, $3 }file.txt# 打印每行的字段数awk{ print NF }file.txt# 打印行号awk{ print NR, $0 }file.txt这里$0是整行$1是第 1 列$2是第 2 列$NF是最后一列。内置变量awk 自带一组内置变量掌握了就能少写一半代码变量含义$0当前整行$1、$2…第 N 个字段NR当前行号Number of RecordNF当前行的字段数Number of FieldFS输入字段分隔符默认空白字符OFS输出字段分隔符默认空格RS输入记录分隔符默认换行ORS输出记录分隔符默认换行FILENAME当前文件名修改分隔符用-F或在 BEGIN 里设# 用冒号切/etc/passwd 用冒号awk-F:{ print $1, $3 }/etc/passwd# 用 BEGIN 改分隔符awkBEGIN{ FS:; OFS| } { print $1, $3 }/etc/passwd模式与动作awk 的核心是「模式 动作」结构。模式能用正则写也能用比较表达式写。不写模式默认对所有行生效# 模式行包含 ERRORawk/ERROR/ { print $0 }app.log# 模式第 3 列大于 100awk$3 100 { print $1, $3 }data.txt# 模式第 1 列等于 404awk$1 404 { print }log.txt# 多个模式用逗号连范围匹配awk/start/,/end/ { print }log.txt修真界里print不带参数默认打印$0。修真界里printf用 C 语言格式化# 格式化输出awk{ printf %-15s %5d\n, $1, $3 }data.txt关联数组awk 的杀手锏修真界里 awk 最大的杀手锏是「关联数组」associative array可以拿字符串当下标# 统计每行第 1 列出现次数awk{ count[$1] } END { for (k in count) print count[k], k }data.txt修真界里count[$1]意思是把第 1 列当下标访问一次自增一次。修真界里 END 段遍历数组打印。修真界里这种「一维表」统计比 Python 的collections.Counter还顺手。修真界里想按值排序怎么办awk 本身不排序要靠管道# 统计 排序 取前 10awk{ count[$1] } END { for (k in count) print count[k], k }data.txt|sort-rn|headsort -rn是按数字倒序排序head取前 N 行。这一串就是「前 10 名频次」统计的标准写法。修真比喻修真界里 awk 是点金炉弟子把矿石按品类字段倒进对应的格子数组。炉子自动点数最后弟子在 END 时打开格子按品类收金。这套「自动分拣计数」是 1977 年 Aho 三人组的遗产比 Python 的 Counter 还早 25 年。正则表达式基础识丹纹的火眼金睛修真界里 sed 和 awk 都依赖正则表达式定位文字。正则不是语言是「字面模式匹配」的小语法。修真界里几个最常用的元字符元字符含义.任意单个字符除换行*前一个字符 0 次或多次前一个字符 1 次或多次?前一个字符 0 次或 1 次^行首$行尾[abc]字符集a、b、c 之一[^abc]非字符集[a-z]范围{n}前一个字符恰好 n 次{n,}前一个字符至少 n 次{n,m}前一个字符 n 到 m 次|或GNU 扩展\b词边界()分组捕获\1、\2反向引用修真界里这几个元字符的组合能描述现实世界大部分「文本形状」。下面几个具体的例子先看一下后面实战会用# 匹配 IP 地址简化版grep-E([0-9]{1,3}\.){3}[0-9]{1,3}access.log# 匹配邮箱grep-E[a-zA-Z0-9][a-zA-Z0-9]\.[a-z]users.txt# 匹配时间戳HH:MM:SSgrep-E[0-9]{2}:[0-9]{2}:[0-9]{2}app.log# 匹配 URL 中的路径sed-nEs|.*GET (/[^ ]*).*|\1|paccess.log这几个例子留个印象就行下面实战再展开。正则有两套语法BREBasic Regular Expression和 EREExtended Regular Expression。BRE 里?|()都是字面字符要变功能得加\转义。ERE 里?|()直接是元字符。grep -E、sed -E、awk默认用 ERE。grep、sed不带-E用的是 BRE。# BRE要转义grep\(foo\|bar\)file.txt# ERE直接用grep-E(foo|bar)file.txt工程里推荐都用 ERE省一半反斜杠。修真比喻修真界里正则像弟子修炼的火眼金睛。修真界里看一堆符纹字符能识别「这个纹路代表 IP 地址」「这个纹路代表邮箱」。修真界里元字符就是金睛的种类.表示任意单字符*和表示重复次数[]是这一类纹路。修真界里金睛一亮纹路对应的实体立刻浮出来。实战分析 nginx 访问日志修真界里这一节拿真实 nginx 日志走一遍。修真界里 nginx 默认 access log 格式长这样192.168.1.10 - - [29/Aug/2026:10:23:45 0800] GET /api/user HTTP/1.1 200 1024 https://example.com/ Mozilla/5.0 ...这一行字段顺序固定先放客户端 IP 和占位符再放时间和请求行再放状态码和响应字节最后挂 Referer 和 User-Agent。括号里是字段含义。修真界里 nginx 配置里用的是变量名log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent;这个默认格式是 nginxcombined类型工业界 99% 的网站都用它。为演练先造一段样本日志# sample.logcat/tmp/sample.logEOF 192.168.1.10 - - [29/Aug/2026:10:00:01 0800] GET /index.html HTTP/1.1 200 1024 - Mozilla/5.0 192.168.1.20 - - [29/Aug/2026:10:00:02 0800] GET /api/user HTTP/1.1 200 512 - curl/7.0 192.168.1.10 - - [29/Aug/2026:10:00:03 0800] POST /api/login HTTP/1.1 401 128 - Mozilla/5.0 192.168.1.30 - - [29/Aug/2026:10:00:04 0800] GET /api/user HTTP/1.1 404 0 - Mozilla/5.0 192.168.1.10 - - [29/Aug/2026:10:00:05 0800] GET /api/order HTTP/1.1 200 2048 - Mozilla/5.0 192.168.1.20 - - [29/Aug/2026:10:00:06 0800] GET /api/user HTTP/1.1 200 512 - curl/7.0 192.168.1.10 - - [29/Aug/2026:10:00:07 0800] GET /api/order HTTP/1.1 200 2048 - Mozilla/5.0 192.168.1.40 - - [29/Aug/2026:10:00:08 0800] GET /api/product/123 HTTP/1.1 404 0 - Mozilla/5.0 192.168.1.20 - - [29/Aug/2026:10:00:09 0800] POST /api/login HTTP/1.1 200 256 - curl/7.0 192.168.1.10 - - [29/Aug/2026:10:00:10 0800] GET /index.html HTTP/1.1 304 0 - Mozilla/5.0 EOFnginx 日志默认用空格切不干净——时间字段里有空格、引号里有空格。直接awk { print $1 }拿到的是 IP但print $7拿到的是1024不是预期的请求行。解法是自定义分隔符# 用双引号当分隔符请求行是带双引号的一段awk-F{ print $2 }/tmp/sample.log|head# 输出# GET /index.html HTTP/1.1# GET /api/user HTTP/1.1# ...awk -F是用双引号当分隔符。修真界里日志被切成三段第 1 段是 IP 和时间第 2 段是请求行第 3 段是状态码、字节大小、Referer 来源、User-Agent 这些尾巴。这一招是分析 nginx 日志的「起手式」。任务 1TOP 10 访问 IP这个问题就用「awk 关联数组 sort」awk{ count[$1] } END { for (ip in count) print count[ip], ip }/tmp/sample.log|sort-rn|head-10输出4 192.168.1.10 3 192.168.1.20 2 192.168.1.30 1 192.168.1.40$1默认就是 IP空格切分第 1 段就是 IP。这一条命令跑在真实日志上输出就是「哪个 IP 在刷接口」。这一招还能扩展把 IP 替换成 URL看 TOP URL# 用双引号切$2 是请求行awk-F{ count[$2] } END { for (u in count) print count[u], u }/tmp/sample.log|sort-rn|head-10输出2 GET /api/order HTTP/1.1 2 GET /api/user HTTP/1.1 2 GET /index.html HTTP/1.1 1 POST /api/login HTTP/1.1 1 GET /api/product/123 HTTP/1.1请求行里把 URL 抠出来用 sed# 把请求行换成只剩 URLGET 和 POST 后面的路径# 注意分隔符用 避免和 ERE 中的 | 算符冲突sed-nEs.*(GET|POST) (/[^ ]*).*\2p/tmp/sample.log|sort|uniq-c|sort-rn|headsed -E是 ERE 模式。(GET|POST) (/[^ ]*)匹配「方法空格路径」。\1是方法\2是路径。uniq -c是「去重计数」这一招是统计频次的「瑞士军刀」。任务 2找出所有 404 请求awk 的$9是状态码默认空格切分下依次是 IP 段、ident 占位、user 名、时间段、请求行、状态码、字节数、referer 链、UA 标识awk$9 404 { print $7 }/tmp/sample.log这条命令输出所有 404 请求的 URL。再配 grep 抓详情# 看 404 详情awk$9 404/tmp/sample.log# 看 4xx 和 5xx 错误awk$9 ~ /^[45]/ { print }/tmp/sample.log-E模式$9 ~ /^[45]/匹配 4xx 或 5xx。~是 awk 里的「匹配」运算符[45]比4|5更直白。任务 3统计总流量$10是响应字节数这个跟 nginx 配置有关。流量累加用 awk 累加器# 总流量字节awk{ total $10 } END { print total, bytes }/tmp/sample.log这一条命令输出全部请求的响应总字节数。想转成 KB/MBawk{ total $10 } END { printf %.2f MB\n, total/1024/1024 }/tmp/sample.logprintf跟 C 语言一样用。%.2f是保留两位小数。任务 4找访问频次最高的 IP 时间段这种任务要结合时间字段。时间字段是$4带方括号先把它提取出来# 提取 IP 和小时time_local 格式[29/Aug/2026:10:00:01 0800]awk-F[:/]{ ip$1; hour$5 } { print hour, ip }/tmp/sample.logawk -F[:/]是用冒号和斜杠当分隔符。这一招把一行切成多段$1192.168.1.10 - - [29$2Aug$32026$410$500$601 0800] GET ...这一招是把日志字段「按时间维度切片」的标准做法。这一节所有命令合起来就是一条「管道流水线」# 完整流水线TOP 10 错误 URLawk$9 ~ /^[45]//tmp/sample.log|\sed-nEs.*(GET|POST) (/[^ ]*).*\2p|\sort|uniq-c|sort-rn|head-10这条流水线跑下来弟子就能给老板交差「昨晚 4xx 错误集中在 /api/login 路径建议查认证逻辑。」这一段就是从海量日志里淘金。修真比喻修真界里分析 nginx 日志像「灵脉巡查」。弟子沿灵脉走grep/sed 过滤数每根灵脉的流量awk 统计最后给长老报一份灵脉图sort head。这一套三把刀配合的活师徒之间传了几百年没换过。输出三把刀流水线输入access.log百万行原始日志grep 探针捞出关心的行sed 凿器锤改字或删行或抽字段awk 点金炉切字段做聚合统计TOP IP 排名错误 URL 列表流量统计报表修真比喻这张图是「矿脉加工流水线」。access.log 是原矿三把刀是加工站最后吐出的是「灵脉图」。每把刀只干一件事但流水线一开就是「成品出」。修仙术语对照表修仙术语技术现实本篇位置三把短刀grep / sed / awk 文本三剑客文本三剑客定位寻矿弟子grep 按模式找行文本三剑客定位凿器锤sed 流编辑器sed 基本用法矿石过传送带sed 一行行处理sed 基本用法点金炉awk 字段处理器awk 基本用法按品类分拣awk 字段分割1 / 1/1/NFawk 基本用法自动点数awk 关联数组 count[k]awk 关联数组火眼金睛正则表达式正则基础识丹纹正则匹配字符串正则基础任意纹路.元字符正则基础重复多次*?元字符正则基础纹路种类[][^]字符集正则基础灵脉巡查日志分析nginx 日志实战淘金流水线grepsedawk 管道nginx 日志实战灵脉图TOP IP / 错误 URL / 流量报表nginx 日志实战进阶条件能用sed s/old/new/g file完成单文件全文替换能用sed -i s/.../.../g *.conf批量改多个配置文件处理 macOS/Linux 的-i差异能用awk { print $1, $NF } file按字段切分并打印能用awk -F: { print $1 } /etc/passwd自定义分隔符处理冒号分隔的文件能用awk { count[$1] } END { for (k in count) print count[k], k }写一维频次统计能用sed -nE s|.*GET (/[^ ]*).*|\1|p access.log抽出所有 GET 请求的 URL能用awk $9 404 access.log过滤 nginx 错误请求能用grep -E ([0-9]{1,3}\.){3}[0-9]{1,3} file匹配 IP 地址区分 BRE/ERE 写法这些条件全通过sed/awk 这一关就算破了。修真路不止下一篇是 090 金丹期毕业能读懂任何系统级代码。弟子把这一摞功法串起来时眼界能拔多高毕业标准怎么列下一篇见分晓。下期预告 互动下一篇090 金丹期毕业能读懂任何系统级代码。这一篇是把 061 到 089 的全部金丹期功法做一次收束。像编译原理、链接加载这类散落的功法弟子要把它们串起来。毕业后应该具备什么能力毕业标准怎么定下一篇会列一份「金丹期毕业自测清单」。互动话题你修真路上写过最长的 awk 脚本有多少行用 awk 干过哪些让你拍大腿的活日志分析时你用 Python pandas 还是直接上三把刀两套工具的适用边界在哪落款本文是「码农修仙传」系列第89篇。系列导航见 xren.ren