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

AWK & SED 生产力教程 / 第 5 章:AWK 进阶

第 5 章:AWK 进阶

如果说 AWK 基础让你能处理行,那么 AWK 进阶让你能处理整个数据世界。

5.1 关联数组

AWK 的数组是关联数组(类似 Python 的字典),下标可以是任意字符串。

基本操作

# 赋值
awk 'BEGIN {
    fruits["apple"] = 5
    fruits["banana"] = 3
    fruits["cherry"] = 8
    for (f in fruits) print f, fruits[f]
}'

# 检查元素是否存在
awk 'BEGIN {
    fruits["apple"] = 5
    if ("apple" in fruits) print "apple exists"
    if ("grape" in fruits) print "grape exists"
}'

# 删除元素
awk 'BEGIN {
    fruits["apple"] = 5
    delete fruits["apple"]
    print length(fruits)
}'

多维数组(模拟)

AWK 没有真正的多维数组,但用 SUBSEP 模拟:

# 用逗号分隔的下标(SUBSEP 模拟)
awk 'BEGIN {
    matrix[1,1] = "A"
    matrix[1,2] = "B"
    matrix[2,1] = "C"
    matrix[2,2] = "D"
    for (idx in matrix) {
        split(idx, sep, SUBSEP)
        print "matrix["sep[1]","sep[2]"] = " matrix[idx]
    }
}'

🏢 业务场景:频率统计

cat > access.log << 'EOF'
192.168.1.1 GET /index.html 200
192.168.1.2 GET /about.html 200
192.168.1.1 POST /login 302
192.168.1.3 GET /index.html 200
192.168.1.1 GET /dashboard 200
192.168.1.2 GET /missing.html 404
EOF

# 统计各 IP 的请求数
$ awk '{count[$1]++} END{for(ip in count) printf "%-15s %d\n", ip, count[ip]}' access.log

# 统计各状态码数量
$ awk '{count[$3]++} END{for(s in count) print s, count[s]}' access.log

# 找出请求最多的 IP
$ awk '{count[$1]++} END{max=0; for(ip in count) {if(count[ip]>max) {max=count[ip]; top=ip}} print top, max}' access.log

数组遍历顺序

# AWK 不保证遍历顺序!需要排序请用管道
awk '{count[$1]++} END{for(ip in count) print count[ip], ip}' file | sort -rn

# GNU AWK 的 PROCINFO["sorted_in"] 可以控制遍历顺序
awk 'BEGIN{PROCINFO["sorted_in"]="@val_num_desc"} 
{count[$1]++} END{for(ip in count) print count[ip], ip}' file

🏢 业务场景:分组统计

cat > sales.csv << 'EOF'
Alice,Electronics,1500
Bob,Clothing,800
Alice,Electronics,2000
Carol,Food,500
Bob,Electronics,1200
Alice,Food,300
EOF

# 按人统计总销售额
$ awk -F, '{total[$1]+=$3} END{for(p in total) printf "%-10s %10.2f\n", p, total[p]}' sales.csv

# 按类别统计总销售额
$ awk -F, '{total[$2]+=$3} END{for(c in total) printf "%-15s %10.2f\n", c, total[c]}' sales.csv

# 按人和类别统计(二维键)
$ awk -F, '{
    key=$1","$2
    total[key]+=$3
    count[key]++
} END {
    for(k in total) {
        split(k, a, ",")
        printf "%-10s %-15s 总额: %10.2f  次数: %d\n", a[1], a[2], total[k], count[k]
    }
}' sales.csv

5.2 用户自定义函数

函数定义

# 基本语法
awk '
function my_func(param1, param2) {
    # 函数体
    return result
}
{
    result = my_func($1, $2)
    print result
}' file

函数示例

# 绝对值函数
awk '
function abs(x) { return (x < 0) ? -x : x }
{ print abs($1) }
' data.txt

# 最大值函数
awk '
function max(a, b) { return (a > b) ? a : b }
{ current_max = max(current_max, $1) }
END { print "最大值:", current_max }
' data.txt

# 格式化字节大小
awk '
function human_size(bytes) {
    if (bytes >= 1073741824) return sprintf("%.2f GB", bytes/1073741824)
    if (bytes >= 1048576)    return sprintf("%.2f MB", bytes/1048576)
    if (bytes >= 1024)       return sprintf("%.2f KB", bytes/1024)
    return sprintf("%d B", bytes)
}
{ print $1, human_size($2) }
' data.txt

# 去除首尾空白
awk '
function trim(s) {
    gsub(/^[[:space:]]+|[[:space:]]+$/, "", s)
    return s
}
{ print trim($0) }
' file.txt

🏢 业务场景:日志时间解析

# 解析日志时间差
awk '
function parse_ts(ts) {
    # 假设格式: 15/Jan/2024:10:23:45
    gsub(/[\[\]\/:]/, " ", ts)
    split(ts, d, " ")
    # 简化处理,返回秒数
    return d[4]*3600 + d[5]*60 + d[6]
}
{
    ts = parse_ts($4)
    if (prev_ts > 0 && ts - prev_ts > 60) {
        print "间隔超过 1 分钟: " prev_line " -> " $0
    }
    prev_ts = ts
    prev_line = $0
}' access.log

函数的作用域

# AWK 的变量默认是全局的
# 使用函数参数来避免命名冲突

# ❌ 不好 — 全局变量可能冲突
awk '{
    temp = $1 + 1
    result = temp * 2
    print result
}'

# ✅ 好 — 使用函数参数
awk '
function calculate(x,    temp) {
    temp = x + 1       # temp 是局部变量(因为它是参数)
    return temp * 2
}
{ print calculate($1) }
' file

# 💡 技巧:额外的参数名用作局部变量声明
awk '
function process(data,    i, n, arr, result) {
    # i, n, arr, result 都是局部变量
    n = split(data, arr, ",")
    for (i = 1; i <= n; i++) result += arr[i]
    return result
}
' file

📌 重要:AWK 没有 local 关键字。函数参数列表中多出来的参数名会被当作局部变量使用。这是 AWK 的独特约定。

5.3 内置函数

字符串函数

函数说明示例
length(s)字符串长度length("hello") → 5
substr(s, i, n)子串substr("abcde", 2, 3) → “bcd”
index(s, t)查找位置index("hello", "ll") → 3
split(s, a, sep)分割split("a:b:c", arr, ":")
sub(r, s, t)替换第一个sub(/o/, "O", $0)
gsub(r, s, t)全局替换gsub(/o/, "O", $0)
match(s, r)正则匹配match($0, /[0-9]+/)
sprintf(fmt, ...)格式化sprintf("%05d", 42)
tolower(s)转小写tolower("HELLO")
toupper(s)转大写toupper("hello")
gensub(r, s, h, t)通用替换 (gawk)gensub(/(.)(.)/, "\\2\\1", "g", "ab")

数学函数

函数说明
sin(x), cos(x), atan2(y,x)三角函数
exp(x), log(x), sqrt(x)指数、对数、平方根
int(x)取整
rand()0-1 随机数
srand(x)设置随机种子
# 生成 1-100 的随机数
awk 'BEGIN { srand(); print int(rand()*100)+1 }'

# 计算标准差
awk '{
    sum += $1
    sumsq += $1 * $1
    n++
}
END {
    mean = sum / n
    variance = sumsq/n - mean*mean
    printf "均值: %.2f, 标准差: %.2f\n", mean, sqrt(variance)
}' data.txt

I/O 函数

# getline — 读取额外输入
awk '{
    cmd = "date +%Y-%m-%d"
    cmd | getline today
    close(cmd)
    print today, $0
}' file

# 管道命令
awk '{
    print $1 | "sort -u"
    close("sort -u")
}' file

⚠️ 注意:使用管道命令后务必 close(),否则会耗尽文件描述符。

5.4 多文件处理

FNR 和 NR 的区别

# FNR — 当前文件内的行号(每读新文件重置)
# NR  — 全局行号(持续递增)

$ awk '{print FILENAME, NR, FNR}' file1.txt file2.txt
→ file1.txt 1 1
→ file1.txt 2 2
→ file2.txt 3 1
→ file2.txt 4 2

🏢 业务场景:关联两个文件

# users.txt — 用户信息
cat > users.txt << 'EOF'
1001 Alice
1002 Bob
1003 Carol
EOF

# orders.txt — 订单数据
cat > orders.txt << 'EOF'
1001 500.00
1001 300.00
1002 200.00
1003 800.00
EOF

# 先加载用户信息到数组,再处理订单
$ awk '
    NR==FNR { name[$1] = $2; next }
    { printf "%-10s %s %10.2f\n", name[$1], $1, $2 }
' users.txt orders.txt
→ Alice      1001     500.00
→ Alice      1001     300.00
→ Bob        1002     200.00
→ Carol      1003     800.00

💡 关键技巧NR==FNR 只在第一个文件中为 true,配合 next 可以区分文件处理逻辑。

用 AWK 实现 JOIN 操作

# INNER JOIN
awk '
NR==FNR { data[$1] = $2; next }
$1 in data { print $0, data[$1] }
' file1.txt file2.txt

# LEFT JOIN(输出所有右表记录,匹配不到填默认值)
awk '
NR==FNR { data[$1] = $2; next }
{ print $0, ($1 in data ? data[$1] : "N/A") }
' file1.txt file2.txt

5.5 管道 I/O 与外部命令

将输出传给外部命令

# 将每行输出传给外部命令
awk '{print $1 | "sort"}' file

# 将所有输出一次性传给外部命令
awk '{print $1}' file | sort

# 读取外部命令输出
awk 'BEGIN {
    while (("date" | getline line) > 0) {
        print "当前时间:", line
    }
    close("date")
}'

🏢 业务场景:执行批量命令

# 对日志中出现的每个 IP 执行 whois 查询
awk '{print $1}' access.log | sort -u | while read ip; do
    echo "=== $ip ==="
    whois "$ip" | awk '/Organization/ {print; exit}'
done

# AWK 内部直接执行
awk '!seen[$1]++ {
    cmd = "dig +short " $1
    cmd | getline result
    close(cmd)
    print $1, "->", result
}' hostnames.txt

5.6 高级格式化输出

精美的表格输出

cat > employees.txt << 'EOF'
Alice Engineering 15000
Bob Marketing 12000
Carol Engineering 16000
Dave Sales 11000
Eve Engineering 14000
EOF

awk '
BEGIN {
    width_name = 12
    width_dept = 15
    width_sal = 12
    total_width = width_name + width_dept + width_sal + 6

    # 打印上边框
    printf "┌"
    for(i=1; i<=width_name+2; i++) printf "─"
    printf "┬"
    for(i=1; i<=width_dept+2; i++) printf "─"
    printf "┬"
    for(i=1; i<=width_sal+2; i++) printf "─"
    printf "┐\n"

    # 打印表头
    printf "│ %-*s │ %-*s │ %*s │\n", width_name, "姓名", width_dept, "部门", width_sal, "薪资"

    # 打印分隔线
    printf "├"
    for(i=1; i<=width_name+2; i++) printf "─"
    printf "┼"
    for(i=1; i<=width_dept+2; i++) printf "─"
    printf "┼"
    for(i=1; i<=width_sal+2; i++) printf "─"
    printf "┤\n"
}
{
    printf "│ %-*s │ %-*s │ %*d │\n", width_name, $1, width_dept, $2, width_sal, $3
    sum += $3
}
END {
    printf "├"
    for(i=1; i<=width_name+2; i++) printf "─"
    printf "┼"
    for(i=1; i<=width_dept+2; i++) printf "─"
    printf "┼"
    for(i=1; i<=width_sal+2; i++) printf "─"
    printf "┤\n"

    printf "│ %-*s │ %-*s │ %*d │\n", width_name, "合计", width_dept, "", width_sal, sum

    printf "└"
    for(i=1; i<=width_name+2; i++) printf "─"
    printf "┴"
    for(i=1; i<=width_dept+2; i++) printf "─"
    printf "┴"
    for(i=1; i<=width_sal+2; i++) printf "─"
    printf "┘\n"
}' employees.txt

生成 HTML 表格

awk '
BEGIN {
    print "<table border=\"1\" cellpadding=\"5\">"
    print "<tr><th>Name</th><th>Department</th><th>Salary</th></tr>"
}
{
    printf "<tr><td>%s</td><td>%s</td><td>%d</td></tr>\n", $1, $2, $3
}
END {
    print "</table>"
}' employees.txt

生成 Markdown 表格

awk '
BEGIN {
    printf "| %-10s | %-15s | %10s |\n", "Name", "Department", "Salary"
    printf "|%-11s|%-16s|%-11s|\n", "-----------", "----------------", "-----------"
}
{ printf "| %-10s | %-15s | %10d |\n", $1, $2, $3 }
END {
    printf "|%-11s|%-16s|%-11s|\n", "-----------", "----------------", "-----------"
}' employees.txt

5.7 BEGINFILE 和 ENDFILE(GNU AWK)

# GNU AWK 4.0+ 支持 BEGINFILE 和 ENDFILE
gawk '
BEGINFILE { print "开始处理:", FILENAME }
FNR==1 { print "--- 文件内容 ---" }
{ print }
ENDFILE { print "结束处理:", FILENAME; print "" }
' file1.txt file2.txt file3.txt

🏢 业务场景:处理多个 CSV 文件并添加文件名列

gawk -F, '
BEGINFILE { fname = FILENAME; sub(/.*\//, "", fname) }
NR > 1 { print $0 "," fname }
' *.csv

5.8 高级数组技巧

用数组模拟集合操作

# 集合去重(类似 sort -u)
awk '!seen[$0]++' file

# 两个文件的交集
awk '
NR==FNR { set[$0]; next }
$0 in set
' file1.txt file2.txt

# 两个文件的差集(在 file1 中但不在 file2 中)
awk '
NR==FNR { set[$0]; next }
!($0 in set)
' file2.txt file1.txt

# 两个文件的并集
awk '!seen[$0]++' file1.txt file2.txt

用数组实现计数器和累加器

# 复杂统计示例
awk -F, '{
    dept = $2
    # 初始化最大最小值
    if (!(dept in max_sal) || $3 > max_sal[dept]) max_sal[dept] = $3
    if (!(dept in min_sal) || $3 < min_sal[dept]) min_sal[dept] = $3
    sum_sal[dept] += $3
    count[dept]++
} END {
    for (d in count) {
        printf "%-15s 人数:%3d 平均:%8.0f 最高:%8d 最低:%8d\n",
            d, count[d], sum_sal[d]/count[d], max_sal[d], min_sal[d]
    }
}' employees.csv

数组排序输出

# GNU AWK 的 PROCINFO["sorted_in"]
awk 'BEGIN {
    PROCINFO["sorted_in"] = "@val_num_desc"
}
{ count[$1]++ }
END {
    for (ip in count) printf "%8d %s\n", count[ip], ip
}' access.log

# 其他排序方式
# @ind_str_asc   — 按字符串下标升序
# @ind_num_asc   — 按数值下标升序
# @val_str_asc   — 按字符串值升序
# @val_num_asc   — 按数值值升序
# @val_num_desc  — 按数值值降序

5.9 COPROC 和网络编程(GNU AWK)

# 使用协进程(coprocess)
awk 'BEGIN {
    while (("date" |& getline line) > 0) {
        print line
    }
    close("date", "to")
}'

💡 扩展:GNU AWK 还支持网络编程(/inet/tcp/...),但这超出了本教程的范围。感兴趣的读者可以查阅 GNU AWK 手册。

5.10 综合实战

🏢 场景一:日志分析仪表板

cat > report.awk << 'EOF'
BEGIN {
    print "========================================"
    print "          访问日志分析报告"
    print "========================================"
    total = 0
    errors = 0
}

{
    total++
    ip_count[$1]++
    status_count[$9]++
    path_count[$7]++
    bytes += $10
    
    if ($9 ~ /^[45]/) errors++
    if ($9 ~ /^404$/) not_found++
}

END {
    print ""
    printf "总请求数:    %d\n", total
    printf "错误请求数:  %d (%.1f%%)\n", errors, errors/total*100
    printf "404 请求数:  %d\n", not_found
    printf "总传输量:    %.2f MB\n", bytes/1048576
    
    print ""
    print "--- Top 10 IP ---"
    # 排序输出(使用外部 sort)
    for (ip in ip_count) {
        printf "%8d %s\n", ip_count[ip], ip
    } | "sort -rn | head -10"
    close("sort -rn | head -10")
    
    print ""
    print "--- 状态码分布 ---"
    for (s in status_count) {
        printf "%-6s %6d (%.1f%%)\n", s, status_count[s], status_count[s]/total*100
    }
}
EOF

$ awk -f report.awk access.log

🏢 场景二:实时监控脚本

#!/bin/bash
# monitor.sh — 实时监控日志

tail -f /var/log/nginx/access.log | awk '
BEGIN {
    print "开始监控..."
    errors = 0
    total = 0
}
{
    total++
    if ($9 ~ /^[45]/) {
        errors++
        printf "\033[31m[警告] %s %s %s\033[0m\n", $1, $7, $9
    }
    if (total % 100 == 0) {
        printf "[统计] 总请求: %d, 错误: %d (%.1f%%)\n", 
            total, errors, (errors/total)*100
    }
}'

5.11 AWK 进阶速查

# 数组
arr[key] = value          # 赋值
key in arr                # 检查存在
delete arr[key]           # 删除元素
for (k in arr) ...        # 遍历
length(arr)               # 元素个数 (gawk)

# 函数
function name(params, locals) { ... }
function abs(x) { return (x < 0) ? -x : x }
function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s }

# 内置函数
length(s)  substr(s,i,n)  index(s,t)  split(s,a,sep)
sub(r,s,t) gsub(r,s,t)    match(s,r)  sprintf(fmt,...)
tolower(s) toupper(s)      int(x)      sqrt(x)

# 多文件
NR==FNR { ...; next }  # 只在第一个文件执行
FNR==1 { ... }         # 每个文件的第一行
FILENAME               # 当前文件名

# 排序输出
for (k in arr) print k, arr[k] | "sort"

扩展阅读


下一章:第 6 章:正则表达式 — 深入理解 BRE、ERE 和 PCRE。