第八章:文本处理三巨头:grep, sed, 与 awk

第八章:文本处理三巨头:grep, sed, 与 awk

摘要: 欢迎来到命令行世界的“内功心法”篇。本章,我们将从简单的文件查看,跃迁至真正的数据处理与驾驭。我们将系统性地解构 Linux 环境下最富传奇色彩的三大文本处理工具:grep 用于信息过滤,sed 用于流式编辑,而 awk 则用于数据提取与报告生成。我们学习的将不只是命令,更是一种全新的思维方式——将数据视为可以被精确切割、转换和分析的“数据流”。通过一系列对真实配置文件和日志文件的实战演练,您将逐一精通每个工具,并最终在“组合拳”环节中,将它们串联成强大的处理流水线,完成一次真实世界的日志分析任务。本章是您从一名命令行“使用者”,蜕变为一名命令行“大师”的关键之桥。


8.1. 准备工作:创建我们的“数字靶场”

工欲善其事,必先利其器。在学习如何使用这些强大的“文本兵器”之前,我们需要一个合适的训练场。我们将专门创建一个包含真实世界场景的“实验靶场”,这将是我们贯穿本章所有操作的素材。

  1. 导航至项目根目录

    1
    cd ~/projects
  2. 创建本章专属的工作目录

    1
    2
    mkdir text-processing-trinity
    cd text-processing-trinity

    现在,我们所有的操作都将在 ~/projects/text-processing-trinity 目录下进行。

  3. 创建素材一:应用配置文件 (app.conf)
    我们将使用一种名为 “Here Document” 的方法,快速创建一个多行的配置文件。这种方法远比用 echo 拼接要优雅。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    cat << 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 文件中。

  4. 创建素材二:Nginx 访问日志 (access.log)
    同理,我们创建一个模拟的、但格式非常规范的 Nginx 日志文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    cat << 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
  5. 验证靶场
    使用 ls -l 检查文件是否创建成功,并用 cat 快速预览一下它们的内容,确保一切就绪。

    1
    2
    ls -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

grep 逐行读取了 access.log,并只打印了那些包含了 “POST” 字符串的行。简单、高效、精确。

8.2.3 核心选项精讲

grep 的强大之处在于其丰富的选项,它们能让你的过滤行为更加精细化。

  • -i (ignore-case): 忽略大小写
    假设我们想查找所有来自 Macintosh 系统的访问记录,但我们不确定日志里写的是 Macintosh 还是 macintosh

    1
    grep -i "macintosh" access.log
  • -v (invert-match): 反向匹配
    这是 grep 最有用的选项之一。它会打印出 不包含 指定模式的行。假设我们想查看所有 非成功(状态码不是 200)的请求。

    技巧: 我们搜索的是 " 200 " 而不是 "200",在数字两边加上空格,是为了避免错误匹配到像 1200 这样的字节数。

    1
    grep -v " 200 " access.log
  • -n (line-number): 显示行号
    在排查问题时,知道匹配项在原始文件中的具体行数非常重要。

    1
    grep -n "404" access.log

    输出结果开头的 5: 清晰地告诉我们,这条 404 错误发生在日志文件的第 5 行。

  • -c (count): 统计匹配行数
    如果你只关心有多少行匹配,而不需要看具体内容,-c 选项非常高效。例如,统计 IP 192.168.1.10 发起了多少次请求。

    1
    grep -c "192.168.1.10" access.log

    我们立刻得到了答案,无需手动去数。

在排查程序故障时,仅仅看到出错的那一行日志往往是不够的。我们通常需要知道错误发生前发生了什么,错误发生后又触发了什么。grep 的上下文掌控选项正是为此而生,它能把“案发现场”周边的信息一并呈现给你。

假设我们定位到了那条返回 500 服务器错误的日志,这是问题的核心,但我们想看看这个请求前后,同一个 IP 还做了些什么。

  • -C (Context): 显示匹配行 N 行的内容
    我们以 500 错误为核心,查看它 前后各 2 行 的日志。

    1
    grep -C 2 " 500 " accaess.log

    注意: 在我们的示例文件中,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

    这三个选项在分析有时序关系的日志文件时,价值千金。


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/

    grep 像一个勤劳的机器人,瞬间扫描了整个项目,并清晰地告诉我们:在哪个文件的哪一行,找到了匹配的内容。这几乎是每个程序员每天都会用到的核心操作。


8.2.5 能力升华:为 grep 装上“精确瞄准镜”——正则表达式入门

如果说普通 grep 是在用“渔网”捞鱼,那么学会了正则表达式的 grep 就是在用“精确制导鱼雷”。正则表达式(Regular Expression, or Regex)是一种描述文本模式的强大语言,它是解锁三巨头全部威力的终极钥匙。

我们将使用 -E (Extended Regex) 选项来开启 grep 的扩展正则引擎,它的语法更现代、更易读。

  • ^ (Caret): 匹配行的开头
    假设我们只想找那些由 IP 10.0.0.5 发起的请求记录。如果只搜 "10.0.0.5",可能会匹配到其他地方。但我们知道,IP 地址一定在行首。

    1
    grep -E "^10.0.0.5" access.log
  • $ (Dollar): 匹配行的结尾
    假设我们想找到那些由特定客户端发起的请求,比如 curlpython-requests,我们知道这些信息通常在日志行的末尾。

    因为 " 在 Shell 中有特殊含义,所以我们在它前面加一个反斜杠 \ 来“转义”,告诉 Shell 把它当作一个普通字符。

    1
    2
    3
    # 查找以 ") " 结尾的行可能会误伤,但我们可以找以特定字符串结尾的
    # 我们找以 curl/7.81.0 " 结尾的行
    grep -E "curl/7.81.0\"$" access.log
  • | (Pipe/OR): 匹配多个模式之一
    如果我们想一次性找出所有 404 403 的错误日志。

    1
    grep -E " 404 | 403 " access.log
  • . (Dot) 和 * (Asterisk): 匹配任意字符
    . 代表 任意单个字符,而 * 代表它前面的那个字符可以出现 0 次或多次。它们组合起来 .* 就意味着“任意数量的任意字符”,这是一个强大的“万能牌”。

    假设我们想找到所有访问 /api/ 目录下的请求,但不关心 /api/ 后面具体是什么。

    1
    grep -E " GET /api/.* HTTP " access.log

    这个模式的含义是:查找包含 “GET /api/”,后面跟着 任意数量的任意字符 (.*),直到遇见 " HTTP" 的行。

grep 的学习到此告一段落。我们已经从一个简单的过滤器,升级到了一个能理解上下文、能跨文件搜索、能使用正则表达式进行精确打击的强大工具。

8.3. 第二巨头 sed:精准制导的“流編輯器”

8.3.1 核心定位

sed (Stream Editor) 的名字已经揭示了它的本质。请把它想象成一条 文本加工流水线 上的一个自动化机器人。

  • 流 (Stream): 文本数据(通常是文件的内容)像流水一样,一行一行地从它面前经过。
  • 编辑器 (Editor): sed 机器人手持一套预设指令,当某一行文本流过,如果符合指令中的条件,它就会对这一行进行修改(比如替换、删除、添加文字),然后将修改后的行继续传送到流水线的下一站(通常是你的屏幕)。

最关键的是,这个过程是 非交互式 的。你事先告诉机器人所有规则,然后它就自动处理整个文件,无需你中途干预。这使得 sed 成为自动化脚本中修改配置文件的王者。

8.3.2 核心武器:替换命令详解

sed 有很多指令,但 95% 的日常使用场景都依赖于它最核心、最强大的武器——s (substitute) 替换命令。它的语法结构非常清晰:s/模式/替换内容/标志

让我们逐一拆解:

  1. s: 命令本身,代表“替换”。
  2. 模式 (pattern): 你想查找的内容。这里同样可以使用我们刚刚在 grep 中学到的 正则表达式
  3. 替换内容 (replacement): 你想把匹配到的内容换成什么。
  4. 标志 (flags): 用来控制替换行为的额外选项。

实战演练:
首先,让我们用 cat 预览一下我们的配置文件 app.conf,以便对比。

1
cat app.conf

现在,假设我们想把配置文件中的 localhost 临时换成 127.0.0.1 来测试一下。

1
sed 's/localhost/127.0.0.1/' app.conf

看!host 那一行的 localhost 已经被成功替换。但此时,请检查一下原始文件:

1
cat app.conf

你会发现,app.conf 文件本身根本没有变化!这正是 sed 默认的安全特性:它只是将 修改后的结果 打印到标准输出(你的屏幕),并不会触碰原文件。

8.3.3 替换标志 (Flags) 的威力

  • g (global): 全局替换
    默认情况下,seds 命令只会替换 每行中第一个 匹配到的内容。让我们创建一个例子来证明这一点:

    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,把它替换成一个 # 号、一个空格,以及 它自身 (&)。

8.3.5 高危操作与安全实践:原地编辑 -i

每次都只是在屏幕上看到结果,如果我真的想修改文件本身呢?这就是 -i (in-place) 选项登场的时刻。

-i 选项会让 sed 直接修改原文件。这是一个非常强大但同样危险的操作,因为它没有“撤销”按钮!

让我们用它来将端口号 真正地 修改掉:

1
2
3
4
sed -i 's/port = 3306/port = 3307/' app.conf

# 现在,我们再查看原文件
cat app.conf

这一次,你会发现 app.conf 文件中的端口号 已经被永久修改3307 了。

专业人士的安全操作方法:
直接使用 -i 风险太高。sed 提供了一个更安全的“原地编辑”模式:在 -i 后面跟一个后缀名,比如 .bak

1
sed -i.bak 's/listen_port = 8080/listen_port = 9000/' app.conf

执行这条命令后,sed 会做两件事:

  1. 创建一个名为 app.conf.bak 的文件,它是 修改前 的原始文件备份。
  2. 修改后 的内容写入原始的 app.conf 文件。

现在,让我们用 lscat 来验证一下:

1
2
3
4
5
6
7
8
ls
# 你会看到 app.conf 和 app.conf.bak 两个文件

cat app.conf
# 显示 listen_port = 9000

cat app.conf.bak
# 显示 listen_port = 8080

这才是使用 -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)"

  • $1192.168.1.10 (IP 地址)
  • $2-
  • $3-
  • $4[17/Sep/2025:10:00:01
  • $7/index.html (请求的路径)
  • $9200 (状态码)
  • $101543 (返回的字节数)

8.4.3 awk 的“语法骨架”:PATTERN { ACTION }

awk 的所有操作都遵循这个简单的“语法骨架”。它的工作逻辑是:

对于每一行,awk 都会判断 PATTERN (模式) 是否成立。如果成立,就执行 { ACTION } (动作) 里的命令。

  • PATTERN: 可以是一个正则表达式,也可以是一个条件判断语句(比如 $9 == 404)。如果省略 PATTERN,则对 所有行 都执行 ACTION
  • { ACTION }: 一系列由花括号包裹的指令,最常用的就是 print。如果省略 ACTION,则默认执行 print $0(打印整行)。

实战演练:

  1. PATTERN,对所有行执行 ACTION: 打印日志中每条记录的 IP 地址 ($1) 和状态码 ($9)。

    1
    awk '{ print $1, $ 9 }' access.log
  2. 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

    这个操作如果用 grep 和其他工具组合会非常复杂,但 awk 一行就搞定了,因为它天生就是为处理“列”而生的。

8.4.4 结构化处理:BEGINEND 模块

awk 还有两个特殊的 PATTERN,它们让 awk 真正成为了一个报表生成工具:

  • BEGIN { ... }: 这里面的 ACTION 会在 awk 处理任何一行文本之前 执行。它只执行一次。通常用来打印报表头,或者初始化变量。
  • END { ... }: 这里面的 ACTION 会在 awk 处理完所有行文本之后 执行。它也只执行一次。通常用来进行最终的计算,并打印报表总结或脚注。

实战演练:计算日志文件中所有请求的总字节数和平均大小

1
2
3
awk 'BEGIN { print "==== = Traffic Analysis Report ==== ="; sum = 0 } { sum += $10 }
END { printf " Total Bytes Transferred: %d bytes\n ", sum; printf " Average Request Size: %.2f bytes\n ",
sum/NR }' access.log

让我们分解这条看似复杂的命令:

  1. BEGIN { ... }: 在开始前,先打印一个报表头,并初始化一个我们自己定义的变量 sum 为 0。
  2. { sum += $10 }: 这是 核心处理逻辑。它没有 PATTERN,所以对 每一行 都会执行。sum += $10 的意思是,将当前行的第 10 个字段(字节数)累加到 sum 变量中。
  3. 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
2
3
4
cat 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)"
... (所有12行日志) ...

第二步:| awk '{print $1}' - 提取目标列 (awk 上场)

原始数据太庞杂,我们只关心 IP 地址。于是,数据流进入了第一个加工站——awk。我们使用 awk 强大的列处理能力,只打印出每一行的第一个字段 ($1),也就是 IP 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
cat access.log | awk '{print $1}'
192.168.1.10
10.0.0.5
192.168.1.10
10.0.0.5
203.0.113.42
192.168.1.10
10.0.0.5
192.168.1.10
10.0.0.5
78.141.223.102
10.0.0.5
192.168.1.10

数据变化: 数据流从完整的日志行,被精炼成了只包含 IP 地址的列表。

第三步:| sort - 排序,为计数做准备

为了统计每个 IP 出现了多少次,我们需要先让所有相同的 IP 地址“排在一起”。sort 命令就是负责这个任务的。它会按字母(和数字)顺序对输入的内容进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
cat access.log | awk '{print $1}' | sort
10.0.0.5
10.0.0.5
10.0.0.5
10.0.0.5
10.0.0.5
192.168.1.10
192.168.1.10
192.168.1.10
192.168.1.10
192.168.1.10
203.0.113.42
78.141.223.102

数据变化: IP 列表变得井然有序,所有相同的 IP 都聚集在了一起。

第四步:| uniq -c - 去重与计数

现在数据已经排好队,轮到 uniq (unique) 命令登场了。uniq 的作用是去除重复的 连续行(这就是为什么前面必须先 sort)。而 -c (count) 选项则是一个“超级加强”,它在去重的同时,还会在每行前面加上该行重复出现的次数。

1
2
3
4
5
cat access.log | awk '{print $1}' | sort | uniq -c
5 10.0.0.5
5 192.168.1.10
1 203.0.113.42
1 78.141.223.102

数据变化: 数据流从一个长长的 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
2
3
4
5
cat access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head -n 10
5 192.168.1.10
5 10.0.0.5
1 78.141.223.102
1 203.0.113.42

任务完成! 我们通过一条流水线,清晰、高效、优雅地解决了问题。这个过程完美地诠释了 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
提取日志文件的特定列
对结构化数据进行计算与统计
生成格式化的文本报告