假设你管理着一家大型中央厨房:每天清晨,一车混装蔬菜从后门运进来,番茄、土豆、洋葱堆成小山。厨房里有三位老师傅,各守一段流水线。第一位叫小 G,眼神像扫描仪,能把土豆一根根从菜堆里挑出来;第二位叫小 A,刀工利落,任何菜到他手里都能按固定间隔切成整齐的小段,切完还会在本子上记一笔;第三位叫小 S,性格沉稳,食材从他面前流过时,他顺手就把坏叶子摘掉、把过长的尾巴修平。三个人一站,传送带一开,无论什么食材,进去是原料,出来就是码得整整齐齐的备料。Linux 世界里处理文本的 grep、awk、sed,正是这三位老师傅。grep 负责筛选行,awk 负责切分字段与统计汇总,sed 负责流式改写。你不需要写几十行脚本,一条管道命令就能完成日志分析、批量替换、数据清洗,把几小时的手工活压缩成几秒钟。

岗位关联

对 AI 从业者而言,训练日志分析、配置文件批量修改、数据集清洗是每天绕不开的杂活;掌握三剑客,等于给这些高频操作装上了“流水线”,把时间省下来留给模型与算法。

目录
  1. 可视化演示
  2. grep搜索基础
  3. awk字段处理
  4. awk编程能力
  5. sed流编辑
  6. 正则表达式
  7. 三剑客协同
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态演示文本处理三剑客grep/aw的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

文本处理三剑客grep/aw:grep grep文本搜索与正则匹配 awk字段处理与内置变量 核心机制可视化 文本处理三剑客grep/aw整体架构与关键组件

grep搜索基础

后厨进货了一整车混装蔬菜,番茄、土豆、洋葱全堆在一起。筛选员小 G 眼神犀利,左手一拨、右手一抛,把每一根土豆都挑进备料筐,其他蔬菜原样放行。Linux 里的 grep 就是这号人物:它不生产、不加工,只负责把满足条件的行,从一大段文本里“挑”出来。

grep 的名字来自 g/re/p,意思是“全局正则搜索并打印”。最基本的用法是 grep "ERROR" app.log,它会将 app.log 里所有包含 ERROR 的行原样输出。你不需要打开编辑器,不必写循环,一行命令就把目标全部搜出来了。它的强大之处在于:搜什么,可以由“规则”而不是“死字”决定。

grep 默认使用基本正则(BRE),如果再配合选项,能组合出极灵活的筛选。-E 开启扩展正则,-v 反转选择,-c 只输出匹配行数,-n 显示行号,-i 忽略大小写,-o 只打印命中的片段。假设输入的所有行组成一个集合 IN,grep 做的事可以概括为:

整个过程食材没有发生任何改变:筛选员只负责“选”,grep 也从不修改文件内容,只是把选中的行送到屏幕上。这就是为什么它最适合放在流水线的最前面——先把需要的原材料挑出来,后面两位师傅才好开工。

OUT={  l∈IN∣match(l,  pattern)=true  }\text{OUT} = \{\; l \in \text{IN} \mid \text{match}(l,\; \text{pattern}) = \text{true} \;\}
这个公式把 grep 的行为压缩成一句话:从输入行集合中,把所有“命中模式”的行挑出来,组成输出集合。
  1. 定义输入集合IN 表示输入的全部文本行,每一行是集合中的一个元素。
    IN={l1,l2,…,ln}\text{IN} = \{ l_1, l_2, \dots, l_n \}
  2. 匹配判断函数match 检查某一行 l 是否命中给定的 pattern,返回 true 或 false。
    match(l,  pattern)∈{true,false}\text{match}(l,\; \text{pattern}) \in \{ \text{true}, \text{false} \}
  3. 筛选输出集合只有判断结果为 true 的行才被保留,这些行构成 grep 的输出。
    OUT={l∈IN:match(l,  pattern)}\text{OUT} = \{ l \in \text{IN} : \text{match}(l,\; \text{pattern}) \}

awk字段处理

筛选好的蔬菜被送到切配台。切配师小 A 拿起一根萝卜,“笃笃笃”按空格切成四段:第一段归一类,第二段归一类,长短不一也能整齐摆开。awk 干的正是这份“切段”活——它把每一行文本按分隔符拆成一个个字段,编号从 1 开始。

awk 把一行读进来后,先按 FS(输入字段分隔符)切成若干字段。$0 表示整行,$1 是第一个字段,$2 是第二个……$NF 是最后一个。记录号 NR 告诉你在处理第几行,字段数 NF 告诉你这一行被切成了几段。例如 awk '{print $1, $NF}' access.log,就能把每行的第一段和最后一段抽出来,中间的内容全部丢弃。

字段拆分可以形式化地看成一种映射:一行文本被映射成一个有序字段元组。

切配师只负责“拆”,不负责“挑”。小 G 筛过来的每一行,到他手里都会被切成标准小段。后面无论是统计还是替换,都只需要报出“第几段”即可,这就是 awk 在流水线中的位置。

l↦(f1,f2,…,fNF)l \mapsto (f_1, f_2, \dots, f_{NF})
这个箭头表示“行”到“字段序列”的变换。awk 的默认分隔符是连续空白,你也可以用 -F 或 FS 变量改成任意字符或正则。
  1. 默认分隔符不指定时,awk 把连续空格或制表符当成一个分隔点。
    FS={space,  tab}+FS = \{\text{space},\; \text{tab}\}^{+}
  2. 字段提取第 i 个字段就是从行 l 按 FS 切分后得到的第 i 块。
    fi=split(l,  FS)[i]f_i = \text{split}(l,\; FS)[i]
  3. 字段编号上限NF 是字段总数,因此最后一个字段可以写成 $f_{NF}$。

awk编程能力

切配师小 A 有个习惯:口袋里总揣着一个小本本。每切到一根土豆,他就在“土豆”那一格画一横;切到洋葱就在“洋葱”格画一横。下班前翻一翻本子,哪种菜进了多少一目了然。awk 最让人惊讶的地方就在这里——它不只会切字段,还是一门完整的“小语言”。

awk 的程序结构由“模式 + 动作”组成:BEGIN 块在读取第一行前执行一次;END 块在处理完所有行后执行一次;中间的代码对每一行执行。它支持变量、数组、for 循环、while 循环和 if 判断。最有名的统计套路是下面这样:每读一行,就把本子对应格子加一。

有了小本本,awk 就不再只是“切菜的”,它还能记账、汇总、生成报表。比如统计日志里每种服务报错多少次、每个用户请求了多少次,一行 awk 就能完成,这正是它在日志分析中不可替代的原因。

Cf1←Cf1+1C_{f_1} \leftarrow C_{f_1} + 1
这里的 C 是 awk 的关联数组(小本本),f₁ 是当前行第一个字段的值。每当看到某个值,就给它的计数值加 1。
  1. 关联数组 CC 的下标是任意字符串,相当于小本本上的“格子名”。
    C:key↦countC: \text{key} \mapsto \text{count}
  2. 计数更新当前行的第一字段 f₁ 作为 key,对应计数加 1,再写回本子。
    Cf1=Cf1+1C_{f_1} = C_{f_1} + 1
  3. 汇总输出所有行处理完后,遍历本子并打印每个 key 和它的计数。
    END: ∑k(k,  Ck)\text{END: } \sum_{k} \left( k,\; C_k \right)

sed流编辑

改刀师傅小 S 的工位在流水线末端。食材从他面前经过时,他并不整筐端走,而是一根一根地处理:把坏叶子摘掉,把过长的尾巴修平,把带泥的皮削干净。sed 就是这样一位“边走边改”的师傅——它逐行地读入、修改、输出,像一个永远开着的传送带。

sed 最常用的动作是 s 替换,写法像 sed 's/ERROR/WARN/g',意思是把每行中所有 ERROR 替换成 WARN。默认情况下它只替换每行第一次命中的内容,加 g 标志才能全部替换。它还支持地址范围,比如 sed '1,5d' 删除前五行;-n 配合 p 命令可以只打印指定行。最重要的一点:不带 -i 时,sed 只把修改后的结果输出到屏幕,原文件保持原样。

每个替换动作可以这样理解:对于输入行 l,如果命中模式 p,就返回替换后的新行;否则原样返回。

改刀师傅“边读边改边吐”,从不囤货,所以 sed 被称作流编辑器。它最适合批量修改文件内容、清理日志格式,尤其在需要“只改某几行”的时候,思路清晰得就像给食材定点修整。

l′={sub(l,  p,  r)if hit(p,  l)lotherwisel' = \begin{cases} \text{sub}(l,\; p,\; r) & \text{if } \text{hit}(p,\; l) \\[4pt] l & \text{otherwise} \end{cases}
这是一个分段函数:命中时执行替换,未命中时原封不动地放行。sed 的流式处理,本质上就是逐行套用这个函数。
  1. 命中判断hit(p, l) 检查模式 p 是否出现在 l 中。
    hit(p,  l)∈{true,false}\text{hit}(p,\; l) \in \{ \text{true}, \text{false} \}
  2. 替换函数sub 把 l 中匹配 p 的子串替换为 r,返回新行。
    sub(l,  p,  r)→l′\text{sub}(l,\; p,\; r) \to l'
  3. 逐行映射每一行都经过同样的判断和替换,最终输出与输入行数相同的新流。
    L′={  sub(l,  p,  r)∣l∈L}L' = \{\; \text{sub}(l,\; p,\; r) \mid l \in L \}

正则表达式

三位师傅虽然分工不同,但手里都捧着同一本配方手册,上面画满了“花纹符号”:* 表示随便多少片,[0-9] 表示只要是数字片,(ab) 表示把“ab”这组动作打包成一个整体。谁学会了这套符号,谁就能指挥三位师傅干精细活。这套符号系统,就是正则表达式。

正则表达式的核心要素并不多:元字符 . 匹配任意字符,* 表示重复零次或多次,+ 表示一次或多次,? 表示零次或一次;[] 定义字符类,^ 和 $ 分别锚定行首与行尾,| 表示“或”。最有用的是分组捕获:用 ( ) 把一个片段包起来,匹配到的内容会被记住,稍后用 \1 引用它。扩展正则(ERE)在 grep -E、sed -E 中直接可用,awk 默认就是扩展风格。

分组与回引用的关系可以画成一条“先打包、再重放”的链条:

三位师傅共用同一本“配方手册”,只是各自的方言略有差异。学会正则,就等于同时拿到了三件套的通用语言。写日志过滤规则、写清洗脚本时,这套花纹符号会贯穿所有操作。

(g1g2⋯gn)⇒capture(1)⇒\1(g_1 g_2 \cdots g_n) \Rightarrow \text{capture}(1) \Rightarrow \backslash 1
箭头左边是捕获动作:括号里的内容匹配后会被记入 1 号插槽;箭头右边是回引用:\1 会把记下的内容原样再“重放”一次。
  1. 捕获分组括号把一组字符绑成一个单元,并保存匹配结果。
    (g1g2⋯gn)(g_1 g_2 \cdots g_n)
  2. 捕获编号第 1 个左括号对应的内容存入编号 1 的捕获槽。
    capture(1)\text{capture}(1)
  3. 回引用\1 代表“再次出现刚才捕获的内容”。
    \1=capture(1)\backslash 1 = \text{capture}(1)

三剑客协同

真正高效的厨房,不是三位师傅各干各的,而是站成一条流水线:传送带从筛选员小 G 面前经过,再到切配师小 A,最后到改刀师傅小 S,食材从一头进去,另一头出来就是标准备料。Shell 里的管道符 |,就是把三位师傅串起来的那条传送带。

一条典型的组合命令长这样:

小 G 从日志里筛出所有报错行,小 A 把每行的第一段和最后一段切出来,小 S 顺手把 ERROR 缩写成 ERR。数据像食材一样源源不断流过,每个命令只做一件事,首尾相接。从数学上看,这就是函数的复合:

三位师傅都是 C 语言写的流处理高手,处理 GB 级日志时内存占用几乎恒定,速度比 Python 逐行循环快一个数量级。对运维与 AI 工程来说,这种“筛 → 拆 → 改”的管道组合,是批量改配置、分析日志、清洗数据时最顺手的利器。

OUT=sed∘awk∘grep  (IN)\text{OUT} = \text{sed} \circ \text{awk} \circ \text{grep} \;(\text{IN})
读作“先 grep,再 awk,最后 sed”。复合符号 ∘ 表示右侧先执行、左侧后执行,正好对应管道从左到右的数据流向。
  1. 第一道工序grep 先从 IN 中筛出匹配行,得到中间结果 M₁。
    M1=grep(IN)M_1 = \text{grep}(\text{IN})
  2. 第二道工序awk 对 M₁ 做字段拆分与选择,得到 M₂。
    M2=awk(M1)M_2 = \text{awk}(M_1)
  3. 第三道工序sed 对 M₂ 做流式改写,得到最终输出。
    OUT=sed(M2)\text{OUT} = \text{sed}(M_2)
bash1 行
grep "ERROR" app.log | awk '{print $1, $NF}' | sed 's/ERROR/ERR/g'

AI 应用场景

深度学习训练日志分析

阶段:模型训练与监控

解决的问题:训练大模型时日志动辄几百 MB,人眼找 loss 趋势不现实;用 Excel 打开卡死,写 Python 脚本又至少要几十行。传统方式费时费力,而 grep 负责筛出含指标的行,awk 提取数值并计算均值,sed 清洗多余前缀,几秒钟就能生成一张干净的指标表。

具体案例:某团队日志每行形如 step=5200 loss=2.31 acc=0.76,用 grep step= log.txt | awk '{print $2, $3}' | sed 's/loss=//' 得到纯净的 step 与 loss 对,再交给 Python 画 loss 曲线,定位过拟合与梯度爆炸。

批量修改AI配置文件

阶段:实验配置管理

解决的问题:微调模型时要在几十个 YAML 里把 learning_rate 从 0.001 换成 0.0003,手改容易漏改或敲错。用 sed 的 -i 直接原地批量替换,一条命令完成全目录更新,且先做备份再替换,出问题可快速回滚。

具体案例:运行 sed -i.bak 's/learning_rate: 0.001/learning_rate: 0.0003/g' configs/*.yaml,把 configs 下所有配置统一改写,同时保留 .bak 备份文件。

数据集预处理与清洗

阶段:数据清洗与特征提取

解决的问题:从原始爬虫日志或 CSV 中提取有效字段、过滤噪声行,是 AI 数据管线的第一步。用 grep 排除空行和异常标记,awk 按分隔符抽取指定列,sed 统一日期格式,组合成管道比写 Python 脚本更轻量,尤其适合 GB 级原始文件的快速探查。

具体案例:grep HTTP/2 access.log | awk '{print $1, $9}' | sed 's/\[//g' 提取 IP 与状态码,再做分布统计,快速评估数据质量。

Python 演示

python47 行
# mini_text_tool.py - 用 Python 重写 grep/awk/sed 的核心逻辑import re def run_grep(lines, pattern):    return [line for line in lines if re.search(pattern, line)] def run_awk_columns(lines, indices, sep=r"\s+"):    rows = []    for line in lines:        parts = re.split(sep, line.strip())        rows.append([parts[i - 1] if 0 <= i - 1 < len(parts) else "" for i in indices])    return rows def run_awk_count(lines, sep=r"\s+"):    counter = {}    for line in lines:        parts = re.split(sep, line.strip())        key = parts[0] if parts else ""        counter[key] = counter.get(key, 0) + 1    return counter def run_sed(lines, pattern, repl, count=0):    return [re.sub(pattern, repl, line, count=count) for line in lines] def main():    logs = [        "INFO 2024-01-05 api.server start",        "ERROR 2024-01-05 db.conn timeout",        "INFO 2024-01-05 cache load 200ms",        "ERROR 2024-01-05 disk.full no space",    ]    print("== grep: 只保留 ERROR 行 ==")    for line in run_grep(logs, r"ERROR"):        print(line)    print("\n== awk: 输出第 1 列和第 3 列 ==")    for row in run_awk_columns(logs, [1, 3]):        print("{:<9} {}".format(row[0], row[1]))    print("\n== awk: 按第 1 列统计行数 ==")    counter = run_awk_count(logs)    for key, val in sorted(counter.items()):        print("{:<8} {}".format(key, val))    print("\n== sed: 把 INFO 替换为 WARN ==")    for line in run_sed(logs, r"INFO", "WARN"):        print(line) if __name__ == "__main__":    main()
目的:用 Python 重写 grep/awk/sed 的核心逻辑,演示“筛选 → 切字段 → 统计 → 替换”这条文本处理流水线,帮助你理解三剑客各自在管道中的角色。
  1. return [line for line in lines if re.search(pattern, line)]grep 的“筛选员”,只保留命中正则的行
  2. parts = re.split(sep, line.strip())awk 的“切配台”,按分隔符把一行拆成字段列表
  3. rows.append([parts[i - 1] if 0 <= i - 1 < len(parts) else "" for i in indices])按用户指定的编号取字段,越界时补空串
  4. counter[key] = counter.get(key, 0) + 1小本本计数法,对应 awk 的数组统计
  5. return [re.sub(pattern, repl, line, count=count) for line in lines]sed 的“改刀”,按正则做流式替换
  6. for key, val in sorted(counter.items()):处理完所有行后,输出统计结果

常见误区

误区“我一直以为 grep 就是查找字符串的,只能写死内容,比如 grep 'ERROR',不能写规则。”

正解grep 天然支持正则。默认是基本正则(BRE),*、.、[]、^、$ 直接可用;+、?、| 需要加 -E 开启扩展正则。筛选员不只会认“土豆”两个字,还能认“所有圆滚滚的根茎类蔬菜”这种规律。

为何常见:简单场景下搜固定字符串足够,很多人因此从未接触正则;加上 grep 与 egrep 的历史命名差异,进一步加深了误解。

误区“awk 不就是 awk '{print $1}' 吗?跟 cut 差不多,取列的时候用一下。”

正解取列只是 awk 最外层的能力。它本质是“模式 + 动作”的完整编程语言,支持 BEGIN/END、变量、数组、循环、判断。字段拆分只是入口,统计汇总才是它真正的强项。把 awk 当 cut 用,就像把切配师当成只会切丝的帮工,忽略了他还会算账。

为何常见:入门教程最常见的例子就是 print $1,看起来与 cut 高度相似;而 awk 的高级能力需要额外学习,多数人浅尝辄止。

误区“我运行了 sed 's/foo/bar/' file,然后发现文件变了,所以 sed 会把原文件改掉。”

正解默认情况下,sed 只是把处理后的文本流输出到屏幕,原文件分毫不动。只有加上 -i 才会原地改写,并且建议带备份后缀,例如 sed -i.bak。改刀师傅只把处理结果递给你,不会抢你的原食材。

为何常见:终端里运行 sed 后屏幕立刻打印修改结果,容易误以为文件已被改写;某些教程直接演示 -i,让人先入为主。

误区“sed 里 `\1` 能用,grep -E 里也能用,那 awk 里应该也能用吧?”

正解sed -E 和 grep -E 支持捕获组与 \1 回引用;但 POSIX awk 的正则不提供反向引用,\1 往往会被当作普通字符。三个工具虽然都叫“正则”,细节却不同,跨工具使用时要先确认语法。

为何常见:三剑客都基于正则,人们默认“正则都一样”,实际不同工具的正则风格(flavor)存在明显差异。

误区“现在都在写 Python,grep/awk/sed 是古董,学它们纯属浪费时间。”

正解三剑客在交互式管道、流式处理、单行命令场景依旧无法替代:C 实现、启动快、内存占用恒定,处理 GB 级日志时 grep 秒级过滤,awk/sed 逐流处理。Python 适合复杂逻辑,但“临时在终端查一下日志”的场景,三剑客效率远超脚本。

为何常见:看到现代语言强大,就误以为历史工具失去价值;实际上 shell 工具链与 Python 是互补关系,各有所长。

面试题

Q1. 用 grep 统计日志中 ERROR 出现的行数,应该使用哪个选项?

单选题 · easy

  • A.grep -v 反转匹配
  • B.grep -c 计数输出← 正确答案
  • C.grep -n 显示行号
  • D.grep -o 只取匹配

答案:B。-c 统计匹配行数。注意它统计的是“行数”不是“次数”,若一行有多个 ERROR 只算一行;想要统计总次数,需要用 -o 把每个匹配拆出来再管道给 wc -l。

Q2. awk 中表示“到目前为止已读入第几行”的内置变量是?

单选题 · easy

  • A.NF 字段个数
  • B.NR 已读行数← 正确答案
  • C.FS 输入分隔符
  • D.OFS 输出分隔符

答案:B。NR 是 Number of Records,记录已读入的行数;NF 是当前行的字段个数;FS 是输入字段分隔符;OFS 是输出字段分隔符。切配师小 A 用 NR 知道自己切到第几根菜了。

Q3. 命令 sed 's/foo/bar/2' 中末尾的 2 表示什么?

单选题 · easy

  • A.只处理第 2 行
  • B.只替换第 2 个匹配← 正确答案
  • C.全局替换 2 次
  • D.第 2 行开始替换

答案:B。sed 的 s 命令可接数字标志,表示“只替换该行第 n 次出现的匹配”,类似改刀师傅只修第二处瑕疵;g 标志才表示全部替换。

Q4. 正则 ^(ab)\1$ 能匹配下列哪个字符串?(\1 为回引用)

单选题 · mid

  • A.匹配 abab← 正确答案
  • B.匹配 ababab
  • C.匹配 aba
  • D.匹配 aabb

答案:A。(ab) 捕获组先记住“ab”,\1 要求再次出现同样的“ab”,因此完整匹配 abab;ababab 多出结尾的 ab,无法满足行首行尾锚定。这就像把打包好的食材再原样复制一份。

Q5. awk '$2 > 3 {print $1}' data.txt 的含义是?

单选题 · mid

  • A.打印第二行大于 3 的第一字段
  • B.打印第二字段大于 3 的行的第一字段← 正确答案
  • C.打印第一字段大于 3 的行的第二字段
  • D.打印行号大于 3 的行的第一字段

答案:B。awk 中 $2 > 3 是“模式”,先判断当前行第二字段是否大于 3;只有为真时才执行后面的动作 {print $1}。顺序是“先判断,再取列”。

Q6. 下列哪条 sed 命令能删除所有匹配“ERROR”的行?

单选题 · mid

  • A.替换掉行内 ERROR
  • B.删除匹配 ERROR 的行← 正确答案
  • C.打印出匹配 ERROR 的行
  • D.把 ERROR 转成小写

答案:B。sed 用 /ERROR/d 删除命中 ERROR 的整行;p 是打印,s 是替换,y 是字符转换。d 命令像改刀师傅直接丢掉坏掉的食材,而不是修修补补。

Q7. 关于 sed 的说法,哪些是正确的?

多选题 · mid

  • A.s 默认只替换每行第一次匹配← 正确答案
  • B.g 标志表示行内全部替换← 正确答案
  • C.-i 选项会直接修改原文件← 正确答案
  • D.p 命令用于删除匹配行

答案:ABC。s 命令默认只替换每行第一个匹配;加 g 变成全局替换;-i 表示原地写回文件。p 命令是打印而不是删除,删除用 d。容易把 p 与删除混淆,记住 p 是“print”的首字母就不会错。

Q8. 以下 grep 选项与作用对应正确的是?

多选题 · mid

  • A.-i 忽略大小写← 正确答案
  • B.-n 显示行号← 正确答案
  • C.-r 递归搜索目录← 正确答案
  • D.-p 打印匹配行

答案:ABC。grep 没有 -p 选项,“打印匹配行”本来就是 grep 的默认行为;-p 是 sed 的打印动作。把工具间的选项搞混,就像把菜谱和维修手册当成同一本书。

Q9. 关于 awk 处理机制,哪些说法是正确的?

多选题 · hard

  • A.FS 支持正则表达式作为分隔符← 正确答案
  • B.BEGIN 块在读取第一行前执行← 正确答案
  • C.END 块每处理一行执行一次
  • D.NR 与 FNR 在单文件下相同← 正确答案

答案:ABD。BEGIN 在处理任何行之前执行一次,END 在所有行处理完后执行一次,不会“每行一次”;FS 可以设置为正则;单文件下 NR 与 FNR 相等,多文件时 NR 累计而 FNR 按文件重置。小本本统计正是靠 BEGIN/END 撑起框架。

Q10. 管道 cat log | grep -E '^[0-9]{4}-' | awk '{print $2}' | sed -n '1,5p' 中 sed 的作用是?

单选题 · extreme

  • A.只打印前 5 行← 正确答案
  • B.删除前 5 行
  • C.打印第 1 列到第 5 列
  • D.替换第 1 到 5 行的内容

答案:A。-n 关闭默认的自动输出,p 命令显式打印,1,5 是地址范围,合起来表示“只打印前 5 行”。这里的行号是 awk 输出之后重新从 1 计的,与原始日志行号无关。整条管道先筛日期开头行,再取第二字段,最后截取前五行。