大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
文本处理
这一课你将能做到
- 用 grep 在海量日志中精准定位错误关键字,用正则表达式过滤掉噪音
- 用 awk 和 sed 对日志做字段提取与批量替换,30 秒完成「查 IP、数次数、排顺序」的完整分析链路
- 把多个命令用管道串成一条数据处理流水线,替代「下载到本地用 Excel 打开」的低效操作
为什么要学
线上排查问题,本质上就是和日志打交道。本站后端(Spring Boot)每天产生几十万行日志,MySQL 慢查询日志、Nginx 访问日志、Redis 日志也都是纯文本。你想知道「今天有多少 500 错误」「哪个 IP 请求最多」「某个用户的操作轨迹是什么」,靠肉眼翻日志是不可能的,必须靠 grep/awk/sed 这些文本处理利器。
面试中,「如何统计日志中出现次数最多的 10 个 IP」「如何提取某列数据」「如何批量替换文件内容」是 Shell 能力考察的标配题。答得好直接加分,答不上来会被认为没有实际运维经验。
先建立图像
文本处理三剑客的职责分工:
grep → 按行过滤:把包含关键字的行留下来,其他扔掉
awk → 按列处理:把每行切成字段,取你要的第 N 列
sed → 按内容修改:查找替换、删除行、插入行
一条典型的日志分析流水线:
cat access.log # 1. 拿到原始日志
| grep "500" # 2. 只要 500 错误的行
| awk '{print $1}' # 3. 取出第 1 列(IP 地址)
| sort # 4. 排序(相同的 IP 排在一起)
| uniq -c # 5. 去重并统计次数
| sort -nr # 6. 按次数倒序
| head -10 # 7. 只看前 10 名
管道的本质:前一个命令的输出(stdout)直接变成后一个命令的输入(stdin),不需要中间文件。
跟着做
先准备一份模拟的 Nginx 访问日志:
mkdir -p /tmp/logdemo && cd /tmp/logdemo
cat > access.log << 'EOF'
192.168.1.10 - - [07/Oct/2026:10:01:01 +0800] "GET /api/users HTTP/1.1" 200 1234
192.168.1.11 - - [07/Oct/2026:10:01:02 +0800] "GET /api/orders HTTP/1.1" 500 532
192.168.1.10 - - [07/Oct/2026:10:01:03 +0800] "POST /api/login HTTP/1.1" 200 856
192.168.1.12 - - [07/Oct/2026:10:01:04 +0800] "GET /api/users HTTP/1.1" 404 210
192.168.1.10 - - [07/Oct/2026:10:01:05 +0800] "GET /api/products HTTP/1.1" 500 445
192.168.1.13 - - [07/Oct/2026:10:01:06 +0800] "GET /api/users HTTP/1.1" 200 990
192.168.1.11 - - [07/Oct/2026:10:01:07 +0800] "DELETE /api/admin HTTP/1.1" 403 180
192.168.1.10 - - [07/Oct/2026:10:01:08 +0800] "GET /api/orders HTTP/1.1" 200 1560
EOF
步骤 1:grep——按行过滤
# 基本用法:找出包含 "500" 的所有行
grep "500" access.log
# -c 只统计行数,不显示内容
grep -c "500" access.log # 输出:2
# -v 反向过滤:排除包含 200 的行
grep -v "200" access.log
# -i 忽略大小写
grep -i "get" access.log
# -n 显示行号(定位问题时非常有用)
grep -n "admin" access.log
# 正则表达式:匹配 4xx 或 5xx 错误
grep -E '" [45][0-9]{2} ' access.log
# 在多个文件中搜索(排查 Spring Boot 日志常用)
grep -rn "OutOfMemoryError" /var/log/myapp/
步骤 2:管道与重定向——数据的传送带
# > 覆盖写入文件
grep "500" access.log > errors.log
# >> 追加写入文件
grep "404" access.log >> errors.log
# | 管道:一个命令的输出给下一个命令当输入
grep "500" access.log | wc -l # 统计 500 错误有几行
# 2> 重定向错误输出(stderr)
find /etc -name "*.conf" 2>/dev/null # 把 Permission denied 错误扔掉
# &> 同时重定向标准输出和错误输出
java -jar app.jar &> app.log & # 后台运行,输出和错误都进日志
步骤 3:awk——按列提取
awk 默认以空格/制表符切分每行为字段,$1 是第一列,$NF 是最后一列。
# 提取所有 IP(第 1 列)
awk '{print $1}' access.log
# 提取 URL 和状态码(第 7 列和第 9 列)
awk '{print $7, $9}' access.log
# 带条件过滤:只看状态码是 500 的行,打印 IP 和 URL
awk '$9 == 500 {print $1, $7}' access.log
# 统计:所有响应字节数(第 10 列)求和
awk '{sum += $10} END {print "总流量:", sum, "字节"}' access.log
# 自定义分隔符:分析 CSV 格式
awk -F',' '{print $2}' data.csv
步骤 4:sort + uniq——排序与去重统计
# 统计每个 IP 的访问次数(经典组合,面试必考)
awk '{print $1}' access.log | sort | uniq -c | sort -nr
# 输出示例:
# 4 192.168.1.10
# 2 192.168.1.11
# 1 192.168.1.13
# 1 192.168.1.12
拆解这条流水线:awk 取 IP → sort 排序(uniq 要求输入有序)→ uniq -c 统计重复次数 → sort -nr 按数字倒序。
步骤 5:sed——批量查找替换
# 把日志中所有 "192.168" 替换为 "10.0"(只打印,不改文件)
sed 's/192\.168/10.0/g' access.log
# -i 直接修改文件(危险!先备份)
cp access.log access.log.bak
sed -i 's/192\.168/10.0/g' access.log
# 删除包含 "DELETE" 的行
sed '/DELETE/d' access.log
# 只打印第 2 到第 4 行
sed -n '2,4p' access.log
生产场景:批量修改 Spring Boot 配置文件中的数据库地址:
sed -i.bak 's/jdbc:mysql:\/\/localhost/jdbc:mysql:\/\/prod-db.internal/g' application.yml
# -i.bak 自动备份原文件为 application.yml.bak
步骤 6:综合实战——分析本站后端日志
# 场景 1:统计 Spring Boot 日志中各 ERROR 出现的次数
grep "ERROR" /var/log/myapp/app.log | awk -F'ERROR' '{print $2}' | sort | uniq -c | sort -nr | head
# 场景 2:找出响应时间超过 1 秒的慢请求(假设最后一列是耗时毫秒)
awk '$NF > 1000 {print $7, $NF}' access.log
# 场景 3:实时监控日志,只看 ERROR
tail -f /var/log/myapp/app.log | grep --line-buffered "ERROR"
步骤 7:其他高频小工具
wc -l access.log # 统计行数
wc -c access.log # 统计字节数
head -5 access.log # 看前 5 行
tail -5 access.log # 看后 5 行
tail -f /var/log/nginx/access.log # 实时追踪日志(排查线上问题第一命令)
cut -d' ' -f1 access.log # cut 按固定分隔符切列,比 awk 更快但功能少
讲清楚要点
- grep 负责「选行」,awk 负责「选列」,sed 负责「改内容」:三者分工明确,用管道串起来就是完整的数据处理流水线。
- grep 的正则分两个档次:默认是基本正则(BRE),
-E开启扩展正则(ERE,支持+、?、{})。记不准就用grep -E。 - awk 的
$0是整行,$NF是最后一列:列数不固定时(如 URL 带空格),$NF取最后一列比数列号靠谱。 - uniq 只对相邻重复行生效:所以
uniq -c前面必须先sort,这是初学者最常踩的坑。 sed -i会直接改文件:生产环境务必用sed -i.bak留下备份,或者先不带-i跑一遍看输出是否符合预期。tail -f是实时排查的第一命令:配合管道tail -f app.log | grep ERROR可以实时过滤错误,加--line-buffered防止 grep 缓冲导致输出延迟。>会覆盖文件,>>才是追加:>误用会清空日志文件,这是真实发生过的生产事故。
你可能会懵的地方
-
为什么
sort | uniq -c里的 sort 不能省?uniq只识别「相邻」的重复行。如果相同的 IP 分散在文件各处,不排序的话 uniq 会把它们当成不同的行。 -
grep 搜中文为什么有时候搜不到? 文件编码问题。日志是 GBK 而终端是 UTF-8 时会搜不到。用
iconv -f GBK -t UTF-8 file.log | grep 关键字先转码。 -
awk 和 cut 有什么区别? cut 只支持单字符分隔符,速度快;awk 支持任意空白分隔、正则分隔符,还能做条件判断和计算。简单切列用 cut,复杂处理用 awk。
-
2>&1和&>是一样的吗? 在 bash 中效果相同,都是把标准输出和标准错误合并。2>&1是 POSIX 标准写法,兼容性更好,面试时写这个更显专业。 -
管道会创建临时文件吗? 不会。管道是内存中的数据流,不落盘。这也是为什么管道处理大文件比「生成中间文件再处理」快得多。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
uniq -c 前面不 sort | 统计结果错误,重复行没被合并 | sort | uniq -c | sort -nr |
grep ERROR app.log > app.log | 输出重定向到自己,文件被清空 | 输出到临时文件再 mv 回去 |
sed -i 's/a/b/g' 线上.conf 直接改 | 改错了没有备份 | sed -i.bak 自动留备份 |
cat file | grep xxx | 多此一举(UUOC 反模式) | grep xxx file |
tail -f log | grep ERROR 无输出 | grep 缓冲了输出 | 加 --line-buffered |
awk 中数字比较写成 $9 = 500 | = 是赋值不是比较 | 比较用 ==:$9 == 500 |
卡点急救
-
现象:管道处理几个 GB 的日志时特别慢 grep 本身很快,瓶颈常在
sort(需要全量数据进内存)。解决:先用 grep 缩小范围再 sort;超大文件用sort -T /tmp指定临时目录,或用LC_ALL=C sort加速。 -
现象:grep 搜日志提示
Binary file matches日志里混进了二进制内容(如上传文件的 dump)。解决:加-a强制当文本处理:grep -a 关键字 log。 -
现象:
tail -f日志文件被 logrotate 轮转后就不更新了tail -f跟踪的是 inode,文件被轮转换新后还在跟踪旧文件。解决:用tail -F(大写),文件被重建后会自动跟踪新文件。
面试常问
-
如何统计 Nginx 日志中访问次数最多的前 10 个 IP?
awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -10。要说清每一步的作用,尤其是 uniq 前必须 sort。 -
>、>>、2>、2>&1分别是什么?>覆盖重定向标准输出;>>追加重定向;2>重定向标准错误;2>&1把标准错误合并到标准输出。常见组合:command > out.log 2>&1(输出和错误都进日志)。 -
如何实时监控日志中的错误?
tail -F app.log | grep --line-buffered ERROR。要点:-F处理日志轮转,--line-buffered避免 grep 缓冲延迟。追问:还可以用journalctl -u myapp -f(systemd 服务)。
自测三问
- 用一条命令统计
/var/log/nginx/access.log中状态码为 404 的请求按 URL 分组计数,按次数倒序取前 5 名。 sed 's/foo/bar/'和sed 's/foo/bar/g'的区别是什么?- 为什么
tail -f在日志轮转后失效?用什么参数解决?
小结一句话
grep 选行、awk 选列、sed 改内容,管道把它们串成流水线——掌握这三件套加一条统计命令链,日志排查效率提升十倍。
延伸
- GNU grep 手册
- GNU awk 用户指南
- 下一课:包管理——用 apt 安装、升级、卸载软件,理解依赖与仓库。