《文本处理三剑客:grep、sed、awk 与 jq 实战》

系统讲解 Linux 文本处理核心工具:grep 的正则表达式与常用选项、sed 的行处理模型与替换删除插入实战、awk 的记录字段模型与关联数组统计聚合、jq 处理 JSON 的过滤变形与构造,以及如何用管道把四者组合成日志分析利器,附带大量可直接复制复用的实战配方与速查表,覆盖访问日志分析与配置审计等真实场景。

引言

服务器运维的日常,一半时间在和文本打交道:从几 GB 的日志里捞出错误、把配置批量改掉、把 JSON 接口响应统计成报表。grep 负责「找」、sed 负责「改」、awk 负责「算」、jq 负责「解」——这四件工具组合起来,几乎能覆盖所有文本处理需求,且无需写一行 Python。

本文按「定位与分工 → 逐个精讲 → 组合管道 → 实战」的顺序展开:先讲 grep 的正则与选项,再讲 sed 的行处理模型与替换删除插入,然后是 awk 的记录字段模型与关联数组统计,接着是 jq 处理 JSON 的过滤与变形,最后用真实日志场景把四者串成完整的分析流水线。

前置:Shell 脚本编程基础。日志来源与结构见 systemd-journald 与结构化日志,性能数据采集见 Linux 性能监控与调优。


目录


1. 文本处理三剑客:定位与分工

grep 按行筛选、sed 按行改写、awk 按字段计算——三者都遵循「流式、逐行、可组合」的 Unix 哲学。

输入流 → [grep 筛选行] → [sed 改写行] → [awk 计算字段] → 输出
              ↑ 找                ↑ 改              ↑ 算
工具核心模型最擅长典型场景
grep按行匹配正则筛选捞错误、找配置
sed按行执行编辑命令替换/删除/插入批量改配置
awk按字段处理记录统计/聚合汇总报表
jq按 JSON 路径处理解析/变形处理 API 响应
# 一个最小示例:找出访问量前 3 的 IP
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -3

一句话:「grep 找、sed 改、awk 算、jq 解」——记住这句分工,遇到文本任务先想「我需要哪一步」,再选工具,绝大多数问题都有现成解法。


2. grep:正则与常用选项

grep 逐行匹配正则,命中即输出整行;它的价值一半在正则,一半在选项。

基础与常用选项:

grep 'error' app.log                 # 基本匹配
grep -i 'error' app.log              # 忽略大小写
grep -v 'debug' app.log              # 反向:不含 debug 的行
grep -c 'error' app.log              # 计数
grep -n 'error' app.log              # 显示行号
grep -r 'timeout' /etc/              # 递归目录
grep -l 'pattern' *.conf             # 只列文件名
grep -A3 -B1 'error' app.log         # 显示上下文 3 行/1 行
grep -E 'error|warn' app.log         # 扩展正则

正则元字符(ERE,-E):

元字符含义示例
.任意单字符a.c 匹配 abc
*前项 0 次或多次ab*c
+前项 1 次或多次ab+c
?前项 0 或 1 次ab?c
^ / $行首 / 行尾^root
[]字符集[0-9]
()分组(ab)+
|或cat|dog
# 匹配 IPv4(简化)
grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log
# 匹配时间戳行
grep -E '^\[?[0-9]{4}-[0-9]{2}-[0-9]{2}' app.log
# 排除空行与注释
grep -vE '^\s*(#|$)' config.conf

一句话:默认用 -E(ERE)而非 BRE——+、?、|、() 在 ERE 下无需转义,可读性高得多;只有需要反向引用时才回头用 BRE。


3. sed:流编辑器的行处理模型

sed 对输入流逐行执行「地址 + 命令」,默认把结果打到标准输出——理解「地址」是掌握 sed 的关键。

对每一行:
  地址匹配吗?──是──→ 执行命令(p/d/s/i/a/c/y…)
        │
        否
        ↓
  输出(除非 -n)

地址与命令:

sed 's/old/new/' file            # 替换每行第一个
sed 's/old/new/g' file           # 全局替换
sed -n '5,10p' file              # 只打印 5~10 行(-n 抑制默认输出)
sed '3d' file                    # 删除第 3 行
sed '/^#/d' file                 # 删除注释行
sed -i 's/foo/bar/g' file        # 原地修改(-i)
sed -i.bak 's/foo/bar/g' file    # 原地修改并备份为 file.bak
地址形式含义示例
n第 n 行5d
n,mn 到 m 行2,5p
$最后一行$d
/re/匹配正则的行/error/d
/re1/,/re2/从匹配 re1 到 re2/BEGIN/,/END/p
!取反/^#/!d

常用命令:

sed -n '1,5p' file               # 打印前 5 行
sed 's/^/PREFIX: /' file         # 每行行首加前缀
sed '2i\inserted line' file      # 第 2 行前插入
sed '2a\appended line' file      # 第 2 行后追加
sed 's/[[:space:]]\+/ /g' file   # 压缩连续空白

一句话:sed 的「地址」就是「在哪几行动手」——sed -n '/BEGIN/,/END/p' 能像 awk 一样截取区间,这是它最被低估的能力。


4. sed 实战:替换、删除与插入

生产里 sed 的三大用途:批量改配置、清理数据、按区间提取——配 -i 前务必先预览。

# 1. 批量改配置(先预览,再 -i)
sed 's/listen 80;/listen 8080;/' nginx.conf          # 预览
sed -i.bak 's/listen 80;/listen 8080;/' nginx.conf   # 执行并备份
# 2. 注释掉某行
sed -i 's/^\(PermitRootLogin\)/#\1/' sshd_config
# 3. 取消注释
sed -i 's/^#\(PermitRootLogin\)/\1/' sshd_config
# 4. 在匹配行后插入
sed -i '/^\[mysqld\]/a\max_connections = 500' my.cnf
# 5. 删除匹配区间
sed -i '/^# BEGIN MANAGED/,/^# END MANAGED/d' config

分组引用与大小写转换:

# 交换两列(用分组 + 反向引用)
echo "name: value" | sed 's/\(.*\): \(.*\)/\2 = \1/'
# 提取括号内容
echo "id(12345)" | sed -n 's/.*(\([0-9]*\)).*/\1/p'
# 转大写 / 小写
echo "hello" | sed 's/.*/\U&/'
echo "HELLO" | sed 's/.*/\L&/'

多命令与脚本文件:

# 多个 -e 或分号分隔
sed -e 's/foo/bar/' -e 's/baz/qux/' file
sed 's/foo/bar/; s/baz/qux/' file
# 用脚本文件
sed -i -f fix.sed file        # fix.sed 内每行一条命令
场景命令
全局替换sed -i 's/a/b/g' f
注释行sed -i 's/^\(X\)/#\1/' f
取消注释sed -i 's/^#\(X\)/\1/' f
删除空行sed -i '/^$/d' f
区间提取sed -n '/A/,/B/p' f
行首/行尾追加sed 's/^/P/' f、sed 's/$/S/' f

一句话:sed -i 前永远先跑一遍不带 -i 的——sed 的替换一旦写错可能改坏整份配置,-i.bak 是最后的后悔药。


5. awk 编程模型:记录、字段与模式动作

awk 是一行一「记录」、空白分隔成「字段」的微型编程语言,结构是「模式 { 动作 }」——模式决定「对哪些行」,动作决定「做什么」。

输入行 ──→ 按 FS 拆分为 $1 $2 ... $NF(NR 行号,NF 字段数)
              │
       模式匹配?──否──→ 跳过
              │是
              ↓
        执行 { 动作 }
              │
        END 块 → 汇总输出

内建变量:

变量含义
$0整行
$1..$n第 n 个字段
$NF最后一个字段
NF字段数
NR当前行号(跨文件累计)
FS输入分隔符
OFS输出分隔符
awk '{print $1, $3}' file                 # 打印第 1、3 列
awk -F: '{print $1}' /etc/passwd          # 指定分隔符
awk -F: '$3 >= 1000 {print $1}' /etc/passwd   # 条件过滤
awk '/error/ {print $0}' app.log          # 模式匹配
awk 'NR==1 {print}' file                  # 只第一行
awk 'END {print NR}' file                 # 统计行数
awk '{sum += $3} END {print sum}' file    # 求和

BEGIN / END 块:

awk 'BEGIN {FS=":"; OFS="\t"} {print $1, $3} END {print "done"}' /etc/passwd

一句话:awk 的「模式」就是「WHERE」,{动作} 就是「SELECT」——awk '$3>100 {s+=$4} END{print s}' 读起来就是一条 SQL,理解这层映射就掌握了一半。


6. awk 实战:统计、聚合与关联数组

awk 的杀手锏是关联数组:arr[key]++ 一行就能做分组统计,配合 for (k in arr) 输出——这是 grep 和 sed 做不到的。

# 1. 统计状态码分布
awk '{print $9}' access.log | sort | uniq -c | sort -rn
# 2. 用关联数组统计每个 IP 的请求数
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn | head
# 3. 求各接口平均响应时间
awk '{sum[$7] += $NF; n[$7]++} END {for (u in sum) printf "%s %.2fms\n", u, sum[u]/n[u]}' access.log
# 4. 找出响应时间超过 1s 的请求
awk '$NF > 1000 {print $1, $7, $NF}' access.log

多条件与格式化输出:

# 5xx 且耗时 > 500ms 的请求
awk '$9 ~ /^5/ && $NF > 500 {print}' access.log
# 按小时统计流量
awk -F'[:[]' '{h[$2]++} END {for (k in h) print k, h[k]}' access.log | sort
# 格式化报表:printf 对齐
awk -F: 'BEGIN {printf "%-20s %s\n", "USER", "UID"}
  $3>=1000 {printf "%-20s %d\n", $1, $3}' /etc/passwd
需求awk 写法
求和{s+=$1} END{print s}
计数{c++} END{print c}
分组计数{a[$1]++} END{for(k in a) print k,a[k]}
分组求和{a[$1]+=$2} END{...}
最大值$1>m{m=$1} END{print m}
条件统计$3>100{c++} END{print c}

一句话:「awk 关联数组 = 命令行里的 GROUP BY」——需要分组、去重、求和时,先想 awk 数组,通常一行就能替代一段脚本。


7. jq:JSON 处理利器

jq 是 JSON 的 sed+awk:用路径表达式 .a.b[0] 取值,用管道 | 变形,用 -r 去引号。

# 基本取值
echo '{"name":"web","port":8080}' | jq '.name'
jq '.port' config.json
jq '.items[0].id' data.json          # 数组索引
jq '.items[]' data.json              # 遍历数组
jq '.items[] | select(.status=="up")' data.json   # 过滤
jq -r '.name' data.json              # 原始字符串(去引号)

常用操作:

# 构造新对象
jq '{host: .name, port: .port}' config.json
# 映射数组
jq '[.items[] | {id, name}]' data.json
# 聚合
jq '[.items[].value] | add' data.json          # 求和
jq '.items | length' data.json                 # 长度
jq 'sort_by(.time)' data.json                  # 排序
表达式作用
.foo取字段
.[]遍历数组/对象
select(cond)条件过滤
add / length求和 / 长度
sort_by(.k)排序
group_by(.k)分组
-r输出原始字符串
-c紧凑输出
# 实战:从 API 响应里统计各状态数量
curl -s https://api.example.com/hosts | \
  jq -r '.hosts[] | .status' | sort | uniq -c

一句话:jq 的核心是「路径 + 管道 + 构造」——先 .[] 展开、再 select() 过滤、最后 {...} 构造,这套三板斧能覆盖 80% 的 JSON 处理。


8. 组合管道:把工具串起来

四件工具的真正威力在管道里:grep 缩小范围 → awk 抽字段 → sort/uniq 聚合 → sed/jq 美化。

# 经典五连:Top 10 访问 IP
grep -v '^#' access.log | \
  awk '{print $1}' | \
  sort | uniq -c | sort -rn | head -10

# 找出错误最多的接口
awk '$9 ~ /^5/' access.log | \
  awk '{print $7}' | sort | uniq -c | sort -rn | head

# 实时监控错误(tail -f 配合 grep 高亮)
tail -f app.log | grep --line-buffered -iE 'error|exception'

管道中的性能与正确性要点:

要点说明
尽早过滤grep 放前面,减少后续数据量
--line-buffered实时管道必加,否则 grep 缓冲导致无输出
sort -u 优于 sort | uniq省一次遍历
LC_ALL=C加速排序(按字节而非本地化)
awk 一次完成能 awk 一步搞定就别串多个进程
# 用 awk 一次替代 grep|awk|sort|uniq(更少进程、更快)
LC_ALL=C awk '/ERROR/ {c[$5]++} END {for (k in c) print c[k], k}' big.log | sort -rn

一句话:管道里「先瘦身再干活」——让 grep 在最前面砍掉 99% 的行,后面每一步都只处理剩余数据,比堆更多 CPU 有效得多。


9. 日志分析实战

把工具组合到真实场景:访问日志分析、错误排查、配置审计——每个配方都能直接复制使用。

# 1. Nginx 访问日志:Top 10 URL(按请求数)
awk '{print $7}' access.log | sort | uniq -c | sort -rn | head -10
# 2. 各状态码占比
awk '{print $9}' access.log | sort | uniq -c | sort -rn | \
  awk '{sum+=$1; a[$2]=$1} END {for (k in a) printf "%s %6.2f%%\n", k, a[k]/sum*100}'
# 4. 每分钟请求量(找流量尖峰)
awk -F'[:[]' '{print $2":"$3}' access.log | uniq -c | sort -rn | head

错误排查配方:

# 5. 统计各类异常出现次数
grep -oE '[A-Za-z]+Exception' app.log | sort | uniq -c | sort -rn
# 6. 提取异常前后的上下文
grep -n -A5 'OutOfMemory' app.log | head -50
# 7. 找出慢请求(>1s)并排序
awk '$NF > 1000 {print $NF, $1, $7}' access.log | sort -rn | head

配置审计配方:

# 9. 列出所有未注释的有效配置项
grep -vE '^\s*(#|$)' nginx.conf
# 10. 找出所有监听端口
grep -rE 'listen\s+[0-9]+' /etc/nginx/ | sed 's/.*listen\s*//'
场景配方
Top Nawk '{print $k}' f | sort | uniq -c | sort -rn | head
异常统计grep -oE '[A-Za-z]+Exception' f | sort | uniq -c
慢请求awk '$NF>1000' f | sort -rn
时间窗口awk '/HH:MM/ && /ERROR/' f | wc -l

一句话:日志分析的套路是「切分 → 过滤 → 聚合 → 排序 → 取头部」——五步走完,90% 的问题都能定位;把常用配方存成 shell 函数,下次一条命令搞定。


10. 速查表

需求命令
忽略大小写查找grep -i 'err' f
反向匹配grep -v 'debug' f
显示上下文grep -A3 -B1 'err' f
扩展正则grep -E 'a|b' f
只输出匹配grep -oE '[0-9]+' f
全局替换sed -i 's/a/b/g' f
删除注释行sed -i '/^#/d' f
区间提取sed -n '/A/,/B/p' f
行首加前缀sed 's/^/P: /' f
打印某列awk '{print $3}' f
条件过滤awk '$3>100' f
分组计数awk '{a[$1]++} END{for(k in a) print k,a[k]}' f
求和awk '{s+=$1} END{print s}' f
jq 取值jq '.a.b' f
jq 过滤jq '.[] | select(.x==1)' f
Top N... | sort | uniq -c | sort -rn | head

一句话记忆:grep 找、sed 改、awk 算、jq 解;awk 关联数组就是命令行里的 GROUP BY;管道里先 grep 瘦身再聚合排序取头部——这四句背熟,日常文本处理基本无需再查文档。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「linux」更多文章

  1. 《Linux 故障排查工具箱:从 strace 到火焰图》
  2. 《磁盘加密与 LUKS 密钥管理实战》
  3. 《Linux 时间同步:chrony、NTP 与高精度时间实践》