强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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. 工具组合:每个工具做它最擅长的事
  5. 测试验证:先小范围测试,再大范围执行
  6. 代码可读:写出自己半年后还能看懂的代码

最后的建议

1. 从简单开始,逐步增加复杂度
2. 积累自己的代码片段库
3. 理解原理比记忆语法更重要
4. 遇到问题先查手册(man awk, man sed)
5. 不要追求一行搞定,追求清晰可维护
6. 性能优化要先测量,再优化
7. 多读别人的代码,学习好的实践

扩展阅读


恭喜你完成了 AWK & SED 生产力教程的全部 15 章!

回到目录:教程概览