目录

Linux-04 文本处理三剑客与正则

前置阅读:Linux-02 文件、链接与归档压缩

线上排查问题,八成时间在跟文本打交道——日志、配置、命令输出。grep/sed/awk 这三个工具的熟练度,直接决定你是「三分钟定位到问题」还是「导出日志到本地用编辑器翻」。

本篇的顺序是:先把正则的三套标准讲清楚(这是最大的混乱来源),再讲三剑客各自的模型,最后是组合起来的实战。

1. 正则:先搞清楚有三套标准

大部分正则的困惑都来自这里:同一个 + 号,在 grep 里要写 \+,在 grep -E 里写 +,在 grep -P 里也写 +。这不是记不住,是因为它们根本是三套不同的语法。

标准 全称 谁在用
BRE Basic Regular Expression grep(默认)、sed(默认)
ERE Extended Regular Expression grep -Esed -Eawkegrep
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 -Esed -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" .

提示:搜代码库时 ripgreprg)比 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 复杂一层:逐行读入 → 放进「模式空间」→ 按脚本处理 → 输出

理解「默认会输出每一行」这一点很关键,它解释了 -np 为什么要成对出现。

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-sedgsed

陷阱二:-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。**这条对 sortawkgrep 全都适用。

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$2b。但一旦用 -F' ' 显式指定单空格,连续空格就会产生空字段。处理 psdf 这类对齐输出时依赖默认行为最省事。

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:grepgrep -Egrep -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 形式都成立(sortawkgrep 一样)。正确做法是用 -i,或者写到临时文件再 mv

Q:awkcut 都能按列取值,什么时候用哪个?

cut 的分隔符是单个固定字符,且不合并连续分隔符——所以 echo "a b" | cut -d' ' -f2 输出是空的。awk 默认把「连续空白」当一个分隔符,处理 psdfls -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 编程与脚本工程化