Linux-04 文本处理三剑客与正则
前置阅读:Linux-02 文件、链接与归档压缩
线上排查问题,八成时间在跟文本打交道——日志、配置、命令输出。grep/sed/awk 这三个工具的熟练度,直接决定你是「三分钟定位到问题」还是「导出日志到本地用编辑器翻」。
本篇的顺序是:先把正则的三套标准讲清楚(这是最大的混乱来源),再讲三剑客各自的模型,最后是组合起来的实战。
1. 正则:先搞清楚有三套标准
大部分正则的困惑都来自这里:同一个 + 号,在 grep 里要写 \+,在 grep -E 里写 +,在 grep -P 里也写 +。这不是记不住,是因为它们根本是三套不同的语法。
| 标准 | 全称 | 谁在用 |
|---|---|---|
| BRE | Basic Regular Expression | grep(默认)、sed(默认) |
| ERE | Extended Regular Expression | grep -E、sed -E、awk、egrep |
| PCRE | Perl Compatible RegExp | grep -P、Python、Go、Java、JS |
1.1 BRE 与 ERE 的唯一区别:谁需要转义
两者的能力几乎一样,区别只在于「哪些字符需要反斜杠」,而且是反着的:
| 含义 | BRE 写法 | ERE 写法 |
|---|---|---|
| 一次或多次 | \+ |
+ |
| 零次或一次 | \? |
? |
| 或(竖线) | 要加反斜杠 | 直接写 |
| 分组 | \(...\) |
(...) |
| 重复 n 次 | a\{3\} |
a{3} |
| 任意字符 | . |
. |
| 零次或多次 | * |
* |
# 匹配一个或多个数字
grep '[0-9]\+' f # BRE:+ 要转义
grep -E '[0-9]+' f # ERE:+ 直接用 <- 推荐
**生产建议:一律用 -E。**BRE 的转义规则纯粹是历史包袱,grep -E 和 sed -E 的写法跟你在 Go/Python/JS 里写的正则一致,不用切换大脑。
1.2 PCRE 多出来的东西
grep -P 支持的 PCRE 才有这些,BRE/ERE 都没有:
| 特性 | 写法 | 说明 |
|---|---|---|
| 简写字符类 | \d \w \s \D \W \S |
数字/单词字符/空白 |
| 非贪婪 | .*? |
只有 PCRE 有 |
| 前后查找 | (?=...) (?<=...) |
零宽断言 |
| 非捕获组 | (?:...) |
|
| 匹配换行 | \n \t |
ERE 里要用 [[:space:]] |
# 提取 IP,用 \d 简洁很多
grep -oP '\d+\.\d+\.\d+\.\d+' access.log
# 只要 status= 后面的值(前向断言,不含 "status=" 本身)
grep -oP '(?<=status=)\d+' app.log
# 非贪婪:提取第一个引号里的内容
echo 'a "x" b "y"' | grep -oP '".*?"'
# "x"
# "y"
# 用贪婪的 ".*" 会匹配成 "x" b "y" 一整段
注意:
grep -P依赖编译时的 PCRE 支持,极简容器镜像(Alpine 的 busybox grep)里可能没有。写脚本时如果要保证可移植,用 ERE 写法;交互式排查随便用-P。用grep -P '' /dev/null 2>&1 && echo ok探测。
1.3 一定要记住的元字符
. 任意单个字符
* 前一项零次或多次
+ 前一项一次或多次 (ERE)
? 前一项零次或一次 (ERE)
{n,m} 前一项 n 到 m 次 (ERE)
[abc] 字符集合,匹配其中之一
[^abc] 取反
[a-z] 范围
^ 行首 <- 在 [] 内表示取反,位置不同含义不同
$ 行尾
| 或 (ERE)
() 分组 + 捕获 (ERE)
\1 反向引用第一个分组
\b 单词边界 (GNU 扩展,ERE 里可用)
POSIX 字符类在处理多语言和多环境时比 [a-zA-Z] 可靠:
[[:digit:]] 数字 等价 [0-9]
[[:alpha:]] 字母
[[:alnum:]] 字母数字
[[:space:]] 空白(空格、tab、换行)
[[:upper:]] 大写
[[:punct:]] 标点
# ⚠️ [a-z] 在某些 locale 下会匹配到大写字母(排序规则不同)
grep -E '^[a-z]+$' # 不完全可靠
grep -E '^[[:lower:]]+$' # 可靠
# 或者干脆锁定 locale
LC_ALL=C grep -E '^[a-z]+$'
LC_ALL=C 还是巨大的性能优化。默认 locale 是 UTF-8 时,grep 要做多字节字符解析;锁成 C(单字节)能快好几倍:
# 在一个 5GB 的日志里搜索
time grep -c "ERROR" big.log
# real 0m12.4s
time LC_ALL=C grep -c "ERROR" big.log
# real 0m2.1s <- 快 6 倍
规律:处理纯 ASCII 的日志时,加 LC_ALL=C。
2. grep:找出匹配的行
grep 的模型最简单:逐行读入,匹配则输出整行。
2.1 必须记住的选项
| 选项 | 作用 | 使用频率 |
|---|---|---|
-i |
忽略大小写 | 高 |
-v |
反选,输出不匹配的行 | 高 |
-n |
显示行号 | 高 |
-c |
只输出计数 | 高 |
-r |
递归目录 | 高 |
-E |
用 ERE | 高 |
-o |
只输出匹配的部分,不是整行 | 高 |
-A n / -B n / -C n |
显示后 n 行 / 前 n 行 / 上下各 n 行 | 排查必备 |
-l |
只列出文件名 | 中 |
-F |
当作固定字符串,不解析正则 | 中(性能) |
-w |
全词匹配 | 中 |
-q |
静默,只用退出码 | 脚本里 |
--exclude-dir |
排除目录 | 中 |
2.2 排查时最有用的几个
-C 看上下文,这是看日志的核心技巧。单看报错行往往没有信息量,前后几行才有请求参数和堆栈:
grep -n -C 5 "panic" app.log
# 报错行前后各 5 行,能看到是哪个请求触发的
-A 配合时间戳定位一段时间的日志:
# 从 14:23 开始往后 200 行
grep -A 200 "2026-08-06 14:23" app.log
-o 提取而非匹配,配合统计特别有用:
# 统计每个 HTTP 状态码出现多少次
grep -oE 'HTTP/1\.[01]" [0-9]{3}' access.log | awk '{print $2}' | sort | uniq -c | sort -rn
# 85234 200
# 1203 404
# 412 500 <- 500 有 412 次,值得看
-F 是重要的性能选项。如果你搜的是固定字符串(不含正则元字符),-F 让 grep 走 Boyer-Moore 算法而不是正则引擎:
# 搜一个包含特殊字符的字符串,-F 既快又不用转义
grep -F 'user.id=1024' app.log
# 从文件读取多个固定模式(比 grep -E 'a|b|c|...' 快得多)
grep -Ff patterns.txt big.log
-r 搜代码库时一定要排除干扰目录:
grep -rn --exclude-dir={.git,node_modules,vendor} --include="*.go" "TODO" .
提示:搜代码库时
ripgrep(rg)比grep -r快一个数量级,且默认遵守.gitignore。但线上机器通常只有 grep,所以 grep 的用法必须熟。
2.3 grep 的退出码
写脚本时很关键:
0 找到了至少一行
1 没找到
2 出错(文件不存在、正则语法错)
# ✅ 用 -q 做判断,找到就立即退出,不读完整个文件
if grep -q "ERROR" app.log; then
echo "有错误"
fi
# ⚠️ 陷阱:set -e 下 grep 没找到会让脚本退出
set -e
COUNT=$(grep -c "ERROR" app.log) # 没找到 -> 退出码 1 -> 脚本直接死
# ✅ 修复
COUNT=$(grep -c "ERROR" app.log || true)
这个陷阱在 CI 脚本里非常常见。set -euo pipefail 是好习惯(第 5 篇讲),但要记得 grep 找不到是「正常情况」而非错误。
3. sed:流式行编辑器
sed 的模型比 grep 复杂一层:逐行读入 → 放进「模式空间」→ 按脚本处理 → 输出。
理解「默认会输出每一行」这一点很关键,它解释了 -n 和 p 为什么要成对出现。
3.1 最常用的替换
sed 's/old/new/' f # 每行只替换第一个匹配
sed 's/old/new/g' f # 全部替换 <- g = global
sed 's/old/new/2' f # 只替换每行第二个匹配
sed 's/old/new/gi' f # 全部 + 忽略大小写
sed -E 's/([0-9]+)/[\1]/g' f # 用捕获组,\1 引用
分隔符可以换,处理路径时能省掉大量转义:
# ❌ 斜杠地狱
sed 's/\/usr\/local\/bin/\/opt\/bin/g' f
# ✅ 换成 | 或 # 或 ,
sed 's|/usr/local/bin|/opt/bin|g' f
& 代表整个匹配,包裹内容时很方便:
echo "error 500" | sed -E 's/[0-9]+/<&>/'
# error <500>
3.2 -n 与 p:精确输出
sed -n '5p' f # 只打印第 5 行
sed -n '5,10p' f # 第 5 到 10 行
sed -n '5,$p' f # 第 5 行到末尾
sed -n '/start/,/end/p' f # 从匹配 start 的行到匹配 end 的行 <- 提取日志片段神器
sed -n '$p' f # 最后一行
sed -n '0~3p' f # 每隔 3 行(GNU 扩展)
-n 关掉默认输出,p 显式打印。不加 -n 的话 sed '5p' 会把第 5 行打印两次(一次是 p,一次是默认输出)。
/start/,/end/p 是排查时的利器——提取一段完整的堆栈或一次请求的完整日志:
# 提取一次 panic 的完整堆栈
sed -n '/panic:/,/^$/p' app.log
# 提取某个 trace-id 相关的所有行之间的内容
sed -n '/trace-id=abc123/,/request completed/p' app.log
3.3 增删改
sed '3d' f # 删第 3 行
sed '/^#/d' f # 删所有注释行
sed '/^$/d' f # 删空行
sed -E '/^\s*$/d' f # 删空白行(含只有空格的)
sed '/^#/d; /^$/d' f # 多个命令用分号 <- 清理配置文件常用
sed '2i\新内容' f # 在第 2 行【前】插入 i = insert
sed '2a\新内容' f # 在第 2 行【后】追加 a = append
sed '2c\新内容' f # 替换第 2 行整行 c = change
sed '$a\最后一行' f # 追加到文件末尾
清理配置文件看真实内容,这个组合很实用:
# 去掉注释和空行,只看实际生效的配置
grep -vE '^\s*(#|$)' /etc/nginx/nginx.conf
# 或
sed -E '/^\s*#/d; /^\s*$/d' /etc/nginx/nginx.conf
3.4 -i 原地修改:三个陷阱
sed -i 's/old/new/g' f # 直接改文件
sed -i.bak 's/old/new/g' f # 改之前先备份成 f.bak <- 推荐
陷阱一:GNU sed 和 BSD sed(macOS)的 -i 语法不兼容
# GNU (Linux)
sed -i 's/a/b/' f # ✅
sed -i.bak 's/a/b/' f # ✅
# BSD (macOS)
sed -i 's/a/b/' f # ❌ 报错:会把 's/a/b/' 当成备份后缀
sed -i '' 's/a/b/' f # ✅ 必须给一个空参数
sed -i.bak 's/a/b/' f # ✅ 两边都支持
所以跨平台脚本一律写 sed -i.bak,这是唯一两边都工作的写法。或者在 macOS 上装 gnu-sed 用 gsed。
陷阱二:-i 不是原地修改,它是「新建 + 重命名」
ls -i f
# 1179657 f
sed -i 's/a/b/' f
ls -i f
# 1179702 f <- inode 变了!
sed -i 实际做的是:创建临时文件 → 写入结果 → rename 覆盖原文件。所以:
- 文件的硬链接会断开(其他链接还指向旧 inode,看到的是旧内容)
- 正在读这个文件的进程仍读旧内容(它持有旧 inode 的 fd)
- 软链接会被替换成普通文件(除非用
--follow-symlinks)
第三点很坑:
ln -s real.conf link.conf
sed -i 's/a/b/' link.conf
ls -l link.conf
# -rw-r--r-- 1 lhx lhx 100 ... <- 不再是软链接了,real.conf 也没被改
# ✅ 正确做法
sed -i --follow-symlinks 's/a/b/' link.conf
陷阱三:不能用重定向就地修改
# ❌ 这会把文件清空!
sed 's/a/b/' f > f
# Shell 先执行重定向(截断 f 为空),sed 才开始读 —— 读到的是空文件
# ✅ 用 -i,或者写临时文件再 mv
sed 's/a/b/' f > f.tmp && mv f.tmp f
**规律:任何 cmd f > f 形式的命令都会清空 f。**这条对 sort、awk、grep 全都适用。
4. awk:字段处理与统计
awk 是三剑客里最强的——它是一门完整的编程语言。但 90% 的场景只用到它的核心模型:
awk 'BEGIN{初始化} 模式{动作} END{收尾}' 文件
对每一行:
1. 按分隔符切成字段 -> $1 $2 $3 ... $NF
2. 如果匹配「模式」,执行「动作」
3. 不写模式 = 每行都执行;不写动作 = 默认 print $0
4.1 内置变量
| 变量 | 含义 |
|---|---|
$0 |
整行 |
$1…$n |
第 n 个字段 |
NF |
字段数($NF 就是最后一个字段) |
NR |
行号(累计,多文件时不重置) |
FNR |
当前文件内的行号 |
FS |
输入字段分隔符(默认空白) |
OFS |
输出字段分隔符(默认空格) |
RS / ORS |
输入/输出记录分隔符(默认换行) |
# 打印第 1 和最后一个字段
awk '{print $1, $NF}' f
# 倒数第二个字段
awk '{print $(NF-1)}' f
# 带行号
awk '{print NR": "$0}' f
# 指定分隔符(两种写法等价)
awk -F: '{print $1}' /etc/passwd
awk 'BEGIN{FS=":"}{print $1}' /etc/passwd
# 多个分隔符(用正则)
awk -F'[:,]' '{print $2}' f
注意:
awk默认的字段分隔符是「连续空白」,不是单个空格。所以a b里$2是b。但一旦用-F' '显式指定单空格,连续空格就会产生空字段。处理ps、df这类对齐输出时依赖默认行为最省事。
4.2 模式过滤
# 正则模式
awk '/ERROR/' app.log # 等价于 grep ERROR
awk '$1 ~ /^2026-08/' app.log # 只对第 1 字段做匹配
awk '$3 !~ /debug/' app.log # 不匹配
# 数值比较 —— 这是 awk 相对 grep 的核心优势
awk '$9 >= 500' access.log # 状态码 >= 500
awk '$NF > 1.0' timing.log # 耗时超过 1 秒
awk 'NF != 5' f # 字段数异常的行(找脏数据)
# 行号范围
awk 'NR >= 100 && NR <= 200' f
awk 'NR % 100 == 0' f # 每 100 行取一行(抽样看大文件)
# 组合条件
awk '$9 >= 500 && $7 ~ /\/api\//' access.log
awk '$9 >= 500' 这类数值比较是 grep 完全做不到的。用正则匹配「大于 500 的数字」极其别扭,而 awk 一个比较符就够了。
4.3 统计:关联数组
这是 awk 真正不可替代的能力。awk 的数组下标可以是字符串,天然就是一个 map:
# 统计每个 IP 的请求数,取前 10
awk '{count[$1]++} END{for(ip in count) print count[ip], ip}' access.log \
| sort -rn | head -10
# 15234 10.0.0.5
# 8921 10.0.0.7
# 按状态码统计
awk '{code[$9]++} END{for(c in code) printf "%s: %d\n", c, code[c]}' access.log
# 求和与平均(第 10 字段是响应时间)
awk '{sum+=$10; n++} END{printf "总计 %.2f 平均 %.3f 请求数 %d\n", sum, sum/n, n}' access.log
# 求最大值
awk '$10 > max {max=$10; line=$0} END{print max; print line}' access.log
# 每个接口的平均耗时(二维统计)
awk '{sum[$7]+=$10; cnt[$7]++} END{for(u in sum) printf "%.3f %d %s\n", sum[u]/cnt[u], cnt[u], u}' \
access.log | sort -rn | head
去重也可以用数组,而且比 sort -u 快且保序:
awk '!seen[$0]++' f # 去重,保持原顺序
awk '!seen[$1]++' f # 按第 1 字段去重,保留首次出现的整行
!seen[$0]++ 是最经典的 awk 一行流:第一次遇到某行时 seen[$0] 是 0,!0 为真所以打印,同时 ++ 让它变 1;之后再遇到 !1 为假就不打印了。
4.4 输出格式化
# print 用 OFS(默认空格)连接
awk '{print $1, $2}' f
# printf 完全控制格式(不自动换行,要写 \n)
awk '{printf "%-20s %8.2f %5d\n", $1, $2, $3}' f
# | | +- 整数,宽 5
# | +-------- 浮点,宽 8 小数 2 位
# +--------------- 字符串,左对齐宽 20
# 改输出分隔符为 tab
awk 'BEGIN{OFS="\t"}{print $1,$2}' f
统计报表用 printf 才对齐,print 的输出在字段长度不一时完全没法看。
5. 配角们:cut / sort / uniq / tr
三剑客之外,这几个小工具在管道里出现频率极高。
5.1 sort 与 uniq 必须一起用
sort f # 按字典序
sort -n f # 按【数值】 <- 忘了 -n 会得到 10 < 9 的结果
sort -h f # 人类可读的大小(1K < 1M < 1G)
sort -r f # 倒序
sort -u f # 去重
sort -k2 f # 按第 2 列
sort -t: -k3 -n /etc/passwd # 用 : 分隔,按第 3 列数值排
sort -k2,2 -k1,1r f # 多级排序
uniq 只能去掉「相邻」的重复行,所以几乎必须先 sort:
# ❌ 不排序,去重不完整
uniq f
# ✅ 标准组合
sort f | uniq
# 最有用的:统计次数
sort f | uniq -c | sort -rn
# ^^^^^^^ 每行前面加计数
# ^^^^^^^^^ 按次数倒序
sort f | uniq -d # 只显示重复的
sort f | uniq -u # 只显示不重复的
sort | uniq -c | sort -rn 是排查日志的第一反应——不管统计什么(IP、状态码、URL、错误类型),都是这个套路。
性能提示:对大文件排序时
sort会用临时文件,-S 2G加大内存缓冲能明显加速,--parallel=8用多核。另外LC_ALL=C sort比 UTF-8 排序快很多。
5.2 cut:按位置切
cut -d: -f1 /etc/passwd # 用 : 分隔,取第 1 段
cut -d: -f1,3 /etc/passwd # 第 1 和 3 段
cut -d: -f3- /etc/passwd # 第 3 段到末尾
cut -c1-10 f # 按【字符】位置切
cut 的局限:不能处理「连续多个分隔符」。
echo "a b c" | cut -d' ' -f2
# <- 输出空的,因为第 2 个字段是空字符串
echo "a b c" | awk '{print $2}'
# b <- awk 默认把连续空白当一个分隔符
规律:分隔符是单个固定字符(:、,、\t)用 cut,是空白用 awk。
5.3 tr:字符级替换
tr 'a-z' 'A-Z' < f # 转大写
tr -d '\r' < f # 删除回车(处理 Windows 换行)
tr -s ' ' < f # 压缩连续空格为一个
tr ':' '\n' <<< "$PATH" # 把 PATH 按 : 拆成多行 <- 常用
tr -cd '[:print:]\n' < f # 只保留可打印字符(清理二进制垃圾)
tr 只能处理单个字符,不能替换字符串——那是 sed 的活。
6. 实战:一次真实的日志分析
场景:接口 P99 突然从 200ms 涨到 3s,需要在 nginx access log 里定位原因。日志格式:
10.0.0.5 - - [06/Aug/2026:14:23:01 +0800] "GET /api/orders?id=123 HTTP/1.1" 200 1024 0.245
$1 $4 $6 $7 $8 $9 $10 $11
① 先确认问题范围:哪个时间段慢
# 按分钟统计平均耗时
awk '{split($4, t, ":"); min=t[2]":"t[3]; sum[min]+=$11; cnt[min]++}
END{for(m in sum) printf "%s %.3f %d\n", m, sum[m]/cnt[m], cnt[m]}' access.log \
| sort | tail -20
# 14:20 0.198 4521
# 14:21 0.203 4488
# 14:22 0.211 4502
# 14:23 2.874 4491 <- 从 14:23 开始劣化
# 14:24 3.102 4390
② 是全部接口都慢,还是某一个
awk '$4 ~ /14:2[3-9]/ {sum[$7]+=$11; cnt[$7]++}
END{for(u in sum) printf "%.3f %6d %s\n", sum[u]/cnt[u], cnt[u], u}' access.log \
| sort -rn | head -10
# 8.234 892 /api/orders/export
# 0.201 2103 /api/orders
# 0.156 1204 /api/users
# ^ 只有 export 接口慢,而且它拉高了整体 P99
③ 慢请求集中在哪些参数上
# 提取 export 接口的查询参数,看是否有异常大的时间范围
grep "/api/orders/export" access.log \
| awk '$11 > 5 {print $7}' \
| grep -oP '(?<=days=)\d+' \
| sort -n | uniq -c
# 12 7
# 845 365 <- 有人在导出 365 天的数据
④ 是谁在打
grep "/api/orders/export" access.log | awk '$11 > 5 {print $1}' | sort | uniq -c | sort -rn | head -5
# 823 10.0.3.17 <- 集中在一个 IP
# 18 10.0.3.22
⑤ 确认结论并量化影响
# 这个 IP 的请求占了多少慢请求
awk '$11 > 5' access.log | wc -l
# 891
awk '$11 > 5 && $1 == "10.0.3.17"' access.log | wc -l
# 823 <- 92% 的慢请求来自一个 IP
结论:某个客户端在 14:23 开始批量调用导出接口,days=365 触发全表扫描,单请求 8s+,占满了连接池,导致其他接口排队。
修复方向:给 export 接口加参数上限和限流,days 超过 90 天走异步任务。
规律:日志分析的顺序永远是「时间 → 维度 → 具体值 → 来源」,每一步都用 sort | uniq -c | sort -rn 收敛范围。
7. 三剑客怎么选
| 需求 | 用什么 |
|---|---|
| 找出包含某个词的行 | grep |
| 找出不包含的行 | grep -v |
| 只要匹配的那一小段 | grep -oP |
| 看报错的上下文 | grep -C 5 |
| 替换文本 | sed 's///g' |
| 删除某些行 | sed '/x/d' 或 grep -v |
| 提取某几行 / 一段区间 | sed -n '5,10p' / sed -n '/a/,/b/p' |
| 按列取值 | awk '{print $3}'(空白分隔)/ cut -d,(固定分隔) |
| 数值比较过滤 | awk '$9 > 500' |
| 分组统计 / 求和 / 求平均 | awk 关联数组 |
| 排序、去重、计数 | sort + uniq -c |
一句话:grep 找行,sed 改行,awk 算列。
超过大约 30 行的文本处理逻辑,就该换 Python 了——awk 能写,但可读性和可维护性会迅速崩塌。
8. 面试题
Q:grep、grep -E、grep -P 有什么区别?
分别对应三套正则标准:默认是 BRE,-E 是 ERE,-P 是 PCRE。BRE 和 ERE 能力几乎相同,差别只在转义规则——BRE 里 + ? ( ) { } 都要加反斜杠,ERE 里直接写。PCRE 额外支持 \d \w \s 简写、非贪婪 .*?、零宽断言 (?=) (?<=),语法与 Go/Python/JS 一致。生产建议用 -E(写法通用),交互式排查可以用 -P(表达力强),但 -P 依赖编译选项,busybox/Alpine 的 grep 可能不支持,写脚本要注意可移植性。
Q:sed -i 是真的「原地修改」吗?
不是。它实际是「创建临时文件 → 写入结果 → rename 覆盖原文件」,所以文件的 inode 会变。三个后果:① 指向原文件的硬链接会断开,它们仍指向旧 inode 看到旧内容;② 正在读该文件的进程持有旧 inode 的 fd,继续看到旧内容;③ 如果目标是软链接,链接本身会被替换成普通文件,源文件反而没被修改——要加 --follow-symlinks。另外 GNU sed 和 macOS 的 BSD sed 的 -i 语法不兼容,跨平台脚本统一写 sed -i.bak。
Q:sed 's/a/b/' f > f 会发生什么?
会把 f 清空。因为 Shell 在执行命令之前就先处理重定向——> f 立即把 f 截断为 0 字节,此时 sed 才开始读,读到的是一个空文件。这条规律对所有 cmd f > f 形式都成立(sort、awk、grep 一样)。正确做法是用 -i,或者写到临时文件再 mv。
Q:awk 和 cut 都能按列取值,什么时候用哪个?
cut 的分隔符是单个固定字符,且不合并连续分隔符——所以 echo "a b" | cut -d' ' -f2 输出是空的。awk 默认把「连续空白」当一个分隔符,处理 ps、df、ls -l 这类对齐输出天然正确。规律:分隔符是 :、,、\t 这种确定的单字符用 cut(更快),是空白或需要正则用 awk。另外只有 awk 能做数值比较和聚合统计,cut 只能切。
Q:怎么统计一个 access log 里请求量最多的 10 个 IP?
awk '{c[$1]++} END{for(i in c) print c[i], i}' access.log | sort -rn | head -10
# 或者不用 awk
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10
第一种用 awk 关联数组,只扫一遍文件、内存里聚合,大文件下明显更快;第二种要对全部行做外部排序,IO 和 CPU 都更重。sort | uniq -c | sort -rn 这个组合是日志统计的通用套路,但数据量大时优先考虑 awk 直接聚合。
Q:uniq 为什么必须配合 sort?
因为 uniq 只能合并相邻的重复行,它是流式处理、不保存全部历史。所以未排序的输入里,分散在各处的相同行不会被去重。如果需要保持原始顺序去重(不能排序),用 awk:awk '!seen[$0]++' f——它用关联数组记住见过的行,第一次出现才打印。
Q:在 5GB 的日志里 grep,怎么让它更快?
几个手段按收益排序:① LC_ALL=C——避免 UTF-8 多字节解析,通常能快 3~6 倍;② -F——搜固定字符串时走 Boyer-Moore 而不是正则引擎;③ -m N 找到 N 条就停;④ 如果搜的是时间段,先用 sed -n '/14:23/,/14:30/p' 缩小范围再 grep;⑤ 多个模式用 grep -Ff patterns.txt 而不是 grep -E 'a|b|c'。另外要注意 grep -P 通常比 -E 慢,能用 ERE 就别用 PCRE。
Q:set -e 的脚本里用 grep -c 统计,为什么脚本会莫名退出?
因为 grep 没找到匹配时退出码是 1,在 set -e 下会直接终止脚本。但「没找到」在统计场景里是完全正常的结果(计数为 0),不该被当作错误。修复方式是 COUNT=$(grep -c "x" f || true),或者用 || COUNT=0。同类的还有 diff(有差异返回 1)。规律:set -e 下调用「用退出码表达业务结果」的命令,都要显式处理退出码。
上一篇:Linux-03 用户、权限与安全模型 | 下一篇:Linux-05 Shell 编程与脚本工程化
xingliuhua