这个系列的第九篇,我想把Linux命令里文本处理这一坨好好揉碎了聊聊。前面几篇基本把文件操作、用户权限、进程管理、网络排查这些基础铺了一遍,但很多朋友在实际干活的时候仍然会卡住:明明grep、awk、sed都单独用过,一遇到要统计日志、批量改配置、从几百M的文件里提取关键状态,就不知道用什么命令配什么参数。说白了,不是命令不认识,而是你还没把命令串成一条线。这一篇就解决这个问题:以文本处理为主线,覆盖grep、sed、awk,以及sort、uniq、wc、xargs这些高频搭档,用真实工作场景把它们串起来。文章既适合刚接触Linux想系统补课的读者,也适合已经混迹终端多年但想查漏补缺的老手。
这一篇的内容安排是:先解释为什么文本处理命令这么重要;然后把grep、sed、awk以及sort、uniq、wc、xargs这些命令掰开揉碎讲一遍;接着用一个真实的模拟日志案例,把这些命令串成一个可执行的统计流程;最后把我踩过的坑集中整理一下。跟着走一遍,你会发现自己处理日志和配置文件的效率能明显上一个台阶。
1. 为什么把文本处理单独拿出来讲
实际工作中你接触到的系统状态、业务记录、配置文件,绝大多数都以纯文本形式存在。日志文件是文本,配置文件是文本,数据导入导出文件也是文本。所以Linux命令里,文本处理命令的使用频率绝对排在最前面。很多新手学Linux时喜欢盯着ls、cd、rm这些“看得见摸得着”的命令,文本处理这类命令却因为抽象容易被忽略,但真实场景里它们才是真正拉开效率差距的东西。
1.1 日常工作中最常见的三类文本场景
第一类是日志分析。Web服务、应用容器、数据库慢查询日志,全是文本。出现故障时,你要在几十万行日志里找某条关键报错,统计某个IP的访问频率,算出某个接口的平均响应时间,这些操作本质上都是文本过滤、提取、聚合。我自己在排查线上问题时,一半以上的时间都花在把原始日志变成可读的统计结果。从原始日志到可用结果,中间涉及到去掉不需要的列、按时间过滤、对字段做聚合排序。这些操作如果全靠打开文件用肉眼搜索,效率低不说,还容易漏。所以文本处理命令不是锦上添花,而是刚需。
第二类是配置文件批量修改。一个项目里十几个环境配置,每个配置里有一两行需要改动,手动打开一个个改很笨,用sed一条命令就能批量处理。尤其当你需要把某段数据库连接地址从一个环境切换成另一个环境时,几十个文件手动改完还要检查遗漏,用sed替换再配合diff校验结果,几分钟就能搞定。第三类是数据清洗。脚本抓下来的数据文件、业务导出的CSV,经常有多余空格、空行、列错位,需要按字段重新整理。爬虫拿到的数据格式不规范是常态,用awk按字段重新编排,能够快速整理成你想要的表格格式。文本处理能力直接决定了运维和开发在Linux下的效率。
1.2 命令组合比单个命令更重要
刚学Linux时,大家都会先记单个命令和参数。但实际工作中,90%的文本处理都是通过管道把三四个命令的输入输出串联起来。例如grep "ERROR" app.log | awk '{print $1}' | sort | uniq -c就是一条非常典型的处理链。理解管道背后“一个命令的标准输出接另一个命令的标准输入”这个模型,比死记硬背几百个参数更有价值。如果前面几篇已经掌握了ls、cat、head、tail这些基础命令,那么这篇就把重点放在怎样用管道和重定向把文本处理的命令组合起来。
命令组合还有一个好处是便于复现。你把几个命令用管道串成一行,或者写进脚本,下次遇到同样的问题直接跑一遍就行,而不是每次手动点来点去。我在团队里推广过一个简单习惯:凡是处理文本的临时操作,尽量用命令行完成,并保留在终端历史里;这样既能快速回看,也能整理成运维脚本。命令组合是手段,不是目的。实际写命令的时候,不用追求一行炫技式长管道,阅读性和可维护性同样重要。尤其当你把命令写成脚本时,建议拆成多行并用反斜杠续行,加上注释说明每一步在做什么。把大的处理拆成有名字的中间步骤,或者写成函数,都比硬凑一行好很多。
另外,重定向也很关键。>覆盖写、>>追加写、2>错误输出、2>&1合并标准输出和错误输出,这些基础如果不熟练,组合命令时会遇到“错误信息为什么跑进结果文件里”的困惑。如果后面写Shell脚本,还要记住<从文件读取输入。文本处理过程中,经常要先把处理结果写入临时文件,再做下一步,避免管道过长导致排错困难。掌握了这些底层连接方式,文本处理命令才能真正变成一套可组合的工具链。
2. 三组核心命令的参数细节与实用技巧
文本处理的三套核心工具我习惯叫它们“三板斧”:grep负责过滤,sed负责流式编辑,awk负责按字段分析。三者各有侧重,组合起来基本能覆盖日常文本处理的大多数场景。但要真正用好它们,光知道最简单的用法是不够的,参数细节和正则风格决定了你的命令是“能用”还是“好用”。这一章我会把每个命令最实用、最容易出错的参数单独拎出来讲。
2.1 grep:过滤文本的“放大镜”,远不止匹配字符串
grep最基本的功能是从文件或标准输入中找出包含指定模式的行。很多人只用到grep "keyword" file,但实际场景中我更常用的是grep -E(扩展正则)、grep -v(反选)、grep -c(计数)、grep -o(只输出匹配部分)和grep -n(显示行号)。比如想找出配置里所有以#开头的注释行,可以grep -n '^#' /etc/nginx/nginx.conf;想排除注释行,用grep -v '^#'。-o非常实用,它能让你从整行里只抠出真正关心的那段字符串,再用sort和uniq去统计,后面实战会用到。比如从日志里把所有IP都抠出来,grep -o '[0-9.]\{7,15\}'配合sort、uniq -c,就能看到每种IP出现的次数。
关于正则,建议优先掌握-E而不是直接用默认的基础正则,因为扩展正则支持+、?、{m,n}、|这些常见写法,读起来和大多数编程语言的正则风格接近。比如grep -E 'error|warn'能同时匹配两个词。-i忽略大小写也要养成习惯,日志里Error和ERROR混着出现时就不用额外操心。-A、-B、-C可以带出匹配行的上下文,排查异常时经常用-B 2 -A 5看报错前后的日志片段。不过要提醒一点,-r递归搜目录虽然方便,但会遍历大量文件,一定配合--include或者--exclude来控制范围,否则可能在几百GB日志目录里卡住。实际我更喜欢先用ls确认文件范围,再针对性地grep,不要上来就全盘递归。
grep有一个很难受的坑是默认基础正则里的元字符和扩展正则不一致。后面常见问题部分我会专门说。这里先给一个速查建议:除了最简单的纯字符串搜索,其余情况一律显式加-E;如果你要搜索的是包含通配符意义的普通字符串,不想做正则解释,那就用grep -F,这样能避免转义各种符号的麻烦,匹配速度也更快。用-c统计匹配行数时也要注意,它返回的是行数,不是匹配次数;如果一行里出现了多次关键字,-c只会算一次。想要统计出现次数,用grep -o加上wc -l。
2.2 sed:流式文本编辑三件套:打印、替换、删除
sed是流编辑器,本意是对输入流做逐行处理。日常工作里我主要用它做三件事:文件内容替换、指定范围打印、批量删除。替换是最常用的,语法是sed 's/旧/新/标志'。默认替换只处理每一行第一个匹配,想要全局替换要在末尾加g标志,例如sed -i 's/127.0.0.1/0.0.0.0/g' app.conf。注意-i是直接修改文件,所以强烈建议先运行不带-i的命令,确认输出没问题再落地修改;如果不放心,用-i.bak生成备份文件,例如sed -i.bak 's/old/new/g' file。这个习惯在修改关键配置时特别重要,后面会再提。
sed的打印和删除功能更多用于按范围取行或者过滤行。比如sed -n '10,20p' app.log只打印第10到20行,-n参数是安静模式,避免每行都输出,这配合p打印动作几乎是必须的记忆公式。删除可以用d,比如sed '/^$/d' file删除空行。正则分组是sed威力最大的地方:sed -n 's/^\([^:]*\):.*$/\1/p' /etc/passwd可以提取出登录名。这个例子里\1表示第一个括号捕获的内容,括号前面要加反斜杠,这是基础正则的写法。我在实际写的时候经常提醒自己:加上-E开启扩展正则后,括号就不用反斜杠了,写法清爽很多,比如sed -nE 's/^([^:]*):.*$/\1/p' /etc/passwd。
| sed常见动作 | 示例 | 作用 |
|---|---|---|
| 替换 | sed 's/old/new/g' file | 全局替换 |
| 打印 | sed -n '10,20p' file | 打印第10到20行 |
| 删除 | sed '/^#/d' file | 删除以#开头的行 |
| 定位替换 | sed -nE 's/^([^,]*),.*/\1/p' | 提取第一个字段并打印 |
sed还有一个常见的用途是做行范围匹配。比如你想在某个配置块之后插入一行,sed '/\[web\]/a port=8080'可以在匹配到[web]的地方追加内容。a是追加,i是插入到前面。这些能力在批量调整配置文件结构时很有用,但使用起来也要小心行范围和模式匹配的范围过大。比较稳妥的方法是先用sed打出行号和内容做预览,确认范围正确后再批量操作。我经常用nl -ba file | sed -n '10,20p'来看清理后的文本结构,避免直接改坏原文。
2.3 awk:给文本加个“表格思维”
awk和grep、sed最大的不同是,它会把每一行按分隔符拆成多个字段,然后可以对每个字段做逻辑判断和运算。默认按空白(空格或Tab)分隔,可以用-F指定其他分隔符。最简单的例子:awk -F: '{print $1}' /etc/passwd,把用户名字段提取出来。awk内置NR(当前行号)和NF(当前行的字段数),比如awk -F: '{print NR, NF, $1}'就是“行号 字段数 用户名”。对于新手来说,$1、$2、$NF、NR这几个概念是最先要记牢的,它们帮助你从“行”的世界进入“字段”的世界。
awk真正的价值在于它是“带逻辑判断的文本处理器”。你可以用awk -F: '$3 >= 1000 {print $1}' /etc/passwd只打印UID大于等于1000的用户,也可以用awk -F, '{sum += $2} END {print sum}' data.csv做一列数字的求和。sum += $2和END块是awk统计时的标志性写法。awk还支持变量、if条件、for循环,几乎是一门微缩语言。所以我建议新手先掌握三件事:按字段打印、BEGIN/END块、条件过滤。有了这三个基础,绝大多数日志聚合一类的问题都能用awk写出很简洁的命令。
awk的END块是我用得最多的地方。awk先按行处理完所有输入数据,最后进入END块做汇总输出。比如记录某个字段的最大值、最小值、总和、平均值,都可以在行处理阶段更新变量,最后在END块打印。awk还支持关联数组,比如count[$1]++用来按第一个字段计数,这在统计日志时几乎是每天都会用到的模式。需要提醒的是,awk默认把连续空格当成一个分隔符,但如果你用-F'|'指定管道符作为分隔符,前后如果有空格,它就不会自动去除。所以日志格式最好自己控制一致,或者在awk里先用gsub清洗空格,否则字段位置容易偏移。
3. 模拟一场真实的日志统计实战
光讲单个命令很难形成直观感受,我把它们串起来跑一遍完整场景。假设你有这样一个任务:某天上午接口服务出现了几次异常,需要从访问日志里快速定位哪些来源IP产生了500和503错误,同时统计各个来源IP的请求频率,还要计算每个接口最近一段时间的平均响应耗时。这个需求一拆开,你会发现全部可以用文本处理命令完成。这一章的示例日志和数据都是模拟的,但方法可以直接套用到真实环境。
3.1 准备一份模拟访问日志
假设某服务记录日志到/var/log/app/api.log,每条日志格式统一为管道分隔的六列:时间、来源IP、请求方法、接口路径、HTTP状态码、耗时(秒)。实际日志可能是JSON或空格分割,但管道分隔便于演示,你们在工作里遇到类似格式可以套用。示例内容如下。
2025-05-12 10:00:01|10.0.0.8|GET|/api/v1/order/list|200|0.123 2025-05-12 10:00:02|10.0.0.9|GET|/api/v1/user/info|404|0.008 2025-05-12 10:00:03|10.0.0.10|POST|/api/v1/order/submit|500|0.211 2025-05-12 10:00:04|10.0.0.8|GET|/api/v1/order/list|200|0.142 2025-05-12 10:00:06|10.0.0.11|GET|/api/v1/product/detail|500|0.321 2025-05-12 10:00:08|10.0.0.12|GET|/api/v1/order/list|200|0.099 2025-05-12 10:00:09|10.0.0.8|POST|/api/v1/order/submit|503|0.045 2025-05-12 10:00:11|10.0.0.13|GET|/api/v1/user/info|200|0.018 2025-05-12 10:00:12|10.0.0.9|GET|/api/v1/product/detail|200|0.086 2025-05-12 10:00:15|10.0.0.10|GET|/api/v1/order/list|500|0.276这份日志只有十行,但已经覆盖了200、404、500、503四种状态,多个IP重复出现,足够展示命令组合的完整流程。如果你的日志是带引号或空格分隔的,awk的-F和字段索引需要做相应调整,后面常见问题里我会提一句。可以先把这个文件存到本地,自己跟着命令敲一遍,比只看不练印象深刻很多。
3.2 按需求依次拆解命令
任务1:统计每个IP的请求次数,按从高到低排序。
awk -F'|' '{print $2}' /var/log/app/api.log | sort | uniq -c | sort -rn第一步awk -F'|' '{print $2}'把IP列提取出来;sort把相同的IP排在一起;uniq -c统计连续且唯一行出现的次数;sort -rn按数字逆序排序。注意uniq -c只能统计相邻出现的重复行,所以必须先sort。如果忘了sort,相同IP分散在不同位置时会统计出多个错误结果。执行后输出大约是:
5 10.0.0.8 2 10.0.0.9 2 10.0.0.10 1 10.0.0.11任务2:找出所有状态码为500或503的请求,并显示来源IP和接口路径。
awk -F'|' '$5 == 500 || $5 == 503 {print $2, $4, $5}' /var/log/app/api.log这里awk先做条件判断,满足条件的行才执行花括号里的打印动作。输出结果类似:
10.0.0.10 /api/v1/order/submit 500 10.0.0.11 /api/v1/product/detail 500 10.0.0.8 /api/v1/order/submit 503 10.0.0.10 /api/v1/order/list 500任务3:统计每个接口的平均耗时,仅统计状态码是200的请求。
awk -F'|' '$5 == 200 {sum[$4] += $6; count[$4]++} END {for (path in sum) print path, sum[path]/count[path]}' /var/log/app/api.log这里用了关联数组,sum和count都是以接口路径为下标的数组,END块里循环输出平均耗时。第一次看awk关联数组不要慌,它其实就是“一个名字对应一个值”的字典。执行结果会比较清晰地列出接口路径和平均耗时,比如/api/v1/order/list的平均值是所有200请求耗时加起来除以次数。实战里如果你想算P99而不是平均,还需要先把所有耗时存进数组,排序后取分位点,这个稍微复杂一些,但思路是一致的。
任务4:把日志里的IP字段替换成internal,并追加到新文件。
sed 's/10\.0\.0\.[0-9]*/internal/' /var/log/app/api.log > /tmp/api_anonymized.log这里sed用正则匹配示例IP段并替换,输出重定向到新文件,原文件不受影响。如果原文件很大,临时文件可以选在/tmp下,注意磁盘空间。替换之后可以再用head查看开头几行确认效果。除了替换,还可以顺便删掉不需要的字段,比如只保留时间、IP、状态码三列:
awk -F'|' '{print $1, $2, $5}' /var/log/app/api.log > /tmp/api_core.log在排查异常时,用grep过滤并带上上下文也很常用。比如你想看500和503附近的内容:
grep -E '500|503' -B 1 -A 2 /var/log/app/api.log这条命令会把每个异常行前后各1行和2行都打出来,方便观察异常前后的请求情况。
3.3 把流程写成可复用脚本
上面的命令适合临时排查,但如果你每天都要做类似统计,建议把它们封装成一个bash脚本。脚本的价值在于:参数可复用、结果可复现、逻辑可维护。比如下面这个脚本可以统计指定日志文件中某一天的TOP N IP。写脚本时注意变量加双引号,防止文件名带空格导致解析问题。
#!/usr/bin/env bash set -euo pipefail log_file="/var/log/app/api.log" date_str="${1:-2025-05-12}" top_n="${2:-5}" grep -- "$date_str" "$log_file" \ | awk -F'|' '{print $2}' \ | sort \ | uniq -c \ | sort -rn \ | head -n "$top_n"脚本里我特意加了set -euo pipefail,这在文本处理脚本里非常关键。-e让命令出错时及时退出,-u让未定义变量直接报错,-o pipefail让管道中任何一个命令失败时整个管道返回非零状态。少了pipefail,如果grep匹配不到内容导致退出码为1,后面的管道还会继续执行,你可能会看到一个空结果但脚本并不会报错,这在自动化环境里很难排查。这段脚本虽然简单,但已经是一个完整的“统计Top N IP”工具。进一步扩展,你还可以用awk把状态码字段也加入到统计维度,脚本会变得更通用。
写脚本的时候,我习惯把日志路径、日期这些可能变动的值放到脚本开头的变量区,用命令行参数传默认值。这样别人拿到脚本时不用深入看懂中间的管道就知道怎么改。还有一个细节是,如果日志文件很大,不要在一次脚本里对同一个文件做多次grep或awk遍历,尽量在一次扫描里把需要的统计量都算出来。多个统计需求合并成单个awk程序,往往比串多条命令再合并结果快很多。磁盘IO往往是文本处理的瓶颈,减少读取次数,比在命令参数上抠细节更重要。
4. 常见问题与排查技巧实录
文本处理命令虽然基础,但实际用起来坑不少。这一章我把自己踩过的一些高频问题整理成速查式的内容,每条都附上排查思路和解决方案。这些问题在文档里不一定写得这么细,但实战中几乎都能碰到。如果你们在用命令时遇到奇怪现象,先对照这一章看看是不是同一个坑。
4.1 grep的正则兼容性:-E和-P别混用
默认grep用的是基础正则,+、?、|这些字符不按元字符处理。大部分人直接grep 'error|warn' file会发现什么都匹配不到,因为这里的|被当成普通字符了。正确的做法是加-E(扩展正则)或者用egrep。而-P是Perl兼容正则,支持\d这类写法。我见过不少人把-P当成默认选项到处用,结果在线上机器上因为grep版本不支持导致报错。所以我的建议是:写正则时显式带上-E,尽量不要依赖系统默认解释,这样可移植性更好。
还要注意正则表达式里的特殊字符转义。比如匹配IP时,点号必须写成\.,否则.会匹配任意字符。匹配括号时括号也要转义。用grep -F可以固定字符串搜索,不做任何正则解释,当你搜的是纯文本(比如某个特殊ID)时,用-F能减少很多转义麻烦,而且性能更好。我自己有个习惯:只要搜索关键字内部包含.、[、*这类字符,第一反应就是用-F试试,匹配不中再看要不要换正则。
4.2 sed -i的跨平台坑
sed -i在Linux和macOS上的行为不太一样。Linux的GNU sed支持sed -i 's/a/b/g' file;macOS自带的BSD sed要求必须带扩展名参数,否则会报错,常见的写法是sed -i '' 's/a/b/g' file,注意中间那个空字符串。我在跨平台脚本里一般是先检测操作系统,再决定用哪种写法,或者统一用sed -i.bak,这样两边都认,改完再把备份文件清掉。如果你用容器或者在纯Linux环境,前面那种Linux写法就足够,但脚本要给别人用的时候千万注意兼容性。
另外一个坑是sed -i会直接修改原文件,如果替换正则写错了,文件内容就被破坏了。我个人的经验是:任何一次批量替换前,先在终端跑一次不带-i的sed,把输出重定向到一个临时文件,diff一下临时文件和原文件,确认无误后再执行真实修改。尤其当你要修改的是Nginx配置、数据库连接配置这类关键文件时,这个习惯能救你很多次。如果公司有配置中心或版本管理,最好改完马上提交,方便回滚。没有版本控制的环境,一定保留.bak备份。
4.3 awk处理大文件慢?先看看是不是用了低效写法
awk性能通常不差,但低效写法会拖慢。最常见的低效是每行都调用外部命令,比如在awk内部用system("date ...")去格式化时间,每行开启一个子进程,大文件几十万行下来慢得没法看。推荐的做法是数据预处理:先用sed或grep把时间格式统一,再交给awk做逻辑判断;或者在awk里用字符串函数处理,比如substr()、split(),尽量少调用外部程序。awk里拿外部命令处理数据虽然看起来简洁,但性能开销经常被忽略。
另一个低效点是反复扫描大文件。如果一份日志你需要先统计IP,再统计接口耗时,建议一次awk读完文件,把多个统计结果都放在数组里,最后统一在END块输出,而不是对同一文件执行三四遍awk或grep。我实测过一份约500MB的日志,单次awk扫描完成多维统计的时间比扫三遍再合并快一倍以上。在处理超大日志时,如果只关心某个时间窗口,先用sed -n '/2025-05-12 10:00:00/,/2025-05-12 10:10:00/p'把窗口切出来,再针对小文件做精细统计,效率会更好。
4.4 管道退出码和文件名空格:两个容易翻车的小细节
管道默认的退出码是最后一个命令的退出码,这意味着grep "abc" file | head -20如果grep没匹配到,head仍然正常退出,整个管道看起来是成功的。在脚本里如果你靠管道退出码判断是否存在匹配,会得到错误结论。解决方式是前面提过的set -o pipefail,或者用中间文件保存grep结果再做判断。写自动化脚本时,如果你发现“明明没有匹配内容,但脚本依然显示成功”,大多数都是因为管道退出码掩盖了中间命令的错误。
文件名带空格或特殊字符时,直接用xargs可能出错。命令替换$(find dir -name "*.log")会把带空格的文件名拆成多个参数,导致命令执行错误或找不到文件。推荐使用find ... -print0 | xargs -0 command,或者现代find配合-exec。处理文件名时,永远不要把简单字符串逻辑套在路径上。我在清理历史备份文件时也遇到过类似问题:文件名里有空格,xargs rm直接拆成了两个文件路径,还好提前用了-print0避免了误删。这个细节虽然小,但在批量处理文件时非常重要。
4.5 文本处理时注意locale环境对排序的影响
sort和uniq在统计文本时,都会受到系统locale环境的影响。今天主要说一个实际案例:在有些系统上,LC_ALL或LANG设置为中文或UTF-8环境,sort对英文字符和数字的排序规则与默认的C环境不同,可能导致结果顺序与预期不一致。比如纯数字字段排序时,如果locale不是C或POSIX,sort可能按字典序处理,把“10”排在“9”前面。解决办法是给sort加上-n(按数字排序),或者在脚本开头统一export LC_ALL=C。在处理英文缩写、大小写混合文本时,也要注意sort -f忽略大小写、sort -u去重这类参数的作用。
另一个和locale相关的问题是grep匹配中文内容。grep本身在UTF-8环境下处理中文没有问题,但如果你用-P写正则匹配中文字符范围,不同系统的正则引擎对Unicode的支持程度不同,可能出现匹配不到或匹配范围异常的情况。我的建议是,处理中文文本时优先用比较明确的字符串常量配合-F,避免写复杂的Unicode字符范围。如果必须用正则,找一个稳定的环境先验证一遍再放到脚本里。日志和数据文件里的字符集不一致(UTF-8、GBK混用)也会导致显示乱码和统计结果异常,这类问题排查起来比较隐蔽,遇到乱码先检查文件编码,不要急着怀疑命令参数。
这一篇的内容到这里差不多该收了。我个人在实际操作中的体会是:文本处理命令就像瑞士军刀,单独看每个功能都不复杂,真正拉开差距的是你在遇到实际问题时能不能快速选出合适的命令组合,并把边界情况处理干净。比如一次性修改配置前先预览、统计多个指标时尽量一次读完文件、脚本里养成加pipefail的习惯,这些技巧都是踩坑踩出来的。如果你能把grep、sed、awk三者和sort、uniq、wc、xargs串成一条处理链,基本就掌握了Linux日志分析和文本处理的八成场景。后续这个系列我还会继续聊进程调度、Shell脚本固化和更多实战案例,欢迎持续关注。