外观
文本三剑客 grep / sed / awk
三个工具各管一件事,混用是效率低下的根源:
| 工具 | 干什么 | 一句话记忆 |
|---|---|---|
| grep | 按模式筛选行 | 谁符合 |
| sed | 按行改写文本(删、换、插) | 改成什么样 |
| awk | 按列取数与统计 | 算出什么结果 |
场景:从日志里找出报错最多的类型
bash
grep -E 'ERROR|Exception' app.log | awk -F'[][]' '{print $2}' | sort | uniq -c | sort -rn | head -5输出:
text
312 2026-10-27 14:22:01.338 ERROR c.b.job.SyncCVJobData - connect timeout
148 2026-10-27 09:11:44.007 ERROR c.b.svc.OrderService - lock conflict
77 2026-10-27 16:02:19.884 WARN c.b.pool.HikariPool - connection leak
31 2026-10-27 11:30:02.115 ERROR c.b.job.MyScheduler - task rejected
9 2026-10-27 08:45:53.620 ERROR c.b.util.HttpUtil - read timed out解释:grep -E 先把候选行缩到几百条(先筛后算,别让 awk 处理整个 2G 文件);awk -F'[][]' 用方括号做分隔符切出时间列;sort | uniq -c | sort -rn 是"计数并倒排"的固定套路,head -5 取前几名。
计数场景优先 sort|uniq -c
sort | uniq -c 需要把数据全排一遍,超过百万行时改用 awk '{c[$0]++} END{for(k in c) print c[k],k}' | sort -rn,内存换时间,通常快 3~5 倍。
grep:把"找"做准
常用参数组合
bash
grep -rin "timeout" /var/log/app/ --include="*.log" --exclude-dir=archive| 参数 | 作用 | 为什么常用 |
|---|---|---|
-r | 递归目录 | 免得起 shell 循环 |
-i | 忽略大小写 | 日志里 Timeout/TIMEOUT 混用 |
-n | 显示行号 | 定位到代码行 |
-w | 整词匹配 | 搜 port 不会命中 support |
-l | 只列文件名 | 想知道"哪些文件里有" |
-c | 只计数 | 看量级不看内容 |
-o | 只输出匹配部分 | 配合 sort/uniq 做提取 |
-A3 -B1 -C5 | 后 3 行 / 前 1 行 / 上下 5 行 | 看异常堆栈必备 |
-v | 反向排除 | 过滤掉噪音行 |
--include/--exclude-dir | 限定范围 | 避免误扫归档目录 |
场景:抓完整异常堆栈
bash
grep -A 20 "NullPointerException" app.log | head -40输出:
text
java.lang.NullPointerException: Cannot invoke "com.bfcube.model.Order.getStatus()" because "order" is null
at com.bfcube.svc.OrderService.sync(OrderService.java:214)
at com.bfcube.job.SyncCVJobData.execute(SyncCVJobData.java:88)
... 12 more解释:Java 堆栈是多行的,grep 只给首行,必须用 -A 把后续行带出来。-A 20 是经验值——多数业务异常在 15 行内结束。
场景:提取所有 IP 并去重
bash
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | sort -u | wc -l解释:-o 只打印匹配到的部分而不是整行,这是"用 grep 提取字段"的关键;-E 用扩展正则省掉一堆反斜杠。
别用 grep 做数值比较
grep -v "^#" 这类文本过滤很擅长,但"取金额大于 100 的行"必须交给 awk:awk '$7 > 100'。用 grep 写数值条件只能靠穷举正则,既慢又错。
sed:按行改写
替换:分隔符与锚点
bash
sed -i.bak 's|/old/path|/new/path|g' /etc/nginx/conf.d/app.conf输出:无(就地修改);app.conf.bak 是自动留下的备份。
解释:替换路径类内容时用 s|a|b| 而不是 s/a/b/,省得给每个 / 加转义。-i.bak 是生产环境必带的写法——出问题一行 mv 就能回退。GNU sed(CentOS/Ubuntu)支持 -i 直接改,macOS/BSD 必须写 -i ''。
只替换第 2 次出现
bash
echo "a-b-c-d" | sed 's/-/|/2'输出:
text
a-b|c-d解释:末尾不加 g 就是"只换每行第一处",加数字则换第 N 处。改版本号、改重复字段时特别有用。
删除与插入
bash
sed -i '/^#/d;/^$/d' redis.conf # 删掉注释行和空行,看配置骨架
sed -i '/^maxmemory/a maxmemory-policy allkeys-lru' redis.conf # 在匹配行后追加解释:d 删除、a 追加到行后、i 插入到行前。改配置文件时先 sed -n '/^maxmemory/p' 确认匹配到了再动手。
-n 与 p:只打印想要的
bash
sed -n '10,20p' big.txt # 取第 10 到 20 行
sed -n '/ERROR/,/^\s*$/p' app.log # 从 ERROR 行打到空行解释:sed 默认打印每一行,-n 关掉默认打印,p 显式打印目标行——两者必须配合使用,只写 -n 会什么都不输出。
awk:按列取数与统计
字段与分隔符
bash
awk '{print $1, $NF}' access.log # 第一列和最后一列
awk -F: '{print $1, $3}' /etc/passwd # 指定分隔符
df -h | awk 'NR>1 {print $1, $5}' # NR>1 跳过表头输出(passwd 例):
text
root 0
bin 1
daemon 2解释:awk 默认按空白切列,$0 是整行、$NF 是最后一列、NR 是行号。NF 是"本行字段数",所以 $NF 自动适配列数变化的日志。
条件过滤与统计
bash
# 磁盘使用率超过 80% 的分区
df -hP | awk 'NR>1 {gsub(/%/,"",$5); if ($5+0 > 80) print $6, $5"%"}'输出:
text
/data 91%
/var/log 84%解释:gsub 去掉百分号,$5+0 强制转数值比较(不加 +0 会按字符串比,"9" 会大于 "84")。-P 让 df 不换行输出,否则设备名过长时列会错位——这是 df+awk 最常见的坑。
关联数组:去重、分组、求和
bash
awk '{cnt[$1]++} END{for (ip in cnt) print cnt[ip], ip}' access.log | sort -rn | head -3输出:
text
18422 10.220.45.145
9317 10.220.45.146
204 203.0.113.7解释:cnt[$1]++ 用第一列当键自动建哈希表,END{} 在文件读完后汇总。这一招能干掉 90% 的"分组统计"需求,不用写脚本。
实战:Nginx 日志的每分钟 QPS
bash
awk -F'\\[' '{split($2,t,":"); print t[1]":"t[2]}' access.log \
| sort | uniq -c | awk '{printf "%s %6d %.1f/s\n", $2, $1, $1/60}'输出:
text
27/Oct/2026:14 52841 880.7/s
27/Oct/2026:15 49120 818.7/s
27/Oct/2026:16 31077 517.9/s解释:-F'\\[' 用左方括号切列(awk 里正则字符要双反斜杠),split 再按冒号拆出"时:分";最后一段 awk 复用前面的计数算平均每秒请求数。
三件套选型
| 需求 | 用什么 | 理由 |
|---|---|---|
| 只看有没有、在哪一行 | grep | 最快,有 -F 固定串模式还能再快一档 |
| 改配置文件、批量替换 | sed | 原地改 + 自动备份 |
| 取列、算数、分组统计 | awk | 唯一有变量和条件的 |
| 多行上下文(堆栈) | grep -A/-B 或 sed 范围 | awk 处理跨行状态很别扭 |
大文件先缩小处理集
2G 日志不要一把梭。先 grep -c 看量级,再 grep 落到临时文件,最后对临时文件做 awk/sed。磁盘顺序读比反复全量扫描快得多。