9444 字
47 分钟
Linux基础(12):awk命令从入门到精通完全指南

Linux运维必备:awk命令从入门到精通完全指南#

作为一名Linux运维工程师,每天都要和大量的文本文件打交道:日志分析、配置文件修改、数据提取、报表生成……在这些场景中,awk无疑是最强大、最高效的工具之一。它不仅仅是一个命令,更是一门完整的编程语言,能够以极简洁的代码完成复杂的文本处理任务。

NOTE为什么运维人员必须掌握它?

很多运维人员只会用awk '{print $1}'这种最基础的用法,却不知道它能做的事情远不止于此。掌握了awk,你可以在几秒钟内完成别人需要几十行Python代码才能实现的功能,极大提升工作效率。 ::

这篇文章将带你从零基础开始,系统学习awk的所有核心知识,并通过大量生产环境中的实战案例,让你真正把awk变成自己的”瑞士军刀”。

一、awk概述:为什么它是Linux下的“Excel”?#

1.1 awk到底是什么?#

对于刚接触Linux的新手来说,awk 这个名字看起来怪怪的,完全猜不出它是干嘛的。其实,它的名字来源于三位大佬(Alfred Aho、Peter Weinberger、Brian Kernighan)姓氏的首字母缩写。

别被“流编辑器”、“模式扫描”这些高大上的词汇吓到。通俗地讲,你可以把 awk 当作 Linux 命令行里的“无界面 Excel”。

当你在面对一份规规矩矩的文本文件(比如用空格、逗号分隔的数据)时,如果你想:

  • “把第3列单独抽出来”
  • “把满足某个条件的行找出来,算算总和”
  • “把第一列和最后一列换个位置”

在 Windows 里你可能会打开 Excel,选中列,拖拽求和。而在 Linux 服务器上没有图形界面,这时候 awk 就是你处理这类“表格型数据”的最强帮手!而且,它不仅是一个命令,更是一门微型的编程语言。

1.2 本质定位:Awk 和 Shell 的区别是什么?#

很多人会问:我已经在学 Shell 脚本了,为什么还要学 Awk?这两者有什么区别?

简单来说:Shell 是统筹工具的“调度员”,Awk 是专攻文本处理的“技术员”。

  1. 执行模型与性能差异

    • Shell:本身只做变量替换、流程控制,几乎所有实际操作都要调用外部命令(grep、cut、sort 等)。如果是用 while read 逐行处理大文件,每次循环都会创建子进程,处理十万行以上文本会明显卡顿。
    • Awk:单进程完成全逻辑!文件读取、字段拆分、计算、输出全在一个进程内完成。面对百万行级日志,速度通常是 Shell 循环的几十至上百倍。
  2. 语法体系差异 Shell 语法是“命令行的延伸”,为方便交互式输入设计,规则特殊、空格敏感度高;而 Awk 语法更接近 C 语言,结构严谨,符合常规编程语言的直觉。

    • 变量赋值:Shell 是 sum=0(等号两边绝对不能有空格);Awk 是 sum = 0(空格不影响,符合常规书写习惯)。
    • 数值运算:Shell 需借助 $((sum + num)) 或 expr/bc;Awk 直接原生支持算术运算 sum += num。
    • 条件判断:Shell 是 if [ $score -ge 60 ]; then ... fi([ 本质是 test 命令,两侧必须加空格,比较用 -ge/-eq 等参数);Awk 是 if (score >= 60) { ... }(和 C 语言完全一致,直接使用比较运算符)。
    • 循环遍历:Shell 是 for i in 1 2 3; do ... done;Awk 是 for(i=1; i<=3; i++) { ... }。
  3. 文本处理能力:核心差距所在 这是学习 awk 的核心价值,两者的原生能力天差地别。

    • Shell 原生文本处理能力极弱:原生只支持简单的变量截取、替换,稍微复杂的需求(按分隔符取列、正则匹配、分组求和)都必须调用外部工具实现。比如对 CSV 第二列求和,Shell 需要拼接多个工具、创建多个进程才能完成:cut -d',' -f2 data.csv | paste -sd+ | bc。
    • Awk 原生内置全套文本处理能力:天生自带逐行读取、自动字段拆分($1/$2/$NF)、内置变量(NF/NR)、正则匹配、关联数组、字符串函数,单条命令就能完成过滤、统计、格式化全套操作。同样的第二列求和,Awk 单进程一行即可完成,逻辑更清晰:awk -F',' '{sum += $2} END{print sum}' data.csv。
  4. 变量与数据结构差异

    • Shell:万物皆字符串,数据结构能力薄弱。所有变量默认存储为字符串,数值运算需要特殊语法;数组能力有限:普通数组仅支持数字下标,关联数组需额外声明,功能受限;函数仅能返回 0-255 的退出状态码,返回字符串需通过 echo 间接捕获;变量默认全局作用域,容易造成命名污染。
    • Awk:自动类型识别,原生支持关联数组。变量自动识别数值与字符串类型,运算无需特殊转换;原生支持关联数组(下标可为字符串),做频次统计、去重极其方便;支持自定义函数,可返回任意类型的值,支持局部变量;有清晰的作用域规则,更接近现代编程语言设计。
  5. 适用场景对比与两者的互补关系

    • 优先使用 Shell:系统运维自动化(服务启停、批量文件操作、权限配置)、命令流水线拼接(多工具通过管道组合完成任务)、进程管理、定时任务、环境变量配置、简单批量操作脚本。
    • 优先使用 Awk:日志分析(统计IP访问量、错误码分布、接口耗时)、结构化文本处理(CSV/TSV 的提取、转换、计算)、数据聚合(按维度计数、求和、求平均值)、文本格式化(对齐补全、生成规整报表)。

两者的关系:互补而非替代 实际工作中二者几乎总是配合使用:

  • Shell 脚本中频繁调用 awk 处理复杂文本,awk 是 Shell 生态中最核心的文本处理工具之一。
  • Awk 也可通过 system() 调用 Shell 命令,但属于非常规用法,违背 awk 的设计初衷。

一句话总结:Shell 负责整体流程与系统操作,Awk 专职文本数据处理。

1.3 awk 能帮你干什么活?(运维典型场景)#

在日常工作中,我们几乎天天都要看日志、看状态,awk 能把这些枯燥的工作变成“一键搞定”:

  • 场景一:查出谁在疯狂访问你的网站(日志分析) 服务器 Nginx 访问日志密密麻麻,用 awk 一行命令就能统计出“访问量最高的前 10 个 IP”,直接抓出恶意爬虫。
  • 场景二:揪出占用内存的“内鬼”(系统监控) 结合 ps 或 top 命令,awk 可以迅速把内存占用超过 80% 的进程过滤出来,并帮你算出它们总共吃了多少内存。
  • 场景三:给几百个账号批量操作(批量处理) 拿到一份名单,awk 可以瞬间把它们拼接成可执行的 Linux 命令,直接交给服务器去批量跑。
相比其他工具的优势

有人可能会问:我用 Python 写个脚本不行吗? 当然可以!但写 Python 需要建文件、写导入、写循环,还要考虑缩进。而使用 awk,你只需要在命令行敲一行代码,几秒钟就能搞定同样的需求。快、轻量、直接集成在系统中,这就是 awk 无法被替代的原因。

1.3 确认你的 awk 版本#

目前绝大多数的 Linux 系统(比如 CentOS、Ubuntu)都默认安装了 awk 的增强版——GNU awk(简称 gawk)。本文的所有教学也是基于 gawk 的,因为它功能最全,也是大家平时工作中最常接触的版本。

你可以通过下面的命令来检查你的系统里有没有它:

check_version.sh
# 查看 awk 的版本信息
awk --version

如果你看到了 GNU Awk 字样,那就说明一切准备就绪了!

如果系统提示找不到命令,可以通过下面的方式一键安装:

install_awk.sh
# CentOS / RHEL / Rocky Linux 系统:
sudo dnf install gawk -y
# Ubuntu / Debian 系统:
sudo apt update && sudo apt install gawk -y

二、awk基础:核心概念与基本语法#

学习 awk,最重要的一步就是理解它的处理逻辑和语法框架。一旦你脑子里有了这个模型,以后写复杂的命令就像搭积木一样自然。

2.1 awk 的工作原理(流水线模型)#

不要把 awk 当成一个只能从头跑到尾的普通命令,你应该把它想象成一家流水线工厂。

想象一下你有一份员工表(user.txt):

user.txt
张三 研发部 15000
李四 销售部 12000
王五 研发部 18000

当 awk 处理这个文件时,它会严格按照以下“三步走”的流水线来工作:

  1. 读取数据:awk 厂长会从文件中一行一行地拿数据,先拿起第一行 张三 研发部 15000。
  2. 切分字段(重点):awk 拿到这一行后,会像切土豆一样,默认按照空格把它切成一块一块的(字段)。张三 是第 1 块,研发部 是第 2 块,15000 是第 3 块。
  3. 匹配与执行:awk 会看你的指令(比如:只要研发部的人),如果满足条件,就把对应的动作(比如:打印出他的工资)执行掉。
  4. 循环往复:第一行处理完,清空手头的数据,去拿第二行 李四...,重复上面的动作,直到最后一行处理完。
牢记流水线思维

很多人学不懂 awk,就是因为把它当成了一次性处理整个文件的工具。记住:awk 永远是“读一行、切一行、处理一行、再读下一行”!

2.2 awk 的基本语法结构#

掌握了流水线原理,我们来看看怎么给这个工厂下达指令。awk 命令的万能语法骨架是这样的:

awk_syntax.sh
awk [选项] '模式 {动作}' 输入文件

看起来很抽象?我们把它拆开说人话:

  • 选项(怎么切):告诉 awk 遇到什么符号就切一刀。默认是空格,如果你遇到的是用逗号分隔的 CSV 表格,就要用选项指定“按逗号切”。
  • 模式(挑哪行):也就是条件。比如 工资>10000 或者 部门是研发部。只有满足这个条件的行,才会被送去处理。如果不写模式,就默认挑出所有行。
  • 动作(干什么):匹配到了行之后,你要对它做什么?比如打印(print)、求和、计数等。注意,动作必须被大括号 {} 包裹起来。
  • 输入文件(原料):你要处理的文本文件叫什么名字。

来看一个最简单的例子:原封不动打印文件里的所有内容。

print_all.sh
# 省略了“模式”,也就是对所有行执行 {print} 打印动作
awk '{print}' user.txt

这其实就等同于 cat user.txt。

2.3 核心魔法变量:$0 和 $N#

在 awk 切割完一行数据后,它会把切出来的每一块分别装进内置的魔法变量里,方便你随时调用。这就是 awk 处理分列数据如此牛逼的根本原因!

  • $0:代表一整行的全部内容。
  • $1:代表切出来的第 1 列(字段)。
  • $2:代表切出来的第 2 列。
  • …以此类推。

实战演示 1:提取指定的列 假设我们只要看 user.txt 里的员工名字和工资(第 1 列和第 3 列):

print_columns.sh
awk '{print $1, $3}' user.txt
# 输出结果:
# 张三 15000
# 李四 12000
# 王五 18000

注意:$1, $3 中间的逗号很重要!加上逗号,输出结果中间会有一个空格隔开;如果不加逗号写成 $1 $3,输出就会连在一起变成 张三15000。

实战演示 2:加上模式(条件过滤) 这次我们只要“研发部”员工的名字和工资。研发部在第 2 列($2):

filter_columns.sh
awk '$2 == "研发部" {print $1, $3}' user.txt
# 输出结果:
# 张三 15000
# 王五 18000

看,这就是 模式 {动作} 的完美结合!

2.4 指定切刀:-F 选项修改分隔符#

在现实的 Linux 世界里,很多文件并不是用空格隔开的。 最典型的就是 Linux 的用户信息文件 /etc/passwd,它是用冒号 : 分隔的: root:x:0:0:root:/root:/bin/bash

如果我们直接用默认的 awk 去处理,awk 看到没有空格,就会把这一整长串当成完整的一块(也就是全塞进 $1 里),这显然不是我们想要的。

这时候就要用到 -F 选项(Field Separator,字段分隔符)来换一把“切刀”了。

实战演示:提取系统所有的用户名(第 1 列)和对应的登录 Shell(第 7 列)

custom_separator.sh
# 告诉 awk,碰到冒号就切一刀
awk -F: '{print $1, $7}' /etc/passwd
# 部分输出结果:
# root /bin/bash
# daemon /usr/sbin/nologin
# bin /usr/sbin/nologin

更高级的切法:同时用多种符号切 有时候数据很乱,比如 张三,研发部|15000,既有逗号又有竖线。我们可以用方括号把多个符号括起来,告诉 awk:“遇到逗号或者竖线,统统切一刀!”

multi_separator.sh
# 准备测试数据
echo "张三,研发部|15000" > test.txt
# 使用逗号或竖线作为分隔符
awk -F'[,|]' '{print $1, $2, $3}' test.txt
# 输出结果:
# 张三 研发部 15000

三、awk变量:内置变量与自定义变量#

在上一节我们认识了 $1、$2 这样的魔法变量,但这只是冰山一角。awk 内部还为你准备了一大批“助理变量”,它们时刻记录着当前处理的状态。同时,你也可以自己创建变量来做统计。

3.1 必须掌握的四大内置变量:NR、NF、FS、OFS#

这四个变量是面试必考、日常必用的“四大金刚”,我们一个个来看:

1. NR (Number of Records) —— 当前是第几行?#

NR 会记录当前 awk 正在处理文件的第几行。它最常见的用法就是打印行号或者指定处理哪一行。

nr_demo.sh
# 场景 1:给文件内容加上行号
awk '{print NR, $0}' user.txt
# 输出:
# 1 张三 研发部 15000
# 2 李四 销售部 12000
# 场景 2:我只要看文件的第 2 行(把 NR 放在“模式”的位置作为条件)
awk 'NR == 2 {print $0}' user.txt
# 输出:李四 销售部 12000

2. NF (Number of Fields) —— 这一行被切成了几块?#

NF 会告诉你当前这一行总共有多少列。如果你的数据长短不一,NF 就非常有用。 还有一个神仙用法:$NF 代表最后一列。

nf_demo.sh
# 场景 1:打印每一行总共有几列
awk '{print "这一行有", NF, "列"}' user.txt
# 场景 2:无论这行有多长,我只要最后一列!(注意 $NF 的 $ 符号)
awk '{print $NF}' user.txt
# 输出:
# 15000
# 12000

3. FS (Field Separator) —— 输入分隔符#

它和我们前面讲的 -F 选项是一回事,只不过是在代码里设置。默认是空格。

4. OFS (Output Field Separator) —— 输出分隔符#

我们用 print $1, $2 的时候,输出的两个词中间默认是个空格。如果你想让输出变成用逗号或者短横线隔开,就可以修改 OFS。

ofs_demo.sh
# 把输出时的空格替换成短横线
awk 'BEGIN{OFS="-"} {print $1, $2, $3}' user.txt
# 输出:
# 张三-研发部-15000

(注:BEGIN 块我们会在下一节详细讲,这里你只要知道它是在正式处理前执行的预设代码即可)


3.2 其他好用的内置环境环境#

除了四大金刚,awk 还提供了一些系统级别的变量:

变量说明实用场景
FILENAME当前正在处理的文件名处理多个文件时,标记每行数据来自哪个文件
ENVIRON一个包含系统环境变量的字典在 awk 脚本里直接读取用户的 $PATH 或 $USER
FNR当前文件中的行号和 NR 类似,但处理多个文件时,每个新文件 FNR 都会重新从 1 开始数,而 NR 会一直累加。
env_demo.sh
# 打印当前系统的用户名
awk 'BEGIN{print "当前执行该命令的用户是:", ENVIRON["USER"]}'

3.3 自定义变量:做统计的绝佳武器#

在 awk 中定义变量超级简单:不需要声明类型,不需要加 $ 符号,直接拿来用就行! 未初始化的变量如果是数字计算,默认就是 0;如果是字符串拼接,默认就是空字符串 ""。

这让 awk 成了做统计的神器。

实战演示:统计文件的总行数和员工总薪水

custom_vars.sh
awk '{
count++ # 每次读一行,count 就加 1
total_salary += $3 # 每次把第 3 列(工资)累加到 total_salary 里
}
END {
print "总人数:", count, "总薪水:", total_salary
}' user.txt
# 输出结果:
# 总人数: 3 总薪水: 45000
TIP与 Shell 变量的区别

新手极易搞混:在 bash shell 里定义变量是 a=1,使用变量是 echo $a。但在 awk 的大括号 {} 里,使用自定义变量绝对不要加 $(写成 print count 即可)。在 awk 里,$ 永远只用来提取列(如 $1)。 ::

四、流程控制与高级处理:BEGIN 和 END 的魔法#

如果你前面理解了 awk 的“流水线模型”(读一行、切一行、处理一行),那么这里要讲的 BEGIN 和 END 就是这个流水线的开机准备和关机总结。结合条件判断和循环,awk 就具备了完整的编程语言能力。

4.1 核心框架:BEGIN → 主循环 → END#

一个完整的 awk 脚本其实由三块组成:

  1. BEGIN {}:在读取文件第一行之前执行。最适合用来做准备工作,比如打印一个表头,或者给变量赋初始值。
  2. {} (主循环):这就是我们前面一直在用的部分,每次读取一行都会执行一次。
  3. END {}:在处理完文件最后一行之后执行。最适合用来做总结,比如打印最终的统计总数。

实战演示:生成一份完整的工资报表

我们继续用 user.txt,这次我们要在开头加上表头,结尾算个总账。

report_demo.sh
awk '
BEGIN {
print "=== 员工工资报表 ==="
print "姓名\t部门\t工资"
total = 0
}
{
print $1, "\t", $2, "\t", $3
total += $3
}
END {
print "-------------------"
print "总计发放薪水:", total
}' user.txt

输出结果:

output.txt
=== 员工工资报表 ===
姓名 部门 工资
张三 研发部 15000
李四 销售部 12000
王五 研发部 18000
-------------------
总计发放薪水: 45000

看!只用了一个命令,我们就把一个纯文本文件变成了一份带统计的精美报表。

4.2 条件判断 (if-else):按需处理数据#

在处理数据时,我们往往需要根据不同的情况做不同的操作,这在 awk 里用 if-else 轻松搞定。语法和 C 语言/Java 完全一样。

实战演示:找出高薪员工和低薪员工 假设我们要给工资大于 14000 的人打上“高薪”标签,其他的打上“普通”标签:

if_else_demo.sh
awk '{
if ($3 > 14000) {
print $1, "是高薪员工,工资:", $3
} else {
print $1, "是普通员工,工资:", $3
}
}' user.txt
TIP简写技巧

如果只是为了做简单的判断过滤,你不需要写在 {} 里面。直接把它作为“模式”(条件)写在前面即可,就像我们在 2.3 节讲的:awk '$3 > 14000 {print $1}',这样代码更短! ::

4.3 循环结构 (for/while)#

有时候我们不只是要处理某一行,而是要对这一行里的每一列进行处理,这就需要用到循环。

比如你有一个文件 scores.txt,里面记录了学生几次考试的成绩,但每次考试的次数不一样(也就是列数不一样):

scores.txt
小明 80 90 85
小红 95 100
小刚 70 60 80 75

这时候我们就可以结合我们学过的 NF(总列数)和 for 循环,算出每个人的总分:

for_loop.sh
awk '{
sum = 0
# 从第 2 列开始循环,一直循环到最后一列 (NF)
for(i=2; i<=NF; i++) {
# $i 就是动态获取第 i 列的值
sum += $i
}
print $1, "的总分是:", sum
}' scores.txt

在这个例子里,$i 是一个非常精妙的用法。如果 i 是 2,$i 就是 $2(第2列的值)。利用循环,我们把不管多长的数据都能瞬间加起来!

五、数组:做分组统计的神器#

如果你问老鸟:awk 最强大的功能是什么?十有八九会回答你是数组。

在 awk 里,数组是关联数组(字典)。什么意思呢?普通的数组只能用数字 1, 2, 3 当作房间号(索引)来存东西;而 awk 的数组,可以用字符串(比如名字、IP地址、部门)当作房间号!

这就让 awk 成了做“分组统计”的绝佳神器。

5.1 基础概念:把字符串当钥匙#

想象你是一个前台,有很多人来存放包裹。你可以建立一个叫 package 的大柜子(数组)。

  • 张三来了,你就把东西放进 package["张三"] 这个格子里。
  • 研发部来放东西,你就放进 package["研发部"] 这个格子里。

在 awk 中,你不需要提前声明柜子有多大,直接往里塞就行了:

array_basic.sh
awk 'BEGIN {
# 把数据存进数组
package["张三"] = "一台电脑"
package["李四"] = "两本书"
# 取出数据
print "张三的包裹是:", package["张三"]
}'

5.2 核心大招:分组去重与计数(经典套路)#

运维日常中,80% 的数组使用场景都是为了**“去重并统计次数”**。比如:统计日志里每个 IP 访问了多少次。

这是一个固定套路,代码极其简短,但背后的逻辑非常精妙。我们先准备一个日志文件 access.log:

access.log
192.168.1.1 访问首页
192.168.1.2 访问个人中心
192.168.1.1 访问订单页
192.168.1.3 访问首页
192.168.1.1 访问支付页
192.168.1.2 访问订单页

实战演示:统计每个 IP 的访问次数

ip_stats.sh
awk '{
# 核心魔法:把第1列(IP)当做房间号,每次遇到就让里面的数字加1
ip_count[$1]++
}
END {
# 遍历数组,打印每个房间号(IP)和里面的数字(次数)
for(ip in ip_count) {
print ip, "访问了", ip_count[ip], "次"
}
}' access.log

输出结果:

output.txt
192.168.1.1 访问了 3 次
192.168.1.2 访问了 2 次
192.168.1.3 访问了 1 次
IMPORTANT这行代码是怎么跑起来的?

ip_count[$1]++ 这一句简直是艺术!我们来拆解一下 awk 是怎么执行的:

  1. 读第一行(IP是 1.1):awk 发现没有 ip_count["192.168.1.1"] 这个房间,就建一个,默认值为 0,然后 ++ 把它变成 1。
  2. 读第二行(IP是 1.2):同理,建一个 ip_count["192.168.1.2"],变成 1。
  3. 读第三行(IP又见 1.1):awk 发现 ip_count["192.168.1.1"] 已经存在且等于 1,于是 ++ 将它变成了 2。 就这样,直到读完所有行,统计自然就完成了! ::

5.3 进阶:按分组求和#

既然能计数,那能不能求和呢?当然可以!

回到我们的员工表 user.txt,这次我们不想算所有人总薪水了,我们想算出每个部门的总薪水。

user.txt
张三 研发部 15000
李四 销售部 12000
王五 研发部 18000
赵六 销售部 10000

代码套路完全一样,只是把 ++ 换成了 +=:

dept_salary.sh
awk '{
# 把第2列(部门)当做房间号,把第3列(工资)累加进去
dept_salary[$2] += $3
}
END {
for(dept in dept_salary) {
print dept, "的总工资是:", dept_salary[dept]
}
}' user.txt

输出结果:

output.txt
销售部 的总工资是: 22000
研发部 的总工资是: 33000

掌握了这个数组套路,以后不管遇到什么“按用户统计”、“按时间段统计”、“按状态码统计”,你都能信手拈来!


六、进阶:内置函数 substr 截取字符串#

在日常运维中,我们不仅要整块整块地提取字段,有时还需要把某个字段“再切碎一点”,比如截取日期的前几个字、提取日志中特定位置的标识。这就需要用到 awk 的内置字符串截取函数:substr。

6.1 substr 的基本语法#

substr(substring 的缩写)的作用是从一个大字符串中,精准地抠出一小段。

Terminal window
substr(要处理的字符串, 起始位置, 截取长度)
  • 起始位置:从第几个字符开始切?注意,awk 的索引是从 1 开始的,不是 0!
  • 截取长度:你要切多长?如果不写第三个参数,默认一直切到字符串的最后。

6.2 实战演示 1:隐藏敏感信息(脱敏)#

假设你有一份用户手机号列表,出于安全考虑,你要把中间四位隐藏掉。

phone.txt
张三 13812345678
李四 13987654321
substr_mask.sh
awk '{
# 提取前3位: substr($2, 1, 3)
# 提取后4位: substr($2, 8, 4)
# 中间用 **** 拼接
safe_phone = substr($2, 1, 3) "****" substr($2, 8, 4)
print $1, safe_phone
}' phone.txt
# 输出:
# 张三 138****5678
# 李四 139****4321

6.3 实战演示 2:按时间段粗粒度统计日志#

这是 substr 在运维中最经典的用法。假设你的 Nginx 日志里时间戳格式是 [10/May/2026:10:25:34],如果你想按小时统计访问量,也就是忽略后面的分钟和秒钟。

我们可以用 substr 提取 10/May/2026:10 这部分,然后配合前面的“数组分组计数”绝招:

log_hour_stats.sh
# 假设时间戳在日志的第 4 列
awk '{
# 从第4列的第2个字符开始截取(跳过左中括号[),截取14个字符
# 拿到类似于 10/May/2026:10 的字符串
hour = substr($4, 2, 14)
# 扔进数组统计
count[hour]++
}
END {
for(h in count) {
print h, "访问量:", count[h]
}
}' access.log

这样,一行命令就能瞬间帮你按小时聚合出整个网站的流量报表!

七、进阶:不规则日志处理(以 Nginx 为例)#

很多新手在处理 Nginx 等标准 Web 日志时,会发现默认的 awk 字段序号全乱了。这不是日志“不规范”,恰恰是标准 Nginx 访问日志格式的特征。

核心原因是:空格既是字段分隔符,又出现在字段内部(比如方括号包裹的时间、双引号包裹的请求行),默认按空格切分会导致错位。这类「带包裹符的非纯分隔符日志」是运维最常见的场景。

7.1 先拆解日志结构,搞清楚错位原因#

标准 Nginx 日志结构如下:

IP - - [时间 时区] "请求方法 路径 协议" 状态码 响应大小

对应样例:

192.168.1.10 - - [10/Jul/2025:09:12:33 +0800] "GET /index.html HTTP/1.1" 200 1024
  • 正常空格分隔的字段:IP、-、-、状态码、响应大小
  • 内部带空格的整体字段:
    1. [10/Jul/2025:09:12:33 +0800]:方括号包裹,中间有1个空格
    2. "GET /index.html HTTP/1.1":双引号包裹,中间有2个空格

默认 awk '{print $4}' 只会拿到 [10/Jul/2025:09:12:33,后面的时区、请求行全被拆成了独立字段,序号全乱。

7.2 方法一:FPAT 字段模式匹配(最推荐)#

这是 GNU awk 专门为这类场景设计的语法,也是目前最简洁、最易维护的写法。

核心原理:默认 -F 是定义「用什么字符切分字段」;而 FPAT 是反过来定义:什么样的内容才算一个字段。只要把「被引号/方括号包裹的内容」和「普通无空格字符串」定义成字段,就能自动避开内部空格的干扰。

fpat_demo.sh
awk 'BEGIN {
# 定义字段规则:普通无空格串 | 双引号包裹串 | 方括号包裹串
FPAT = "([^ ]+)|(\"[^\"]+\")|(\\[[^]]+\\])"
}
{
# 现在字段序号完全对应,不会错位
ip = $1
time = $4
request = $5
status = $6
size = $7
print ip, status, request
}' access.log

进阶:二次拆分请求行 $5 是完整请求行,想单独拿出请求方法、URL 路径,再做一次拆分即可:

fpat_split.sh
awk 'BEGIN {
FPAT = "([^ ]+)|(\"[^\"]+\")|(\\[[^]]+\\])"
}
{
# 去掉请求行两边的双引号
req = $5
gsub(/"/, "", req)
# 按空格拆成 方法/路径/协议 三部分存入 req_arr 数组
split(req, req_arr, " ")
method = req_arr[1] # GET
path = req_arr[2] # /index.html
print $1, method, path, $6
}' access.log

7.3 方法二:正则捕获提取(通用性强)#

直接用正则匹配+捕获组提取,通用性最强。 match(整行, 正则, 结果数组) 把正则中 () 捕获到的内容,依次存到数组里。

match_extract.sh
awk '{
# 提取 IP、方法、路径、状态码
match($0, /^([0-9.]+) .*"([A-Z]+) ([^ ]+) .*" ([0-9]+) /, arr)
ip = arr[1]
method = arr[2]
path = arr[3]
status = arr[4]
print ip, status, path
}' access.log

7.4 同类复杂日志的通用处理思路#

以后再遇到 CSV、Apache 日志、JSON 行等非简单分隔的文本,都按这个步骤走:

  1. 识别「包裹型字段」:找哪些符号是成对出现的(""、[]、{}),这些符号包裹的内容里,分隔符不算切分标记。
  2. 选处理方案:Linux 优先 FPAT;只取少量字段用 match 捕获。
  3. 二次拆分子字段:大字段拆出来后,如果内部还有子结构,用 split、substr、gsub 处理。
  4. 先验证后写逻辑:先 print $1, $2... 打印前几行,确认字段序号对了,再写统计逻辑。

7.5 实战应用:高级日志统计命令#

结合 FPAT,我们可以写出更精准的 Nginx 统计命令:

nginx_fpat_stats.sh
# 1. 统计访问量 Top10 的 IP
awk 'BEGIN { FPAT = "([^ ]+)|(\"[^\"]+\")|(\\[[^]]+\\])" } {print $1}' access.log | sort | uniq -c | sort -nr | head -10
# 2. 统计所有 4xx/5xx 错误请求
awk 'BEGIN { FPAT = "([^ ]+)|(\"[^\"]+\")|(\\[[^]]+\\])" } $6 ~ /^[45]/ {print $1, $6, $5}' access.log
# 3. 统计各个 HTTP 状态码的数量
awk 'BEGIN { FPAT = "([^ ]+)|(\"[^\"]+\")|(\\[[^]]+\\])" } {count[$6]++} END {for(s in count) print s, count[s]}' access.log

八、运维人员的 awk 实战工具箱#

学了这么多基础,是时候看看 awk 在真实服务器上是怎么大显身手的了。下面这些命令,建议你直接收藏作为工作备忘录。

案例 1:Nginx 访问日志分析(找茬利器)#

对于运维来说,排查网站变慢或被攻击的第一步,就是看 Nginx 日志。

nginx_analysis.sh
# 1. 抓出访问量最高的前 10 个 IP(防刷/防攻击)
# 解析:把IP($1)塞进数组计数,最后交给 sort 倒序排列,拿前 10 个
awk '{ip[$1]++} END{for(i in ip) print ip[i], i}' access.log | sort -nr | head -10
# 2. 看看网站今天出了多少个 404 或 500 错误(状态码统计)
# 解析:Nginx 默认日志中,状态码在第 9 列 ($9)
awk '{status[$9]++} END{for(s in status) print s, "出现次数:", status[s]}' access.log | sort -n
# 3. 统计大家最爱访问哪个页面(热门页面分析)
# 解析:请求的 URL 一般在第 7 列 ($7)
awk '{url[$7]++} END{for(u in url) print url[u], u}' access.log | sort -nr | head -10
# 4. 揪出响应时间超过 1 秒的慢请求
# 解析:前提是你在 Nginx 配置里把响应时间加到了最后一列 ($NF)
awk '$NF > 1 {print "慢请求:", $0}' access.log

案例 2:系统监控(一眼看穿服务器状态)#

有时候不想装复杂的监控软件,用 awk 配合系统命令,瞬间提取关键指标。

system_monitor.sh
# 1. 一键提取当前内存使用率 (配合 free 命令)
# 解析:找到带有 Mem 的行,用 已用内存($3) / 总内存($2) 算百分比
free | awk '/Mem/ {printf "内存使用率: %.2f%%\n", $3/$2*100}'
# 2. 一键提取所有磁盘分区的使用率 (配合 df 命令)
# 解析:排除掉挂载点不是以 /dev 开头的虚拟磁盘,打印分区名($1)和使用率($5)
df -h | awk '/^\/dev/ {print "磁盘:", $1, "使用率:", $5}'
# 3. 统计当前服务器的 TCP 连接状态(谁连着我,状态如何)
# 解析:配合 netstat,提取以 tcp 开头的行,对状态($6)进行分组统计
netstat -an | awk '/^tcp/ {state[$6]++} END{for(s in state) print s, state[s]}'

案例 3:巧妙合并两个文件的数据#

这是 awk 最让新手惊艳的功能之一,相当于数据库里的 JOIN 操作!

假设你从两个系统导出了两份数据:

data.txt
# 文件1 (id 和 名字)
# 1 张三
# 2 李四
# 文件2 (id 和 年龄)
# 1 20
# 2 25

现在你要把它们合并成:1 张三 20

merge_files.sh
# 神仙命令:
awk 'NR==FNR {age[$1]=$2; next} {print $1, $2, age[$1]}' file2.txt file1.txt

原理拆解(进阶必学): NR==FNR 是处理多文件的经典套路。

  • FNR 是当前文件的行号,NR 是总行号。
  • 只有在读第一个文件(file2.txt)时,它们俩才相等。此时把年龄存进 age 数组,然后用 next 直接跳过,去读下一行。
  • 等读到第二个文件(file1.txt)时,NR 继续变大,FNR 重新变成 1,条件不成立。直接执行后面的 {print},把名字和刚才存好的年龄一起打印出来。

九、新手最容易踩的 4 个坑#

awk 虽好,但刚开始用总会遇到一些“灵异现象”。避开下面四个坑,能帮你省下无数个挠头的夜晚。

9.1 单双引号陷阱:为什么 awk 必须用单引号?#

很多新手在写 awk 命令时,随手把单引号换成了双引号,结果疯狂报错:

quote_error.sh
# ❌ 错误:用双引号,直接报语法错误
awk -F"," "{print $2, $5}" data.csv
# ✅ 正确:用单引号
awk -F"," '{print $2, $5}' data.csv

核心结论:这不是 awk 和 sed 的规则不一样,而是 Shell 引号的解析规则在起作用。

Shell 在执行命令前,会先解析引号,再把处理后的内容传给 awk/sed。两种引号的权限完全不同:

引号类型解析规则核心特点
单引号 ' '强引用,完全原样传递里面的 $、\、反引号全部失效,Shell 不做任何展开
双引号 " "弱引用,会解析特殊字符会自动展开 $变量、反引号、转义符,处理完再传给命令

踩坑的根源:

  1. Shell 先看到双引号,发现里面有 $2 和 $5,立刻把它们当成 Shell 自身的位置参数变量;
  2. 你在终端执行时没有传参数,所以 $2 和 $5 都被展开成了空字符串;
  3. 最终 Shell 传给 awk 的脚本变成了:{print , };
  4. awk 拿到这个残缺的语法,自然就报语法错误了。

而用单引号时,Shell 不做任何展开,原封不动传给 awk,awk 就能自己识别 $2 为「第2个字段」。

为什么 sed 换双引号通常没事? 因为你平时写的 sed 命令里(比如 sed "s/old/new/g"),大多没有 $ 符号,Shell 没东西可展开。但如果你的 sed 命令里有 $(比如 sed "$d" file 删最后一行),用双引号一样会报错!底层规则是完全统一的。

一句话总结:日常使用强烈建议一律用单引号包裹 awk/sed 脚本,最省心!

9.2 分隔符陷阱:肉眼看不见的制表符(Tab)#

WARNING多个连续空格的陷阱

默认情况下,awk 会把连续的多个空格当成一个分隔符。这在看日志时很爽,但如果你的数据是标准的 TSV(用制表符 Tab 隔开的),一旦某一列为空,awk 的默认切分就会全乱套! ::

separator_fix.sh
# ❌ 错误:如果字段之间是用 Tab 分隔的,默认分割极易错位
awk '{print $1}' file.tsv
# ✅ 正确:显式指定制表符 \t 为分隔符,严格一刀切
awk -F'\t' '{print $1}' file.tsv

9.3 变量污染:没声明就当全局变量用#

在 awk 写自定义函数(通常在长脚本里)时,很多人习惯直接用 x=10。注意!在 awk 里,只要没加特殊声明,所有变量都是全局的! 这极易导致变量被覆盖。

variable_scope.awk
# ❌ 错误示例:函数里的 x 把外面的 x 覆盖了
function test() {
x=10 # 它是全局的!
}
BEGIN {
x=5
test()
print x # 结果输出10,你本来期望是5
}
# ✅ 正确示例:GNU awk 独有特性,把局部变量写在参数列表里
function test( x) { # 注意这里的空格,表示 x 是局部变量
x=10
}
BEGIN {
x=5
test()
print x # 安全输出5
}

9.4 正则转义:被吃掉的“点”#

CAUTION匹配点号需当心

在正则表达式中,.(点)代表“匹配任意字符”。很多新手在过滤 IP 地址时直接写 /192.168.1.1/,结果不仅匹配了 IP,连 192a168b1c1 这种乱码也被匹配出来了。 ::

regex_escape.sh
# ❌ 错误:点号会匹配任意字符
awk '/192.168.1.1/ {print}' access.log
# ✅ 正确:精确匹配 IP 需要给点号加上反斜杠转义
awk '/192\.168\.1\.1/ {print}' access.log

十、总结#

awk 不仅仅是一个文本处理工具,它是一门微型的、专为数据处理而生的语言。

  • 如果只是简单地找出一行文字,用 grep。
  • 如果只是简单地替换某个词,用 sed。
  • 但如果面对的是有结构、分列的数据,需要提取、统计、出报表,awk 绝对是你的不二之选。

熟练掌握 awk 的流水线模型、内置变量、以及关联数组,你处理日志和数据的速度将会产生质的飞跃。

如果你想进一步深入学习,强烈推荐阅读 awk 三位作者亲自编撰的经典书籍《The AWK Programming Language》。多写、多练,早日让这把“瑞士军刀”成为你的肌肉记忆!

Linux基础(12):awk命令从入门到精通完全指南
https://www.6ixblog.site/posts/linux-basic-12/
作者
Licwic
发布于
2025-05-25
许可协议
CC BY-NC-SA 4.0