18 - 正则与文本处理三剑客

grep、sed、awk 是 Linux 文本处理的三大核心工具。掌握它们,你可以在几秒内完成日志分析、配置修改和数据报表等任务。


18.1 正则表达式基础

基本正则 vs 扩展正则

# 基本正则表达式(BRE)— grep 和 sed 默认使用
# 扩展正则表达式(ERE)— grep -E 和 awk 使用
 
# BRE 需要反斜杠转义的特殊字符: + ? | ( ) { }
# ERE 不需要转义这些字符,但 { } 需要转义
 
# 常用元字符
. # 匹配任意单个字符
^ # 行首
$ # 行尾
* # 前一个字符出现 0 次或多次
\+ (BRE) # 前一个字符出现 1 次或多次
? (BRE) # 前一个字符出现 0 次或 1 次(ERE 直接写 ?)
{n} # 前一个字符恰好出现 n 次
{n,} # 前一个字符出现至少 n 次
{n,m} # 前一个字符出现 n 到 m 次
[abc] # 字符集,匹配 a、b 或 c
[^abc] # 否定字符集,不匹配 a、b、c
[a-z] # 范围:小写字母
[0-9] # 范围:数字
\d (BRE) # 数字(部分工具支持)
\s # 空白字符(部分工具支持)
\w # 单词字符(部分工具支持)
| (BRE) # 或(ERE 直接写 |)
() (BRE) # 分组(ERE 直接写 ())

常见正则模式

# 匹配 IP 地址(简化版)
^([0-9]{1,3}\.){3}[0-9]{1,3}$
 
# 匹配邮箱
^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$
 
# 匹配 URL
^https?://[^ ]+$
 
# 匹配日期 YYYY-MM-DD
^[0-9]{4}-[0-9]{2}-[0-9]{2}$
 
# 匹配十六进制颜色
^#[0-9A-Fa-f]{6}$
 
# 匹配空行
^$
 
# 匹配注释行(以 # 开头)
^[[:space:]]*#
 
# POSIX 字符类(比 [a-z] 更可移植,支持本地化)
[[:alnum:]] # 字母和数字
[[:alpha:]] # 字母
[[:digit:]] # 数字
[[:lower:]] # 小写字母
[[:upper:]] # 大写字母
[[:space:]] # 空白字符
[[:punct:]] # 标点符号
[[:print:]] # 可打印字符

18.2 grep — 文本搜索利器

基本选项

# 基本匹配
grep "pattern" file.txt
grep "root" /etc/passwd
grep "error" /var/log/syslog
 
# 常用选项
grep -i "Error" app.log # 忽略大小写
grep -v "DEBUG" app.log # 反向匹配(不包含的行)
grep -n "error" app.log # 显示行号
grep -c "error" app.log # 只输出匹配行数
grep -l "TODO" src/**/*.py # 只输出文件名
grep -L "TODO" src/**/*.py # 只输出不匹配的文件名
grep -r "function" ./src/ # 递归搜索目录
grep -w "int" code.c # 匹配整个单词(不匹配 "printf")
grep -x "exact line" file.txt # 匹配整行
grep -m 5 "error" app.log # 最多显示前 5 个匹配
 
# 上下文输出
grep -A 3 "ERROR" app.log # 显示匹配行及之后 3 行
grep -B 2 "ERROR" app.log # 显示匹配行及之前 2 行
grep -C 5 "ERROR" app.log # 显示匹配行及前后各 5 行
 
# 只输出匹配部分
grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+" access.log # 只输出 IP
 
# 统计匹配并排序
grep -o "HTTP/[0-9.]\+ [0-9]\+" access.log | sort | uniq -c | sort -rn

正则模式匹配(-E 扩展正则)

# -E 扩展正则(推荐,语法更直观)
grep -E "error|warning|critical" app.log # 或匹配
grep -E "^[0-9]{4}-[0-9]{2}-[0-9]{2}" file # 日期开头的行
grep -E "\.(jpg|png|gif)$" file # 以图片后缀结尾
grep -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" file # IP 地址
grep -E "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" access.log # 精确 IP
 
# -F 固定字符串模式(不解释正则,更快)
grep -F "[ERROR]" app.log # 搜索字面字符串 "[ERROR]"
 
# -P Perl 兼容正则(GNU grep 支持)
grep -P '\d{4}-\d{2}-\d{2}' file # Perl 风格
 
# 组合使用
grep -rniE "todo|fixme|hack" ./src/ --include="*.py" --exclude-dir="__pycache__"

grep 实战示例

# 查找所有非注释、非空白的配置行
grep -vE '^[[:space:]]*#|^$' /etc/ssh/sshd_config
 
# 统计每个 IP 的访问次数
grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
 
# 查找文件中是否包含二进制内容
grep -I "pattern" files/* # -I 跳过二进制文件
 
# 高亮显示匹配(大部分系统默认启用)
grep --color=always "pattern" file | less -R
 
# 在压缩文件中搜索
zgrep "error" /var/log/syslog.*.gz
 
# 多文件匹配,显示文件名
grep -H "TODO" src/**/*.{c,h}
 
# 统计代码行数(排除空行和注释)
grep -vE '^\s*($|#|//|/\*|\*)' script.sh | wc -l

18.3 sed — 流编辑器

替换操作(最常用)

# 基本替换
sed 's/old/new/' file # 每行替换第一个匹配
sed 's/old/new/g' file # 全局替换
sed 's/old/new/2' file # 替换每行第二个匹配
sed 's/old/new/ig' file # 忽略大小写全局替换
 
# 直接修改文件(-i 原地编辑)
sed -i 's/foo/bar/g' file.txt
sed -i.bak 's/foo/bar/g' file.txt # 备份原文件为 file.txt.bak
 
# 使用不同分隔符(当搜索模式包含 / 时)
sed 's|/usr/local/bin|/opt/bin|g' file
sed 's#http://old.com#https://new.com#g' file
sed 's:old:new:g' file
 
# 使用捕获组
sed 's/\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)/\3\/\2\/\1/g' file # YYYY-MM-DD → DD/MM/YYYY
# 或使用 -E 避免转义
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/g' file
 
# & 表示匹配的全部内容
sed 's/[0-9]\+/[&]/g' file # 将数字用方括号包裹

删除、打印和选择性操作

# 打印特定行(-n 静默模式,配合 p 输出)
sed -n '5p' file # 只打印第 5 行
sed -n '3,8p' file # 打印 3-8 行
sed -n '/pattern/p' file # 打印匹配行
sed -n '/start/,/end/p' file # 打印从 start 到 end 之间的行
 
# 删除
sed '/pattern/d' file # 删除匹配行
sed '/^$/d' file # 删除空行
sed '/^[[:space:]]*$/d' file # 删除空白行(含空格/Tab)
sed '/^#/d' file # 删除注释行
sed '2,5d' file # 删除第 2-5 行
sed '$d' file # 删除最后一行
 
# 行寻址
sed '3s/foo/bar/' file # 仅在第 3 行替换
sed '3,7s/foo/bar/' file # 在第 3-7 行替换
sed '/pattern/s/foo/bar/' file # 在匹配 pattern 的行替换

插入、追加和修改

# 在行前/后插入
sed '3i\这是在第3行前插入的新行' file
sed '3a\这是在第3行后追加的新行' file
sed '/pattern/i\在匹配行前插入' file
sed '$a\在文件末尾追加' file
 
# 修改整行
sed '3c\替换整个第3行为此行' file
sed '/pattern/c\替换匹配行为此行' file
 
# 转换字符
sed 'y/abc/ABC/' file # 将 a→A, b→B, c→C

高级操作:模式空间与保持空间

# sed 有两个缓冲区:模式空间(pattern space)和保持空间(hold space)
# 模式空间:sed 处理每一行时的临时工作区
# 保持空间:可以暂存数据的存储区
 
# 命令速查
# h — 将模式空间复制到保持空间
# H — 将模式空间追加到保持空间
# g — 将保持空间复制到模式空间
# G — 将保持空间追加到模式空间
# x — 交换模式空间和保持空间
 
# 示例 1:反转文件行顺序
sed -n '1!G; h; $p' file # 类似 tac
 
# 示例 2:相邻行合并(将奇数行和偶数行合并)
sed 'N; s/\n/ /' file
 
# 示例 3:删除连续的重复行
sed '$!N; /^\(.*\)\n\1$/!P; D' file
 
# 示例 4:每两行之间插入空行
sed 'G' file
 
# 示例 5:段落间插入空行(单独的空行视为段落分隔)
sed '/^$/d; G' file
 
# 示例 6:多行模式:将以 \ 结尾的行与下一行合并
sed -e :a -e '/\\$/N; s/\\\n//; ta' file

sed 实战示例

# 批量修改配置文件端口
sed -i 's/^Port 22$/Port 2222/' /etc/ssh/sshd_config
 
# 从日志中提取时间戳和错误消息
sed -n 's/^\([0-9:\-T ]\+\) ERROR \(.*\)$/\1 → \2/p' app.log
 
# 删除 HTML 标签(简化版)
sed 's/<[^>]*>//g' page.html
 
# 格式化代码缩进(将 4 空格转为 Tab)
sed 's/ /\t/g' script.py
 
# 在文件开头添加 shebang
sed -i '1i #!/bin/bash' script.sh
 
# 删除行尾空白
sed -i 's/[[:space:]]*$//' file.txt
 
# 提取所有邮箱地址
sed -nE 's/.*([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}).*/\1/p' file.txt
 
# 多文件批处理
find . -name "*.conf" -exec sed -i 's/old_server/new_server/g' {} \;

18.4 awk — 数据处理的瑞士军刀

awk 核心概念

# awk 按 记录 → 字段 模型处理数据
# 默认:一行为一个记录,空白为字段分隔符
# 结构:pattern { action } — pattern 匹配时执行 action
 
# 内置变量
NR # 当前记录号(行号)
FNR # 当前文件内的记录号(多文件时与 NR 不同)
NF # 当前记录的字段数
FS # 输入字段分隔符(默认空白)
OFS # 输出字段分隔符(默认空格)
RS # 输入记录分隔符(默认换行)
ORS # 输出记录分隔符(默认换行)
FILENAME # 当前处理的文件名
$0 # 当前记录的全部内容
$1..$N # 第 1 到第 N 个字段
$NF # 最后一个字段
$(NF-1) # 倒数第二个字段

基本用法

# 打印特定字段
awk '{print $1, $3}' file.txt # 打印第 1 和第 3 字段
awk -F: '{print $1, $NF}' /etc/passwd # 指定分隔符为冒号
awk -F '[ ,]+' '{print $1}' file # 多字符分隔符(空格或逗号)
 
# 条件过滤
awk '$3 > 1000' data.txt # 第 3 字段大于 1000 的行
awk '$3 > 1000 {print $1, $3}' data.txt # 过滤并只输出特定字段
awk '/error/ {print $0}' app.log # 匹配含 error 的行
awk 'NR >= 5 && NR <= 10 {print}' file # 打印 5-10 行
awk 'NF > 3 {print}' file # 只打印字段数 > 3 的行
awk 'length($0) > 80 {print}' file # 只打印长度 > 80 的行
 
# BEGIN 和 END 块
awk 'BEGIN {print "开始处理"} {sum+=$1} END {print "总和:", sum}' nums.txt
awk 'BEGIN {FS=":"; OFS="\t"} {print $1, $3, $NF}' /etc/passwd
awk 'END {print "总行数:", NR}' file
 
# 格式化输出
awk '{printf "%-20s %10d\n", $1, $2}' data.txt
awk -F: '{printf "用户: %-15s UID: %-5d Shell: %s\n", $1, $3, $7}' /etc/passwd

awk 内置函数

# 字符串函数
awk '{print length($0)}' file # 行长度
awk '{print substr($0, 1, 10)}' file # 子字符串
awk '{print index($0, "error")}' file # 查找位置
awk '{print toupper($0)}' file # 转大写
awk '{print tolower($0)}' file # 转小写
awk '{gsub(/old/, "new"); print}' file # 全局替换
awk '{sub(/^[ \t]+/, ""); print}' file # 去除行首空白
awk '{split($0, arr, ":"); print arr[1]}' file # 分割字符串到数组
 
# 数学函数
awk '{print int($1)}' file # 取整
awk '{printf "%.2f\n", $1}' file # 保留两位小数
awk '{print sqrt($1)}' file # 平方根
awk '{print rand()}' /dev/null # 随机数
awk 'BEGIN{srand(); print int(rand()*100)}' # 0-99 随机整数
 
# 时间和日期(GNU awk)
awk 'BEGIN{print strftime("%Y-%m-%d %H:%M:%S")}'

awk 数组

# awk 的数组是关联数组(键可以是数字或字符串)
 
# 统计单词频率
awk '{for(i=1; i<=NF; i++) words[$i]++} END{for(w in words) print words[w], w}' file | sort -rn
 
# 按字段分组求和
awk -F, '{sales[$1] += $2} END{for(s in sales) print s, sales[s]}' sales.csv
 
# 使用数组进行去重
awk '!seen[$0]++' file # 类似 sort -u,但保留原顺序
 
# 多维数组模拟(GNU awk)
awk '{count[$1][$2]++} END{for(i in count) for(j in count[i]) print i, j, count[i][j]}' file

awk 实战示例

# 示例 1:分析 nginx 访问日志,统计状态码分布
awk '{status[$9]++; total++} END {
 for (code in status) {
 printf "HTTP %s: %d (%.1f%%)\n", code, status[code], (status[code]/total)*100
 }
}' /var/log/nginx/access.log | sort
 
# 示例 2:计算文件中所有数字的平均值
awk '{sum+=$1; count++} END {if(count>0) print sum/count}' numbers.txt
 
# 示例 3:将 CSV 转为 JSON
awk -F, 'NR==1{for(i=1;i<=NF;i++) header[i]=$i; next}
 {printf "{";
 for(i=1;i<=NF;i++) {
 printf "\"%s\":\"%s\"", header[i], $i;
 if(i<NF) printf ", "
 }
 print "}"
 }' data.csv
 
# 示例 4:找出文件中最长的一行
awk 'length > max {max = length; longest = $0} END {print longest}' file.txt
 
# 示例 5:计算磁盘使用率(跨平台)
df -h | awk 'NR>1 {printf "%-20s %-10s %-10s %s\n", $1, $3, $2, $5}'
 
# 示例 6:分析 auth.log 中登录失败的用户
awk '/Failed password/{users[$9]++; ips[$11]++}
 END {
 print "=== 失败用户 ==="
 for(u in users) printf "%-20s %d\n", u, users[u]
 print "=== 失败 IP ==="
 for(i in ips) printf "%-20s %d\n", i, ips[i]
 }' /var/log/auth.log | sort -t' ' -k2 -rn | head
 
# 示例 7:合并两个文件(类似 SQL JOIN)
awk 'NR==FNR {map[$1]=$2; next} {print $0, map[$1] ? map[$1] : "N/A"}' file2 file1
 
# 示例 8:处理多行记录(RS="" 表示按段落)
awk 'BEGIN{RS=""; FS="\n"} {print $1}' addresses.txt

高级:awk 脚本文件

# calculate.awk — 保存为独立脚本
#!/usr/bin/awk -f
BEGIN {
 FS = ","
 OFS = "\t"
 print "产品", "数量", "单价", "小计"
 print "══════════════════════════════════"
}
NR > 1 {
 subtotal = $2 * $3
 total += subtotal
 printf "%-20s %8d %10.2f %12.2f\n", $1, $2, $3, subtotal
}
END {
 print "══════════════════════════════════"
 printf "%47s %12.2f\n", "总计:", total
}
 
# 使用: awk -f calculate.awk sales.csv

18.5 三剑客协作实战

日志分析流水线

# 场景:分析 Nginx 日志中访问量前 10 的 URL 及响应时间
awk '{print $7, $NF}' /var/log/nginx/access.log \
 | grep -vE '\.(css|js|png|jpg|ico)$' \
 | sed 's/?.*//' \
 | sort \
 | uniq -c \
 | sort -rn \
 | head -10
 
# 场景:提取 Apache 日志中 5xx 错误,按 IP 聚合
grep -E '" [5][0-9]{2} ' access.log \
 | awk '{ ips[$1]++ } END { for(ip in ips) print ips[ip], ip }' \
 | sort -rn \
 | head -10

配置文件批量处理

# 场景:批量修改多个 ini 文件中特定 section 的值
for conf in /etc/myapp/*.ini; do
 # 在 [database] section 中更新 host 值
 sed -i '/^\[database\]/,/^\[/{ s/^host[[:space:]]*=.*/host = db-prod.example.com/ }' "$conf"
done
 
# 场景:提取所有配置中非默认值
grep -Ev '^#|^$|^;' config.ini \
 | sed 's/[[:space:]]*=[[:space:]]*/=/' \
 | awk -F= '!defaults[$1] || $2 != defaults[$1]'

数据报表生成

# 场景:生成磁盘使用报告
#!/bin/bash
{
 echo "════════ 磁盘使用报告 $(date '+%Y-%m-%d') ════════"
 echo ""
 df -h | awk 'NR>1 && /^\/dev\// {printf "%-20s %-6s / %-6s (%s)\n", $6, $3, $2, $5}' \
 | sed 's/^/ /'
 echo ""
 echo " 使用率超过 80% 的分区:"
 df -h | awk 'NR>1 && /^\/dev\// && substr($5,1,length($5)-1)+0 > 80 {printf " %-20s %s\n", $6, $5}'
} | tee disk-report.txt

18.6 性能与选型建议

# 选型原则
 
# 使用 grep — 只需查找/过滤文本时
grep "ERROR" app.log
 
# 使用 sed — 需要替换、删除、插入文本时
sed -i 's/old/new/g' file.txt
 
# 使用 awk — 需要处理结构化数据、计算、格式化输出时
awk '{sum+=$1} END{print sum}' data.txt
 
# 使用 Python/Perl — 逻辑复杂、需要大量字符串操作时
python3 -c "
import sys
for line in sys.stdin:
 if 'ERROR' in line:
 parts = line.split()
 print(f'{parts[0]} {parts[2]}')
" < app.log
 
# 性能对比(大致,实际因数据而异)
# grep > sed > awk >> Python >> Bash while read 循环
# 对于简单任务,grep/sed/awk 比 Python 快 5-50 倍
# 但逻辑复杂时 Python 更易维护
 
# 大文件处理性能技巧
# 1. grep 先过滤再交给 awk(减少 awk 处理的行数)
# 2. 避免在循环中反复调用外部命令
# 3. 使用 LC_ALL=C 禁用本地化(加速排序和搜索)
time LC_ALL=C grep "pattern" huge_file.txt
time LC_ALL=C sort huge_file.txt
 
# 4. 使用 ripgrep (rg) 替代 grep(Rust 实现,通常更快)
# rg "pattern" /var/log/
# rg --type-add 'log:*.log' -t log "ERROR"
 
# 5. 使用 mawk 替代 gawk(某些场景下更快)

18.7 常见陷阱与注意事项

# 1. grep 的正则默认是 BRE,需要 \+ 而不是 +
grep "a+" file # 匹配 "a+" 这个字面字符串!
grep "a\+" file # 匹配一个或多个 a
grep -E "a+" file # 扩展正则中 + 就是元字符
 
# 2. sed 在 macOS 和 Linux 的差异
# Linux (GNU sed):
sed -i 's/foo/bar/g' file
# macOS (BSD sed):
sed -i '' 's/foo/bar/g' file # -i 后必须跟参数(空字符串表示不备份)
 
# 3. awk 字段分隔符 FS 是正则表达式
awk -F '[ ,]' '{print $3}' # 以空格或逗号分隔
# 而非
awk -F '[, ]' '{print $3}' # 字符集语法不同但效果部分相同
 
# 4. 管道中的退出码
grep "pattern" file | wc -l
echo "${PIPESTATUS[0]}" # 获取管道中第一个命令的退出码
# 如果 grep 没有匹配,退出码是 1,但管道整体退出码是 0
 
# 5. grep 处理二进制文件
# 默认 grep 可能输出 "Binary file matches",使用 -a 强制作为文本处理
grep -a "pattern" binary_file
 
# 6. 处理文件名中的换行符
find . -print0 | xargs -0 grep "pattern" # -print0 和 -0 配对使用

18.8 扩展工具

# ripgrep (rg) — 快速递归搜索,默认忽略 .gitignore 中的文件
rg "pattern" # 递归搜索当前目录
rg -l "pattern" # 只显示文件名
rg -c "pattern" # 计数
rg -t py "pattern" # 只在 Python 文件中搜索
rg -g "*.{md,txt}" "pattern" # 按通配符过滤
rg -C 3 "pattern" # 上下文
rg --type-add 'log:*.log' -t log "ERROR"
 
# fd — 快速文件查找(替代 find)
fd "\.py$" # 查找所有 .py 文件
fd -e txt -e md "pattern" # 查找 .txt 和 .md 文件
fd -x grep "TODO" {} \; # 结合命令执行
 
# ag (The Silver Searcher) — 更早的代码搜索工具
ag "pattern" src/
ag --python "def myfunc"
 
# fzf — 交互式模糊搜索
grep -rl "TODO" | fzf # 交互式选择文件

延伸阅读: 18-Bash编程基础 提供了 Shell 语法基础。21-IO重定向与管道深入 详解管道、重定向和 xargs 组合使用。22-Shell脚本实战 展示真实场景的文本处理脚本。