一提起“文本处理工具”很多人第一反应就是“那我写个Python脚本吧”。这个系列写到第8篇我想换个角度聊聊实际工作里七成以上的文本处理任务根本不需要写脚本一个终端、几个经典命令就能解决而且解决得比你想象中更快、更稳、更好维护。尤其当你面对的是日志分析、数据清洗、配置批量修改这类场景时命令行文本处理工具才是真正的主角。这篇不是命令手册而是我这几年实际处理文本内容时沉淀下来的思路和习惯。我会从“检索 vs 变换”的底层逻辑讲起把我常驻工具箱里的工具逐个过一遍再拿一个真实到有点脏的日志文件做完整实战最后把那些让我踩过不止一次的坑一并交代清楚。无论你是刚接触命令行还是已经写了几年代码但平时不常用Unix工具这篇文章都值得你花几分钟读完。1. 先想清楚一件事你是在“查”还是在“改”很多人在处理文本时犯的第一个错误不是工具用得不好而是根本还没搞清楚自己要做什么。文本处理任务看起来五花八门但抽象到底层其实只有两类一类是检索一类是变换。检索是你想知道文本里“有什么”比如某个关键词出现在哪些行、某个错误码一共出现多少次、某段时间内哪些IP访问了某个接口。这类任务的共同特点是你不打算改动原始内容只是去寻找、过滤、统计。变换是你要让文本“变成什么”比如把日志里的时间格式从ISO改成自定义格式、把CSV的某一列提取出来放到新文件里、把所有“”分隔符换成逗号、把JSON里的某个嵌套字段取出来。变换意味着输出和输入的结构不同你需要对内容做重新组织。这两类任务对应的工具栈是完全不同的。检索类我用得最多的是rgripgrep配合grep的兜底方案变换类则是sed、awk、jq、tr这些各司其职。为什么非要先做这个分类因为我见过太多人遇到问题就直接在编辑器里打开文件手动一处处改或者在Google搜索框里找一段“批处理脚本”复制粘贴。前者在处理几百MB日志时根本不可行后者往往是拿一个看似相似但又不完全匹配的代码硬套最后跑出来一堆脏数据。更实际的原因是分清楚检索和变换之后你的思路会清晰很多。比如“我想统计每个接口的平均响应时间”这个需求分解下来就是先检索出包含接口名和响应时间的行再对响应时间做变换转换成数值、分组、求均值。如果你一开始就意识到这里有检索和变换两个阶段自然就会写两步命令而不是想着一步到位的“大招”。这两种操作经常会组合出现但组合中检索永远是第一步变换永远是第二步。先确认你处理的数据范围再对范围内容做修改这是一个比任何工具技巧都重要的原则。我见过有人拿到文件后二话不说直接跑sed替换结果把注释里的示例文本也改了原因就是他没有先检索确认“要改的到底是哪些行”。这个顺序只要颠倒一次后面的工作量通常就是翻倍。另外一个非常反直觉却能节约大量时间的结论是很多时候检索本身就等于完成了半个变换任务。比如你只需要“含有错误码500的行”grep匹配出来的结果就已经是一个干净的中间数据集后续所有分析都基于这个结果展开。如果一开始就想着用awk去全量处理几百个字段反而会让命令变得极其臃肿且难以排错。所以我的习惯是拿到任何文本处理需求先花十秒钟问自己一句这是要“查”还是要“改”还是先查再改这个判断决定了你接下来怎么选型、怎么写命令、怎么验证结果。别小看这十秒它比任何工具技巧都能提高你的一次通过率。2. 我平时常驻工具箱里的六个命令和一个脚本以下是我日常处理文本时一定会用到的工具组合。它们每一个都有替代品功能上也有重叠但我在真实场景里反复对比后留下的这套组合已经能覆盖我遇到的绝大多数文本处理需求。工具定位典型使用场景一句话心得rg极速检索在大目录中按关键词找文件、过滤日志速度碾压grep默认尊重.gitignoresed流式替换批量替换文本、按行删改、行号定位输出主要用s替换命令其他命令很少用awk列处理按分隔符拆列、计算、分组统计文本处理里的“瑞士军刀”jqJSON变换提取JSON字段、美化输出、过滤数组处理JSON千万别用awk和sed硬刚tr / cut / sort / uniq轻量组合拳字符替换、切片、排序去重统计单个都不起眼组合起来才是杀器iconv编码转换乱码文件恢复、转换编码处理乱码第一选择比任何工具都可靠python3兜底脚本复杂循环、条件分支、非规则结构化文本命令搞不定的最后一层保障不勉强先说说rg。我为什么把检索定位给rg而不是grep因为它真的快。处理一个几百MB的日志文件rg的并发过滤能力是grep的数倍而且它默认不搜索二进制文件、自动跳过.gitignore里的目录这些都是检索日志或代码时最头疼的细节。但grep依然是我的兜底方案因为某些最小化容器或老系统里没有rg你总得保证自己离开舒适区也能干活。sed在变换里扮演的角色是“无改动文本的原地重排”特别适合那些规则明确的机械替换。我每个月都要把一堆配置文件里的测试域名批量改成生产域名一条sed指令就能解决根本不需要打开编辑器。但sed有个限制经常有人忽略它默认是逐行处理的跨行匹配得动用好几个高级技巧遇到这类需求我通常直接跳过sed换Python。awk则是处理“有结构的行”的王者。只要文本能按某个分隔符拆成列awk就能做过滤、重排、累加、求平均、格式化输出。它的语法初看有点奇怪但你只要掌握“按分隔符拆列、用条件过滤行、用BEGIN和END做初始化与汇总”这三个核心就已经能解决大部分列处理需求了。很多网上教程喜欢把awk讲成完整的编程语言我觉得这反而吓退了新手——你不需要学会它的全部你只需要把它当成一个强大的列操作器。jq是JSON时代的必需品。现在接口返回、日志结构化、配置文件到处都是JSON但你用sed去改JSON十有八九会改出格式错误。jq的用法核心是“点路径”和“管道过滤”比如.items[].name就能把数组里的name字段全部提出来。哪怕你只需要做一件极小的事比如打印某个嵌套字段也建议坚持用jq而不是拍脑袋写替代方案因为它不会破坏JSON结构这是它最大的价值。tr、cut、sort、uniq这套组合我之所以放在一起讲是因为它们单个都极其简单但串联起来能完成大量惊人的事。最经典的一幕是tr -s file | cut -d -f1 | sort | uniq -c | sort -rn这行代码完成了“把连续空格压缩、提取第一列、排序、统计每个值出现次数、按次数降序排列”这一整套统计流程。你甚至不需要理解每一步的实现原理只需要知道它们组合起来解决了什么。最后是那个Python脚本。我从来不会因为偏爱命令行就拒绝脚本凡是遇到超过三个管道、有if-else分支、有复杂的跨行匹配、有需要多次回读写文件的需求我都会果断切换到Python。命令行工具适合“线性流水线”一旦逻辑出现分叉流水线就会变得难以维护这时候写一个明确的小脚本是更好的选择。这六个命令加一个脚本的搭配核心逻辑是简单任务用简单工具复杂任务用编程语言绝不勉强任何一方做它不擅长的事。3. 一次完整实战把乱成麻的请求日志拆成可统计的结构化数据工具介绍得再多不如跑一遍实战。我用一个很常见的场景来演示分析一份Nginx访问日志找出访问量最高的几个接口并计算它们的平均响应时间。难点在于这份日志格式并不规整不同行的字段数量不一致偶尔还有引号把多个字段黏在一起。原始日志长这样我提前脱敏了192.168.1.23 - - [12/Jul/2024:10:15:23 0800] GET /api/user/info?id1024 HTTP/1.1 200 326 Mozilla/5.0 0.045 192.168.1.45 - - [12/Jul/2024:10:15:25 0800] POST /api/order/create HTTP/1.1 201 189 Mozilla/5.0 0.132 192.168.1.67 - - [12/Jul/2024:10:15:26 0800] GET /api/user/info?id2048 HTTP/1.1 200 312 Mozilla/5.0 0.038 192.168.1.23 - - [12/Jul/2024:10:15:31 0800] GET /api/product/list?page2 HTTP/1.1 200 876 Mozilla/5.0 0.067 192.168.1.99 - - [12/Jul/2024:10:15:33 0800] POST /api/order/create HTTP/1.1 500 234 Mozilla/5.0 0.408如果这是干净格式用awk一个命令就能拆。但真实日志里总有人为了调试在请求参数里加空格或者引号没有正确闭合这时候直接一刀切按空格拆列列索引就乱了。这也是我在实战中强调“先看再动”的原因。第一步先看数据长什么样。我会执行head -n 50 access.log | cat -Acat的-A参数会把行尾的$、制表符^I这些不可见字符显示出来。这一步能帮我确认三件事字段的分隔符到底是空格还是制表符、每行结尾是LF还是CRLF、有没有奇怪的编码字符混在里面。很多人在这一步省掉结果后续全程在错误的数据结构上做判断怎么改都别扭。第二步过滤掉有效响应之外的行。我们要统计的是访问接口像状态码为500的请求虽然是有效数据但在分析“接口平均响应时间”的时候如果你把异常请求也算进去会把真实体验拉偏。所以我先用awk $7 ~ /^\/api\//把请求路径以/api/开头的行筛出来。这里的$7对应拆分后第7列也就是请求行里的URL部分。这一步的本质是先做“检索定位”确认我在处理的是我需要的子集。第三步切割出核心字段。我用awk提取三个关键信息请求路径去掉查询字符串、状态码、响应时间。由于请求行里包含引号和空格我可以用awk的分隔符技巧把也当成分隔符的一部分。实际操作中我会这么写awk -F[ ] /^\/api\// {print $7, $9, $NF} access.log | head -n 5这里的-F[ ]意思是把连续的引号或空格都当作一个分隔符这样$7就是请求行里的URL$9是状态码$NF是最后一个字段也就是响应时间。这种方式比固定按空格拆更健壮能适应引号带来的列偏移。第四步把查询字符串去掉只保留接口路径。这一步用sed就够了sed -E s/^(.*)\?.*$/\1/把问号及其后面的参数全部删除。为什么要在这一步做因为我们需要按接口聚合/api/user/info?id1024和/api/user/info?id2048应该属于同一个接口如果不去掉参数统计结果会被打得七零八落。第五步统计每个接口的访问次数。这里就是前面提到的组合拳登场的时刻awk -F[ ] $7 ~ /^\/api\// {print $7} access.log | sed -E s/\?.*$// | sort | uniq -c | sort -rn | head -n 10这行命令的语义非常清晰把日志里的URL列提取出来去掉参数排序统计去重按出现次数降序排列取前十个。看起来像魔法但每一步都是单个工具在干一件简单事组合起来就成了一个完整的统计逻辑。第六步计算平均响应时间。这次要用到awk的累加能力awk -F[ ] $7 ~ /^\/api\// {url$7; sub(/\?.*$/, , url); sum[url]$NF; count[url]} END {for (u in sum) printf %s %.3f %d\n, u, sum[u]/count[u], count[u]} access.log | sort -k2 -rn | head -n 10我解释一下这段awk在干什么每当匹配到一行先取出URL存到变量url里用sub把查询参数删掉然后用两个数组分别累加响应时间和请求次数。等所有行都处理完后在END块里遍历数组输出接口名、平均响应时间、请求次数。最后再用sort按第二列数值降序排列。这套流程跑完你会得到一张非常直观的表格哪些接口访问量最大、哪些接口平均响应时间最长、访问量大的接口是否也存在性能问题。整个过程没有写任何脚本文件也没有打开编辑器全部在终端里完成。而且每一步的中间结果都可以单独查看这对排查问题非常友好——如果最终结果不对你可以按步骤回溯很快就知道是哪一步的过滤逻辑出了偏差。我还想特意强调一下验证这个环节。很多新手跑完命令看到有输出就觉得大功告成但输出结果对不对需要你拿一两个样本人工校验。我的习惯是每一步都加上| head -n 10先看一眼中间结果确认字段顺序、分隔方式、过滤条件都符合预期再继续下一步。宁可多花两分钟看中间结果也不要等最后拿到一份完全错误的数据后从头排查。4. 让命令配合起来管道、xargs和临时脚本的边界单条命令的能力再强也总有触达不到的地方。文本处理真正的高手和普通人的差距往往不在某个工具用得多熟练而在于能不能把多个工具合理地组合成一个工作流。这种组合最常见的实现方式就是管道而管道之外还有一个经常被低估的xargs以及一条界定“该用管道还是该写脚本”的分界线。管道的设计哲学很朴素每个程序只做一件事程序之间通过标准输入输出连接起来前一个的输出就是后一个的输入。这个设计在文本处理里带来了一个巨大的好处——中间结果可以是文本流而文本是最好调试的东西。你可以随时在管道中间插一个head或tee来查看中间状态这在编程语言里反而要麻烦得多。我举一个具体的例子。假设我需要找出当前目录下所有日志文件里包含“ERROR”的行同时统计每个文件里出现了多少处ERROR。正向思路可能是写一个Python脚本遍历目录、打开文件、逐行匹配、再按文件汇总。但用命令行组合整个过程是rg -c ERROR *.log | sort -t: -k2 -rnrg的-c参数会统计每个文件里匹配行数输出格式是“文件名:数量”sort再按冒号后的数值降序排列一个问题瞬间变成了两行命令。这里的关键转折就在于我把“统计每个文件错误数”拆成了“每个文件自己输出一个计数”和“把这些计数排序”两个子任务前者是rg的本职后者是sort的本职每个工具都在做擅长的事。管道之外xargs解决的是完全不同的场景。管道传递的是文本流但有些命令不接受标准输入只接受命令行参数。比如rm、mkdir、chmod这类命令你不能把文件名通过管道直接喂给它们。这时候xargs的作用就是把前一个命令的输出“掰成参数”传给下一个命令find . -name *.tmp -print0 | xargs -0 rm这里有个细节非常重要find -print0用空字符而不是换行来分隔文件名xargs -0也按空字符来拆解参数。为什么要这么配合因为文件名本身可能包含空格甚至换行如果按默认的空白字符分拆一个名字叫“my file.tmp”的文件会被误当成两个参数造成灾难性的后果。这是我在处理批量文件操作时最常提醒自己的一个点只要文件名可能包含空格就必须用-print0和-0这对组合。xargs还有个特性是批量执行你可以用-n指定每次传多少个参数这在批量并发处理时特别有用。比如要压缩1000个备份文件find . -name *.bak -print0 | xargs -0 -n 10 gzip就会每10个文件分一批执行既避免了单次参数过多导致命令行过长又在等待时间上更顺滑。但管道并不是万能的。它最大的局限在于管道里的每个命令之间没有“编程语言级别的上下文”你没法方便地写循环、条件分支、状态记录。一个典型的例子是“读取某个文件列表对每个文件做检查如果满足A条件就修改否则跳过”。这种逻辑如果硬要用管道实现要么堆出七八个子命令的超级长链要么用awk的动态模式既不直观也难以维护。我给自己定了一条实践经验如果一条管道链超过了三个环节或者任务逻辑里明显出现了“如果……否则……”的分支我就会考虑写个临时脚本。这个脚本不一定要多严谨甚至可以是一段写完之后用完就删的代码它的存在是为了让复杂逻辑变成可读、可改、可逐步调试的步骤。我通常用Python写这类临时脚本因为它的文本处理能力足够强又不需要处理编译过程跑起来就能看到结果。举一个“必须写脚本”的典型例子日志里有多行的堆栈信息你需要把一个完整的异常块从“ERROR”开始到空行结束合并成一条记录。这个任务里“跨行”是关键难点sed和awk实现起来都非常别扭但用Python只需要维护一个布尔状态变量遇到ERROR开头就打开捕获遇到空行就结束捕获并输出。这类逻辑用命令反而会被自己的偏执困住写脚本才是正确选择。所以我的最终态度是管道、xargs、脚本不是三级阶梯而是三条平行路径按任务复杂度选择即可。简单过滤走管道批量传参走xargs复杂逻辑走脚本三者各司其职。真正的高手从来不会为了炫耀“我用一行命令解决了问题”而硬写出又臭又长、别人看不懂的管道链他们选择管道的唯一标准是这样写是否让任务更清晰、更可维护。5. 踩过三次以上的文本处理坑希望你是第一次看到如果只看工具用法很多问题都能靠资料解决。但真实工作里真正拖垮效率的往往是一些看起来不起眼的细节问题。这些坑我踩过不止一次每次踩完都要花额外的时间定位希望你在看这篇之后直接把它们绕开。第一个坑是编码问题。这几乎是文本处理里最隐蔽、也最让人崩溃的坑。你从某个系统导出的文件看起来是一堆乱码或者某个日志里的中文全变成了问号这时候第一反应不应该是“用替换把乱码改掉”而是先用file命令确认文件的真实编码再用iconv做转换。我最常用的操作是file -i filename.log iconv -f GBK -t UTF-8 filename.log filename_utf8.logiconv不仅能从GBK转到UTF-8也能处理UTF-16、Latin-1等常见编码。还有一个特别容易忽略的坑是UTF-8的BOM头。BOM是藏在文件开头的三个不可见字节它在Windows下没问题但在Linux下会让文件的第一列字段莫名其妙地多出一个\ufeff字符导致awk统计第一列时全都对不上号。处理方式是用sed把BOM删掉sed -i 1s/^\xef\xbb\xbf// filename.txt第二个坑是正则的灾难性回溯。这个问题在文本量很大的时候尤其致命——看起来一条简单的正则跑起来却会吃掉整个CPU几分钟都出不了结果。原因在于正则引擎遇到一个可能匹配也可以不匹配的量词时会不断尝试所有可能性如果待匹配文本又长又复杂回溯次数就会指数级上升。我自己遇到过一次用正则在一个几万行的日志里匹配URL参数因为模式写成.*.*这种嵌套贪婪量词导致进程卡死。解决思路是尽量不用.*去匹配可以被更精确字符类替代的内容量词能用就不用*能准确限定次数就用{m,n}。如果是在做日志分析更推荐用rg或awk的字符串匹配函数因为它们简化了不少正则复杂度出问题的概率也低不少。第三个坑是shell变量在awk里的使用。很多人想用shell里定义好的变量去awk里做比较但直接写awk $1 $var时$var会被awk当成一个字段引用而不是shell变量结果输出完全不对。正确的写法是先用-v把shell变量传进去keywordERROR awk -v kw$keyword $2 kw log.txt这个坑的隐蔽之处在于它在某些shell环境或某些版本的awk里可能“碰巧能跑通”让错误没有被及时暴露等换了一个环境就突然开始产生错误结果。所以我的习惯是只要awk里要引用shell变量一律用-v传参永远不做例外。第四个坑是管道里使用while循环时循环内部修改变量却在循环结束后丢失。这是我处理统计数据时踩过最多次的暗坑。比如用cat file | while read line; do count$((count1)); done; echo $countcount在循环结束后输出来还是0。原因是管道右侧的while是在子shell里执行的子shell里的变量修改不影响父shell。解决方法也很简单——把循环放到进程替换里或者把整个循环逻辑改写成awk。现在我做这类统计基本直接用awk几乎不会再踩这个坑。第五个坑是CRLF行结束符。从Windows环境拿来的文件每行结尾除了换行符LF之外前面还多了一个回车符CR这个字符在终端里看不到但会导致awk的最后一个字段带上\r排序和比较时莫名失败。最简单的处理方式是在进入后续处理前统一用sed -i s/\r$//把行尾的CR删掉。也可以在执行命令前用file命令检查输出里如果显示“with CRLF line terminators”就该注意了。这些坑有一个共同特征它们都不在你的主逻辑里而藏在数据本身或环境差异里。处理文本时我第一次学会的教训就是拿到任何外部数据先不要急着跑分析命令先用file、head | cat -A、iconv这几件套确认数据的编码、换行符、不可见字符把数据环境搞清楚后面所有命令才有可靠性可言。这个过程看起来多花了一分钟但能省下之后至少半小时的排查时间。我平时在终端里处理文本的心得说到底也就一句话别急着展示你能写出多复杂的命令先确认你在处理的数据是什么、你要做的属于检索还是变换、每一步的中间结果是否符合预期。能一条命令解决的事不写脚本能管道串联的事不落临时文件遇到规则复杂、跨行多、有分支判断的场景就果断切到Python不跟工具较劲。这大概就是做文本处理最舒服、也最高效的状态。