
处理文本这件事只要你在命令行里待得够久早晚会和sed正面相遇。它没有grep那么出名也没有awk那么“高深”但它是我在 shell 里用得最频繁的文本处理命令之一。很多刚接触 shell 编程的朋友第一次看到sed s/foo/bar/g file.txt这样的写法会觉得这是一串“魔法咒语”其实拆开看非常简单。这篇内容我想把 sed 从原理到实操完整捋一遍。包括它处理文本的核心工作方式、日常用得最多的几个指令、一些容易踩的坑以及怎么把它和管道、脚本组合起来真正用到日志分析、配置修改这类场景里。不管你是刚开始学 shell 脚本还是已经在用 sed 但总感觉没吃透这篇文章应该都能给你一些实在的东西。1. 先搞懂 sed 的定位和价值1.1 为什么文本处理绕不开 sed先说个我自己的经历。有段时间我每天要分析 Nginx 访问日志把 404 的请求、来源 IP、访问时间都筛出来做一次汇总。最开始我用编辑器打开日志文件肉眼找再用 Excel 统计折腾到第二天上午才搞完。后来换成一条sed加一条sort | uniq -c整个流程从小时级降到了秒级。sed 的全称是 stream editor流编辑器。它不像 vim 那样打开整个文件等着你手动改而是像一条流水线一样把文本数据“流”着处理完。每一行数据从输入端进来经过你写好的操作规则再从输出端出去。这个模型决定了它特别适合做三件事批量替换把文件里所有http://改成https://行筛选与删除删掉配置文件里的注释行和空行文本抽取与变换从日志里提取某个字段或者给某些行加上前缀后缀它和grep、awk经常被拿来一起比较。简单说grep 擅长“找行”sed 擅长“改流”awk 擅长“拆列”。三者的核心边界我已经整理成了自己的速查表工具核心能力最典型的场景grep按模式筛选行找出日志里所有 ERROR 行sed按模式/行号编辑文本流把所有 ERROR 改成 WARN 并输出awk按字段/条件做结构化处理提取 ERROR 行的时间、IP、URL 三列并求和实际工作中它们经常混用。先grep筛出关键行再sed做格式变换最后awk统计字段这是非常经典的管道组合。1.2 什么时候用 sed什么时候别用这是很多初学者没想明白的。sed 擅长的是对文本做规则明确的、逐行的编辑操作比如“删掉某一段”“替换某个模式”“在第 N 行前插入内容”。但如果你的任务是“统计某个字段的总和”或者“做一个几十行的报表格式转换”那awk或者干脆用 Python 更合适。如果只是想在屏幕上看看哪些行匹配那grep就够了没必要搬出 sed。另外一个容易忽略的点是sed 处理的是“流”所以它天然适合处理超大文件。普通编辑器打开几个 GB 的日志会直接卡死但 sed 可以像水一样流过去内存占用维持在一个很低的水平。我处理过最大的文件是接近 20 GB 的日志归档用 sed 做字段重排几分钟跑完机器毫无压力。这一点是很多图形化工具做不到的。2. sed 的核心运行机制与三个必备动作2.1 模式空间sed 每次只“读一行”的工作模型要用好 sed先得理解它内部那个叫 pattern space模式空间的概念。你可以把它想象成一个只有一个坑位的“操作台”。sed 每执行一轮就做四件事从输入流里读一行放进模式空间对你的指令逐一执行把模式空间的内容输出到标准输出除非你用-n关了自动打印清空模式空间读下一行继续循环这个机制解释了 sed 的一个特点它天生是“逐行、无记忆”的。它不知道上一行是什么除非你用H、G这类命令主动把内容存到另一个叫 hold space保持空间的地方。所以我平时写 sed 指令时会尽可能把每条命令设计成“对单行完成的操作”。一旦你需要跨行拼接、合并段落sed 写起来就会很别扭这时候我一般直接换awk或者perl -0777。理解模式空间还有个实际好处你能预测 sed 的输出。比如sed /foo/p file会把匹配 foo 的行打印两遍因为匹配的行先被 pattern space 自动打印一次又被p命令打印一次。很多人初学时会困惑“为什么我明明只写了 p输出却是双份的”原因就在这里。2.2 难点精讲地址定址与三种寻址方式sed 的命令基本格式是sed 地址操作 文件名“地址”就是你要操作哪些行。“操作”就是对这些行做什么。地址定址有三种最常见的方式分别对应不同场景按行号sed 3d file代表删掉第 3 行sed 1,5p代表打印第 1 到第 5 行按正则sed /error/d file代表删除所有包含 error 的行这里的正则用/包起来按范围组合sed /start/,/end/d file代表从匹配 start 的行开始删一直删到匹配 end 的行第三种写法非常实用。比如你有一个 markdown 文件想删掉两个标记之间的整段内容一条命令就完成了sed /!-- 待删除开始 --/,/!-- 待删除结束 --/d doc.md另外还有一些比较进阶的定址方式比如1~2代表奇数行从第 1 行开始步长为 2$代表最后一行。用1~2p可以把所有奇数行抽出来看这在小规模数据预览时很顺手。很多朋友初次接触时容易混淆“正则匹配”和“行号匹配”的语法。我的建议是看到/就想到“我在找包含某种特征的行”看到数字就想到“我在定位物理位置”。两者可单独用也可以组合比如sed 10,/error/d意思是从第 10 行开始一直删到第一个出现 error 的行。这种组合在清理日志头部、截取信息时非常有用。2.3 三个高频指令 p、d、s 的用法与防坑sed 的指令有几十个但日常使用中p、d、s这三个占了九成以上。pprint打印sed -n 2,5p file.txt-n和p是固定搭档。-n关掉了“每行自动输出”的默认行为p则手动指定要打印的行。如果不加-n匹配的行会被打印两次这是初学 sed 最常见的困惑之一。说句实话我正式干活时-n加p的用法比不加-n的频率高得多因为它让输出完全可控。ddelete删除sed /^#/d config.conf sed /^$/d config.confd不是“把内容变成空白”而是“整行扔掉、完全不输出”。这两条命令一个删注释行一个删空行是清理配置文件的经典组合。注意d之后这一行不会再走后续指令所以如果把d和别的指令写在同一个 sed 表达式里顺序会影响结果。ssubstitute替换sed s/old/new/ file.txt sed s/old/new/g file.txts是 sed 的灵魂。基本的替换逻辑已经被人说烂了我重点说几个容易忽略的地方没有g标志时每行只替换第一个匹配加上g才替换行内所有匹配/是默认分隔符但如果匹配的内容里本身就带/比如路径/usr/local/bin写起来会非常痛苦。这时候可以换分隔符比如sed s#/usr/local/bin#/opt/bin#gs命令配合-i可以直接改文件但改之前一定要先做一次不带-i的试运行确认输出是你想要的再动真格这几个指令单看都不复杂难的是组合。比如你既要删空行又要替换关键字还要打印部分内容那就把多个-e表达式串起来sed -e s/foo/bar/g -e /^$/d -e s/^/前缀: / file.txt写多个-e比在一行里写一堆分号更容易读也更容易排查是哪个环节出了问题。这个习惯让我少踩了很多组合语句的坑。3. 实战用 sed 完成日志清洗、配置修改与批量替换3.1 第一个能落地的任务从访问日志里抓出 404 并统计理论讲再多不如一个能直接上手的任务。假设我有一个 Nginx 访问日志access.log每行格式大概是这样的192.168.1.10 - - [18/May/2025:09:31:02 0800] GET /api/users HTTP/1.1 404 153我现在想统计两件事到底哪些 URL 返回了 404各自出现了多少次。这个问题拆成两步解决第一步用 sed 把 404 行全部抽出来并且只保留 URL 字段sed -n / 404 /s/.*GET \([^ ]*\).*/\1/p access.log这里解释一下。-n关闭自动打印/404/只处理包含 404 的行s把整行替换成 URL\([^ ]*\)是捕获组用来抓取 GET 后面的网址最后的p输出替换结果。跑完你会得到一个纯 URL 列表。第二步接上排序和统计sed -n / 404 /s/.*GET \([^ ]*\).*/\1/p access.log | sort | uniq -c | sort -rn看到没有sed负责“提取需要的字段”sort和uniq -c负责“聚合统计”管道一层层接起来整个任务就成了一行命令。我第一次跑通这个组合的时候最大的感受是原来处理日志可以不需要写一个几十行的 Python 脚本。3.2 第二个能落地的任务批量修改 nginx 配置中的参数再举一个改配置的例子。假设我在维护几台服务器需要把nginx.conf里的worker_processes从 4 改成 8同时把监听端口从 8080 换成 80。用 sed 做全局替换sed -i s/worker_processes 4;/worker_processes 8;/ nginx.conf sed -i s/listen 8080;/listen 80;/g nginx.conf但我不建议直接原地改线上配置。比较稳妥的做法是先替换并输出到新文件确认无误后mv覆盖原文件sed s/worker_processes 4;/worker_processes 8;/ nginx.conf nginx.conf.new diff nginx.conf nginx.conf.new mv nginx.conf.new nginx.confdiff这一步能让你肉眼确认这次改动涉及了哪些行。有一次我写漏了行尾分号幸好有这层校验在mv之前发现了问题。批量改服务器配置这件事信任 sed 的能力没问题但千万别信任自己一次就能写对。3.3 第三个能落地的任务跨文件批量替换与备份跨文件操作是 sed 的另一个高频场景。比如我有一个目录conf.d/下面的所有配置文件里都把http://写成了旧域名现在要统一切到https://。用 find 和 sed 组合可以一键完成find conf.d/ -type f -name *.conf -exec sed -i.bak s#http://#https://#g {} \;这里两个细节值得强调。-i.bak表示在修改文件的同时生成一个.bak备份文件万一改错了能用备份立刻恢复。分隔符选#是因为被替换的内容里带有://如果再用/做分隔符整个表达式会变成s/http:\/\/\/https...可读性断崖式下降。改完之后可以快速抽查一下结果grep -r https:// conf.d/ | head -20我自己备份文件会至少保留一星期再清理。线上环境里一条 sed 就能改几十个文件也正是因为影响面大才更要留好退路。4. 正则与替换细节、\1 和小括号组的正确打开方式4.1 后向引用别再用 s/foo/bar/ 解决所有问题替换不是只能把固定文本 A 变成固定文本 B。sed 的正则支持捕获组也就是说你可以在匹配时把某一段内容“抓出来”然后在替换内容里引用它。这在日志清洗、格式重排时极其重要。最经典的一个例子把2025-05-18这样的日期格式改成18/05/2025echo 2025-05-18 | sed s/\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)/\3\/\2\/\1/这里\(...\)是捕获组\1、\2、\3分别引用第一个、第二个、第三个捕获的内容。所以替换后的顺序就是“日/月/年”。刚开始用的时候我总觉得这个写法很反人类为什么不用(...)而是\(...\)这是 sed 的老传统基础正则表达式BRE里小括号和加号都必须加反斜杠才有特殊含义。如果你更习惯(pattern)这种写法可以加-E开启扩展正则echo 2025-05-18 | sed -E s/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/我个人的习惯是短小的一次性命令用默认 BRE稍微复杂的表达式一律加-E。可读性高的时候正确率也会高很多。4.2 贪婪匹配与惰性思维sed 默认吃满整行正则的贪婪匹配问题是新手最容易踩的坑。*默认会尽可能多地匹配字符。比如我有这样一行日志INFO: 用户 admin 执行了 删除操作操作结果成功我想把“用户 xxx 执行了”提取出来写成了这样sed -n s/.*: 用户 \(.*\) 执行了.*/\1/p结果抓出来的不是admin而是admin 执行了 删除操作操作结果成功。原因就是.*太贪心它一直往后吞直到最后一个“执行了”才停下。正确写法应该用排除法明确“我不要什么”sed -n s/.*: 用户 \([^ ]*\) 执行了.*/\1/p[^ ]*表示“连续的非空格字符”它遇到空格就会停下来正好匹配一个完整的用户名。这就是我常说的惰性思维想让正则停在某个位置与其指望它“少匹配”不如明确告诉它“到这里就结束”。4.3 特殊字符转义清单在 sed 的正则里有些元字符需要转义有些在替换部分有特殊含义。我把最常见的几种情况列一下字符位置含义.匹配任意字符转义后匹配字面点号匹配 IP 时建议写\.*前一个字符重复 0 次或多次注意贪婪性^$行首、行尾锚点不是所有版本都支持\b词边界替换部分中代表“整个匹配到的文本”常用于给关键词加高亮标记\1到\9替换部分中代表第 N 个捕获组必须在正则里先有捕获组举个例子如果你想给每一行的第一处warning加上中括号或任意标记就很好用sed s/warning/[]/ app.log这会输出[warning]。不用的话就得写sed s/warning/[warning]/一旦关键词长了重复写的成本和不一致风险都上来了。5. 常见坑与排查技巧实录5.1 改了半天文件没变大概率是忘了 -i这个坑我在新手期踩过不下三次。sed 默认的作用对象是“流”也就是说它只把处理结果输出到屏幕不会碰原文件。如果你写sed s/foo/bar/ data.txt然后打开data.txt发现什么都没变这不是命令写错了而是你没告诉 sed 要写回文件。加-i才会原地修改sed -i s/foo/bar/ data.txt但我在 3.2 节也说过不要上来就-i。宁可先不加-i跑一遍肉眼确认输出没问题再加-i执行或者加-i.bak留个备份。这个习惯值多少钱呢我在生产环境上因为多确认这一步避免了好几次把配置全部改坏的惨案。5.2 macOS 和 Linux 的 sed 差异另一个高频坑是跨平台差异。Linux 上常用的 GNU sed 和 macOS 自带的 BSD sed 在-i参数上行为不一样。Linux 支持sed -i s/foo/bar/ file但 macOS 会报错它要求必须写-i sed -i s/foo/bar/ file在 macOS 上-i后面必须跟一个附加字符串空字符串代表“不生成备份文件”。如果你写的脚本要在多平台跑最稳妥的兼容写法反而是用-i.bak这种带后缀的方式两边都能接受。我自己的做法是写脚本时先检测平台再设一个变量if [[ $(uname) Darwin ]]; then SED_IN_PLACE-i else SED_IN_PLACE-i fi然后再执行sed $SED_IN_PLACE s/foo/bar/ file注意变量分词问题实际脚本里更推荐用数组或直接分两条分支写。5.3 处理日志时经常踩的编码与多行坑两个问题在日志处理时特别常见编码问题如果你的日志里有中文而 shell 的环境不是 UTF-8s替换可能会导致中文部分变成乱码或者匹配失效。我在处理旧系统日志时遇到过方法是处理前先file access.log确认编码再用iconv转成 UTF-8 再交给 sed。多行问题默认情况下 sed 一行行独立处理没法直接替换跨越多行的内容。比如你有一段 XML 或 JSON 被格式化成了多行用 sed 去替换其中某个属性就很不顺手。遇到这种情况我会先问自己非用 sed 不可吗如果数据量不大用awk的段落模式或者直接perl -0777 -pe s/.../.../s更合适。很多人在这一步陷入了“硬用 sed 解决一切”的误区。记住工具是服务目标的不是目标本身。5.4 排查辅助先用 -n 和 号定位问题行最后说一个排查技巧。你写了一条很长的 sed结果输出完全不符合预期第一反应不应该是反复修改正则而是先定位“到底哪一行在被我的命令影响”。用sed -n file可以输出每一行的行号配合p指令你可以精确查看某一段内容sed -n 20,30p file.txt sed -n 20,30 file.txt或者写成一条sed -n -e 20,30p -e 20,30 file.txt这种方法在排查“我以为匹配了但实际没匹配”的问题时特别有效。有一次我想删掉文档里所有包含“TODO”的行命令没生效怎么也想不通后来打印行号才发现原来文档里有全角空格混在单词前面。正则这事眼见为实。6. 把 sed 放进更大的 shell 工作流里6.1 与 find、xargs 组合做批量处理单独用 sed 处理一个文件只是基本功真正体现威力的是把它嵌进工作流。前面已经演示了find ... -exec sed -i ...的组合这里再补充一种用xargs的写法find ./logs -name *.log -print0 | xargs -0 sed -i s/ERROR/ERR/g-print0和xargs -0是为了处理文件名里带空格的情况。日志文件经常按日期命名比如2025-05-18.log文件名里没有空格但你不能假设所有文件都这么规矩。写批量命令时多写一个-print0不会多花几秒钟但能避免“文件找不到”这种莫名奇妙的报错。6.2 用管道把 sed 接到 awk、sort、uniq 后面在管道链里sed 的最佳位置通常是“第一道加工线”。它把粗糙的原始文本修剪成接近“结构化数据”的样子再交给sort、uniq、awk去做聚合分析。比如从日志里抽取耗时超过 1 秒的请求sed -n s/.*request_time\([0-9.]*\) .*/\1/p access.log | awk $1 1.0 {print $1, slow request} | head -20这里的思路是先用 sed 把不需要的信息全剥掉只留一个数字再用 awk 做条件判断。如果把所有逻辑都堆在 awk 里写也行但那样正则会特别长可读性和维护性都差。所以我的原则是复杂的正则抽取用 sed数值比较和字段聚合用 awk各干各最擅长的活。6.3 在 shell 脚本里安全使用 sed 的几个实践sed 写进 shell 脚本后有几个安全性和可维护性的细节值得注意。第一尽量用单引号包裹脚本主体避免 shell 变量插值干扰。如果你确实要把变量传进 sed 的替换内容里注意变量中可能带有/或等特殊字符。一个比较实用的技巧是使用其他分隔符或者用sed s|$var|new|g这样带引号的形式。第二如果文件内容里可能含有替换后的文本会把它当成“整个匹配内容”展开导致结果异常。比如你想把文本里的name替换成变量xxx会被解释成name输出就完全变了。解决方案是对变量里的特殊字符做转义或者用awk的gsub来避免这种解释。第三脚本里凡是写了sed -i的地方我在-i后面永远带上后缀哪怕是.bak。这样如果脚本中途出错至少还有一份原文件兜底。脚本处理的是配置、数据这种重要文件时我甚至会在 sed 运行前先cp到临时目录确保万无一失。这些习惯单看都不算什么高深技巧但组合在一起能让你的 sed 脚本从“能跑”进化到“敢在生产环境跑”。最后分享一点我的选择逻辑我自己的体会是任何一个文本任务先别急着写脚本把它拆成“筛选、变换、统计、归档”四步然后逐个决定用哪个工具。sed 在“变换”这一步的地位几乎不可替代它用最少的代码、最高的效率完成了那些在编辑器里手动操作会崩溃的批量修改工作。用熟了之后你会发现面对一堆日志、一份配置、甚至一个大型代码文件你的第一反应不再是“打开看看”而是“先跑一条 sed 看看结构”。这种工作方式上的转变才是比记住几个参数更重要的收获。