grep、sed 和 awk:Linux 终端文本处理示例
grep 按模式查找行(grep -rn error /var/log),sed 在文本流或文件中替换内容(sed -i 's/old/new/g' file),awk 按列处理并统计(awk '{print $1}' access.log)。三者通过管道组合,可以替代大量脚本:grep 过滤行,sed 修改文本,awk 提取字段并汇总。
grep、sed 和 awk 是 Linux 中三个经典的文本处理工具。grep 按模式查找行,sed 不必打开编辑器就能替换和删除文本流中的内容,而 awk 可以把每一行拆分成列并进行计算。下面是 grep sed awk 的实用示例,覆盖分析日志、检查配置文件和处理命令输出时真正会用到的场景,并列出常见错误。
什么时候用哪个工具
三个工具都逐行读取文本,并把结果输出到标准输出,因此可以很方便地用 | 连接起来。区别在于每个工具最擅长解决的任务。
| 任务 | 工具 | 示例 |
|---|---|---|
| 查找包含某段文字的行 | grep | grep ERROR app.log |
| 替换文字 | sed | sed 's/http:/https:/g' file |
| 按行号显示或删除行 | sed | sed -n '10,20p' file |
| 提取某一列 | awk | awk '{print $1}' access.log |
| 求和、求平均、分组统计 | awk | awk '{s+=$10} END {print s}' |
简单的规则:只需要过滤就用 grep;需要修改文本就用 sed;文本中有列或需要算术运算就用 awk。
grep:按模式查找行
grep 基础示例
# 文件中包含 error 一词的行
grep error /var/log/syslog
# 忽略大小写:Error、ERROR、error
grep -i error app.log
# 显示行号
grep -n "Connection refused" app.log
# 递归搜索目录,并显示文件名
grep -rn "DB_HOST" /etc/myapp/
# 只列出有匹配内容的文件名
grep -rl "TODO" src/
# 反向匹配:排除注释和空行之外的所有内容
grep -v -e '^#' -e '^$' /etc/ssh/sshd_config
# 统计匹配行的数量
grep -c " 500 " access.log
参数 -w 按整词匹配:grep -w log 不会匹配到 login。参数 -F 关闭正则表达式,按字面字符串搜索,适用于模式中含有点号、括号或星号的情况。
匹配行的上下文
日志中的错误通常只看一行很难理解,还需要看到引发它的堆栈或请求。为此可以使用上下文参数:
# 匹配行之后的 3 行(After)
grep -A 3 "Traceback" app.log
# 匹配行之前的 2 行(Before)
grep -B 2 "OOM" /var/log/kern.log
# 匹配行前后各 5 行(Context)
grep -C 5 "segfault" /var/log/syslog
grep 用一行 -- 分隔各个匹配组,因此输出很容易阅读。
grep 与正则表达式
grep 默认支持基本正则表达式(BRE)。加上 -E 后启用扩展正则(ERE),此时 +、?、| 和括号无需反斜杠即可使用。
| 写法 | 含义 | 示例 |
|---|---|---|
^ | 行首 | grep '^root' /etc/passwd |
$ | 行尾 | grep 'bash$' /etc/passwd |
. | 任意单个字符 | grep 'h.t' |
* | 重复 0 次或多次 | grep 'ab*c' |
+(ERE) | 重复 1 次或多次 | grep -E '[0-9]+' |
|(ERE) | 或 | grep -E 'error|warn' |
[...] | 集合中的任意一个字符 | grep '[Ee]rror' |
# 包含错误或警告的行
grep -E "ERROR|WARN" app.log
# 只提取日志中的 IP 地址,每行一个(-o 表示只输出匹配的部分)
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log
grep -P:提取两段已知文本之间的值
GNU grep 提供了 -P 参数,支持 Perl 风格的正则。借助 \K(丢弃目前为止匹配的内容)和前瞻断言,可以只提取其中的值:
$ echo 'user_id=4821 status=ok' | grep -oP 'user_id=\K[0-9]+'
4821
$ grep -oP '"version":\s*"\K[^"]+' package.json
2.4.1
并非所有 grep 版本都支持 -P(例如 macOS 自带的 BSD grep 就没有),因此在需要跨平台的脚本中最好使用 sed 或 awk。
脚本条件中的 grep -q
grep -q 不输出任何内容,只返回退出码:0 表示找到,1 表示没找到。这样可以直接把检查写进 if 中:
if grep -q "^PermitRootLogin no" /etc/ssh/sshd_config; then
echo "root 登录已禁用"
fi
这比把输出保存到变量再判断是否为空更简洁,而且 grep 在第一次匹配后就会停止。
sed:替换与删除文本
替换:命令 s
sed 最常用的命令是 s/模式/替换内容/标志。不加 g 标志时,每行只替换第一处匹配。
# 输出替换后的文本(文件本身不变)
sed 's/foo/bar/' file.txt
# 替换一行中的所有匹配
sed 's/foo/bar/g' file.txt
# 忽略大小写(GNU sed)
sed 's/error/ERROR/gI' app.log
# 文本中含有斜杠时使用其他分隔符
sed 's|/var/www|/srv/www|g' nginx.conf
sed -i:直接修改文件
参数 -i 会把结果写回文件。为了不丢失原文件,可以指定备份后缀:
# 生成 config.ini.bak 备份,并修改 config.ini
sed -i.bak 's/^debug=true/debug=false/' config.ini
# 替换目录中所有 .conf 文件里的内容
find /etc/myapp -name "*.conf" -exec sed -i 's/old-host/new-host/g' {} +
在批量修改之前,先去掉 -i 运行一次,检查输出。在 macOS(BSD sed)上 -i 需要一个必填参数:sed -i '' 's/a/b/' file。
按行号和模式处理行
# 显示第 10–20 行(-n 关闭自动输出,p 表示打印)
sed -n '10,20p' file.txt
# 删除第 5 行
sed '5d' file.txt
# 删除最后一行
sed '$d' file.txt
# 删除空行和注释行
sed -e '/^$/d' -e '/^#/d' nginx.conf
# 只在包含 "listen" 的行中替换
sed '/listen/s/80/8080/' nginx.conf
# 在匹配行之后插入一行(GNU sed)
sed '/^\[mysqld\]/a max_connections = 500' my.cnf
反向引用与 &
括号中的部分模式可以在替换内容中通过 \1、\2 引用;符号 & 表示整个匹配的文本。
# 交换名和姓(通过 -E 使用扩展正则)
$ echo "Иванов Пётр" | sed -E 's/(\S+) (\S+)/\2 \1/'
Пётр Иванов
# 给所有数字加上方括号
$ echo "port 8080, timeout 30" | sed -E 's/[0-9]+/[&]/g'
port [8080], timeout [30]
# 把日期 2026-10-14 转换为 14.10.2026
$ echo 2026-10-14 | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3.\2.\1/'
14.10.2026
awk:列、条件与统计
输出需要的列
awk 以空格和制表符把每一行拆分成字段:$1 是第一个字段,$2 是第二个,$NF 是最后一个,$0 是整行。
# nginx 日志中的 IP 地址(第一列)
awk '{print $1}' /var/log/nginx/access.log
# /etc/passwd 中的用户名和 shell(分隔符为冒号)
awk -F: '{print $1, $7}' /etc/passwd
# 最后一列
df -h | awk '{print $NF}'
# 输出时使用自定义分隔符
awk -F: -v OFS=';' '{print $1, $3}' /etc/passwd
条件:只打印需要的行
可以在花括号块之前写一个条件,awk 只对满足条件的行执行该操作。
# UID 从 1000 开始的用户(普通用户,而非系统用户)
awk -F: '$3 >= 1000 {print $1}' /etc/passwd
# 响应码为 500 及以上的请求(combined 格式中的第 9 个字段)
awk '$9 >= 500' access.log
# 第 6 列匹配正则的行
awk '$6 ~ /POST/' access.log
# ls -l 输出中大于 100 MB 的文件
ls -l | awk '$5 > 100*1024*1024 {print $9}'
BEGIN、END 和 NR:awk 作为小程序
块 BEGIN 在读取第一行之前执行,END 在最后一行之后执行。NR 保存当前行号,在 END 块中它就是总行数。
# 响应大小(第 10 个字段)之和,以兆字节显示
awk '{sum += $10} END {printf "%.1f MB\n", sum/1024/1024}' access.log
# 最后一列的平均响应时间
awk '{s += $NF} END {if (NR) print s/NR}' timings.log
# 跳过 CSV 的表头
awk -F, 'NR > 1 {print $2}' users.csv
# 按 IP 统计请求次数:关联数组
awk '{count[$1]++} END {for (ip in count) print count[ip], ip}' access.log | sort -rn | head
最后一个例子是分析日志的经典写法:以 IP 为键的数组 count 可以替代 sort | uniq -c 的组合,并且很容易扩展,例如计算每个地址的流量总和。
管道:grep、sed 和 awk 组合使用
真正的威力在于组合。每个工具完成自己的部分,| 再把结果传给下一个。
访问 5xx 错误最多的前 10 个 IP
grep ' 50[0-9] ' /var/log/nginx/access.log \
| awk '{print $1}' \
| sort | uniq -c | sort -rn | head -10
412 203.0.113.45
118 198.51.100.7
36 192.0.2.19
这里 grep 粗略地筛掉无关行,awk 取出字段,sort | uniq -c 统计重复次数。也可以只用一个 awk 完成:awk '$9 ~ /^5/ {c[$1]++} END {for (i in c) print c[i], i}' access.log | sort -rn | head。这种写法更精确,因为它检查的是响应码所在的那一列。
最常被访问的 URL
awk '{print $7}' access.log | sed 's/?.*//' | sort | uniq -c | sort -rn | head
sed 在这里去掉了查询字符串,使 /search?q=a 和 /search?q=b 被计为同一个页面。
去掉注释后读取配置中的值
grep -v '^\s*#' /etc/myapp/app.env | grep '=' | awk -F= '{print $1}'
某一小时内的错误
grep '14/Oct/2026:03:' access.log | awk '$9 >= 500 {print $4, $7, $9}'
常见错误
- 在 awk 中使用双引号而不是单引号。 在
awk "{print $1}"中,shell 会在 awk 启动之前替换掉它自己的变量$1。awk 程序和 sed 表达式应使用单引号。 - 忘记 sed 的
g标志。 没有它,一行中只替换第一处匹配,其余的保持不变。 - 没有检查就使用
sed -i。 错误的正则会悄无声息地破坏文件。先去掉-i运行,或使用-i.bak。 - 在 grep 中
+和|不起作用。 这是扩展语法,需要grep -E(或sed -E)。 - 模式中的点号。
grep 192.168.1.1也会匹配192x168y1z1,因为点号表示“任意字符”。请使用grep -F或写成\.。 - 列中含有空格。 如果字段中可能包含空格(CSV、路径),请显式指定分隔符:
awk -F,或awk -F'\t'。 - 多余的 cat。
cat file | grep x也能工作,但grep x file更短,并且不会多启动一个进程。
可以用什么替代 grep、sed 和 awk
对于 JSON,这些工具并不方便:结构最好用 jq 解析(jq '.items[] | select(.status=="active") | .name'),YAML 则用 yq。对于大型项目中的快速递归搜索,常用 ripgrep(命令 rg),它默认遵循 .gitignore。如果你更习惯 Perl 语法,perl -pe 's/a/b/g' 和 perl -lane 'print $F[0]' 也能用一行命令替代 sed 和 awk。但 grep、sed 和 awk 几乎在所有服务器上都预装好了,无需安装,因此首先应该掌握它们。
总结
grep 查找行,sed 修改行,awk 处理列并统计。记住这组基本用法:grep -rniE 以及 -A/-B/-C;sed 's/.../.../g' 配合 -i.bak 和 -n 'Np';awk -F 配合 $1、$NF、条件和 END 块。用这些基本组件几乎可以组合出任何日志分析。这些 grep sed awk 示例与 find、jq 和 curl 一起,在 Linux 课程中关于终端文本处理的课时里有实际演示。
常见问题
grep 和 egrep 有什么区别?
egrep 是 grep -E(扩展正则)的旧别名。现代 GNU grep 调用 egrep 时会给出警告,因此应当使用 grep -E。
如何在目录中的所有文件里搜索文本?
使用 grep -rn "文本" /路径:-r 表示递归搜索,-n 显示行号。要限定文件类型,可加上 --include="*.php"。
如何一次替换许多文件中的文本?
把搜索和 sed 结合起来:grep -rl "old" . | xargs sed -i 's/old/new/g'。操作之前最好先提交代码或做好备份。
如何在 awk 中以制表符分隔输出多列?
设置输出分隔符:awk -v OFS='\t' '{print $1, $3}' file。如果不设置 OFS,输出字段之间默认用空格分隔。
如何用 sed 删除行尾的空格?
命令 sed -i 's/[[:space:]]*$//' file 会删除文件每一行末尾的空格和制表符。