正则三剑客grep sed awk:Shell文本处理实战指南
发布时间:2026/10/1 17:20:53来源:尧图网络
1. 从一次日志排查开始为什么你必须掌握正则和三剑客我先说个真实场景。上个月某业务服务半夜报警一小时内日志刷了几十万行上下游都在甩锅。我登上服务器先用 grep 把错误码捞出来看分布再用 awk 按分钟统计报错频率最后用 sed 把脱敏字段替换掉把精简后的日志甩给值班群。前后十分钟问题定位到是某个接口的超时阈值配置错了。整个过程没用任何监控平台就靠三句话grep -E ERROR|Exception app.log | awk {print $1} | sort | uniq -c sed -i s/1[3-7][0-9]{9}/***/g app.log awk -F \t {count[$2]} END {for (k in count) print k, count[k]} app.log这就是 Shell 文本处理的价值。你不需要装 Python、不需要上 Kibana只要服务器能跑 bash正则表达式加上 grep、sed、awk 这三个命令就能解决日常工作中八成以上的文本处理需求。这篇文章我打算把这三件套从头到尾拆开讲。它会覆盖正则表达式的基础语法和常见陷阱、grep 的过滤和上下文控制、sed 的流式编辑和批量替换、awk 的字段统计和进阶数据处理。适合刚接触 Shell 脚本的入门者也适合写了两三年脚本但总觉得自己在背参数的人。读完之后你再看到用 grep 捞日志、用 sed 改配置、用 awk 做统计这类需求时不会再去搜命令而是直接上手写。需要说明的是本文所有示例基于 GNU 版本的 grep、sed、awk也就是 Linux 发行版默认自带的版本。macOS 自带的 BSD 版本个别参数略有差异我会在涉及的地方标注。2. 正则表达式基础字符、量词、分组与转义正则表达式是三剑客的共同语言。grep 查匹配行、sed 做替换、awk 做字段分割底层都依赖正则来定义什么样的文本是我们要处理的。这一章先把语法讲透后面讲命令时才不会卡壳。2.1 匹配单个字符普通字符与字符类正则里最简单的匹配就是普通字符。你写grep error它就逐行找包含 error 这五个连续字符的行。这里有个新手容易忽略的点grep 默认是包含即匹配不是整行匹配。比如grep error会把error_code、internal_error都捞出来。想要精确匹配要么用-w参数匹配整个单词要么在正则里加上边界标记\b或\、\。当我们需要匹配一类字符时就要用字符类Character Class。方括号[]里列出候选字符匹配其中任意一个grep [Ee]rror # 匹配 Error 或 error grep [0-9] app.log # 匹配任意数字 grep [a-zA-Z_] source # 匹配任意字母或下划线字符类里还有两个常用否定写法。[^0-9]表示匹配任意非数字字符这里的^放在方括号开头表示取反和后面要讲的行首锚定含义完全不同。另外像\d、\w这类简写符在 grep 的基本正则BRE里是不支持的只有 grep 的扩展正则ERE或 Perl 兼容正则PCRE才支持这点后文会专门讲。2.2 量词与贪婪匹配*、、?、{}量词决定前面的字符或字符类重复多少次。这是新手最容易出 bug 的地方特别是*的语义。量词含义示例匹配结果*前导字符重复 0 次或多次ab*cac、abc、abbc\ERE 为重复 1 次或多次abcabc、abbc不匹配ac\?ERE 为?重复 0 次或 1 次ab?cac、abc{n,m}重复 n 到 m 次a{2,4}aa、aaa、aaaa注意上表里的转义写法。grep 默认使用基本正则BRE在 BRE 中、?、|、{}这些字符如果要作为量词或逻辑符号必须在前面加反斜杠。这导致了很多经典坑比如想匹配ab这个字面字符串时在 BRE 里会被误认为是一个或多个 a 后面跟 b想表示至少一个却写a而不是a\结果啥也匹配不到。最直接的解决办法有两种要么用 grep-E切换到 ERE要么用grep -P使用 PCRE后面不再用转义写法。贪婪匹配是另一个值得单独强调的点。默认情况下*和都是贪婪的会尽可能多地匹配字符。比如文本abc123abc456模式a.*c会匹配整个abc123abc而不是只匹配开头的abc。在 PCRE 中.*?表示懒惰匹配只要满足条件就立刻停止。grep 的 BRE 和 ERE 不支持懒惰量词sed 的替换也不支持。这是三剑客和 Perl、Python 正则最大的差别之一后面组合处理文本时要注意。2.3 锚定与分组^、$、()、|^匹配行首$匹配行尾它们不消费字符只确认位置。grep ^ERROR app.log只找行首为 ERROR 的日志行grep timeout$只找行尾是 timeout 的行。这两个锚定在日志关键字匹配里用途极大因为日志前缀相同但内容不同的情况太多了行首过滤能砍掉大量噪声。分组用圆括号作用有两个一是把多个字符打包成一个整体配合量词使用比如(ab){2}匹配abab二是捕获匹配内容供后面引用。捕获后怎么引用分两种情况在正则内部用\1、\2反向引用。比如grep -E ([a-z]) \1能找出单词 空格 相同单词的行。在 sed 替换命令中用\1引用分组捕获的内容来拼装新的文本这是 sed 做字符串提取和重排的根基后面细讲。|是或的意思表示匹配左右任意一个分支。在 BRE 里要写\|在 ERE 里直接写|。比如grep -E error|warning同时匹配两种级别的日志。|的优先级比较诡异它几乎是最低的低于拼接。所以error|warning实际是error 或 warning而不是error 或 warning的什么组合。想表达匹配 error 或者 warning 开头的行必须写成^(error|warning)否则^error|warning表示error 开头的行或任意位置有 warning 的行。2.4 字符转义与特殊符号的坑\b、\.、\/正则里的特殊字符包括.、*、、?、[]、()、{}、|、^、$、\。如果我们要匹配这些字符的字面含义就得加反斜杠转义。最常见的一个例子是匹配 IP 地址或带点的域名grep -E 192\.168\.1\.[0-9] app.log192.168.1.1中如果写成192.168.1.1小数点会被当成匹配任意字符那192x168x1x1也会匹配这显然不对。所以点号必须写成\.。类似地路径里常见的/在正则里没有特殊含义一般不用转义但 sed 的s///命令把/当作分隔符时你要替换的内容里含/就要写成\/或者换用其他分隔符比如s#old/path#new/path#后面讲 sed 时会演示。另一个高频坑是转义层级。在 Shell 双引号内写正则时\b里的反斜杠在双引号中不会被 Shell 特殊处理能原样传给 grep但在单引号内也一样。真正出问题的是在双引号里写$var变量里的反斜杠可能被 Shell 或正则两层吃掉。稳妥的做法是正则表达式一律用单引号包裹除非你需要 Shell 展开变量。到这里正则的基础语法已经覆盖到位。接下来进入三剑客各自的实战我会把命令参数、常见用法、工作场景揉到一起讲。3. grep 实战从定位关键字到上下文分析grep 是三剑客里曝光率最高的一个但大多数人对它的理解停留在搜索关键字。实际上 grep 的参数设计非常精巧把它用透了能解决很多写循环才能干的脏活。3.1 基础参数-E、-P、-i、-v、-w的取舍拿最基本的参数梳理。-E让 grep 进入扩展正则模式解决 BRE 的转义烦恼-P使用 PCRE额外支持\d、\w、\s、懒惰量词、零宽断言。绝大多数 Linux 发行版的 grep 都编译了 PCRE 支持直接用就是。我在生产环境里默认写grep -E因为 ERE 已经覆盖九成需求而且可读性比 PCRE 的\d要差一些的是[0-9]但我个人不排斥-P它写起来确实短。只是要注意-P在跨平台环境下并不一定可用脚本要分发到别的机器时慎用。-i忽略大小写-v反选输出不匹配的行-w整词匹配。这四个参数组合起来能玩出很多花活。比如你想看日志里除了 INFO 之外的所有级别grep -viE info|debug app.log再比如统计日志中非空非注释行grep -vE ^\s*$|^# config.conf3.2 上下文控制-A、-B、-C让报错信息完整浮出水面日志排查时只捞到一行 ERROR 往往不够你还要看它前后的相关日志。-A n显示匹配行之后 n 行-B n显示之前 n 行-C n前后各 n 行。举例grep -E OutOfMemoryError -C 5 app.log这条命令会输出包含 OOM 的行及其前后各 5 行。-C 5是每次排查时我必带的参数——只捞关键字容易断章取义看到上下文才能还原现场。有个小技巧如果错误码是贯穿性的但你想看清楚每次报错的前因后果可以先grep -n看行号再用sed -n切特定区间这个组合后面会讲。3.3 多文件与递归-r、-l、-c、-h从单文件到批量扫描当你在一个项目目录里找关键字时grep -r TODO src/会递归遍历所有子目录。加上-l只输出文件名不输出匹配内容加上-c只输出每个文件的行数统计加上-n输出行号。这俩在批量检索里特别有用。比如你接手一个别人的项目想搞清楚哪些文件里调用了old_apigrep -rn old_api --include*.py . | head -50--include限定文件类型能跳过二进制和无关目录。同样可以--exclude-dirnode_modules之类的排除项。这里顺便提一句-l和-c的典型配合先grep -rl拿到文件清单再xargs或for循环对每个文件做后续处理——这是后面 awk 统计的常见前置步骤。3.4 输出格式细节-o只打印匹配的部分-o是一个容易被忽视但极其有用的参数。它告诉 grep 不要输出整行而是只输出匹配到的子串。配合正则的捕获分组能实现类似数据抽取的效果。比如从日志中把所有手机号拉出来去重grep -oE 1[3-9][0-9]{9} app.log | sort -u从 nginx 日志中提取所有访问 IP 并统计次数grep -oE ^[0-9]\.[0-9]\.[0-9]\.[0-9] access.log | sort | uniq -c | sort -rn | head有一类操作特别考验基本功先grep -o抽字段再sort | uniq -c计数再sort -rn按次数降序排。这套管道是日志分析的万能骨架能把哪些 IP 打得最多哪个接口调得最频繁哪种错误出现最多这类问题三行解决。我在给新人演示 Shell 处理能力时一般就演示这个组合效果很直观。4. sed 实战流式编辑的替换、插入与区间处理sed 的全称是 stream editor它不修改原文件除非加-i而是把文本按行读入模式空间pattern space处理后输出。理解这个按行流式处理的模型是掌握 sed 的关键。它不像 vim 那样需要交互适合在管道里对数据做变换。4.1 核心命令s替换与捕获引用sed 最常用的就是替换命令s/旧/新/。默认只替换每行第一个匹配行尾加g表示全局替换sed s/foo/bar/ file # 每行第一个 foo 换成 bar sed s/foo/bar/g file # 每行所有 foo 都换 sed s/foo/bar/2 file # 只替换每行第二个匹配替换的新部分可以用代表整个匹配的字符串也可以用\1代表第一个分组。比如把keyvalue改成value is keyecho namezhangsan | sed s/\([a-z]*\)\([a-z]*\)/\2 is \1/输出结果就是zhangsan is name。这里有两个容易踩的坑。第一个sed 默认使用 BRE分组必须写成\(写(反而会被当普通字符处理。想要写成 ERE 风格需要sed -E。第二个替换文本里如果有/字符会直接和定界符冲突解决方案是换定界符比如用#或,sed s#/usr/local#/opt# paths.txt sed s,/usr/local,/opt, paths.txt4.2 区间寻址只处理指定范围的行sed 的一大优势是能精确定位哪些行需要处理而不是一股脑全改。寻址方式有四种行号sed -n 5p打印第 5 行sed 5d删除第 5 行。行号范围sed -n 10,20p打印第 10 到 20 行。正则范围sed -n /^ERROR/,/^END/p从 ERROR 开始一直打印到 END 为止的所有行。混合sed -n 10,/^END/p从第 10 行到第一次出现 END 的行。这些操作在日志切割里很常用。比如某次报错的完整堆栈从Exception开始到第一个空行结束你想把每段堆栈单独拎出来就可以用/Exception/,/^$/作为寻址区间。实际工作中我最常用的是-n加p做按正则抽片段。举例你想看 app.log 里从今天凌晨 0 点开始到第一次出现 END 标记之间的所有行sed -n /^2026-01-01 00:00:00/,/^END/p app.log这个用法比 grep 的上下文模式更精确因为它是到某个结束标记为止而不是固定行数。4.3 常用操作命令d、a、i、c与原地修改-i的注意事项sed 不止能替换还能删除、追加、插入、修改。命令对照如下命令作用示例说明d删除行sed /^#/d file删除所有注释行a行后追加sed /^version/a\new_line file在匹配行后追加一行i行前插入sed /^version/i\before_line file在匹配行前插入一行c整行替换sed /^version/c\version2.0 file把匹配行替换成新内容-i原地修改是 sed 最危险也最有用的参数。说它危险是因为很多新手直接sed -i s/foo/bar/ config.conf结果配置文件被改坏且没有备份。我的习惯是任何-i操作都显式加备份后缀sed -i.bak s/foo/bar/ config.conf这样改错了还能用备份找回来改对了再清理备份文件。这个习惯在线上服务器操作时尤其重要不要嫌备份文件碍事。4.4 处理反斜杠与特殊字符一个配置文件修改的实际案例给你一个综合案例。假设 PostgreSQL 的配置文件里有这么一行listen_addresses localhost你想把监听地址改成内网 IP192.168.1.10。如果直接写sed s/listen_addresses localhost/listen_addresses 192.168.1.10/并把 double quote 换成单引号正则里的*不会出问题但内含的会让命令很别扭。更稳的写法是用变量和不同定界符new_ip192.168.1.10 sed -i.bak s/listen_addresses localhost/listen_addresses $new_ip/ postgresql.conf注意这里我用了双引号因为要展开$new_ip变量。如果 IP 值里恰好有正则特殊字符比如点号应该先转义再传入escaped_ip$(echo $new_ip | sed s/\./\\./g)这个把用户输入转成正则安全串的技巧在写通用脚本时非常有用。它背后的逻辑是凡是外部传入的变量进入 sed 替换文本默认都要做转义处理否则一个.就能让匹配范围从精确 IP变成任意字符改错文件就晚了。5. awk 实战字段统计与数据重塑如果说 grep 是过滤、sed 是变换那 awk 就是计算和聚合。awk 天生为按列处理而生把每一行按分隔符切分成字段然后可以用类 C 的语法做条件判断、循环、数组统计。它的能力远超很多人的预期但前提是理解它的模式-动作结构。5.1 工作原理与内置变量FS、OFS、NR、NF、$0、$1awk 的基本结构是条件 { 动作 }它对输入逐行执行当某行满足条件时就执行对应动作。省略条件表示对所有行执行动作。这里的条件可以是正则、表达式、区间甚至直接是1恒真。几个内置变量必须刻在脑子里FS字段分隔符默认是任意空白符空格或 tab。可以用-F ,改为逗号分隔。OFS输出字段分隔符默认空格影响print $1, $2时字段之间的分隔。NR当前读取的总行号。NF当前行的字段数量。$0整行内容。$1、$2等第 1 个字段、第 2 个字段。最简单的案例打印第 2 字段和第 4 字段。awk -F , {print $2, $4} data.csv如果我想让输出用 tab 分隔就设置 OFSawk -F , BEGIN {OFS\t} {print $2, $4} data.csvBEGIN是一个特殊的模式它会在读取任何输入之前执行一次适合做一些初始化操作比如设置分隔符、打印表头、初始化统计变量。5.2 模式匹配与条件过滤正则与比较运算组合查询awk 和 grep 一样可以在动作之前加正则条件。区别是 awk 能根据某个字段的内容做更精细的控制。比如你要过滤出第 3 列大于 100 的所有行awk -F , $3 100 {print} data.csv再比如第 2 列以ERR开头的行打印第 1 列和第 2 列awk -F , $2 ~ /^ERR/ {print $1, $2} app.log这里的~表示匹配正则!~表示不匹配。逻辑组合用和||awk -F , $1 ~ /^192\.168\./ $4 5000 {print} data.csv这条命令把来源 IP 是内网段且响应时间超过 5000ms的记录全部筛出来在接口性能排查里非常实用。与 grep 相比awk 的优势在于grep 是对整行做正则匹配awk 是对某几列做判断。数据有结构时awk 的表达力强太多。5.3 数组与统计用 awk 实现group by效果awk 的数组关联数组是实现分组统计的杀手锏。想在日志里按访问接口统计请求次数并输出 Top 10awk {count[$7]} END {for (url in count) print count[url], url} access.log | sort -rn | head -10解读一下每读一行以第 7 列为 key对关联数组count的对应项加一。END 模式在所有行读完后执行遍历数组并输出。得到的结果再接sort -rn排序完成一次标准的 Top 统计。这比写 Python 脚本或 SQL 查询都快因为不需要额外工具服务器上原生就有。更进阶一点做分组聚合。假设日志里有时间、接口、耗时三列我想按接口统计平均耗时awk {total[$2] $3; num[$2]} END {for (k in total) print k, total[k]/num[k]} perf.log这就是准 SQL的能力。关键是理解 awk 的流式处理模型——它不必把整个文件放进内存每一行处理完即丢弃只保留分组状态所以哪怕处理几十 GB 的文件也不会内存爆炸。这个特性是 Python 逐行解析很难天然具备的。5.4 内置函数与格式化输出printf、length、sub、gsubawk 内置了一批函数让你不用求助外部命令就能完成很多字符串处理。最常用的是printf格式化输出。它和 C 语言的 printf 用法一致awk {printf %-20s %8.2f\n, $1, $3} data.txt%-20s表示左对齐占 20 字符宽度%8.2f表示浮点数占 8 位且保留两位小数。这在生成对齐工整的报表时非常有用打印出来的表格比默认的空格分隔好看得多。字符串替换有sub和gsub。sub(/old/, new)只替换每行第一个gsub(/old/, new)全局替换返回值是替换次数。一个经典场景把 CSV 文件中的空值替换为NULLawk -F , {gsub(/^$/, NULL, $0); print} data.csvlength函数返回字符串长度或数组元素个数配合条件可以做过滤掉长度异常的字段之类的校验awk -F , length($2) 100 {print 字段过长:, NR, $2} data.csv5.5 多文件处理与 FNR循环遍历多个文件时的坑当 awk 同时处理多个文件时NR是累计行号FNR是当前文件行号。这个差异在按文件分别处理时很容易踩坑。举例你想在每个文件的第 10 行插入一个文件头标记awk FNR 10 {print file } {print} *.txt如果用NR 10那就只会在所有文件的第 10 个累计行触发一次结果完全不对。另一个多文件场景是根据第一个文件的内容筛选第二个文件的行这需要把第一个文件的 key 读进数组第二个文件里再判断。这个模式其实不复杂awk NR FNR {keys[$1] 1; next} {if ($1 in keys) print} key_file.txt data_file.txtNR FNR的判断逻辑是当 awk 读取第一个文件时行号等于累计行号进入第二个文件后FNR 重新计数二者不再相等。这个技巧在关联两个表格时非常高效很多需要join的活用这个就解决了。6. 三剑客的协作从管道组合到脚本化三剑客单独用已经很强了更高级的用法是通过管道把它们串起来让每个命令做自己最擅长的一步。数据流的处理模式是grep 负责筛选、sed 负责变换、awk 负责统计最后统一输出。6.1 管道串联的经典模式grep 过滤后交给 awk 统计最典型的协作就是grep 过滤awk 聚合。比如从 nginx 日志里统计 500 错误码对应的请求 URL 分布grep -E 500 access.log | awk {print $7} | sort | uniq -c | sort -rn第一步用 grep 把包含 500的行筛出来引号是为了防止误匹配到 5000 这类状态码第二步 awk 提取请求路径第 7 列再交给sort | uniq -c计数。这条链路的每一步都极简但组合起来能回答哪些接口最近在大量报 500这个业务问题。再如从应用日志里统计最近一小时内每个服务实例的报错次数grep -E ^2026-01-01 10: app.log | grep -E ERROR|WARN | awk {print $4} | sort | uniq -c这里$4是日志格式里的实例 ID 字段具体第几列取决于你的日志模板。管道化思路的关键不要试图用一个命令解决所有问题而是把问题拆分成筛选、提取、聚合三个环节每个环节各司其职。6.2 用 sed 做数据清洗后交给工具处理另一种协作方式是sed 清洗后续消费。比如从 csv 文件中剔除引号内的逗号干扰假设引号内的逗号不该作为分隔符或者把字段之间的多个空格压成单空格让 awk 按空格切分更干净sed s/ */ /g messy.txt | awk {print $2, $5}s/ */ /g把所有连续空格压成单个空格。这个操作在处理ps aux、df -h这类输出时很常用——它们默认用多个空格对齐列直接用 awk 的默认 FS 其实也能处理默认空白符包括多个空格但如果你先清了空格后续排序或进一步处理会更稳。再举一个综合场景日志里有敏感的手机号需要脱敏后把统计结果发出去。做法是先用 sed 替换手机号中间四位为星号再 awk 统计地域分布sed s/1[3-9][0-9]{9}身份证号正则模式/1[3-9][0-9]{4}****[0-9]{4}/ raw.log | awk {print $2} | sort | uniq -c注意上面示例里的替换正则手机号是 11 位分成前 3 位、中间 4 位替换为*、后 4 位。正则写法可以直接写1[3-9][0-9]{9}但替换符号里的{4}可能被 sed 解释成量词所以中间那个位置要么直接写四个*字符要么用[0-9]{4}匹配后再替换。稳妥写法是sed -E s/(1[3-9][0-9]{4})[0-9]{4}([0-9]{4})/\1****\2/ raw.log用-E扩展正则用分组捕获前 7 位和后 4 位中间四位置换成星号。这就是 sed 做数据清洗的标准姿势。6.3 结合循环对多个文件或参数批量处理当你要处理多个文件、且每个文件的处理逻辑一致时Shell 的 for 循环和三剑客是绝配。比如把所有.log.20260101到.log.20260107的文件里的 ERROR 提出来汇总成一个文件for f in app.log.2026010{1..7}; do grep -H ERROR $f done errors_all.log或者每个文件单独统计后追加到一个汇总表for f in *.log; do count$(grep -c ERROR $f) echo $f $count done | sort -k2 -rn | head -5这种循环批量处理的模式适合多文件、固定逻辑、结果汇总的需求。6.4 性能对比什么时候该用 grep什么时候该用 awk很多新手纠结同一个需求到底该用 grep 还是 sed 还是 awk。我直接给一个选型判断表需求类型首选工具原因只筛选行不需要改内容grep专门干这个性能最好按行做替换、删除、插入sed流编辑的强项按字段统计、计算、聚合awk有字段概念和内置函数需要跨文件关联数据awk用数组模拟 join只想要匹配到的子串grep -o比 sed 提取简单得多对不规则的文本做多步变换组合一步命令解决不了就管道从性能角度讲grep 通常是最快的过滤工具因为它的实现高度优化能快速跳过不匹配的行。sed 第二awk 最慢但功能最全。所以一个经验法则是能用 grep 先过滤掉大部分行就不要把全部数据塞给 awk。比如awk /ERROR/ {print $3} app.log这个完全可以写成grep ERROR app.log | awk {print $3}在某些极端情况下后者的整体耗时可能不到前者的三分之一。数据量小时无所谓但处理 GB 级日志时这种优化立竿见影。7. 实战案例用一行命令组合完成访问日志分析理论讲得再多不如把整个分析流程走一遍。这里我用一个接近真实的访问日志分析案例把前面所有知识点串起来。假设 nginx 的 access.log 格式是典型的组合日志192.168.1.10 - - [01/Jan/2026:10:15:22 0800] GET /api/user/info HTTP/1.1 200 532 https://example.com/page Mozilla/5.0 192.168.1.11 - - [01/Jan/2026:10:17:45 0800] POST /api/login HTTP/1.1 500 124 - curl/7.68.0列位依次是IP、-、-、时间、请求行方法路径协议、状态码、响应字节数、Referer、User-Agent。我要分析几个问题问题一每个 IP 的请求次数 Top 5。awk {count[$1]} END {for (ip in count) print count[ip], ip} access.log | sort -rn | head -5问题二请求失败状态码 400的接口 Top 10。awk $9 400 {print $7} access.log | sort | uniq -c | sort -rn | head -10这条命令里$9是状态码$7是请求路径。awk 的条件过滤一步到位不需要 grep 再包一层。问题三统计每小时按日志时间前两小时不这里用小时间隔的请求量找到波峰时段。先提取小时字段。日志时间形如[01/Jan/2026:10:15:22 0800]小技巧是用gsub或者直接按:分割。简便做法awk {split($4, t, :); hourt[1] : t[2]; count[hour]} END {for (h in count) print h, count[h]} access.log | sortsplit函数把第 4 列的[01/Jan/2026:10:15:22按:分割取第一部分里的日期和第 2 段的小时拼出10:15这种粒度。如果你想按整点统计就只要t[2]awk {split($4, t, :); count[t[2]]} END {for (h in count) print h, count[h]} access.log | sort -n | tail -5问题四找出响应时间读起来像过慢接口的高延迟样本。如果日志里有响应时间字段假设第 10 列是耗时 ms直接awk $10 3000 {print $1, $7, $10} access.log | sort -k3 -rn | head -20问题五过滤掉健康检查请求再做上述分析。健康检查通常会请求/healthz路径awk $7 ! /healthz {count[$1]} END {for (ip in count) print count[ip], ip} access.log | sort -rn | head -5这个案例覆盖了字段提取、条件过滤、分组统计、排序截断、排除特定值。做完这几步访问日志的核心分析基本就不再需要其它工具了。8. 踩坑清单转义、编码、平台差异与性能陷阱最后一部分我把这些年实际工作中踩过的和三剑客相关的坑集中列一遍。这些坑有一个共性不在语法书里但在线上环境里一定会遇到。8.1 转义层级Shell 一层、正则一层最常见的坑就是转义被 Shell 吃掉。你在双引号里写\dShell 可能会把\d原样传给 grep也可能在某些 shell 里处理掉\d。更麻烦的是$符号——在双引号里$会被当成变量引用的开头所以正则里的行尾锚定$必须写成\$或者用单引号# 错误示范 grep error$ file # 如果 file 里有一行结尾是 error双引号里 $ 会被 Shell 误判大多数情况直接报错或匹配异常 # 正确示范 grep error$ file写 Shell 脚本时一条铁律是正则表达式除非要展开变量否则一律用单引号。如果必须用双引号展开变量那变量中可能存在的正则特殊字符要单独做转义处理第八章里 sed 改配置的例子就是这么处理的。8.2 中文和多字节字符的处理locale 与编码如果你处理的文本包含中文、日文等多字节字符grep、sed、awk 的行为受 locale 影响很大。默认情况下如果LANGzh_CN.UTF-8正则会按字符而不是字节去匹配这通常是我们想要的。但如果 locale 设置不对比如LC_ALLC正则里的.、[^x]这类元字符会按字节匹配一个 UTF-8 中文占 3 个字节.就只能匹配其中一个字节结果就是匹配结果碎裂输出乱码。解决方法是在脚本开头明确设置 localeexport LC_ALLC但注意LC_ALLC下正则.是按字节工作的如果你明确要做字节级处理这是优点但需要按字符匹配时要设成 UTF-8 的 locale。没有一个设置永远正确取决于你的数据处理需求。如果文本是 UTF-8 中文又想按字符匹配就用export LC_ALLen_US.UTF-8[0-9]在 UTF-8 locale 下还会匹配全角数字某些语言环境这可能是坑也可能不是。多数情况下我只关心 ASCII 数字这时写[0-9]还是[[:digit:]]都行但后者语义更明确。8.3 大文件下的性能流式处理 vs 全文加载grep、sed、awk 都是流式处理工具理论上占用内存很小。但如果误用某些参数也会把内存吃爆。最容易出问题的场景是grep -o配合超大单行文件。比如一个 5GB 的 JSON 文件里所有内容都在一行grep -oE 某模式会把这个长行切出来的所有匹配存进内存再输出匹配数量巨大时内存可能直接打满。另一个坑是 sed 的-i在超大文件上的行为GNU sed 是原地处理会生成临时文件再替换所以磁盘需要至少两倍文件大小的空间。如果磁盘快满了sed -i会中途失败。这时可以先df -h检查空间不够就改成分批处理head -1000000 bigfile part1 sed -E s/foo/bar/ part1 part1_new cat part1_new part2 newfileawk 的数组统计遇到海量唯一 key 也会吃内存。比如统计每个 IP 的请求数如果 IP 是伪造的、变化无穷数组无限增长内存压力巨大。这时可以先用sort | uniq -c这类外部排序工具把唯一的 key 先在磁盘上归并再给 awk 喂聚合后的数据能大幅降低内存占用。8.4 GNU 与 BSD 差异macOS 上的兼容性坑Linux 和 macOS 都能跑 bash但 grep、sed、awk 的实现不同。最典型的差异在 sed 的-i参数GNU sed 支持-i可加后缀BSD sed 要求-i后面必须跟后缀直接写sed -i s/a/b/会报错因为 BSD 把s/a/b/当成了后缀。所以跨平台的 sed 原地替换要么写成sed -i.bak要么就用-e配合临时文件。awk 在 macOS 上默认是 BWK awk也叫 nawk和 Linux 的 gawk 有一些功能差异gawk 支持-v传入变量、asort、gensub等nawk 不支持。写脚本时尽量只用 POSIX awk 共有的语法否则就把目标环境钉死在 Linux。8.5 空行、行尾符和 BOM 头肉眼看不出来的敌人文本处理里最隐蔽的问题是看不见的字符。Windows 制作的文件通常带 CRLF 行尾Linux 下读到的每一行末尾都挂着一个\r。如果你用awk -F , {print $NF}提取末尾字段字段里藏了\r比较和输出都会出问题。解决办法是先洗数据sed -i s/\r$// file.csv或者直接在 awk 里用sub(/\r$/, , $0)。另一个问题是 UTF-8 的 BOM 头文件开头藏了三个不可见字节导致第一个字段前面多了乱码。用 sed 去掉sed -i 1s/^\xEF\xBB\xBF// file.csv这类问题排查时很难一眼看出来你会觉得明明字段内容看起来一样为什么不等于。但只要在脚本入口统一定时清理行尾符就能避免大量莫名其妙的 bug。9. 写在最后把三剑客变成肌肉记忆回到文章开头的那次线上排查。事后我把那台服务器上用到的命令整理成了一个小脚本收在团队的运维工具目录里。后来遇到类似问题同事们第一时间就会想到跑一下那个脚本而不是到处找监控面板。这就是 Shell 文本处理能力的价值——它不是某个场景的一次性解法而是能内化成一套思维方式的工具集。我个人的体会是掌握三剑客的捷径不是记参数而是反复做清洗数据、提取字段、统计聚合这三类基础练习。日志分析、配置批量修改、报表生成本质上都是这三件事的排列组合。等你把grep -E、sed -E s///、awk {print}这几个最常见动作练到不看手册就能写出来再去翻文档学冷门参数会发现那些参数理解起来快很多。最后再分享一个小技巧避免在脚本里写超长的一行管道命令。每一条管道拆成多行用反斜杠续行中间加注释。线上脚本是要给别人维护的命令的可读性往往比执行效率更重要。比如grep -E ERROR|WARN app.log \ | sed -E s/^([0-9-] [0-9:]).*/\1/ \ | sort | uniq -c \ | sort -rn | head -20这样写三个月后的你和你的同事都能一眼看明白这段命令在干什么。
网站建设高端定制企业官网