引言
服务器运维的日常,一半时间在和文本打交道:从几 GB 的日志里捞出错误、把配置批量改掉、把 JSON 接口响应统计成报表。grep 负责「找」、sed 负责「改」、awk 负责「算」、jq 负责「解」——这四件工具组合起来,几乎能覆盖所有文本处理需求,且无需写一行 Python。
本文按「定位与分工 → 逐个精讲 → 组合管道 → 实战」的顺序展开:先讲 grep 的正则与选项,再讲 sed 的行处理模型与替换删除插入,然后是 awk 的记录字段模型与关联数组统计,接着是 jq 处理 JSON 的过滤与变形,最后用真实日志场景把四者串成完整的分析流水线。
前置:Shell 脚本编程基础。日志来源与结构见 systemd-journald 与结构化日志,性能数据采集见 Linux 性能监控与调优。
目录
- 1. 文本处理三剑客:定位与分工
- 2. grep:正则与常用选项
- 3. sed:流编辑器的行处理模型
- 4. sed 实战:替换、删除与插入
- 5. awk 编程模型:记录、字段与模式动作
- 6. awk 实战:统计、聚合与关联数组
- 7. jq:JSON 处理利器
- 8. 组合管道:把工具串起来
- 9. 日志分析实战
- 10. 速查表
- 延伸阅读
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,m | n 到 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 N | awk '{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 瘦身再聚合排序取头部——这四句背熟,日常文本处理基本无需再查文档。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。