AWK & SED 生产力教程 / 第 15 章:最佳实践
第 15 章:最佳实践
最好的代码不是最短的,而是最容易理解和维护的。这一章,我们总结 AWK/SED 的最佳实践。
15.1 代码风格
命名规范
# AWK 变量命名:使用有意义的名称
awk '{
# ✅ 好
total_count = NR
error_count = 0
ip_address = $1
request_path = $7
status_code = $9 + 0
# ❌ 不好
n = NR
e = 0
a = $1
b = $7
c = $9
}' file
# AWK 函数命名:动词 + 名词
awk '
function calculate_average(sum, count) { ... }
function format_currency(amount) { ... }
function validate_email(email) { ... }
' file
注释规范
awk '
# ============================================================
# 日志分析脚本
# 用法: awk -f analyze.awk access.log
# 输入: Nginx 访问日志(标准格式)
# 输出: 统计报告(文本格式)
# ============================================================
BEGIN {
# 初始化字段分隔符
FS = " "
# 初始化计数器
total_requests = 0
error_count = 0
}
# 跳过空行
/^$/ { next }
# 处理每行日志
{
total_requests++
# 统计错误请求数(状态码 >= 400)
if ($9 >= 400) error_count++
}
END {
# 输出统计结果
printf "总请求数: %d\n", total_requests
printf "错误请求数: %d\n", error_count
}' file
代码格式化
# ✅ 好:使用缩进和换行
awk '
BEGIN {
FS = ","
OFS = "\t"
total = 0
}
NR > 1 {
if ($3 > 100) {
printf "%s\t%s\t%d\n", $1, $2, $3
total += $3
}
}
END {
printf "总计: %d\n", total
}' data.csv
# ❌ 不好:所有命令挤在一行
awk 'BEGIN{FS=","; OFS="\t"; total=0} NR>1{if($3>100){printf "%s\t%s\t%d\n",$1,$2,$3; total+=$3}} END{printf "总计: %d\n",total}' data.csv
15.2 常见陷阱
AWK 陷阱
陷阱 1:字符串与数值比较
# ❌ 错误:字符串比较
$ echo "9" | awk '{print ($1 < "10") ? "yes" : "no"}'
→ no # 因为 "9" > "1"(字典序)
# ✅ 正确:数值比较
$ echo "9" | awk '{print ($1 < 10) ? "yes" : "no"}'
→ yes
# 💡 提示:使用 +0 强制转换为数值
$ echo "9" | awk '{print ($1+0 < 10) ? "yes" : "no"}'
→ yes
陷阱 2:FS 与字段分割
# ❌ 错误:默认 FS 会合并连续空格
$ echo "a b c" | awk '{print $2}'
→ b
# ✅ 正确:使用正则 FS
$ echo "a b c" | awk -F' +' '{print $2}'
→ b
# 💡 提示:默认 FS=" " 会自动合并连续空格和制表符
$ echo "a b c" | awk '{print $2}' # 默认 FS=" "
→ b # 正确,因为默认 FS 会合并连续空白
陷阱 3:修改字段后 $0 重建
# ❌ 问题:修改字段后 OFS 未生效
$ echo "a b c" | awk 'BEGIN{OFS=","} {$2="X"; print}'
→ a X c # OFS 未生效,因为 $0 还是原始内容
# ✅ 正确:触发 $0 重建
$ echo "a b c" | awk 'BEGIN{OFS=","} {$2="X"; $1=$1; print}'
→ a,X,c
# 💡 提示:修改任何字段后,$0 会用 OFS 重建
$ echo "a b c" | awk 'BEGIN{OFS=","} {$1=$1; print}'
→ a,b,c
陷阱 4:NR 与 FNR
# ❌ 错误:在多文件处理时使用 NR
$ awk '{if (NR == 1) print}' file1.txt file2.txt
→ 只打印 file1.txt 的第一行
# ✅ 正确:使用 FNR
$ awk '{if (FNR == 1) print}' file1.txt file2.txt
→ 打印两个文件的第一行
# 💡 提示:NR 是全局行号,FNR 是文件内行号
陷阱 5:数组遍历顺序
# ❌ 问题:数组遍历顺序不确定
$ awk '{count[$1]++} END{for(k in count) print k, count[k]}' file
# 输出顺序可能每次不同
# ✅ 正确:使用管道排序
$ awk '{count[$1]++} END{for(k in count) print count[k], k}' file | sort -rn
# ✅ 或使用 GNU AWK 的 PROCINFO
$ awk 'BEGIN{PROCINFO["sorted_in"]="@val_num_desc"} {count[$1]++} END{for(k in count) print count[k], k}' file
SED 陷阱
陷阱 1:macOS 与 Linux 的 -i 差异
# Linux (GNU sed)
$ sed -i 's/old/new/g' file
# macOS (BSD sed)
$ sed -i '' 's/old/new/g' file
# ✅ 跨平台写法
$ sed -i.bak 's/old/new/g' file && rm file.bak
陷阱 2:贪婪匹配
# ❌ 问题:贪婪匹配过多
$ echo "<b>hello</b> world" | sed 's/<.*>//'
→ world # 匹配了 <b>hello</b> world
# ✅ 正确:非贪婪匹配(使用字符类)
$ echo "<b>hello</b> world" | sed 's/<[^>]*>//g'
→ hello world
陷阱 3:分隔符冲突
# ❌ 问题:路径中的斜杠
$ echo "/usr/local/bin" | sed 's//usr/local/bin//opt/bin//'
# 错误!
# ✅ 正确:使用其他分隔符
$ echo "/usr/local/bin" | sed 's#/usr/local/bin#/opt/bin#'
陷阱 4:忘记地址范围的副作用
# ❌ 问题:地址范围会持续到文件末尾
$ sed -n '/START/,/END/p' file
# 如果没有 END,会从 START 一直打印到文件末尾
# ✅ 正确:使用行号或更精确的模式
$ sed -n '/^START$/,/^END$/p' file
15.3 调试技巧
AWK 调试
# 1. 使用 print > "/dev/stderr" 输出调试信息
awk '{
print "DEBUG: NR=" NR ", NF=" NF ", $1=" $1 > "/dev/stderr"
# 正常处理
}' file
# 2. 使用 END 块检查最终状态
awk '{
count[$1]++
}
END {
print "DEBUG: 总行数=" NR > "/dev/stderr"
print "DEBUG: 唯一键数=" length(count) > "/dev/stderr"
# 正常输出
}' file
# 3. 使用 GNU AWK 的调试模式
gawk --debug 'BEGIN{print "start"} {print} END{print "end"}' file
# 4. 使用 l 命令显示不可见字符
awk '{print | "cat -A"}' file
# 5. 限制处理行数进行快速测试
awk 'NR <= 10 {print}' file
SED 调试
# 1. 使用 l 命令显示不可见字符
sed -n '/pattern/{l;p}' file
# 2. 分步测试
sed 's/step1/replacement1/' file > /tmp/step1.txt
diff file /tmp/step1.txt
# 3. 限制范围进行测试
sed '1,10s/old/new/g' file
# 4. 使用 w 命令保存中间结果
sed 's/old/new/; w /tmp/debug.txt' file
# 5. 使用 --debug 选项(GNU sed 4.2.2+)
sed --debug 's/old/new/g' file
通用调试技巧
# 1. 使用 set -x 查看执行过程
set -x
awk '{print $1}' file | sort | uniq -c
set +x
# 2. 使用 tee 查看中间结果
cat file | tee /tmp/debug1.txt | awk '{print $1}' | tee /tmp/debug2.txt | sort
# 3. 使用 pv 监控管道吞吐量
cat file | pv -l | awk '{print $1}' | pv -l > /dev/null
# 4. 使用 strace 查看系统调用
strace -e trace=read,write awk '{print $1}' file 2>&1 | head -20
# 5. 使用 time 测量执行时间
time awk '{print $1}' file > /dev/null
15.4 生产力提升
常用命令片段库
# 文本处理
alias count-lines='awk "END{print NR}"'
alias unique-lines='awk "!seen[\$0]++"'
alias first-col='awk "{print \$1}"'
alias last-col='awk "{print \$NF}"'
alias sum-col='awk "{sum+=\$1} END{print sum}"'
alias avg-col='awk "{sum+=\$1; n++} END{print sum/n}"'
# 日志分析
alias count-ips='awk "{count[\$1]++} END{for(k in count) print count[k], k}" | sort -rn'
alias error-count='awk "\$9 >= 400 {count++} END{print count+0}"'
alias top-paths='awk "{count[\$7]++} END{for(k in count) print count[k], k}" | sort -rn | head'
# 系统管理
alias disk-usage='df -h | awk "NR>1 {gsub(/%/,\"\",\$5); if(\$5+0>80) print \"⚠️\",\$6,\$5\"%\"}"'
alias mem-usage='free -m | awk "/^Mem:/ {printf \"内存使用率: %.1f%%\\n\",\$3/\$2*100}"'
快速脚本模板
#!/bin/bash
# template.sh — 脚本模板
set -euo pipefail
# 默认配置
VERBOSE=false
INPUT_FILE=""
# 函数定义
usage() {
cat << EOF
用法: $0 [选项] <输入文件>
选项:
-v 详细输出
-h 显示帮助
EOF
exit 0
}
log() { [[ "$VERBOSE" == "true" ]] && echo "[LOG] $*" >&2 || true; }
error() { echo "[ERROR] $*" >&2; exit 1; }
# 参数解析
while getopts "vh" opt; do
case $opt in
v) VERBOSE=true ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
INPUT_FILE="${1:?请指定输入文件}"
[[ -f "$INPUT_FILE" ]] || error "文件不存在: $INPUT_FILE"
# 主逻辑
log "开始处理: $INPUT_FILE"
awk '{print}' "$INPUT_FILE"
log "处理完成"
命令行快捷键
# Bash 快捷键
Ctrl+R # 反向搜索历史命令
Ctrl+A # 移动到行首
Ctrl+E # 移动到行尾
Ctrl+U # 删除到行首
Ctrl+K # 删除到行尾
Ctrl+W # 删除前一个单词
Alt+. # 插入上一个命令的最后一个参数
# 历史命令
!! # 执行上一条命令
!$ # 上一条命令的最后一个参数
!awk # 执行最近的 awk 命令
工具推荐
| 工具 | 用途 | 安装 |
|---|---|---|
jq | JSON 处理 | apt install jq |
yq | YAML 处理 | snap install yq |
csvkit | CSV 处理 | pip install csvkit |
ripgrep | 快速搜索 | apt install ripgrep |
fd | 快速查找 | apt install fd-find |
fzf | 模糊搜索 | apt install fzf |
bat | 带语法高亮的 cat | apt install bat |
delta | 更好的 diff | apt install delta |
parallel | 并行处理 | apt install parallel |
pv | 管道监控 | apt install pv |
watch | 定时执行 | 系统自带 |
tmux | 终端复用 | apt install tmux |
15.5 实战:重构示例
重构前
# ❌ 难以维护的代码
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10 | awk '{print $2,$1}' | sed 's/ /: /' | awk '{printf "%-16s %s 次\n",$1,$2}'
重构后
# ✅ 清晰易懂的代码
#!/bin/bash
# top_ips.sh — 统计访问量最多的 IP
LOG_FILE="${1:-/var/log/nginx/access.log}"
TOP_N=10
awk -v top_n="$TOP_N" '
{
ip_count[$1]++ # 统计每个 IP 的请求数
}
END {
# 按请求数降序输出前 N 个 IP
for (ip in ip_count)
printf "%8d %s\n", ip_count[ip], ip
}
' "$LOG_FILE" | sort -rn | head -"$TOP_N" | awk '{
printf "%-16s %s 次请求\n", $2, $1
}'
重构原则
| 原则 | 说明 |
|---|---|
| 单一职责 | 每个命令/函数只做一件事 |
| 变量提取 | 把魔法数字和硬编码值提取为变量 |
| 注释说明 | 解释"为什么"而不是"做什么" |
| 错误处理 | 添加输入验证和错误提示 |
| 可测试性 | 设计为可以分步测试的结构 |
| 可读性 | 使用有意义的变量名和适当的格式 |
15.6 学习路线总结
初学者路线(2 周)
Week 1:
✅ 第 1 章:入门导论
✅ 第 2 章:SED 基础
✅ 第 4 章:AWK 基础
✅ 第 6 章:正则表达式基础
Week 2:
✅ 第 7 章:文本处理实战
✅ 第 9 章:管道组合
✅ 第 15 章:最佳实践(常见陷阱部分)
进阶路线(1 个月)
Week 3:
✅ 第 3 章:SED 进阶
✅ 第 5 章:AWK 进阶
✅ 第 8 章:数据提取
Week 4:
✅ 第 10 章:系统管理
✅ 第 11 章:日志分析
✅ 第 12 章:报告生成
专家路线(按需)
✅ 第 13 章:脚本编写
✅ 第 14 章:性能优化
✅ 第 15 章:最佳实践(全部)
15.7 速查卡
AWK 速查
# 基本语法
awk 'pattern {action}' file
awk -F, '{print $1}' file
awk 'BEGIN{...} {...} END{...}' file
# 内置变量
NR # 行号
NF # 字段数
$0 # 整行
$1..N # 字段
FS # 输入分隔符
OFS # 输出分隔符
FILENAME # 文件名
# 常用模式
/pattern/ # 正则匹配
$3 > 100 # 数值比较
$1 == "value" # 字符串比较
NR >= 10 && NR <= 20 # 行范围
# 常用动作
{print $1, $2} # 输出字段
{count[$1]++} # 计数
{sum+=$3} # 求和
{printf "..."} # 格式化输出
# 常用函数
length(s) # 字符串长度
substr(s, i, n) # 子串
split(s, a, sep) # 分割
gsub(r, s, t) # 全局替换
sprintf(fmt, ...) # 格式化
SED 速查
# 基本语法
sed 'command' file
sed -e 'cmd1' -e 'cmd2' file
sed -i 'command' file
# 常用命令
s/old/new/ # 替换
s/old/new/g # 全局替换
/pattern/d # 删除匹配行
/pattern/p # 打印匹配行(需 -n)
i\text # 在行前插入
a\text # 在行后追加
c\text # 替换整行
# 地址
3 # 第 3 行
$ # 最后一行
3,7 # 第 3-7 行
/pattern/ # 匹配行
/pat1/,/pat2/ # 范围
# 标志
g # 全局
p # 打印
i # 忽略大小写
w file # 写入文件
15.8 结语
“那些不能记住过去的人,注定要重复它。” — George Santayana
在文本处理的世界里,AWK 和 SED 已经存在了近 50 年。它们的哲学——做一件事并把它做好——至今仍然是软件工程的核心原则。
核心要点回顾
- 管道思维:复杂问题分解为简单步骤
- 流式处理:一次处理一行,避免内存问题
- 模式-动作:根据数据特征决定处理逻辑
- 工具组合:每个工具做它最擅长的事
- 测试验证:先小范围测试,再大范围执行
- 代码可读:写出自己半年后还能看懂的代码
最后的建议
1. 从简单开始,逐步增加复杂度
2. 积累自己的代码片段库
3. 理解原理比记忆语法更重要
4. 遇到问题先查手册(man awk, man sed)
5. 不要追求一行搞定,追求清晰可维护
6. 性能优化要先测量,再优化
7. 多读别人的代码,学习好的实践
扩展阅读
- The AWK Programming Language — Aho, Kernighan, Weinberger
- sed & awk — O’Reilly
- GNU AWK Manual
- GNU SED Manual
- UNIX Power Tools — O’Reilly
- The Unix Philosophy — Mike Gancarz
恭喜你完成了 AWK & SED 生产力教程的全部 15 章!
回到目录:教程概览