awk 文本处理工具完整指南
awk 学习笔记 -- 文本分析与数据处理利器
一、awk 概述与基本概念
1.1 什么是 awk
awk 是一种强大的文本分析工具,其名称来源于三位创始人的姓氏首字母:Alfred Aho、Peter Weinberger 和 Brian Kernighan。awk 诞生于 1977 年的贝尔实验室,最初目的是在 Unix 环境下处理结构化文本数据。它既是一种命令——一种编程语言,也是一种数据处理引擎。
awk 的本质
awk 本质上是一种数据驱动的编程语言,它逐行读取输入文件,自动将每行分割成字段,然后根据用户编写的模式-动作(pattern-action)规则来处理数据。与许多编程语言不同,awk 特别擅长处理按行列组织的结构化文本,如日志文件、CSV 表格、系统配置文件等。
1.2 awk 的发展历史
awk 在几十年的发展中经历了多个重要版本:
年代 版本 特点
1977 原始 awk 由 Aho、Weinberger、Kernighan 在贝尔实验室开发,功能较简单
1985 新版 awk (nawk) 增加了更多内置函数和动态正则表达式
1988 GNU awk (gawk) 自由软件基金会实现,功能最丰富,成为 Linux 标准 awk
1990s gawk 持续演进 增加网络支持、多字节字符、精确算术等
2012 gawk 4.0 支持协程、双向管道、间接函数调用
2018 gawk 5.0 改进的解析引擎,增强的正则表达式能力
至今 gawk 持续维护 保持向后兼容,持续修复和优化
核心事实: 大多数 Linux 发行版中的 awk 实际上是 gawk(GNU awk)。gawk 完全兼容 POSIX awk 规范,并提供了大量扩展功能。在 macOS 上,默认 awk 是 2007 年的 BSD 版本(基于 nawk),功能相对有限,可通过 Homebrew 安装 gawk 获得完整能力。
1.3 awk 的设计哲学
awk 的设计遵循了 Unix 哲学的核心理念——"小而专"以及"数据驱动"。其独到之处在于:
数据驱动而非流程驱动: 你描述的是"对什么样的数据做什么处理",而非手动画出每一个执行步骤
隐式主循环: awk 自动读取输入、分割字段、执行规则,用户不需要编写显式的文件读取和循环代码
文本与数字的无缝切换: awk 自动在字符串和数字之间进行类型转换,大大减少了类型处理的麻烦
关联数组作为核心数据结构: 内置的关联数组(哈希表)使得键值对操作和统计汇总变得极其简洁
1.4 awk 与同类工具的对比
工具 擅长领域 语言复杂度 适用场景
awk 结构化文本分析、报表生成 中等(独立语言) 列操作、统计汇总、格式化输出
sed 文本流编辑与替换 低 查找替换、行操作、批量编辑
grep 文本搜索与模式匹配 低 搜索过滤、匹配行查找
Perl 通用文本处理 高 复杂文本解析、正则达人
Python 通用编程 + 文本处理 高 复杂数据处理、Web 服务
学习建议
如果你需要快速完成日常的文本分析任务(如分析日志、统计数据、格式化报表),awk 是命令行环境下效率最高的选择。当任务需要复杂的字符串处理逻辑或面向对象设计时,建议切换至 Python 或 Perl。掌握 awk 可以让你在命令行环境中如虎添翼,极大提高数据处理效率。
二、安装与基本使用
2.1 检查是否已安装 awk
大多数 Unix/Linux 系统和类 Unix 环境(包括 macOS 和 WSL)都已预装 awk。你可以通过以下命令确认:
awk --version
awk -V
which awk
2.2 安装 awk
如果系统未安装 awk 或需要最新版 gawk,可以按以下方式安装:
平台 安装命令
Debian/Ubuntu sudo apt install gawk
RHEL/CentOS/Fedora sudo yum install gawk 或 sudo dnf install gawk
Arch Linux sudo pacman -S gawk
macOS (Homebrew) brew install gawk
macOS (MacPorts) sudo port install gawk
Windows (WSL) 在 WSL 对应发行版中使用上述 Linux 命令
Windows (Cygwin) 在 Cygwin 安装程序中选择 gawk 包
2.3 运行 awk 程序的三种方式
awk 程序可以通过三种方式执行:
方式一:命令行直接执行
适用于简短的单行程序,直接在终端中输入 awk 命令和程序代码:
awk '{ print $1 }' data.txt
cat data.txt | awk '{ print $1, $3 }'
awk '{ print }' < data.txt
方式二:脚本文件执行
适用于较长的多行程序,将 awk 代码写入文件,然后用 -f 参数指定:
awk -f script.awk data.txt
awk -f begin.awk -f main.awk -f end.awk data.txt
方式三:可执行脚本(Shebang)
在 awk 脚本文件首行添加 #!/usr/bin/awk -f,赋予执行权限后可直接运行:
BEGIN { print "姓名", "总分"; print "---", "---" }
{ total = $2 + $3 + $4; print $1, total }
chmod +x process.awk
./process.awk scores.txt
实用技巧
在日常使用中,80% 的 awk 任务可以通过命令行的一行程序完成。当程序超过 5-6 行时,建议使用脚本文件方式以方便维护和复用。对于非常复杂的 awk 程序,可执行脚本文件是最高效的管理方式。
三、基本语法结构
3.1 awk 程序的基本结构
awk 程序的核心结构由三部分组成:BEGIN 块、主处理块(pattern-action)、END 块。
awk 'BEGIN { 初始化操作 }
/pattern/ { 对匹配的行执行操作 }
END { 收尾操作 }' input.txt
程序结构解读:
BEGIN 块 -- 在读取任何输入之前执行。适用于变量初始化、打印表头、设置分隔符等
主处理块 -- 对每一行输入执行(可选地匹配模式后执行)。这是 awk 的核心处理逻辑
END 块 -- 在所有输入处理完毕后执行。适用于打印汇总结果、关闭资源等
3.2 执行流程详解
awk 程序的执行流程清晰且自动化:
读取输入
→
变量初始化 (BEGIN 块)
→
读一行
→
分割字段
→
匹配模式
→
执行动作
→
还有行?
→
END 块
执行 BEGIN 块中的代码(仅一次)
从输入文件(或标准输入)读取一行
按字段分隔符(FS,默认空白)将行分割为字段
将 $0 设为整行,$1、$2、... 设为各字段,更新 NF、NR、FNR 等内置变量
依次检查每个模式-动作规则:如果模式匹配,则执行对应动作
重复步骤 2-5,直到文件末尾
执行 END 块中的代码(仅一次)
3.3 最简单的 awk 程序
awk '{ print }' file.txt
awk 'NF { print }' file.txt
awk '{ print NR, $0 }' file.txt
awk '{ print $1, $2 }' file.txt
awk 'BEGIN { print "姓名\t年龄\t城市" }
{ print $1 "\t" $2 "\t" $3 }
END { print "--- 打印完毕 ---" }' persons.txt
3.4 模式-动作(Pattern-Action)规则
awk 的基本编程单元是"模式-动作"对。模式决定何时执行动作,动作决定做什么:
模式 含义 示例
/regex/正则表达式匹配 /error/ { print }
$n ~ /regex/ 或 $n !~ /regex/特定字段匹配/不匹配正则 $3 ~ /^1[0-9]/
expression条件表达式为真 $3 > 100 { print }
BEGIN在处理任何输入之前 BEGIN { FS="," }
END在所有输入处理之后 END { print NR }
pattern1, pattern2范围模式(从匹配 p1 到匹配 p2) /start/, /end/
空模式 匹配所有行 { print }
awk '/ERROR/ { print NR, $0 }' app.log
awk '$3 > 1000 { print $1, $3 }' transactions.txt
awk '/BEGIN_DATA/, /END_DATA/' config.txt
awk '$3 > 1000 && $4 ~ /USD/ { print }' data.txt
awk '/ERROR/ || /FATAL/ { print NR, $0 }' app.log
awk '!/DEBUG/ { print }' app.log
awk '$1 ~ /^192\.168/ { print }' access.log
模式省略的注意事项
如果省略模式,动作会对每一行执行,等价于模式为"真"。如果省略动作(只有模式),awk 默认执行 { print },即打印匹配的行。这与 grep 的行为类似。
四、内置变量与字段处理
4.1 核心内置变量
awk 在读取每一行数据时,会自动设置一系列内置变量,这些变量是 awk 编程的基础:
变量 含义 示例值
$0当前处理的整行内容 "John 85 90 88"
$1, $2, ..., $NF按分隔符分割后的第 N 个字段 $1="John", $2="85"
NFNumber of Fields,当前行的字段数 4
NRNumber of Records,已读取的总行数 1, 2, 3, ...
FNR当前文件的行号(多文件时重置) 1, 2, ...
FSField Separator,字段分隔符(默认空格/Tab) " "
OFSOutput Field Separator,输出字段分隔符(默认空格) " "
RSRecord Separator,记录分隔符(默认换行符 \n) "\n"
ORSOutput Record Separator,输出记录分隔符(默认换行符) "\n"
FILENAME当前处理的文件名 "data.txt"
ARGC命令行参数数量 2
ARGV命令行参数数组 ARGV[0]="awk", ARGV[1]="data.txt"
CONVFMT数字转字符串的格式(默认 "%.6g") "%.6g"
OFMT数字输出格式(默认 "%.6g") "%.6g"
RLENGTHmatch() 函数匹配到的字符串长度 5
RSTARTmatch() 函数匹配到的起始位置 3
SUBSEP多维数组下标分隔符(默认 "\034") "\034"
字段引用要点:
$NF 表示最后一个字段(NF 是字段数,$NF 就是最后一个字段的值)
$(NF-1) 表示倒数第二个字段
当字段数不确定时,$NF 是最常用的技巧之一
字段从 1 开始编号,$0 是整行
字段引用可以被赋值:$1 = "NewValue" 会修改该字段,$0 也会被重建
4.2 字段分隔符配置
awk 的灵活之处在于可以自由配置字段分隔符:
awk -F ',' '{ print $1, $2 }' data.csv
awk -F ':' '{ print $1, $3 }' /etc/passwd
awk -F '\t' '{ print $1, $2 }' data.tsv
awk -F '[ \t]+' '{ print }' data.txt
awk -F '[|]' '{ print $1, $2 }' data.psv
awk 'BEGIN { FS="," } { print $1, $2 }' data.csv
awk -F '[,;:]' '{ print $1, $2 }' data.txt
awk -F '"[^"]*"' '{ print $2 }' data.txt
awk 'FNR==1 { if (FILENAME=="a.csv") FS=","; else FS="\t" } { print $1 }' a.csv b.tsv
4.3 输出字段分隔符(OFS)
awk '{ print $1, $2 }' <<< "a|b|c"
awk 'BEGIN { OFS="," } { print $1, $2 }' data.txt
awk '{ printf "%-20s %8d\n", $1, $2 }' data.txt
awk 'BEGIN { OFS=" | "; ORS="\n---\n" } { print $1, $2, $3 }' data.txt
print 与 printf 的区别
print -- 使用 OFS 连接参数,使用 ORS 作为行尾。参数以逗号分隔时自动插入 OFS
printf -- 与 C 语言的 printf 相同,完全控制格式。不会自动添加任何分隔符
通常来说,简单输出用 print,需要对齐或格式化时用 printf
4.4 记录分隔符(RS)
默认情况下,awk 以换行符作为记录分隔符。但你可以更改 RS 来处理多行记录:
awk 'BEGIN { RS="" } { print "段落:", $0 }' article.txt
awk 'BEGIN { RS="\n\n"; FS="\n" } { print "主机块:", NR, $1 }' /etc/hosts
awk 'BEGIN { RS="====" } { print "记录 " NR ":", substr($0, 1, 50) }' sections.txt
awk 'BEGIN { RS="[0-9]+\\." } { print "节:", $0 }' chapter.txt
五、模式匹配与正则表达式
5.1 awk 中的正则表达式
awk 支持功能丰富的正则表达式,是进行文本模式匹配的基础。正则表达式可以出现在模式匹配运算符 ~(匹配)和 !~(不匹配)中,也可以写在 /regex/ 包围的模式中。
元字符 含义 示例
.匹配任意单个字符(除换行符) /a.c/ 匹配 "abc", "a1c"
*匹配前一个字符 0 次或多次 /ab*c/ 匹配 "ac", "abc", "abbc"
+匹配前一个字符 1 次或多次(gawk 扩展) /ab+c/ 匹配 "abc", "abbc"
?匹配前一个字符 0 次或 1 次(gawk 扩展) /ab?c/ 匹配 "ac", "abc"
^匹配行首 /^#/ 匹配以 # 开头的行
$匹配行尾 /error$/ 匹配以 error 结尾的行
[...]字符集,匹配括号内任一字符 /[aeiou]/ 匹配任意元音字母
[^...]否定字符集,匹配不在括号内的字符 /[^0-9]/ 匹配非数字字符
(...)分组,将多个字符视为一个整体 /(abc)+/ 匹配 "abcabc"
|或,匹配左边或右边的模式 /error|failed/ 匹配 "error" 或 "failed"
\转义字符 /\./ 匹配字面量点号
{n,m}重复次数范围(gawk 扩展) /[0-9]{3,5}/ 匹配 3-5 个数字
\< \>词首/词尾边界(gawk 扩展) /\<foo\>/ 匹配独立单词 "foo"
5.2 正则表达式匹配运算符
awk '$1 ~ /^192\.168/ { print }' access.log
awk '$3 ~ /[0-9]+\.[0-9]+/ { print }' data
awk '$2 !~ /^#/ { print }' config.conf
awk '$1 !~ /^$/ { print }' data.txt
awk '/^[0-9]/ { print }' data.txt
awk '/[[:upper:]]/ { print }' data.txt
awk '!/^#|^$/' config.conf
5.3 常见模式匹配示例
awk '$1 ~ /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/' data.txt
awk '$2 ~ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/' users.txt
awk '$3 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/' records.txt
awk '$3 >= 100 && $3 <= 500' data.txt
awk '$0 ~ /https?:\/\/[^ ]+/' urls.txt
awk '$1 ~ /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/' network.txt
awk '$0 ~ /ERROR/ && $0 ~ /timeout/ && $3 > 500' app.log
5.4 字符串函数与正则配合
awk '{ if (match($0, /[0-9]+\.[0-9]+/)) print "找到数字:", substr($0, RSTART, RLENGTH) }' data.txt
awk '{ sub(/old/, "new"); print }' data.txt
awk '{ gsub(/[ \t]+/, ","); print }' data.txt
awk '{ gsub(/"/, ""); print }' data.csv
awk '{ new = gensub(/([0-9]+)-([0-9]+)/, "\\2/\\1", "g"); print new }' data.txt
正则匹配实用建议
在 awk 中,正则表达式主要处理整行或某个字段。对于复杂的多层条件,建议使用 && 和 || 组合多个简单条件,而不是编写一个复杂的正则表达式。这不仅使代码更易读,也更容易调试和维护。awk 的字符串函数(match、sub、gsub)通常比纯正则表达式更灵活。
六、流程控制与循环
6.1 条件语句:if/else
awk 提供了完整的条件控制结构,语法与 C 语言类似:
awk '{
if ($3 >= 90)
grade = "A"
else if ($3 >= 80)
grade = "B"
else if ($3 >= 70)
grade = "C"
else if ($3 >= 60)
grade = "D"
else
grade = "F"
print $1, grade
}' scores.txt
awk '{ result = ($3 >= 60) ? "及格" : "不及格"; print $1, result }' scores.txt
awk '$3 >= 90 { print $1, "优秀" } $3 < 60 { print $1, "不及格" }' scores.txt
6.2 循环结构
awk 支持 for、while、do-while 三种循环结构:
awk '{
sum = 0
for (i = 1; i <= NF; i++) {
sum += $i
}
avg = sum / NF
print "平均分:", avg
}' scores.txt
awk '{
count[$1]++
}
END {
for (key in count) {
print key, count[key]
}
}' data.txt
awk '{
i = 1
while (i <= NF) {
printf "%s ", $i * 2
i++
}
printf "\n"
}' numbers.txt
awk '{
i = 1
do {
printf "%s ", $i
i++
} while (i <= NF)
printf "\n"
}' data.txt
6.3 break、continue 和 next
awk '{
for (i = 1; i <= NF; i++) {
if ($i == 0) break
sum += $i
}
print sum
}' data.txt
awk '{
for (i = 1; i <= NF; i++) {
if ($i % 2 == 0) continue
printf "%s ", $i
}
printf "\n"
}' numbers.txt
awk '{
if (/^#/) next # 跳过注释行
if (NF == 0) next # 跳过空行
print "有效行:", $0
}' config.conf
awk 'FNR == 1 && FILENAME ~ /^\./ { nextfile } { print FILENAME, $0 }' .[^.]* *.txt
awk '{
if ($1 == "STOP") exit
print $0
}
END { print "处理完毕, 共处理", NR, "行" }' data.txt
6.4 数组遍历的顺序问题
重要:关联数组的遍历顺序
在 awk 中,使用 for (key in array) 遍历关联数组时,遍历顺序是不确定的 。取决于 awk 实现和哈希表内部状态。如果需要对输出进行排序,必须在遍历后显式排序,或使用 PROCINFO["sorted_in"](gawk 4.0+)控制排序策略。
awk '{ count[$1]++ } END { for (k in count) print count[k], k }' data.txt | sort -rn
awk 'BEGIN { PROCINFO["sorted_in"] = "@val_num_desc" }
{ count[$1]++ }
END { for (k in count) print count[k], k }' data.txt
七、内置函数详解
awk 提供了丰富的内置函数,涵盖字符串操作、数学计算、时间处理、类型转换等多个领域。
7.1 字符串函数
函数 语法 说明 示例
lengthlength([str])返回字符串长度(无参数时返回 $0 长度) length("hello") → 5
indexindex(str, sub)返回子串位置(从 1 开始),未找到返回 0 index("hello", "ll") → 3
substrsubstr(str, start [, len])返回子串,从 start 开始取 len 个字符 substr("hello", 2, 3) → "ell"
matchmatch(str, regex)正则匹配,设置 RSTART 和 RLENGTH match("abc123", /[0-9]+/) → 4
splitsplit(str, arr [, regex])将字符串按分隔符分割到数组,返回元素数 split("a:b:c", a, ":") → 3
subsub(regex, repl [, target])替换第一个匹配,返回替换次数 sub(/old/, "new")
gsubgsub(regex, repl [, target])替换所有匹配,返回替换次数 gsub(/old/, "new")
gensubgensub(regex, repl, how [, target])gawk 扩展,返回替换结果(不修改原串) gensub(/(.)(.)/, "\\2\\1", "g", "ab") → "ba"
touppertoupper(str)字符串转大写 toupper("hello") → "HELLO"
tolowertolower(str)字符串转小写 tolower("HELLO") → "hello"
sprintfsprintf(fmt, expr-list)格式化字符串并返回(不打印) sprintf("%.2f", 3.14159) → "3.14"
asortasort(arr [, dest])gawk 扩展,对数组值排序,返回元素数 asort(a)
asortiasorti(arr [, dest])gawk 扩展,对数组键排序,返回元素数 asorti(a)
patsplitpatsplit(str, arr [, regex])gawk 扩展,用正则模式分割字符串 patsplit("ab12cd34", a, /[0-9]+/)
awk '{
pos = index($0, "ERROR:")
if (pos > 0) {
msg = substr($0, pos + 6)
print "错误信息:", msg
}
}' app.log
awk '{
n = split($0, fields, /[,;:|]/)
for (i = 1; i <= n; i++) {
printf "字段%d: %s ", i, fields[i]
}
printf "\n"
}' data.txt
awk '{
gsub(/^[ \t]+|[ \t]+$/, "") # 去除首尾空白
gsub(/^"|"$/, "") # 去除首尾引号
print
}' dirty.csv
awk '{
formatted = sprintf("姓名: %-10s 年龄: %3d 分数: %5.1f", $1, $2, $3)
print formatted
}' data.txt
awk 'index($0, "FATAL") { print "致命错误:", $0 }' app.log
awk '{
n = split($1, parts, ".")
if (n > 1)
print "文件名:", $1, "扩展名:", parts[n]
}' filelist.txt
7.2 数学函数
函数 语法 说明
intint(x)取整(截断小数部分)
sqrtsqrt(x)平方根
expexp(x)e 的 x 次幂
loglog(x)自然对数(以 e 为底)
sinsin(x)正弦(弧度)
coscos(x)余弦(弧度)
atan2atan2(y, x)y/x 的反正切(弧度)
randrand()随机数 [0, 1)
srandsrand([x])设置随机数种子
awk 'BEGIN { srand(); for (i=0; i<10; i++) print int(rand() * 100) + 1 }'
awk '{
sum += $1
sumsq += $1 * $1
count++
}
END {
mean = sum / count
variance = sumsq / count - mean * mean
stddev = sqrt(variance)
printf "总数: %d, 均值: %.2f, 标准差: %.2f\n", count, mean, stddev
}' numbers.txt
awk 'BEGIN {
principal = 10000
rate = 0.05
years = 10
amount = principal * exp(rate * years)
printf "本金: %.2f, 年利率: %.2f%%, %d年后: %.2f\n", principal, rate*100, years, amount
}'
7.3 时间函数(gawk 扩展)
函数 语法 说明
systimesystime()返回当前时间戳(秒,从 1970-01-01 UTC 开始)
mktimemktime("YYYY MM DD HH MM SS")将时间字符串转为时间戳
strftimestrftime([format [, timestamp]])将时间戳格式化为字符串
awk 'BEGIN {
now = systime()
print "当前时间戳:", now
print "格式化时间:", strftime("%Y-%m-%d %H:%M:%S", now)
print "日期:", strftime("%F", now)
print "星期几:", strftime("%A", now)
}'
awk 'BEGIN {
ts = mktime("2026 05 08 12 00 00")
print "时间戳:", ts
print "格式化:", strftime("%c", ts)
}'
awk 'BEGIN {
start = mktime("2026 05 08 00 00 00")
end = mktime("2026 05 08 23 59 59")
}
{
# 假设日志第1列是时间戳
if ($1 >= start && $1 <= end)
print
}' app.log
awk 'BEGIN {
d1 = mktime("2026 01 01 00 00 00")
d2 = mktime("2026 05 08 00 00 00")
days = (d2 - d1) / 86400
print "相差:", int(days), "天"
}'
7.4 类型转换与位运算函数
awk 'BEGIN {
print strtonum("0xFF") # 255 (十六进制)
print strtonum("0123") # 83 (八进制)
print strtonum("123.45") # 123.45 (十进制)
}'
awk '{
if ($1 + 0 == $1) # 如果 $1 是数字
print $1, "是数字"
else
print $1, "是字符串"
}' data.txt
awk 'BEGIN {
a = 60 # 二进制: 0011 1100
b = 13 # 二进制: 0000 1101
print and(a, b) # 12 (0000 1100)
print or(a, b) # 61 (0011 1101)
print xor(a, b) # 49 (0011 0001)
print compl(a) # -61 (1100 0011, 补码)
print lshift(a, 2) # 240 (1111 0000)
print rshift(a, 2) # 15 (0000 1111)
}'
八、数组与关联数组
8.1 关联数组的基本概念
awk 的数组本质上是关联数组(associative array),也称为字典或哈希表。与许多编程语言不同,awk 数组的下标可以是任意字符串(或数字,数字会自动转换为字符串)。这一特性使得 awk 在数据统计和分组汇总方面极其强大。
数组特性总结:
下标可以是字符串或数字(数字自动转为字符串)
不需要预先声明大小
动态增长,按需分配内存
可以使用 in 运算符检查下标是否存在
可以使用 delete 删除元素或整个数组
下标为整数时仍保持关联数组的特性(可能有空洞)
8.2 数组的基本操作
awk '{
# 使用字符串下标
cities["shanghai"] = "上海"
cities["beijing"] = "北京"
# 使用数字下标(仍然是字符串键)
arr[1] = "one"
arr[2] = "two"
print cities["shanghai"]
print arr[1]
}' /dev/null
awk 'BEGIN {
# 增
fruits["apple"] = 5
fruits["banana"] = 3
# 改
fruits["apple"] = 10
# 查(使用 in 运算符安全检查)
if ("apple" in fruits)
print "apple 存在,数量:", fruits["apple"]
# 删
delete fruits["banana"]
# 遍历
for (key in fruits)
print key, fruits[key]
# 检查数组长度(gawk 扩展)
print "元素总数:", length(fruits)
}' /dev/null
8.3 数组的典型应用模式
awk '{ for (i=1; i<=NF; i++) word_count[$i]++ } END { for (w in word_count) print w, word_count[w] }' article.txt
awk '{ ip[$1]++ } END { for (i in ip) print i, ip[i] }' access.log | sort -k2 -rn | head -10
awk -F, 'NR>1 { dept[$2] += $3; count[$2]++ } END { for (d in dept) printf "部门: %s, 总额: %.2f, 平均: %.2f\n", d, dept[d], dept[d]/count[d] }' sales.csv
awk '!seen[$0]++' duplicates.txt
awk '!seen[$1]++' data.txt
awk '{
if (!(FILENAME in seen_files)) {
seen_files[FILENAME] = 1
file_count++
}
}
END { print "处理了", file_count, "个文件" }' *.txt
awk 'NR==FNR { name[$1] = $2; next } { print $1, name[$1], $3 }' names.txt scores.txt
awk '{
# 用 SUBSEP 连接多个键模拟多维数组
key = "year:" $1 SUBSEP "month:" $2
data[key] += $3
}
END {
for (k in data) {
split(k, parts, SUBSEP)
print parts[1], parts[2], data[k]
}
}' sales_data.txt
经典模式:NR==FNR 多文件处理
当 awk 处理多个文件时,NR(总行号)持续增长,而 FNR(当前文件行号)在每个文件开头重置为 1。因此 NR == FNR 只在处理第一个文件时为真。这个模式被广泛用于"先读取映射表到数组,再处理数据文件"的场景。
8.4 多维数组(gawk 扩展)
awk '{
# 真正的多维数组
matrix[1][1] = 10
matrix[1][2] = 20
matrix[2][1] = 30
matrix[2][2] = 40
# 遍历多维数组(需要递归遍历)
for (i in matrix)
for (j in matrix[i])
printf "matrix[%d][%d] = %d\n", i, j, matrix[i][j]
}' /dev/null
awk 'BEGIN {
a[1][1] = 1; a[1][2] = 2; a[2][1] = 3
print length(a) # 2(外层元素数)
print length(a[1]) # 2(内层元素数)
}' /dev/null
8.5 数组排序
awk 'BEGIN {
arr["z"] = 3; arr["a"] = 1; arr["m"] = 2
n = asort(arr, sorted)
for (i = 1; i <= n; i++)
print i, sorted[i]
}' /dev/null
awk 'BEGIN {
arr["z"] = 3; arr["a"] = 1; arr["m"] = 2
n = asorti(arr, sorted)
for (i = 1; i <= n; i++)
print i, sorted[i], arr[sorted[i]]
}' /dev/null
awk 'BEGIN {
PROCINFO["sorted_in"] = "@ind_str_asc"
arr["z"] = 3; arr["a"] = 1; arr["m"] = 2
for (i in arr)
print i, arr[i]
}' /dev/null
九、高级 awk 编程
9.1 用户自定义函数
awk 支持用户自定义函数,可以将复杂的处理逻辑封装为可复用的代码块:
awk '
function min(a, b) {
return a < b ? a : b
}
function max(a, b) {
return a > b ? a : b
}
function clamp(val, low, high) {
if (val < low) return low
if (val > high) return high
return val
}
{
print "最小值:", min($1, $2)
print "最大值:", max($1, $2)
print "限制:", clamp($3, 0, 100)
}' data.txt
awk '
# 计算平均分
function average(values, count) {
sum = 0
for (i = 1; i <= count; i++)
sum += values[i]
return sum / count
}
{
# 将当前行的所有字段存入数组
for (i = 1; i <= NF; i++)
vals[i] = $i
print "行", NR, "平均值:", average(vals, NF)
}' scores.txt
函数注意事项:
函数定义可以放在 awk 程序中的任何位置(通常会放在 BEGIN 块之前或脚本文件开头)
函数参数是局部变量,函数体内使用的额外变量会变为全局变量(需要谨慎)
为了避免变量污染,将额外的局部变量写在参数列表末尾,并用额外空格分隔以表明这是局部变量:function f(a, b, tmp, i)
函数可以递归调用,但递归深度受系统限制
awk '
# 注意:tmp 和 i 写在参数列表最后,它们是局部变量
function bubble_sort(arr, n, tmp, i, j) {
for (i = 1; i < n; i++) {
for (j = i + 1; j <= n; j++) {
if (arr[i] > arr[j]) {
tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
}
}
}
}
{
for (i = 1; i <= NF; i++) nums[i] = $i
bubble_sort(nums, NF)
for (i = 1; i <= NF; i++) printf "%s ", nums[i]
printf "\n"
}' numbers.txt
awk '
function factorial(n) {
if (n <= 1)
return 1
return n * factorial(n - 1)
}
{
print $1, "的阶乘:", factorial($1)
}' /dev/null <<< "5"
9.2 间接函数调用(gawk 4.0+)
awk '
function add(a, b) { return a + b }
function mul(a, b) { return a * b }
{
op = $1
result = @op($2, $3) # @ 符号实现间接调用
print $1, $2, $3, "=", result
}' <<< "add 10 20"
9.3 协程与双向管道(gawk 扩展)
awk 'BEGIN {
# 向 sort 命令发送数据并读取结果
cmd = "sort -n"
print 5 |& cmd
print 3 |& cmd
print 1 |& cmd
close(cmd, "to")
while ((cmd |& getline line) > 0)
print "排序结果:", line
close(cmd)
}' /dev/null
awk 'BEGIN {
# 使用 bc 进行精确计算
cmd = "bc -l"
print "scale=10; 355/113" |& cmd
close(cmd, "to")
cmd |& getline result
close(cmd)
print "355/113 =", result
}' /dev/null
9.4 调试与开发技巧
awk '{
print "DEBUG: NR=" NR, "NF=" NF, "$0=" $0 > "/dev/stderr"
# 实际处理...
print $1, $2
}' data.txt
awk '{
printf "字段1: 值=%s, 类型=%s\n", $1, (typeof($1) == "number" ? "数字" : "字符串")
}' data.txt
可移植性警告
以下 gawk 扩展在 BSD awk(macOS 默认)和其他 awk 实现中可能不可用,编写可移植脚本时应避免:
gensub()、asort()、asorti()
systime()、strftime()、mktime()
PROCINFO["sorted_in"]
length(array)
多维数组语法 a[i][j]
协程和 |& 双向管道
nextfile、switch/case
+ 和 ? 正则元字符(基本 POSIX ERE 不支持)
若需在多种环境下运行,建议在脚本头使用 #!/usr/bin/gawk -f 明确依赖 gawk。
十、实战应用场景
10.1 日志分析
日志分析是 awk 最常见的应用场景之一。以下示例均以 Nginx 访问日志(标准 combined 格式)为例。
awk '{ status[$9]++ } END { for (s in status) print s, status[s] }' access.log | sort -k2 -rn
awk '$9 == 404 { urls[$7]++ } END { for (u in urls) print urls[u], u }' access.log | sort -rn | head -10
awk '{ ip[$1]++ } END { for (i in ip) printf "%6d %s\n", ip[i], i }' access.log | sort -rn | head -20
awk '{
hour = substr($4, 14, 2) # 从时间戳中提取小时
hourly[hour]++
}
END {
for (h = 0; h < 24; h++) {
idx = sprintf("%02d", h)
printf "时 %s: %d\n", idx, hourly[idx]+0
}
}' access.log
awk '{
total += $NF
if ($NF > max) max = $NF
if (min == 0 || $NF < min) min = $NF
count++
}
END {
printf "请求数: %d\n", count
printf "总耗时: %.3f s\n", total
printf "平均耗时: %.3f s\n", total / count
printf "最大耗时: %.3f s\n", max
printf "最小耗时: %.3f s\n", min
}' app.log
awk '$NF > 3 { slow++; print } END { print "慢查询总数:", slow }' app.log
10.2 报表生成与数据处理
awk 'BEGIN {
FS = ","
OFS = ","
print "姓名", "语文", "数学", "英语", "总分", "平均分"
}
NR > 1 {
total = $2 + $3 + $4
avg = sprintf("%.1f", total / 3)
print $1, $2, $3, $4, total, avg
}' scores.csv > report.csv
awk -F, 'NR>1 {
dept = $2
quarter = $3
sales = $4 + 0
pivot[dept, quarter] += sales
depts[dept]
quars[quarter]
}
END {
printf "%-15s", "部门"
n = asorti(quars, sorted_quars)
for (i = 1; i <= n; i++)
printf "\t%s", sorted_quars[i]
print "\t合计"
for (d in depts) {
printf "%-15s", d
dept_total = 0
for (i = 1; i <= n; i++) {
q = sorted_quars[i]
val = pivot[d, q] + 0
printf "\t%.2f", val
dept_total += val
}
printf "\t%.2f\n", dept_total
}
}' sales.csv
awk 'NR % 10 == 1' large_data.txt > sample.txt
awk 'BEGIN {
printf "+%s+%s+%s+\n", "--------------------", "--------", "--------"
printf "| %-18s | %6s | %6s |\n", "姓名", "年龄", "分数"
printf "+%s+%s+%s+\n", "--------------------", "--------", "--------"
}
{
printf "| %-18s | %6d | %6.1f |\n", $1, $2, $3
}
END {
printf "+%s+%s+%s+\n", "--------------------", "--------", "--------"
}' data.txt
10.3 数据提取与转换
awk '/^server_name/ { gsub(/[; ]/, ""); print "域名:", $2 }' nginx.conf
awk '{
split($7, parts, "?")
if (parts[2] != "") {
print "URL:", parts[1]
print "参数:", parts[2]
}
}' access.log
awk '{
match($0, /"name":"([^"]+)"/, arr)
name = arr[1]
match($0, /"age":([0-9]+)/, arr)
age = arr[1]
print name, age
}' data.json
awk '{
# 假设第1列是 Unix 时间戳(秒)
print strftime("%F %T", $1), $2, $3
}' timestamps.log
awk '{
gsub(/^[[:alpha:]]+ +[0-9]+ [0-9:]+ /, "") # 去除系统日志前缀
print FILENAME, NR, $0
}' /var/log/syslog > syslog.csv
实战经验
在处理大型日志文件时(数百 MB 或 GB 级别),awk 的性能表现非常出色。它采用流式处理模式,内存占用仅与数据结构的规模有关,与输入文件大小无关。这使得 awk 能够轻松处理远大于系统内存的文件。
10.4 系统管理任务
df -h | awk 'NR>1 { print $1, $5, $6 }' | sort -t% -k2 -rn
ps aux | awk 'NR>1 { print $3, $11 }' | sort -rn | head -5
ps aux | awk 'NR>1 { mem[$11] += $6 } END { for (p in mem) printf "%8d KB %s\n", mem[p], p }' | sort -rn | head -10
netstat -an | awk '/^tcp/ { state[$6]++ } END { for (s in state) print s, state[s] }'
find / -type f -size +100M -exec ls -lh {} \; 2>/dev/null | awk '{ print $5, $NF }' | sort -rn | head -20
10.5 文本处理流水线
cat /var/log/syslog /var/log/auth.log 2>/dev/null \
| awk '{
# 提取日期、时间、进程信息
if (match($0, /([[:alpha:]]+ +[0-9]+ [0-9:]+) ([^ ]+) ([^ ]+): (.*)/, parts)) {
entries[parts[3]]++
if (parts[4] ~ /error|failed|timeout|denied/i) {
errors[parts[3]]++
}
}
}
END {
print "=== 进程活动统计 ==="
for (p in entries)
printf "%-20s %5d\n", p, entries[p] | "sort -k2 -rn"
print "\n=== 错误统计 ==="
for (p in errors)
printf "%-20s %5d\n", p, errors[p] | "sort -k2 -rn"
}' | head -40
awk '{
# 提取日期(Nginx 日志时间格式: 04/May/2026)
match($4, /[0-9]{2}\/[[:alpha:]]+\/[0-9]{4}/)
day = substr($4, RSTART, RLENGTH)
# 统计 PV(页面浏览量)
pv[day]++
# 统计 UV(独立访客,按 IP 去重)
ip_key = day SUBSEP $1
if (!(ip_key in seen_ips)) {
seen_ips[ip_key] = 1
uv[day]++
}
}
END {
print "日期,PV,UV"
for (d in pv)
printf "%s,%d,%d\n", d, pv[d], uv[d]+0 | "sort"
}' access.log > daily_stats.csv
十一、与 Claude Code 结合使用
11.1 Claude Code 中的 awk 使用场景
Claude Code 作为命令行 AI 编程助手,可以高效地利用 awk 来完成各种文本分析任务。以下是常见的使用场景:
场景 说明 典型用法
代码分析 统计代码行数、复杂度、模式 awk '{...}' src/*.py
日志排查 快速定位异常,统计错误分布 awk '/ERROR/ { print }'
数据统计 汇总数据,计算平均值、总和 awk '{ sum+=$1 } END { ... }'
格式转换 CSV/JSON/TSV 格式互转 awk -F, '{...}' data.csv
报表生成 格式化输出分析结果 awk 'BEGIN { ... } { ... }'
文件信息提取 提取文件名、大小、权限等信息 ls -l | awk '{...}'
Claude Code 中的 awk 使用原则:
优先使用 awk 完成可以在单行中解决的问题,保持命令行简洁
复杂逻辑建议写成 awk 脚本文件(.awk),用 -f 参数加载
注意在 Claude Code 中,每个 Bash 执行是独立的,需要将完整命令放在同一个 Bash 调用中
利用 awk 处理大型文件比 Python 脚本更快速,无需加载完整 Python 解释器
11.2 在 Claude Code 中实践 awk
以下是一些在 Claude Code 对话中可以直接使用的 awk 命令:
find . -type f \( -name "*.py" -o -name "*.js" -o -name "*.ts" \) \
-exec wc -l {} + | awk '{ ext = $NF; sub(/.*\./, "", ext); lines[ext] += $1; count[ext]++ } END { for (e in lines) printf "%s: %d 文件, %d 行\n", e, count[e], lines[e] }'
grep -rn "TODO\|FIXME\|HACK\|XXX" src/ --include="*.{py,js,ts,rs}" \
| awk -F: '{ file[$1]++ } END { for (f in file) print file[f], f }' \
| sort -rn | head -10
awk '/^(PASSED|FAILED|SKIP)/ { status[$1]++ } END { for (s in status) print s, status[s] }' test_output.log
git log --format="%ai" --since="1 month ago" \
| awk '{ hour = substr($2, 1, 2); commit_hour[hour]++ } END { for (h=0; h<24; h++) { idx = sprintf("%02d", h); printf "%s: %d\n", idx, commit_hour[idx]+0 } }'
awk '/"dependencies":/,/}/ { if ($0 ~ /": "/) { gsub(/[",]/, ""); print } }' package.json
11.3 在 Claude Code 中使用 awk 脚本
Claude Code 可以创建和执行多行 awk 脚本文件,适合需要复用的复杂分析任务:
BEGIN {
complexity = 1
print "文件: " FILENAME
}
/if |else |for |while |case |&&|\|\||catch/ {
complexity++
}
/^function |^def |^pub fn / {
if (NR > 1) {
printf " 行 %d: 复杂度 = %d\n", func_line, complexity
}
func_name = $0
func_line = NR
complexity = 1
}
END {
printf " 行 %d: 复杂度 = %d\n", func_line, complexity
}
Claude Code 中的 awk 编程建议
善用 heredoc: 可以直接在 Claude Code 中使用 heredoc 将 awk 脚本内容传递给 awk 命令
测试先行: 先用小数据样本测试 awk 命令的准确性,再应用到完整数据集
分步调试: 先用简单条件打印中间结果,确认后再构建完整的处理逻辑
利用管道组合: awk 通常只有数据处理的一部分,与 grep/sed/sort 等工具配合使用效果最佳
十二、常见问题与排错
12.1 常见错误及解决方案
错误现象 原因 解决方案
打印出空白行或缺少输出 字段编号错误或模式不匹配 先用 { print NR, NF, $0 } 检查行结构和字段数
数字比较结果错误 字符串比较 < 而非数字比较 < 确保字段是数值类型,用 $1 + 0 强制转为数字
FS 设为逗号后仍未分割 忘记使用 -F 或在 BEGIN 中设置 使用 -F',' 或在 BEGIN 块中 FS=","
数组遍历输出顺序不稳定 关联数组遍历顺序未定义 使用管道排序或 PROCINFO["sorted_in"]
文件太大运行缓慢 不必要地保留所有数据在内存中 尽量在主循环中完成计算,数组只存储汇总数据而非原始行
macOS 上函数不可用 BSD awk 不支持某些 gawk 扩展 安装 gawk (brew install gawk) 并用 gawk 命令
正则表达式匹配不到 特殊字符未转义 检查 .、*、[ 等元字符是否需要 \ 转义
print 输出格式异常 OFS 设置不当或混用 print/printf 需要精确格式时统一使用 printf
12.2 调试技巧
awk '{ print "行 " NR ": NF=" NF ", $0=" $0 }' data.txt | head -5
awk '{ print "字段1=[" $1 "], 字段2=[" $2 "], 字段3=[" $3 "]" }' data.txt | head -5
awk -F: '{ print NF, $1, $2, $3 }' /etc/passwd | head -3
awk '{ print "DEBUG: NR=" NR > "/dev/stderr"; print }' data.txt 2>debug.log | head
12.3 性能优化建议
建议 说明 效果
减少字段引用次数 将频繁使用的字段赋值给变量 减少内部查找开销
提前过滤行 使用模式匹配减少主循环处理量 减少不必要的动作执行
使用数组汇总而非保存原始行 统计值存在数组而不是存行本身 大幅减少内存占用
避免在循环中使用正则匹配 将正则编译结果赋值给变量 提高匹配速度
减少 print 调用次数 拼接字符串后一次性打印 减少 I/O 操作
使用 -v 传递参数而非 BEGIN 赋值 awk -v var="value"避免字符串拼接问题
awk '{ if ($3 > 100 && $5 ~ /error/) print $1, $3, $5, $NF }' bigfile.log
awk '{ a=$1; b=$3; c=$5; d=$NF; if (b > 100 && c ~ /error/) print a, b, c, d }' bigfile.log
awk '{ if ($0 ~ /ERROR/) { gsub(/[\[\(\]]/, ""); print } }' bigfile.log
awk '/ERROR/ { gsub(/[\[\(\]]/, ""); print }' bigfile.log
大文件处理注意事项
当处理 GB 级别的大文件时,注意以下几点:
不要在数组中存储每一行的内容,只存储需要的汇总数据
使用 head -n 1000 bigfile.log | awk '...' 先在小样本上测试
善用 gawk --profile 分析程序性能热点
考虑使用 mawk(另一个快速 awk 实现)处理纯文本提取任务
十三、核心总结
awk 学习路线图
掌握 awk 是一个逐步深入的过程,以下是建议的学习路径:
基本概念
→
字段处理
→
模式匹配
→
内置变量
→
流程控制
→
内置函数
→
关联数组
→
实战应用
→
高级编程
13.1 最常用的 awk 命令速查
功能 命令 一句话说明
打印指定列 awk '{print $1, $3}' file打印第 1 和第 3 列
打印最后一列 awk '{print $NF}' file$NF 代表最后一个字段
设置分隔符 awk -F',' '{print $1}' file.csv用逗号分隔字段
行号输出 awk '{print NR, $0}' file打印行号和内容
条件过滤 awk '$3 > 100' file只打印第 3 列大于 100 的行
正则匹配 awk '/pattern/' file打印匹配 pattern 的行
求和 awk '{s+=$1} END {print s}' file计算第 1 列的总和
平均值 awk '{s+=$1} END {print s/NR}' file计算第 1 列的平均值
频次统计 awk '{c[$1]++} END {for(k in c) print k,c[k]}' file按第 1 列统计次数
去重 awk '!seen[$0]++' file去除完全重复的行
格式化输出 awk '{printf "%-10s %5d\n", $1, $2}' file对齐打印两列
记录分隔 awk 'BEGIN {RS=""; FS="\n"}' file以空行分隔记录
13.2 三大核心心法
心法一:模式-动作是 awk 的灵魂
awk 程序的核心就是"对匹配特定模式的数据执行特定动作"。理解模式(pattern)和动作(action)的关系,就等于理解了 awk 的百分之八十。模式可以是正则表达式、条件表达式、范围模式,也可以省略(匹配所有行)。动作是用花括号包围的代码块。
心法二:关联数组是 awk 的瑞士军刀
awk 的关联数组是其最强大的特性之一。频率统计、分组汇总、数据去重、多表关联——各种常见的数据处理需求都可以通过关联数组优雅地解决。掌握数组的增删改查、遍历和排序,就能解决 90% 的文本分析问题。
心法三:管道组合 > 全能单工具
awk 擅长的是"每行每列的结构化数据处理",而不是"从大文件中精确提取特定模式的文本段"。通常的最优方案是:grep 做粗过滤 → awk 做结构化处理 → sort/uniq 做排序去重。每个工具做好自己最擅长的事。
13.3 经典一行命令大全
awk '{ sum += $1 } END { print sum }' file
awk '{ sum += $1; count++ } END { print sum/count }' file
awk '$1 > max { max=$1 } END { print max }' file
awk 'NR==1 || $1 < min { min=$1 } END { print min }' file
awk 'NR >= 10 && NR <= 20' file
awk 'NR % 2 == 0' file
awk '!seen[$0]++' file
awk 'length > 80' file
awk '{ $1=""; print $0 }' file
awk '{ print $NF }' file
awk '{ print $(NF-1) }' file
awk -F: '{ print $1, $3 }' /etc/passwd
awk 'BEGIN { OFS="," } { print $1, $2 }' file
13.4 学习资源推荐
最终寄语
awk 是一个诞生了近 50 年的工具,但它至今仍是 Unix/Linux 命令行下最强大的文本分析利器。它的设计哲学——数据驱动、模式-动作模型、关联数组——至今仍然启发着许多现代数据处理工具。
学习 awk 不仅是为了掌握一个具体的工具,更是为了理解"小而专"的 Unix 哲学。当你需要在海量日志中快速定位问题、在命令行中瞬间完成数据统计、或生成格式化报表时,awk 会成为你最得力的助手。
记住:awk 的优美之处在于,五行代码能做的事,绝不用十行。
10004
本笔记根据 GNU awk 官方文档及实践使用经验整理总结
本学习笔记为本人学习资料,不得转载
免责声明: 本学习笔记只供学习使用,具体操作请以官方文档为准。笔记中的命令示例仅供参考,请在实际使用前充分测试,对系统操作导致的任何问题不承担责任。