ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AIGC时代必备:grep、awk、sed三剑客高效处理文本

AIGC时代必备:grep、awk、sed三剑客高效处理文本

1. 为什么在AIGC时代还要死磕这三个老命令

1.1 一个真实的工作场景切入

前阵子帮一个做模型训练的朋友排查日志问题,他那边跑一个图像生成任务,几十万条训练日志散落在十几个文本文件里,想找出所有loss异常飙升的片段,还要统计每个epoch的平均耗时。他第一反应是写个Python脚本,装pandas、读文件、正则匹配、聚合统计,折腾了快一个小时,脚本还因为编码问题报错。我看了眼他的终端,直接甩了三行命令过去,十秒钟出结果。他愣了半天说,这东西这么好用怎么没人早告诉我。

这就是我想聊的核心:grep、awk、sed这三个命令,是文本处理领域的老三样,但在AIGC内容爆炸的今天,它们的价值不降反升。为什么?因为不管你是处理模型训练日志、清洗语料数据、批量改写配置文件,还是从海量生成内容里做模式提取,本质上都是在跟纯文本打交道。而这三个命令的设计哲学就是“一行命令解决一类文本问题”,不需要起环境、不需要装依赖、不需要写几十行代码,管道一接就能干活。

很多人对这三个命令的印象停留在“Linux基础课背过参数,考完就忘”。我见过太多人,grep只会grep 关键词 文件,awk只会awk '{print $1}',sed只会sed 's/a/b/',然后就觉得“这东西功能也就那样”。实际上,这三个命令的组合能力远超大多数人的想象,它们各自负责文本处理流水线上的不同环节:grep负责筛选,sed负责编辑替换,awk负责结构化计算。你把它们串起来,就是一条完整的文本处理流水线。

这篇文章适合谁看?如果你是刚接触命令行不久的新手,我会从最核心的用法讲起,每个参数都告诉你“为什么这么设计”;如果你是有一定基础但用得不够深的开发者,我会重点讲组合技巧和实战场景,尤其是AIGC相关的日志分析、语料清洗、批量处理这些高频需求。全文的实例都可以直接复制到终端里跑,我尽量用虚构的示例文件来演示,你跟着敲一遍就能掌握。

1.2 三个命令的分工逻辑:筛选、编辑、计算

先把这个底层逻辑讲清楚,后面所有内容都围绕它展开。你可以把文本处理想象成一条工厂流水线:原材料是原始文本,经过一道道工序,最后产出你想要的结果。

grep是质检员,它的工作是从一堆文本里挑出符合条件的行。你给它一个模式,它逐行检查,匹配的留下,不匹配的扔掉。它的核心能力是“按行筛选”,不关心行里面具体长什么样,只关心“这行符不符合我的条件”。

sed是编辑工,它的工作是对文本进行修改。替换、删除、插入、追加,都是它的活。它的特点是“流式处理”,读一行处理一行,不需要把整个文件加载到内存里,所以处理大文件特别快。sed的核心能力是“按规则改写”。

awk是统计员兼报表生成器,它的工作是把文本按列拆开,然后做计算、做判断、做格式化输出。它内置了变量、循环、条件判断,本质上是一门小型的文本处理编程语言。awk的核心能力是“按字段处理并计算”。

这三个命令通过管道符|串联起来,就形成了“筛选→编辑→计算”的完整链路。举个最简单的例子:从日志里找出所有错误行,把时间戳格式改一下,再统计每种错误出现的次数。一条命令搞定,不需要写脚本。

注意:这三个命令在绝大多数Linux发行版和macOS上都是预装的,Windows用户可以通过WSL或者Git Bash来使用。我后面所有的示例都基于bash环境,如果你用的是其他shell,个别语法可能需要微调。

2. grep:文本筛选的第一道关卡

2.1 核心参数拆解与选择逻辑

grep的全称是“global regular expression print”,翻译过来就是“全局正则表达式打印”。名字已经把它的工作方式说清楚了:用正则表达式去匹配文本,把匹配到的行打印出来。

先看最基础的用法。假设你有一个日志文件train.log,内容大概是这样的:

2024-01-15 10:23:01 INFO epoch=1 loss=2.341 lr=0.001 2024-01-15 10:23:45 INFO epoch=1 loss=2.108 lr=0.001 2024-01-15 10:24:30 WARN epoch=1 loss=nan lr=0.001 2024-01-15 10:25:12 INFO epoch=2 loss=1.876 lr=0.0009 2024-01-15 10:26:03 ERROR epoch=2 loss=inf lr=0.0009

你想找出所有包含ERROR的行,直接grep "ERROR" train.log就行。但实际工作中,光这样远远不够。我列一下最常用的参数组合,以及每个参数背后的设计考量:

参数作用什么时候用
-i忽略大小写日志里error/Error/ERROR混着写的时候
-v反向匹配排除掉噪音行,只看“不是什么”
-c只输出匹配行数快速统计出现次数,不关心具体内容
-n显示行号定位问题在文件第几行,方便后续处理
-r递归搜索目录日志分散在多个文件里的时候
-l只输出文件名想知道“哪些文件包含这个模式”
-E使用扩展正则需要写复杂模式的时候,比基本正则好用
-A/-B/-C显示上下文行看错误日志时,前后几行往往包含关键信息

这里重点说几个容易被忽略但极其好用的参数。-A 3表示匹配行之后额外显示3行,-B 3是之前3行,-C 3是前后各3行。排查错误日志的时候,光看错误那一行往往不够,前一行可能是触发错误的输入,后一行可能是错误堆栈的起始,-C一把梭就能把上下文带出来。

-v的反向匹配也是高频操作。比如你想看日志里除了INFO之外的所有行,grep -v "INFO" train.log就完事了。再配合-c,grep -vc "INFO" train.log直接告诉你有多少条非INFO记录。

-E和基本正则的区别值得单独说一下。基本正则里,+、?、|、()这些元字符都要加反斜杠转义才能用,写起来很别扭。-E开启扩展正则之后,这些符号直接就能用。我的习惯是,只要模式里超过两个元字符,一律上-E,省得跟转义较劲。

2.2 正则表达式在grep里的实战写法

grep的灵魂是正则表达式,但很多人对正则有一种天然的畏惧感。我换个角度讲:正则就是“用符号描述文本长什么样”,你只需要记住几个最常用的符号,就能覆盖80%的场景。

先看几个AIGC日志分析里最常见的需求:

需求一:找出所有loss为nan或inf的行。模式写成loss=(nan|inf),配合-E就是grep -E "loss=(nan|inf)" train.log。这里|表示“或”,()表示分组,意思是“loss=后面跟nan或者inf”。

需求二:找出时间戳在某个区间的行。比如10点23分到10点25分之间,模式可以写成10:2[3-5],[3-5]表示这个位置可以是3、4、5中任意一个字符。完整命令grep -E "10:2[3-5]" train.log。

需求三:找出epoch编号为两位数的行。模式epoch=[0-9]{2},{2}表示前面的字符类重复两次。注意基本正则里{}需要转义,用-E就不用。

需求四:匹配行首或行尾。^表示行首,$表示行尾。比如找出所有以2024开头的行:grep "^2024" train.log。找出所有以lr=0.001结尾的行:grep "lr=0.001$" train.log。

需求五:排除空行和注释行。处理配置文件或者语料的时候特别常用:grep -vE "^\s*($|#)" config.txt。拆开看,^\s*表示行首可能有空白字符,$表示空行,#表示注释,|是或,整体意思就是“排除掉空行和注释行”。

实操心得:写正则的时候,先用grep -E "模式" 文件 | head -5看一眼匹配结果对不对,确认没问题再去跑全量。我见过太多人正则写错了直接跑几十万行文件,结果出来一堆垃圾数据还得重来。head先验证,是个保命的习惯。

还有一个很实用的技巧:grep -o只输出匹配到的部分,而不是整行。比如日志里你想把所有loss=xxx的数值单独抽出来,grep -oE "loss=[0-9.]+" train.log,输出就是一行一个loss=2.341这样的片段。这个在后续要接awk做计算的时候特别有用,因为抽出来的格式很干净。

2.3 递归搜索与文件过滤的组合拳

实际项目里,日志和语料很少是单个文件,通常是一个目录下几十上百个文件。grep -r递归搜索是基本操作,但直接grep -r "ERROR" ./logs/可能会把二进制文件、压缩包也扫进去,输出一堆乱码。

正确的做法是配合--include和--exclude做文件过滤:

grep -r --include="*.log" --include="*.txt" "ERROR" ./logs/

这行命令的意思是:在./logs/目录下递归搜索,只搜.log和.txt结尾的文件,找包含ERROR的行。--include可以写多个,--exclude同理。还有一个--exclude-dir用来排除目录,比如排除.git目录:--exclude-dir=".git"。

再进阶一点,-l参数只输出包含匹配的文件名,不输出具体行。这个在“我想知道哪些文件有问题”的场景下特别好用。比如你有一批语料文件,想找出哪些文件里包含敏感词或者格式异常的内容:

grep -rl --include="*.txt" "异常模式" ./corpus/

输出就是一列文件名,干净利落。拿到文件名列表之后,可以接xargs做批量处理,比如批量重命名、批量移动,这个后面讲组合技巧的时候会展开。

3. sed:流式文本编辑的利器

3.1 替换操作的完整参数体系

sed最广为人知的用法就是替换:sed 's/旧/新/' 文件。但这里有几个关键细节,决定了你是“会用”还是“用对”。

第一个细节:默认只替换每行的第一个匹配。如果你想把一行里所有的a都换成b,必须加g标志:sed 's/a/b/g'。这个g是global的意思。我见过不少人踩这个坑,替换完发现只改了一处,还以为sed有bug。

第二个细节:默认不修改原文件,只输出到终端。想直接改文件,加-i参数:sed -i 's/a/b/g' file.txt。但这里有个坑,macOS上的sed -i必须跟一个备份后缀,比如sed -i '' 's/a/b/g' file.txt,空字符串表示不备份。Linux上则可以直接sed -i。跨平台脚本里我一般写成sed -i.bak,两边都能跑,跑完手动删.bak文件。

第三个细节:分隔符不一定是斜杠。当你的替换内容里本身包含斜杠时,比如替换文件路径,用斜杠做分隔符就要疯狂转义。这时候可以换分隔符,sed 's|/old/path|/new/path|g',用竖线、井号、逗号都行,选一个内容里不出现的字符就好。

第四个细节:支持反向引用。\(\)在基本正则里表示分组,\1、\2引用分组内容。比如把2024-01-15格式的日期改成15/01/2024:

sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' train.log

-E开启扩展正则后,分组直接写(),引用还是\1这种形式。这行命令的意思是:匹配“四位数字-两位数字-两位数字”,然后按“第三组/第二组/第一组”的顺序重新排列。日期格式转换、字段顺序调整,全靠这个反向引用。

3.2 删除、插入与多行操作的场景化用法

替换之外,sed的删除和插入能力在文本清洗里用得非常多。

删除指定行:sed '3d' file.txt删除第3行,sed '2,5d'删除第2到5行,sed '/pattern/d'删除包含pattern的行。组合起来,sed '/^$/d'删除所有空行,sed '/^#/d'删除所有注释行。清洗配置文件或者语料的时候,这两条命令几乎是标配。

在指定位置插入内容:i在匹配行之前插入,a在匹配行之后追加。比如给每个epoch行前面加一个分隔标记:

sed '/epoch=/i --- 新的epoch开始 ---' train.log

多行操作:sed默认按行处理,但有些场景需要跨行匹配。比如把连续两个空行压缩成一个,可以用N命令把下一行读进来一起处理:

sed '/^$/{N;/^\n$/d}' file.txt

这行的逻辑是:遇到空行时,把下一行也读进来,如果两行都是空行,就删掉。N是sed的多行模式里最常用的命令,配合P、D可以做更复杂的段落处理。

注意:sed的-i直接改文件是不可逆的,跑之前一定先不加-i看一眼输出。我的习惯是,任何sed -i命令,先在终端里跑一遍不带-i的版本,确认输出符合预期,再补上-i执行。这个习惯帮我避免过好几次批量改错文件的事故。

3.3 用sed做批量配置修改的实操记录

说一个我实际做过的场景。有一批模型配置文件,格式大概是这样:

model_name: resnet50 batch_size: 32 learning_rate: 0.001 epochs: 100

现在需要把所有文件的batch_size从32改成64,learning_rate从0.001改成0.0005。手动改几十个文件不现实,一条sed命令搞定:

sed -i.bak -E 's/^(batch_size:\s*)32/\164/; s/^(learning_rate:\s*)0\.001/\10.0005/' *.conf

拆开看:^(batch_size:\s*)匹配行首的batch_size:加上后面的空白,用()分组捕获,\1在替换时引用这个分组,后面接新值。分号分隔两条替换规则,一次执行。-i.bak保留备份,万一改错了还能回滚。

这里有个细节值得说:为什么用^(batch_size:\s*)而不是直接batch_size: 32?因为配置文件里可能有注释行也包含batch_size: 32这样的文本,加^锚定行首能避免误伤注释。这种“锚定行首”的习惯,在批量修改配置时能挡掉很多意外。

再补一个技巧:如果只想改某个区间的行,可以用地址范围。sed '10,20s/old/new/g'只替换第10到20行里的内容。配合/pattern/做地址,sed '/^\[section\]/,/^\[/s/old/new/g'可以只改某个配置段内的内容,遇到下一个段头就停。这个在改ini风格配置文件时特别好用。

4. awk:结构化文本计算的核心引擎

4.1 字段处理与内置变量的运行机制

awk和前面两个命令最大的区别在于,它是一门完整的编程语言。你写awk '{print $1}'的时候,其实是在写一个“对每一行执行print第一个字段”的程序。理解这一点,后面的所有用法就顺了。

awk的基本执行模型是:读入一行,按分隔符拆成字段,然后执行你写的模式-动作对。内置变量是理解awk的钥匙:

变量含义典型用途
$0整行内容原样输出或做整行匹配
$1~$n第n个字段提取特定列
NF当前行的字段数判断字段数量、取最后一个字段$NF
NR当前行号(跨文件累计)加行号、按行号过滤
FNR当前文件内的行号多文件处理时区分文件
FS输入字段分隔符指定按什么拆列
OFS输出字段分隔符指定输出时用什么连接
RS输入行分隔符处理非换行分隔的文本

默认分隔符是空白字符(空格和Tab),所以awk '{print $1}'能直接提取第一列。如果文本是逗号分隔的CSV,就要用-F,指定分隔符:awk -F, '{print $2}' data.csv。-F后面跟的可以是单个字符,也可以是正则,比如-F'[,;]'表示逗号和分号都当分隔符。

NF这个变量特别实用。取最后一列不用数第几列,直接$NF。取倒数第二列是$(NF-1)。判断一行是不是空行,NF==0就行。我在处理格式不规整的日志时,经常用NF做校验,比如awk 'NF!=5 {print NR": 字段数异常"}',快速定位格式有问题的行。

NR和FNR的区别在多文件处理时才显现。NR是跨文件累计的行号,FNR是每个文件重新计数的行号。比如awk '{print FILENAME, FNR, $0}' file1 file2,输出里能清楚看到每个文件各自的行号。FILENAME也是内置变量,当前处理的文件名。

4.2 条件判断、循环与聚合统计的写法

awk真正强大的地方在于它能做计算和聚合。看几个AIGC日志分析里的典型需求。

需求一:统计每种日志级别的出现次数。日志第二列是级别(INFO/WARN/ERROR),用awk的关联数组:

awk '{count[$2]++} END {for (level in count) print level, count[level]}' train.log

count[$2]++对每一行执行,以第二列的值作为数组下标,计数加一。END块在所有行处理完后执行,遍历数组输出结果。这就是一个完整的词频统计,换成统计loss值出现次数、统计每个epoch的行数,逻辑一模一样。

需求二:计算loss的平均值、最大值、最小值。假设loss在第四列,格式是loss=2.341,先用gsub去掉前缀再计算:

awk '{gsub(/loss=/,"",$4); sum+=$4; if($4>max||NR==1)max=$4; if($4<min||NR==1)min=$4; n++} END {print "avg="sum/n, "max="max, "min="min}' train.log

gsub是awk内置的替换函数,把loss=替换成空字符串,$4就变成纯数字了。sum+=$4累加,max和min用条件判断更新,NR==1保证第一行能正确初始化。最后END块输出三个统计值。这段逻辑如果写成Python,至少十几行,awk一行搞定。

需求三:按条件筛选并格式化输出。找出loss大于2.0的行,输出时间戳和loss值,用制表符分隔:

awk '{gsub(/loss=/,"",$4); if($4+0>2.0) print $1"\t"$2"\t"$4}' train.log

$4+0强制转成数字比较,避免字符串比较的坑。\t是制表符,输出对齐好看。

需求四:按某个字段分组做聚合。比如按epoch分组统计平均loss:

awk '{gsub(/loss=/,"",$4); gsub(/epoch=/,"",$3); sum[$3]+=$4; cnt[$3]++} END {for (e in sum) print "epoch="e, "avg_loss="sum[e]/cnt[e]}' train.log

两个关联数组,一个存累加和,一个存计数,END块里遍历输出。这种“分组聚合”的模式,在awk里就是数组加循环,写熟了比SQL还顺手。

4.3 用awk生成结构化报表的完整实例

单独拿一个完整场景出来讲,因为这是awk最能体现价值的地方。假设你有一份模型评测结果文件eval.txt,每行格式是:

model_A accuracy=0.92 latency=45 model_B accuracy=0.88 latency=32 model_A accuracy=0.94 latency=47 model_B accuracy=0.86 latency=30

同一个模型有多条记录,你想输出每个模型的平均accuracy、平均latency、记录条数,并且按accuracy从高到低排序。分两步走,awk负责聚合,sort负责排序:

awk '{acc[$1]+=$2; lat[$1]+=$3; n[$1]++} END {for (m in acc) printf "%-10s acc=%.3f lat=%.1f n=%d\n", m, acc[m]/n[m], lat[m]/n[m], n[m]}' eval.txt | sort -t= -k2 -rn

awk部分:三个数组分别存accuracy累加、latency累加、计数,END块用printf格式化输出,%-10s左对齐10字符宽,%.3f保留三位小数。sort部分:-t=指定等号为分隔符,-k2按第二个字段排序,-rn表示反向数值排序。输出就是一张按accuracy降序排列的报表。

这个“awk聚合+sort排序”的组合,是我做数据汇总时的默认套路。awk负责算,sort负责排,各司其职,比在一个工具里硬塞所有功能清晰得多。

实操心得:awk的printf格式化输出,字段宽度和对齐方式值得花十分钟研究一下。%-10s左对齐、%10s右对齐、%.2f两位小数,这几个组合能覆盖绝大多数报表需求。输出整齐的报表,比一堆散乱数字可读性高一个量级,给同事看的时候也显得专业。

5. 三剑客组合实战:从日志到报表的完整链路

5.1 管道串联的思维模型

单独用每个命令只是入门,真正的威力在组合。管道符|把前一个命令的输出变成后一个命令的输入,形成流水线。我总结了一个思维模型:grep做减法,sed做变形,awk做聚合,sort做排序,uniq做去重。遇到任何文本处理需求,先想清楚每一步用哪个工具,串起来就行。

举个完整的例子。需求是:从训练日志里找出所有ERROR和WARN级别的记录,提取时间戳和loss值,统计每种级别下loss超过阈值的次数,最后按次数降序输出。

第一步,grep筛选出ERROR和WARN行:

grep -E "ERROR|WARN" train.log

第二步,sed把loss=前缀去掉,方便后续计算:

| sed 's/loss=//'

第三步,awk提取级别和loss值,做条件判断和计数:

| awk '{if($4+0>1.5) cnt[$2]++} END {for (l in cnt) print l, cnt[l]}'

第四步,sort按次数降序:

| sort -k2 -rn

完整命令:

grep -E "ERROR|WARN" train.log | sed 's/loss=//' | awk '{if($4+0>1.5) cnt[$2]++} END {for (l in cnt) print l, cnt[l]}' | sort -k2 -rn

一条命令,四个环节,每个环节职责单一,组合起来解决一个完整需求。这种写法比写脚本的优势在于:调试方便,你可以随时在中间加一个| head -5看当前输出,确认没问题再往下接。

5.2 语料清洗流水线的搭建过程

再说一个AIGC场景里高频的需求:清洗语料文件。假设你有一批文本语料,需要做以下处理:去掉空行、去掉纯标点行、去掉超短行(少于10个字符)、把连续多个空格压缩成一个、最后统计清洗前后的行数变化。

分步拆解:

# 原始行数 wc -l corpus.txt # 清洗流水线 sed '/^$/d' corpus.txt | grep -vE '^[[:punct:]]+$' | awk 'length($0)>=10' | sed -E 's/ +/ /g' > corpus_clean.txt # 清洗后行数 wc -l corpus_clean.txt

逐段解释:sed '/^$/d'删空行;grep -vE '^[[:punct:]]+$'排除纯标点行,[[:punct:]]是POSIX字符类,匹配所有标点符号;awk 'length($0)>=10'保留长度大于等于10的行,length是awk内置函数;sed -E 's/ +/ /g'把连续空格压缩成一个。最后重定向到新文件。

这条流水线的好处是每一步都可以单独验证。你先跑sed '/^$/d' corpus.txt | wc -l看删了多少空行,再逐步往后加,每一步的输入输出都清清楚楚。我处理陌生语料时都是这么干的,先摸清楚数据长什么样,再决定清洗规则,而不是一上来就写个大脚本。

5.3 批量文件重命名与内容替换的自动化脚本

最后一个组合场景:批量处理目录下的文件。需求是:把./data/目录下所有.txt文件里的旧模型名替换成新模型名,同时把文件名里的old改成new。

内容替换用sed配合find:

find ./data -name "*.txt" -exec sed -i.bak 's/旧模型名/新模型名/g' {} \;

find找出所有.txt文件,-exec对每个文件执行sed,{}代表当前文件名,\;表示命令结束。-i.bak保留备份,跑完确认没问题再批量删.bak。

文件名重命名用rename或者循环:

for f in ./data/*old*.txt; do mv "$f" "${f//old/new}"; done

${f//old/new}是bash的字符串替换语法,把变量f里所有的old替换成new。这个循环遍历所有含old的txt文件,逐个改名。

注意:批量操作前一定先echo预览。把mv换成echo mv,先看一遍要执行什么,确认无误再去掉echo。这个习惯救过我很多次,尤其是文件名里有空格或者特殊字符的时候,预览能提前发现问题。

6. 踩坑实录与高频问题速查

6.1 编码、分隔符与特殊字符的坑

坑一:中文编码问题。grep在匹配中文时,如果文件编码和终端编码不一致,可能匹配不到。先file 文件名看编码,必要时用iconv转成UTF-8再处理。iconv -f GBK -t UTF-8 input.txt > output.txt。

坑二:字段分隔符不是单个空格。日志里经常有多个空格对齐的情况,awk '{print $2}'默认按空白字符拆分,多个空格会被当成一个分隔符,这个行为其实是对的。但如果你用-F' '指定单个空格,连续空格就会产生空字段,$2可能拿到空字符串。所以除非明确知道是单空格分隔,否则不要手动指定-F' ',用默认的空白拆分更稳。

坑三:sed替换内容里有特殊字符。替换内容包含&时,&在sed替换串里表示“整个匹配内容”,需要转义成\&。包含反斜杠时也要转义。最省事的办法是换分隔符,并且用单引号包裹整个sed表达式,避免shell先做一层转义。

坑四:grep匹配到二进制文件。递归搜索时遇到二进制文件,grep会输出“Binary file xxx matches”。加-I参数忽略二进制文件,或者用--include限定文件类型。

坑五:awk的数值比较陷阱。从文本里提取的数字默认是字符串,直接比较可能出问题。$4>2.0在某些情况下会按字符串比较,加+0强制转数值:$4+0>2.0。这个坑我在处理带前导零的数字时踩过,"08">"10"按字符串比较是true,按数值比较是false,加+0才能得到正确结果。

6.2 性能优化:大文件处理的经验之谈

处理GB级别的大文件时,三个命令的性能差异和优化技巧值得单独说。

grep是最快的筛选工具,没有之一。它用了高度优化的字符串匹配算法,处理大文件时尽量用grep做第一道筛选,把数据量降下来再交给后面的命令。grep -F用固定字符串匹配而不是正则,速度更快,模式里没有元字符时优先用-F。

sed是流式处理,内存占用恒定,处理大文件不会爆内存。但sed -i会创建临时文件,磁盘空间要留够。超大文件(几十GB)建议用split先切分再并行处理。

awk的性能取决于你写的逻辑。简单的字段提取和grep差不多快,但复杂的关联数组聚合会消耗较多内存。如果聚合的key特别多(比如几百万个不同的key),考虑先用sort | uniq -c做初步聚合,再交给awk做二次计算。

一个通用的优化原则:把过滤条件尽量往前放。grep "ERROR" bigfile | awk '{...}'比awk '/ERROR/{...}' bigfile快,因为grep先筛掉了大部分行,awk只需要处理少量数据。管道里每个环节的数据量应该是递减的,这是设计流水线时的基本考量。

6.3 常见问题速查表

问题现象可能原因解决方法
grep匹配不到中文编码不一致file查编码,iconv转UTF-8
sed -i报错macOS需要备份后缀sed -i ''或sed -i.bak
awk字段数不对分隔符不符合预期用-F指定,或先awk '{print NF}'检查
替换后格式乱了特殊字符未转义换分隔符,单引号包裹表达式
大文件处理卡死内存不足或逻辑太重先grep过滤,再分步处理
递归搜索输出乱码扫到了二进制文件加-I或--include限定类型
数值比较结果不对字符串比较陷阱加+0强制转数值
管道中间结果不对某环节输出格式变了在中间加head逐段验证

这张表里的每一条,都是我在实际项目里真实遇到过的。尤其是“管道中间结果不对”这一条,组合命令出问题时,最快的排查方式就是从管道开头开始,一段一段加| head -5看输出,定位到哪一段开始不符合预期,问题就找到了。

7. 我个人在实际操作中的几点体会

写了这么多,最后聊几句掏心窝子的。这三个命令我用了很多年,最大的感受是:它们的价值不在于参数多,而在于组合灵活。你不需要记住所有参数,只需要把最常用的十几个用熟,剩下的遇到问题时查一下man page就够了。真正拉开差距的,是你对“什么场景该用什么工具”的判断力。

我的建议是,别抱着“学完再用”的心态,直接拿手头的真实文件练。找一份日志、一份配置、一份语料,给自己提需求:找出所有异常行、统计某个字段的分布、批量改一批文件。每完成一个需求,你对这三个命令的理解就深一层。参数是死的,场景是活的,在真实数据上踩过的坑,才是真正记住的东西。

另外,别迷信“一条命令解决所有问题”。有时候拆成两三步,中间结果存个临时文件,反而更清晰、更好调试。管道写得再长,如果中间某一步出了问题你定位不到,那还不如分步来。工具是为人服务的,怎么顺手怎么来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进