Skip to content

文本三剑客 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。磁盘顺序读比反复全量扫描快得多。