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

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 命令

工具推荐

工具用途安装
jqJSON 处理apt install jq
yqYAML 处理snap install yq
csvkitCSV 处理pip install csvkit
ripgrep快速搜索apt install ripgrep
fd快速查找apt install fd-find
fzf模糊搜索apt install fzf
bat带语法高亮的 catapt install bat
delta更好的 diffapt 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 章!

回到目录:教程概览