第八章:文本处理三巨头:grep, sed, 与 awk
第八章:文本处理三巨头:grep, sed, 与 awk
Prorise第八章:文本处理三巨头:grep
, sed
, 与 awk
摘要: 欢迎来到命令行世界的“内功心法”篇。本章,我们将从简单的文件查看,跃迁至真正的数据处理与驾驭。我们将系统性地解构 Linux 环境下最富传奇色彩的三大文本处理工具:grep
用于信息过滤,sed
用于流式编辑,而 awk
则用于数据提取与报告生成。我们学习的将不只是命令,更是一种全新的思维方式——将数据视为可以被精确切割、转换和分析的“数据流”。通过一系列对真实配置文件和日志文件的实战演练,您将逐一精通每个工具,并最终在“组合拳”环节中,将它们串联成强大的处理流水线,完成一次真实世界的日志分析任务。本章是您从一名命令行“使用者”,蜕变为一名命令行“大师”的关键之桥。
8.1. 准备工作:创建我们的“数字靶场”
工欲善其事,必先利其器。在学习如何使用这些强大的“文本兵器”之前,我们需要一个合适的训练场。我们将专门创建一个包含真实世界场景的“实验靶场”,这将是我们贯穿本章所有操作的素材。
导航至项目根目录
1
cd ~/projects
创建本章专属的工作目录
1
2mkdir text-processing-trinity
cd text-processing-trinity现在,我们所有的操作都将在
~/projects/text-processing-trinity
目录下进行。创建素材一:应用配置文件 (
app.conf
)
我们将使用一种名为 “Here Document” 的方法,快速创建一个多行的配置文件。这种方法远比用echo
拼接要优雅。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18cat << EOF > app.conf
# General Application Settings
app_name = "ProRise Web App"
version = 1.0.5
debug = true
[database]
# Database connection details
host = localhost
port = 3306
user = admin
password = "complex_password_123"
[server]
listen_address = 0.0.0.0
listen_port = 8080
ssl_enabled = false
EOF这条命令的含义是:将从
<< EOF
开始,到下一个EOF
结束的所有内容,都当作cat
命令的输入,并将其重定向 (>
) 到app.conf
文件中。创建素材二:Nginx 访问日志 (
access.log
)
同理,我们创建一个模拟的、但格式非常规范的 Nginx 日志文件。1
2
3
4
5
6
7
8
9
10
11
12
13
14cat << EOF > access.log
192.168.1.10 - - [17/Sep/2025:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 1543 "-" "Mozilla/5.0 (Windows NT 10.0)"
10.0.0.5 - - [17/Sep/2025:10:00:02 +0000] "GET /styles/main.css HTTP/1.1" 200 5678 "-" "Mozilla/5.0 (Windows NT 10.0)"
192.168.1.10 - - [17/Sep/2025:10:00:03 +0000] "POST /api/login HTTP/1.1" 200 312 "-" "Mozilla/5.0 (Windows NT 10.0)"
10.0.0.5 - - [17/Sep/2025:10:00:04 +0000] "GET /images/logo.png HTTP/1.1" 200 12045 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
203.0.113.42 - - [17/Sep/2025:10:01:10 +0000] "GET /products/item123 HTTP/1.1" 404 150 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"
192.168.1.10 - - [17/Sep/2025:10:01:15 +0000] "GET /api/user/profile HTTP/1.1" 200 850 "-" "curl/7.81.0"
10.0.0.5 - - [17/Sep/2025:10:02:01 +0000] "POST /api/submit-form HTTP/1.1" 200 450 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
192.168.1.10 - - [17/Sep/2025:10:02:05 +0000] "GET /logout HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Windows NT 10.0)"
10.0.0.5 - - [17/Sep/2025:10:03:00 +0000] "GET /favicon.ico HTTP/1.1" 200 1500 "-" "Mozilla/5.0 (Windows NT 10.0)"
78.141.223.102 - - [17/Sep/2025:10:03:30 +0000] "GET /admin/panel HTTP/1.1" 403 150 "-" "Mozilla/5.0 (X11; Linux x86_64)"
10.0.0.5 - - [17/Sep/2025:10:04:00 +0000] "GET /about.html HTTP/1.1" 200 2345 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
192.168.1.10 - - [17/Sep/2025:10:05:00 +0000] "POST /api/v2/data HTTP/1.1" 500 50 "-" "python-requests/2.28.1"
EOF验证靶场
使用ls -l
检查文件是否创建成功,并用cat
快速预览一下它们的内容,确保一切就绪。1
2ls -l
cat app.conf至此,我们的靶场已经建好,弹药已经上膛。现在,让我们请出第一位巨头。
8.2. 第一巨头 grep
:大海捞针的“信息声纳”
8.2.1 核心定位
grep
(Global regular expression print) 是三大工具中最直接、最常用的一个。请将它想象成一个功能极其强大的“信息声纳”或“过滤器”。你给它一个模式(声纳要探测的目标),再给它一片数据海洋(文件或数据流),它就能精准地将包含这个模式的所有信息行(声纳的回波)捞出来给你。
8.2.2 基础过滤
grep
最基础的用法就是 grep "要查找的字符串" 文件名
。
假设我们想在 access.log
中找到所有 POST
请求的记录。
1 | grep "POST" access.log |
1
2
3
192.168.1.10 - - [17/Sep/2025:10:00:03 +0000] "POST /api/login HTTP/1.1" 200 312 "-" "Mozilla/5.0 (Windows NT 10.0)"
10.0.0.5 - - [17/Sep/2025:10:02:01 +0000] "POST /api/submit-form HTTP/1.1" 200 450 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
192.168.1.10 - - [17/Sep/2025:10:05:00 +0000] "POST /api/v2/data HTTP/1.1" 500 50 "-" "python-requests/2.28.1"
grep
逐行读取了 access.log
,并只打印了那些包含了 “POST” 字符串的行。简单、高效、精确。
8.2.3 核心选项精讲
grep
的强大之处在于其丰富的选项,它们能让你的过滤行为更加精细化。
-i
(ignore-case): 忽略大小写
假设我们想查找所有来自Macintosh
系统的访问记录,但我们不确定日志里写的是Macintosh
还是macintosh
。1
grep -i "macintosh" access.log
1
2
310.0.0.5 - - [17/Sep/2025:10:00:04 +0000] "GET /images/logo.png HTTP/1.1" 200 12045 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
10.0.0.5 - - [17/Sep/2025:10:02:01 +0000] "POST /api/submit-form HTTP/1.1" 200 450 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
10.0.0.5 - - [17/Sep/2025:10:04:00 +0000] "GET /about.html HTTP/1.1" 200 2345 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"-v
(invert-match): 反向匹配
这是grep
最有用的选项之一。它会打印出 不包含 指定模式的行。假设我们想查看所有 非成功(状态码不是 200)的请求。技巧: 我们搜索的是
" 200 "
而不是"200"
,在数字两边加上空格,是为了避免错误匹配到像1200
这样的字节数。1
grep -v " 200 " access.log
1
2
3
4203.0.113.42 - - [17/Sep/2025:10:01:10 +0000] "GET /products/item123 HTTP/1.1" 404 150 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"
192.168.1.10 - - [17/Sep/2025:10:02:05 +0000] "GET /logout HTTP/1.1" 302 0 "-" "Mozilla/5.0 (Windows NT 10.0)"
78.141.223.102 - - [17/Sep/2025:10:03:30 +0000] "GET /admin/panel HTTP/1.1" 403 150 "-" "Mozilla/5.0 (X11; Linux x86_64)"
192.168.1.10 - - [17/Sep/2025:10:05:00 +0000] "POST /api/v2/data HTTP/1.1" 500 50 "-" "python-requests/2.28.1"-n
(line-number): 显示行号
在排查问题时,知道匹配项在原始文件中的具体行数非常重要。1
grep -n "404" access.log
1
5:203.0.113.42 - - [17/Sep/2025:10:01:10 +0000] "GET /products/item123 HTTP/1.1" 404 150 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"
输出结果开头的
5:
清晰地告诉我们,这条 404 错误发生在日志文件的第 5 行。-c
(count): 统计匹配行数
如果你只关心有多少行匹配,而不需要看具体内容,-c
选项非常高效。例如,统计 IP192.168.1.10
发起了多少次请求。1
grep -c "192.168.1.10" access.log
1
5
我们立刻得到了答案,无需手动去数。
在排查程序故障时,仅仅看到出错的那一行日志往往是不够的。我们通常需要知道错误发生前发生了什么,错误发生后又触发了什么。grep
的上下文掌控选项正是为此而生,它能把“案发现场”周边的信息一并呈现给你。
假设我们定位到了那条返回 500
服务器错误的日志,这是问题的核心,但我们想看看这个请求前后,同一个 IP 还做了些什么。
-C
(Context): 显示匹配行 前 后 N 行的内容
我们以500
错误为核心,查看它 前后各 2 行 的日志。1
grep -C 2 " 500 " accaess.log
1
210.0.0.5 - - [17/Sep/2025:10:04:00 +0000] "GET /about.html HTTP/1.1" 200 2345 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
192.168.1.10 - - [17/Sep/2025:10:05:00 +0000] "POST /api/v2/data HTTP/1.1" 500 50 "-" "python-requests/2.28.1"注意: 在我们的示例文件中,500 错误是最后一行,所以它只能显示出前面的日志。如果它在文件中间,则会同时显示前后内容。
-A
(After): 显示匹配行以及后 N 行的内容
查看500
错误 之后 的 2 行(在我们的例子中,因为是最后一行,所以他只会输出自己)。1
grep -A 2 " 500 " access.log
-B
(Before): 显示匹配行以及前 N 行的内容
查看500
错误 之前 的 2 行。1
grep -B 2 " 500 " access.log
1
2
378.141.223.102 - - [17/Sep/2025:10:03:30 +0000] "GET /admin/panel HTTP/1.1" 403 150 "-" "Mozilla/5.0 (X11; Linux x86_64)"
10.0.0.5 - - [17/Sep/2025:10:04:00 +0000] "GET /about.html HTTP/1.1" 200 2345 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
192.168.1.10 - - [17/Sep/2025:10:05:00 +0000] "POST /api/v2/data HTTP/1.1" 500 50 "-" "python-requests/2.28.1"这三个选项在分析有时序关系的日志文件时,价值千金。
8.2.4 范围搜索
到目前为止,我们都只在单个文件中搜索。但在真实开发中,我们往往需要在整个项目成百上千个文件中,查找某个函数或变量的所有引用。这正是 -r
选项的用武之地。
-r
(Recursive): 递归搜索-r
选项会从你指定的目录开始,递归地 搜索该目录下以及所有子目录下的全部文件。让我们来模拟一个真实场景。首先,创建一个项目目录结构:
1
2
3
4
5
6
7
8
9# 创建项目结构
mkdir my_web_project
mkdir my_web_project/js
mkdir my_web_project/css
# 在不同文件中写入包含 "color" 的内容
echo ".main { color: #333; }" > my_web_project/css/style.css
echo "let favorite_color = 'blue';" > my_web_project/js/main.js
echo "This is a colorful day" > my_web_project/README.md现在,假设我们想在
my_web_project
这个项目中,找到所有提到color
的地方,但我们不确定大小写。1
grep -ir "color" my_web_project/
1
2
3my_web_project/README.md:This is a colorful day
my_web_project/js/main.js:let favorite_color = 'blue';
my_web_project/css/style.css:.main { color: #333; }grep
像一个勤劳的机器人,瞬间扫描了整个项目,并清晰地告诉我们:在哪个文件的哪一行,找到了匹配的内容。这几乎是每个程序员每天都会用到的核心操作。
8.2.5 能力升华:为 grep
装上“精确瞄准镜”——正则表达式入门
如果说普通 grep
是在用“渔网”捞鱼,那么学会了正则表达式的 grep
就是在用“精确制导鱼雷”。正则表达式(Regular Expression, or Regex)是一种描述文本模式的强大语言,它是解锁三巨头全部威力的终极钥匙。
我们将使用 -E
(Extended Regex) 选项来开启 grep
的扩展正则引擎,它的语法更现代、更易读。
^
(Caret): 匹配行的开头
假设我们只想找那些由 IP10.0.0.5
发起的请求记录。如果只搜"10.0.0.5"
,可能会匹配到其他地方。但我们知道,IP 地址一定在行首。1
grep -E "^10.0.0.5" access.log
1
2
3
4
510.0.0.5 - - [17/Sep/2025:10:00:02 +0000] "GET /styles/main.css HTTP/1.1" 200 5678 "-" "Mozilla/5.0 (Windows NT 10.0)"
10.0.0.5 - - [17/Sep/2025:10:00:04 +0000] "GET /images/logo.png HTTP/1.1" 200 12045 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
10.0.0.5 - - [17/Sep/2025:10:02:01 +0000] "POST /api/submit-form HTTP/1.1" 200 450 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
10.0.0.5 - - [17/Sep/2025:10:03:00 +0000] "GET /favicon.ico HTTP/1.1" 200 1500 "-" "Mozilla/5.0 (Windows NT 10.0)"
10.0.0.5 - - [17/Sep/2025:10:04:00 +0000] "GET /about.html HTTP/1.1" 200 2345 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"$
(Dollar): 匹配行的结尾
假设我们想找到那些由特定客户端发起的请求,比如curl
或python-requests
,我们知道这些信息通常在日志行的末尾。因为
"
在 Shell 中有特殊含义,所以我们在它前面加一个反斜杠\
来“转义”,告诉 Shell 把它当作一个普通字符。1
2
3# 查找以 ") " 结尾的行可能会误伤,但我们可以找以特定字符串结尾的
# 我们找以 curl/7.81.0 " 结尾的行
grep -E "curl/7.81.0\"$" access.log1
192.168.1.10 - - [17/Sep/2025:10:01:15 +0000] "GET /api/user/profile HTTP/1.1" 200 850 "-" "curl/7.81.0"
|
(Pipe/OR): 匹配多个模式之一
如果我们想一次性找出所有404
或403
的错误日志。1
grep -E " 404 | 403 " access.log
1
2203.0.113.42 - - [17/Sep/2025:10:01:10 +0000] "GET /products/item123 HTTP/1.1" 404 150 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"
78.141.223.102 - - [17/Sep/2025:10:03:30 +0000] "GET /admin/panel HTTP/1.1" 403 150 "-" "Mozilla/5.0 (X11; Linux x86_64)".
(Dot) 和*
(Asterisk): 匹配任意字符.
代表 任意单个字符,而*
代表它前面的那个字符可以出现 0 次或多次。它们组合起来.*
就意味着“任意数量的任意字符”,这是一个强大的“万能牌”。假设我们想找到所有访问
/api/
目录下的请求,但不关心/api/
后面具体是什么。1
grep -E " GET /api/.* HTTP " access.log
这个模式的含义是:查找包含 “GET /api/”,后面跟着 任意数量的任意字符 (
.*
),直到遇见 " HTTP" 的行。1
2
3
4192.168.1.10 - - [17/Sep/2025:10:00:03 +0000] "POST /api/login HTTP/1.1" 200 312 "-" "Mozilla/5.0 (Windows NT 10.0)"
192.168.1.10 - - [17/Sep/2025:10:01:15 +0000] "GET /api/user/profile HTTP/1.1" 200 850 "-" "curl/7.81.0"
10.0.0.5 - - [17/Sep/2025:10:02:01 +0000] "POST /api/submit-form HTTP/1.1" 200 450 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
192.168.1.10 - - [17/Sep/2025:10:05:00 +0000] "POST /api/v2/data HTTP/1.1" 500 50 "-" "python-requests/2.28.1"
grep
的学习到此告一段落。我们已经从一个简单的过滤器,升级到了一个能理解上下文、能跨文件搜索、能使用正则表达式进行精确打击的强大工具。
8.3. 第二巨头 sed
:精准制导的“流編輯器”
8.3.1 核心定位
sed
(Stream Editor) 的名字已经揭示了它的本质。请把它想象成一条 文本加工流水线 上的一个自动化机器人。
- 流 (Stream): 文本数据(通常是文件的内容)像流水一样,一行一行地从它面前经过。
- 编辑器 (Editor):
sed
机器人手持一套预设指令,当某一行文本流过,如果符合指令中的条件,它就会对这一行进行修改(比如替换、删除、添加文字),然后将修改后的行继续传送到流水线的下一站(通常是你的屏幕)。
最关键的是,这个过程是 非交互式 的。你事先告诉机器人所有规则,然后它就自动处理整个文件,无需你中途干预。这使得 sed
成为自动化脚本中修改配置文件的王者。
8.3.2 核心武器:替换命令详解
sed
有很多指令,但 95% 的日常使用场景都依赖于它最核心、最强大的武器——s
(substitute) 替换命令。它的语法结构非常清晰:s/模式/替换内容/标志
。
让我们逐一拆解:
s
: 命令本身,代表“替换”。模式 (pattern)
: 你想查找的内容。这里同样可以使用我们刚刚在grep
中学到的 正则表达式!替换内容 (replacement)
: 你想把匹配到的内容换成什么。标志 (flags)
: 用来控制替换行为的额外选项。
实战演练:
首先,让我们用 cat
预览一下我们的配置文件 app.conf
,以便对比。
1 | cat app.conf |
现在,假设我们想把配置文件中的 localhost
临时换成 127.0.0.1
来测试一下。
1 | sed 's/localhost/127.0.0.1/' app.conf |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# General Application Settings
app_name = "ProRise Web App"
version = 1.0.5
debug = true
[database]
# Database connection details
host = 127.0.0.1
port = 3306
user = admin
password = "complex_password_123"
[server]
listen_address = 0.0.0.0
listen_port = 8080
ssl_enabled = false
看!host
那一行的 localhost
已经被成功替换。但此时,请检查一下原始文件:
1 | cat app.conf |
你会发现,app.conf
文件本身根本没有变化!这正是 sed
默认的安全特性:它只是将 修改后的结果 打印到标准输出(你的屏幕),并不会触碰原文件。
8.3.3 替换标志 (Flags) 的威力
g
(global): 全局替换
默认情况下,sed
的s
命令只会替换 每行中第一个 匹配到的内容。让我们创建一个例子来证明这一点:1
echo "hello world, hello linux" | sed 's/hello/hi/'
你会看到输出是 hi world, hello linux
,只有第一个 hello
被替换了。
如果想替换一行中所有匹配项,就必须使用 `g` 标志。
1
echo "hello world, hello linux" | sed 's/hello/hi/g'
这次,输出就是 hi world, hi linux
。
i
(ignore-case): 忽略大小写
假设我们要把app.conf
里的user = admin
换成user = root
,但我们不确定配置文件里写的是user
还是USER
。1
sed 's/user = admin/user = root/i' app.conf
8.3.4 实战演练:批量修改配置文件
现在我们来执行一个更真实的任务:将 app.conf
中的数据库端口从 3306
修改为 3307
。
1 | sed 's/port = 3306/port = 3307/' app.conf |
再来一个高级点的:我们想把 debug = true
这一行整个 注释掉。我们可以匹配 debug = true
,然后把它替换成 # debug = true
。这里有一个神奇的特殊字符 &
,它在替换内容中代表 整个被匹配到的模式。
1 | sed 's/debug = true/# &/' app.conf |
这个命令的含义是:找到 debug = true
,把它替换成一个 #
号、一个空格,以及 它自身 (&
)。
1
# debug = true
8.3.5 高危操作与安全实践:原地编辑 -i
每次都只是在屏幕上看到结果,如果我真的想修改文件本身呢?这就是 -i
(in-place) 选项登场的时刻。
-i
选项会让 sed
直接修改原文件。这是一个非常强大但同样危险的操作,因为它没有“撤销”按钮!
让我们用它来将端口号 真正地 修改掉:
1 | sed -i 's/port = 3306/port = 3307/' app.conf |
这一次,你会发现 app.conf
文件中的端口号 已经被永久修改 为 3307
了。
专业人士的安全操作方法:
直接使用 -i
风险太高。sed
提供了一个更安全的“原地编辑”模式:在 -i
后面跟一个后缀名,比如 .bak
。
1 | sed -i.bak 's/listen_port = 8080/listen_port = 9000/' app.conf |
执行这条命令后,sed
会做两件事:
- 创建一个名为
app.conf.bak
的文件,它是 修改前 的原始文件备份。 - 将 修改后 的内容写入原始的
app.conf
文件。
现在,让我们用 ls
和 cat
来验证一下:
1 | ls |
这才是使用 -i
选项的 最佳实践。如果修改出了问题,你随时可以用备份文件 app.conf.bak
来恢复。
sed
的学习核心是掌握 s
命令和安全的 -i.bak
操作。它是在自动化脚本中进行“无人值守”式文本修改的不二之选。
8.4. 第三巨头 awk
:数据挖掘的“报表生成器”
8.4.1 核心定位
如果说 grep
是“过滤器”,sed
是“编辑器”,那么 awk
就是“数据分析师”。
请牢牢记住这个比喻:awk
就是命令行的 Excel。
- Excel 把数据整理成 行 和 列。
awk
也是。 - Excel 可以对某一 列 进行求和、计算平均值。
awk
也可以。 - Excel 可以根据某一 列 的条件,筛选出特定的 行。
awk
也可以。 - Excel 可以生成格式化的 报表。
awk
更可以。
一旦你用“处理电子表格”的思维来理解 awk
,它所有的古怪语法都会瞬间变得清晰起来。
8.4.2 核心概念:记录 (Records) 与字段 (Fields)
awk
的世界里,只有两个核心概念:
- 记录 (Record): 默认情况下,
awk
将文本中的 每一行 视为一条记录。这就像 Excel 中的“一行”。 - 字段 (Field):
awk
会自动将每条记录(每一行),按 分隔符(默认为空格或 Tab)切割成多个部分,每个部分就是一个字段。这就像 Excel 中的“一列”。
为了操作这些字段,awk
提供了极其重要的 内置变量:
$0
: 代表 整条记录(整行内容)。$1
,$2
,$3
…: 分别代表第 1、第 2、第 3 个字段。NF
(Number of Fields): 代表 当前记录 拥有的字段总数。NR
(Number of Records): 代表awk
到目前为止已经处理过的记录总数,也就是 当前行号。
让我们用 access.log
的第一行来做个“人工 awk
”分析:192.168.1.10 - - [17/Sep/2025:10:00:01 +0000] "GET /index.html HTTP/1.1" 200 1543 "-" "Mozilla/5.0 (Windows NT 10.0)"
$1
是192.168.1.10
(IP 地址)$2
是-
$3
是-
$4
是[17/Sep/2025:10:00:01
$7
是/index.html
(请求的路径)$9
是200
(状态码)$10
是1543
(返回的字节数)
8.4.3 awk
的“语法骨架”:PATTERN { ACTION }
awk
的所有操作都遵循这个简单的“语法骨架”。它的工作逻辑是:
对于每一行,awk
都会判断 PATTERN
(模式) 是否成立。如果成立,就执行 { ACTION }
(动作) 里的命令。
PATTERN
: 可以是一个正则表达式,也可以是一个条件判断语句(比如$9 == 404
)。如果省略PATTERN
,则对 所有行 都执行ACTION
。{ ACTION }
: 一系列由花括号包裹的指令,最常用的就是print
。如果省略ACTION
,则默认执行print $0
(打印整行)。
实战演练:
无
PATTERN
,对所有行执行ACTION
: 打印日志中每条记录的 IP 地址 ($1
) 和状态码 ($9
)。1
awk '{ print $1, $ 9 }' access.log
1
2
3
4
5
6192.168.1.10 200
10.0.0.5 200
192.168.1.10 200
10.0.0.5 200
203.0.113.42 404
...有
PATTERN
,对匹配行执行ACTION
: 从access.log
中,提取所有状态码为404
的记录,并只打印其请求的 IP 地址 ($1
) 和 访问的 URL ($7
)。注意: 在
awk
的条件判断中,字符串需要用双引号"
包起来。1
awk '$9 == "404" { print "Client IP:", $ 1, "tried to access URL:", $7 }' access.log
1
Client IP: 203.0.113.42 tried to access URL: /products/item123
这个操作如果用
grep
和其他工具组合会非常复杂,但awk
一行就搞定了,因为它天生就是为处理“列”而生的。
8.4.4 结构化处理:BEGIN
与 END
模块
awk
还有两个特殊的 PATTERN
,它们让 awk
真正成为了一个报表生成工具:
BEGIN { ... }
: 这里面的ACTION
会在awk
处理任何一行文本之前 执行。它只执行一次。通常用来打印报表头,或者初始化变量。END { ... }
: 这里面的ACTION
会在awk
处理完所有行文本之后 执行。它也只执行一次。通常用来进行最终的计算,并打印报表总结或脚注。
实战演练:计算日志文件中所有请求的总字节数和平均大小
1 | awk 'BEGIN { print "==== = Traffic Analysis Report ==== ="; sum = 0 } { sum += $10 } |
1
2
3
==== = Traffic Analysis Report ==== =
Total Bytes Transferred: 26424 bytes
Average Request Size: 2202.00 bytes
让我们分解这条看似复杂的命令:
BEGIN { ... }
: 在开始前,先打印一个报表头,并初始化一个我们自己定义的变量sum
为 0。{ sum += $10 }
: 这是 核心处理逻辑。它没有PATTERN
,所以对 每一行 都会执行。sum += $10
的意思是,将当前行的第 10 个字段(字节数)累加到sum
变量中。END { ... }
: 在处理完所有行后,sum
变量里已经存了总字节数。我们用printf
(一个格式化打印命令) 来输出最终结果,包括用总字节数sum
除以总行数NR
得到的平均值。
看到这个结果,您是否感受到了 awk
的威力?我们没有使用任何外部工具,仅凭 awk
自身,就完成了一次真正的数据统计和报表生成。
8.5. 实战:三巨头组合拳,分析 Web 日志 Top 10 IP
任务目标
这是运维和数据分析领域一个最经典、最真实的任务:从成百上千行的 access.log
文件中,分析出访问我们网站最频繁的 Top 10 IP 地址,并统计出它们的具体访问次数。
这个问题,如果让你用 Python 或 Java 来写,可能需要几十行代码,包括读取文件、切分字符串、用哈希表计数、排序等等。但在命令行世界,我们只需要一条由管道连接起来的命令流水线。
这就是那条传说中的命令:
1 | cat access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head -n 10 |
第一次看到它,可能会觉得像一段神秘的咒语。但现在,您已经掌握了大部分“法术”的原理。我们将把这条流水线 一步一步地拆解,像观察一个精密的机械装置一样,看数据流是如何在其中被一步步打磨、加工,最终变成我们想要的报告。
流水线步骤详解
第一步:cat access.log
- 准备原始数据流
这一步很简单,我们用 cat
命令将 access.log
的全部内容不做任何修改,完整地输出,作为整个流水线的源头。
1 | cat access.log |
第二步:| awk '{print $1}'
- 提取目标列 (awk
上场)
原始数据太庞杂,我们只关心 IP 地址。于是,数据流进入了第一个加工站——awk
。我们使用 awk
强大的列处理能力,只打印出每一行的第一个字段 ($1
),也就是 IP 地址。
1 | cat access.log | awk '{print $1}' |
数据变化: 数据流从完整的日志行,被精炼成了只包含 IP 地址的列表。
第三步:| sort
- 排序,为计数做准备
为了统计每个 IP 出现了多少次,我们需要先让所有相同的 IP 地址“排在一起”。sort
命令就是负责这个任务的。它会按字母(和数字)顺序对输入的内容进行排序。
1 | cat access.log | awk '{print $1}' | sort |
数据变化: IP 列表变得井然有序,所有相同的 IP 都聚集在了一起。
第四步:| uniq -c
- 去重与计数
现在数据已经排好队,轮到 uniq
(unique) 命令登场了。uniq
的作用是去除重复的 连续行(这就是为什么前面必须先 sort
)。而 -c
(count) 选项则是一个“超级加强”,它在去重的同时,还会在每行前面加上该行重复出现的次数。
1 | cat access.log | awk '{print $1}' | sort | uniq -c |
数据变化: 数据流从一个长长的 IP 列表,变成了“次数 + IP”的统计报告。我们的目标已经接近完成了!
第五步:| sort -nr
- 按访问次数排序
我们想知道谁是 Top 10,所以需要按访问次数从高到低排序。这里我们再次调用 sort
命令,但加了两个强大的选项:
-n
(numeric-sort): 告诉sort
按照 数字 大小来排序,而不是按文字顺序(否则10
会排在2
的前面)。-r
(reverse-sort): 将排序结果 反转,实现从大到小的“降序”排列。
1 | cat access.log | awk '{print $1}' | sort | uniq -c | sort -nr |
数据变化: 统计报告已经按访问量从高到低完美排序。
第六步:| head -n 10
- 提取最终结果
万事俱备,只欠东风。我们只需要这个排序后列表的“头”几名。head
命令就是做这个的。-n 10
告诉它,我们只需要输出前面 10 行。
1 | cat access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head -n 10 |
任务完成! 我们通过一条流水线,清晰、高效、优雅地解决了问题。这个过程完美地诠释了 Unix 哲学的精髓:每个程序只做一件事并做好,将它们组合起来,就能完成任何复杂的任务。
8.6. 本章核心速查总结
工具 | 核心定位 | 常用命令/语法 | 核心选项/变量 | 典型应用场景 |
---|---|---|---|---|
grep | 文本过滤器 (大海捞针) | grep "pattern" file | -i : 忽略大小写-v : 反向匹配-r : 递归搜索-E : 扩展正则-C : 上下文 | 在日志中查找错误信息 在代码库中搜索函数引用 过滤出不符合条件的行 |
sed | 流编辑器 (批量修改) | sed 's/old/new/g' file | -i : 原地编辑-i.bak : 编辑并备份d : 删除行& : 引用匹配内容 | 批量修改配置文件 自动化脚本中替换变量 删除文件中的注释行或空行 |
awk | 报表生成器 (列处理大师) | awk 'PATTERN {ACTION}' file | $0, $1, $2... : 字段NF : 字段总数NR : 行号-F : 指定分隔符BEGIN/END | 提取日志文件的特定列 对结构化数据进行计算与统计 生成格式化的文本报告 |