☰
深入理解sed流编辑器:核心原理与实战应用
2026/10/15 5:05:41 网站建设 项目流程

1. 从一次痛苦的日志处理说起

大概两年前,我接手了一个维护老系统的活儿。那系统跑在一台只有基本环境的生产服务器上,没装什么花哨的工具。每周要处理一批业务日志,格式大致是“时间戳|级别|模块|描述|附加参数”,数据量不大不小,几万行。需求也简单:把上周所有级别为 ERROR 且模块为“支付接口”的行挑出来,把描述里的敏感信息抹掉,然后按时间排序输出成一个报表文件。就这么个活,我一开始用 vim 打开硬找,几百行还行,几万行眼睛直接看花。后来写 Python 脚本,本机调试没问题,一上生产服务器,python3 都没有,装环境还得走审批流程,一等就是两三天。

那会儿我还没把 sed 当回事,觉得它就是“替换单词用的”。直到我手里只有一套最小化安装的 Linux 系统,系统里最全的工具就是 grep、sed、awk 这老三样时,我才真正开始研究 sed 到底能干嘛。结果就是把那套每周报表的活,从“手工 + 脚本 + 审批 + 半天时间”压缩成了一条管线的命令组合,跑一次几十毫秒。从那以后,我算是彻底服了这三个字:Linux 三剑客之 sed。

这篇东西,我准备从 sed 的核心运行模型讲起,然后过一遍最常用的命令和地址定址技巧,再把多行模式和保持空间这种进阶玩法用大白话拆开,最后分享几个我在真实生产环境里落地过的案例。不管你是刚接触 Linux 的小白,还是已经写了几年脚本但一直没细看 sed 的开发者,这篇内容都值得你花十分钟仔细看完。

2. 为什么 sed 被叫做“流编辑器”

2.1 一次性读一行,和“内存大户”说再见

很多人第一次接触 sed,会下意识拿它和 vim 比。vim 是交互式编辑器,你得打开文件、移动光标、输入命令,最后保存退出。sed 完全不是这个路子,它更像一条流水线上的工人:文件像一条传送带,sed 站在传送带旁边,每次只接住一行,处理完这一行,直接输出到下一道工序(屏幕、文件、或者另一个命令),然后立刻处理下一行。全程不会把整个文件一次性加载到内存里。

这个设计在当年内存按 KB 计算的时代是天才之举,放到现在依然实用。我处理过最大的一个文本文件是几十个 GB 的数据库导出文件,vim 直接打不开,Python 读取也吃力,但用 sed 配合流式处理,机器几乎不喘气。这就是“流”这个字的精髓:不用等全部读完,来一行处理一行,来多少处理多少。

2.2 模式空间是什么,为什么它是 sed 的心脏

理解 sed 最关键的一个概念叫“模式空间”(pattern space)。你可以把它理解成 sed 的“工作台”。sed 从输入流中读入一行,把它放到模式空间里,然后执行你给它的命令脚本,命令处理的对象就是工作台上的这行文字。处理完毕,模式空间里的内容(可能被修改过)会被自动打印到输出流,然后清空,接下一行。

打个比方:模式空间就像一个厨房操作台,输入文件是仓库里的食材,sed 的命令是厨师的操作步骤。每送来一份食材,厨师在操作台上切、炒、装盘,做完一份,台面清理干净,再等下一份。这个模型简单,但威力巨大,因为所有 sed 命令都围绕模式空间展开。你看懂了模式空间,就看懂了一半的 sed。后面要讲的保持空间(hold space),可以理解为操作台旁边的一个临时储物柜,用来暂存某些行的内容,以备后续使用。

3. sed 的语法骨架与命令拼装规则

3.1 命令行的三种调用姿势

sed 的调用方式有讲究。最常用的是在命令行直接写脚本,格式如下:

sed [选项] 'sed脚本' 输入文件

第二种适用于脚本较长、可读性要求高的场景,用-f指定脚本文件:

sed -f 我的脚本.sed 输入文件

第三种是管道模式,这也是三剑客最常见的使用方式,不直接指定文件,而是接收前一个命令的输出:

cat 日志.log | sed 's/旧内容/新内容/' | grep "ERROR"

这里有个新手最容易犯的错误:sed 默认会把处理后的结果输出到标准输出,但不会直接修改原文件。如果你想让修改直接写回原文件,需要加-i选项。注意,-i是“直接编辑”的意思,在部分老版本系统上写作-i.bak,会先生成一个带后缀的备份文件再修改原文件,这一步很多踩坑文章没提全。

3.2 看懂“地址”与“命令”的组合规则

sed 的命令格式遵循一个固定模式:先定位,再操作。定位用的就是“地址”,操作用的就是“命令”。地址可以是行号、正则表达式、行号范围、正则范围等。命令是s(替换)、d(删除)、p(打印)、a(追加)等。定位不到的,命令就不执行。这个逻辑和编程里的“if 条件,then 动作”如出一辙。

我举个例子,假设有一个文本文件demo.txt,内容如下:

第1行:hello world 第2行:this is a test 第3行:goodbye world 第4行:this is sed

我只想打印第2行到第3行之间的所有行:

sed -n '2,3p' demo.txt

-n是关闭默认输出(后面细说),2,3是地址范围,p是打印命令。输出就是第2行和第3行。

如果我想用正则匹配“包含 hello 或 goodbye 的行”,可以这样写:

sed -n '/hello\|goodbye/p' demo.txt

这里\是转义竖线,表示“或”关系。不过不同版本的 sed 对“或”的支持不完全一致,有的支持\|,有的支持|,这个兼容性坑我后面在常见问题里专门讲。

3.3 为什么必须理解“默认输出”这个概念

默认输出是新手理解 sed 行为的第一道坎。sed 的完整执行流程是:读入一行到模式空间 → 执行命令脚本 → 把模式空间的内容打印到屏幕(如果没有-n的话)→ 读入下一行。这意味着,即使你什么都没做,sed 文件名也会把文件内容原样输出一遍。

看几个例子体会一下:

# 这条命令会打印文件所有行(默认输出) sed '' demo.txt # 这条命令会把所有行打印一遍,同时把匹配“hello”的行打印第二遍(因为p命令又打印了一次) sed '/hello/p' demo.txt

所以我经常告诉初学者:如果你只想看到被命令处理过的行,必须加-n,然后显式地用p打印;如果你不加-n,看到的结果往往是“混叠”的,匹配行出现两遍,不匹配行出现一遍,看着乱,逻辑也没毛病。理解这个机制以后,你一眼就能看懂为什么网上那些 sed 示例到处都有-n。

4. 常用命令逐个拆解

4.1 替换命令 s:最核心、最常用的那个

sed 's/原始内容/替换内容/标志',这是 sed 使用频率最高的命令。你只需要把“原始内容”写成正则表达式,“替换内容”写成目标字符串,然后指定处理标志即可。常见的标志有三个:

  • g:全局替换。不加g时,每行只替换第一个匹配项,这是一个高频踩坑点。比如把每行第一个“a”换成“A”,不加g就只换每行第一个,不是你想要的“所有 a 都换”。
  • p:替换后打印该行,通常配合-n用来只查看发生替换的行。
  • i:忽略大小写(注意不是“insert”插入的意思,易混)。

实战一个:把日志文件里的“ERROR”全部替换为“错误”,并且同时打印原始数据和替换后结果的前几行看看效果:

sed -n 's/ERROR/错误/gp' app.log | head -5

还有一个非常实用的技巧:替换内容里的&符号代表“整个匹配到的内容”。比如你写了这样的命令:

sed 's/[0-9]\{4\}/[&]/g' demo.txt

它的含义是,把每行中所有连续4位数字的数字串用方括号包起来。这里&引用的就是被正则匹配到的那个具体值,比如“2024”会被替换成“[2024]”。这个符号在需要给匹配内容加前缀后缀时极其好用,省去写一大堆反向引用的时间。

反向引用\1、\2也是 s 命令的重头戏。比如你有一个“姓名-电话”格式的通讯录,现在想改成“电话-姓名”:

echo '张三-13800138000' | sed 's/\(.*\)-\(.*\)/\2-\1/'

输出是13800138000-张三。这里\(.*\)是分组捕获,\1代表第一个括号匹配的内容,\2代表第二个括号匹配的内容。注意,sed 的分组括号必须转义,不转义的小括号只是普通字符,这在前面正则示例里也有体现。

4.2 删除命令 d 与打印命令 p:过滤行的利器

d命令会把匹配到的行直接从输出里删掉,常用于“排除”某些内容。比如我想去掉配置文件里的空行和注释行(以#开头):

sed -e '/^#/d' -e '/^$/d' /etc/某服务配置.conf

这里-e是连续拼接多个命令的选项。多命令还有一个常见写法,用分号分隔:

sed '/^#/d; /^$/d' /etc/某服务配置.conf

p命令上面已经见过了,通常和-n搭配用来快速提取匹配行。比方说,我要把日志里所有包含“500”状态码的行提取出来:

sed -n '/"code":500/p' api.log

在实际使用中,d和p看起来功能相反,一个删除一个保留,但处理逻辑完全不同。d是“删掉不符合的”,p是“挑出符合的”。用哪个更顺手,取决于你的需求是“排除型”还是“筛选型”。

4.3 追加、插入与修改行:a、i、c

当你不只想改文本内容,还想在文件里插入新行时,就需要用a(append)、i(insert)、c(change)这三个命令。它们的区别简单说:

  • a:在匹配行后面追加新内容。
  • i:在匹配行前面插入新内容。
  • c:把匹配的整行替换成新内容。

比如我想在第3行后追加一行“这是追加的内容”:

sed '3a 这是追加的内容' demo.txt

想在匹配“hello”的行前面加一条横线分割线:

sed '/hello/i ----------' demo.txt

想把匹配到“bad line”的那一行直接改为空行:

sed '/bad line/c ' demo.txt

这几个命令在生产环境常用于临时生成带有标记行、头部说明的配置文件,比如自动给一个空配置模板插入注释头。

4.4 行号、转换与退出:=、y、q

有些命令虽然冷门,但在特定场景下能救命:

  • =:打印当前行号。和-n搭配可以快速统计匹配行的行号。
sed -n '/ERROR/=' app.log

输出就是所有含 ERROR 的行号列表,配合p一起用可以同时看到行号和内容:

sed -n '/ERROR/{=;p}' app.log

这里用花括号把多个命令括起来,表示对同一地址执行一组命令。

  • y:按字符一一映射替换。注意它和s不同,y是固定的字符级替换,不支持正则。比如把所有小写字母 a、b、c 变成大写 A、B、C:
echo 'abc' | sed 'y/abc/ABC/'

输出是ABC。

  • q:立即退出 sed,不再继续读取文件内容。比如我只想打印文件最前面的10行(效果类似于head -10,但是性质不同):
sed '10q' bigfile.txt

这种写法在处理超大文件时特别实用,它在读到第10行后立刻停下来,不用把整个大到离谱的文件读完。

5. 地址定址与范围匹配的进阶姿势

5.1 正则定址与“取反”技巧

前面说过的2,3p是行号定址。其实地址还可以是正则表达式,格式为/正则/。定址后可以执行任意命令,不只是p、d、s。比如:

sed '/^ERROR/ s/时间戳/timestamp/g' app.log

这行命令的含义是:只对以 ERROR 开头的行,执行把“时间戳”替换成“timestamp”的操作,其他行不做变动。这就是“先定位,再操作”的完整示范。

还有一个不易察觉但极其好用的技巧:地址前加!表示“取反”。比如我想找出所有不以#开头的行并打印:

sed -n '/^#/!p' config.ini

这个写法在筛选“排除某些行但保留其他所有行”时很优雅。配合地址范围使用更能玩出花样,比如“只在第10行到第20行之间执行替换”,写法是:

sed '10,20s/foo/bar/'

5.2 两个正则之间的范围化操作

范围寻址不仅仅可以写数字范围2,5,还可以写两个正则之间的范围,这是处理结构化文本的利器。比如有一个记录文件,从“开始标记”到“结束标记”之间的内容需要整体处理:

sed '/START/,/END/p' log.txt

这个命令会打印从包含 START 的行开始,到包含 END 的行结束的所有内容。注意,如果文件中没有匹配到第二个地址,范围会一直延伸到最后一行,这个行为要特别小心,容易造成误处理。

我之前处理过一份多章节的文档,每个章节以“Chapter N”开头,下一章节开始前是正文。如果我只想把第3章的正文取出来,用这种范围定址非常高效:

sed -n '/Chapter 3/,/Chapter 4/p' book.txt

这里边界行为很有意思,它会把 Chapter 3 行本身也包括进去,同时不会包括 Chapter 4 行(因为范围是“从匹配A到匹配B”,B作为结束标记会被排除在外),如果你想要到 Chapter 4 之前结束,这个行为刚好满足需求。如果需要把 Chapter 3 这行本身也去掉,还要再套一层管道过滤或者加命令。

5.3 步进地址与取模操作

sed 还支持一种较少见的地址写法:first~step,表示“从第 first 行开始,每 step 行处理一次”。比如1~2表示所有奇数行,2~2表示所有偶数行。这个技巧在隔行抽取数据时很好用:

# 打印所有奇数行 sed -n '1~2p' data.txt # 打印所有偶数行 sed -n '2~2p' data.txt

实际中我还遇到过这样的情况:一个日志文件由多行组成一条完整记录,每条记录以“时间戳 ID”开头,后面跟着若干明细行。我想把第2、第5、第8行等“每隔3行”的明细取出来,用2~3p就能精确实现。这种步进定址方式在处理规则性明显的文本时,比循环脚本简洁得多。

6. 敢用保持空间:sed 的“跨行记忆”能力

6.1 两个空间到底怎么搭配

前面讲过模式空间是“工作台”,那保持空间就是“储物柜”。默认情况下,保持空间是空的。你可以用以下命令在这两个空间之间搬运内容:

  • h:把模式空间的内容复制到保持空间(覆盖)。
  • H:把模式空间的内容追加到保持空间(换行拼接)。
  • g:把保持空间的内容复制回模式空间(覆盖)。
  • G:把保持空间的内容追加到模式空间(换行拼接)。
  • x:交换两个空间的内容。

很多人一看到这组命令就头大,觉得抽象。我提供一个理解视角:把它们当成两个变量,一个变量是当前行内容,另一个变量是缓存内容,你在用命令操作它们之间的赋值、拼接与互换。想清楚“内容从哪来、到哪去”,逻辑就顺了。

一个最简单的例子,把文件内容整体倒序输出(类似tac命令的效果):

sed '1!G;h;$!d' demo.txt

拆解一下:

  • 1!G:不是第1行时,把保持空间的内容追加到当前行后面。因为保持空间存的是“前面所有行”,所以这里就是倒序拼接的关键。
  • h:把当前行(拼接后的)复制到保持空间。
  • $!d:如果不是最后一行,删除当前模式空间内容,不打印。

整个过程相当于:第1行进缓存,第2行进来时把第1行拼接到第2行后面,删除第2行,缓存变成“2\n1”,第3行进来时把缓存拼到第3行后面,删除第3行,缓存变成“3\n2\n1”……直到最后一行,$!d不执行,自动打印出拼接后的完整倒序内容。这就是保持空间的实际战斗力所在。

6.2 行块级别的处理案例

保持空间还有一种常见用法:处理“相邻行”,比如把每一对相邻行合并成一行。给定一个文件,每两行一组,想让它们变成一行:

sed 'N; s/\n/ /' demo.txt

这里的N命令是把下一行追加到模式空间当前行后面(用换行分隔),然后s/\n/ /把拼接产生的换行符替换成空格,最终效果就是每两行合并成一行。

这引出了另一个命令族:多行模式命令N、P、D。N是把下一行拉进模式空间,P是打印模式空间的第一行部分(直到换行符为止),D是删除模式空间的第一行部分并重新执行命令脚本。这套命令可以用于处理跨行匹配等场景,比如匹配“一个关键词结束,但下一行必须紧随才能构成完整记录”的情况。不过说实话,如果你不是天天做文本数据清洗,保持空间和多行模式不需要天天用,但知道它们的原理,遇到跨行处理需求时,你能知道“sed 也可以干这个”,而不是直接放弃转写脚本。

7. 实战:三个我从生产环境里提炼出的 sed 落地场景

7.1 场景一:批量修改配置文件,不手滑

运维的同学肯定经历过改配置文件改到手抽筋的时候。比如某服务有 50 个子配置文件,每个里面都有一段“连接超时=30秒”,现在新规范要求改成 60 秒。手工逐个打开文件修改,效率低且容易漏。

我用一条命令搞定:

sed -i 's/连接超时=30/连接超时=60/g' /etc/某服务/conf.d/*.conf

-i直接写回原文件,*.conf通配符展开成所有配置文件。唯一需要留神的是,-i直接修改原文件前一定要确认你的正则匹配路径没问题,否则改错了想回退都难。所以我个人的习惯是先用-n 's/旧/新/p'只打印将要变化的行,肉眼核对一遍,确认无误后再加-i执行。

如果你对“先看后改”有更严格的要求,sed 还提供了那种带备份的原地编辑选项,比如-i.bak会先为每个修改的文件生成一个.bak备份文件。这样即使改完不满意,也能快速恢复现场,强烈推荐首次使用-i的新手这么干。

7.2 场景二:提取日志关键字段,做成报表

开头提到的日志处理任务,我用 sed 的实战方案是分三步。第一步提取符合规则的原始行,第二步脱敏,第三步按需求排序:

grep "支付接口.*ERROR" pay.log \ | sed 's/密码:[^|]*/密码:***/g' \ | sort -t '|' -k1,1 > 错误报表.txt

这里 sed 的作用是:把每行“密码:xxx”这种格式里的信息抹掉,替换成占位符。利用正则[^|]*来表示“直到下一个竖线为止的所有字符”,不用关心密码具体是什么、长度多少,都能脱敏。这个场景我相信很多后端同学都遇到过,不用写复杂脚本,直接管道组合就把事办完了。

有时候日志字段分隔符不是竖线,可能是 Tab、逗号、空格,原理一样,只需要调整地址正则的分隔符即可。

7.3 场景三:代码批量替换,重构不发愁

我做过一次系统重构,旧代码里有一处函数名oldFunction()要改成newFunction(),但是只改一类特殊调用,不带参数的那种,直接全局替换会殃及带参数的调用。

用 sed 的正则精确圈定边界:

sed -i 's/oldFunction()/newFunction()/g' src/**/*.js

这里()是普通字符,不需要转义,所以只匹配到不带参数的调用。加上-i直接写回所有源码文件。就这一条命令,节约了至少一小时手工替换时间。改完后用 git diff 快速检查一下变更,确认改动范围符合预期,就可以提交了。

8. 常见问题与避坑实录

8.1 为什么我的 s 命令只替换了每行的第一个匹配项

这是新手第一问。sed 's/旧/新/'默认不加g标志时,每行只替换第一个匹配项。如果你要处理一行里的多处匹配,必须加g。比如:

echo 'a b a b' | sed 's/a/A/' # 输出:A b a b echo 'a b a b' | sed 's/a/A/g' # 输出:A b A b

理解了默认行为,你就明白为什么网上所有 sed 替换示例都强调“加上 g”。

8.2-i到底是改原文件还是输出到屏幕

这个坑很多人深有体会。新手执行sed 's/旧/新/' 文件后,发现屏幕上确实变了,但打开文件一看,纹丝不动。原因就在于 sed 默认只输出到标准输出(终端),不写回原文件。要真正改文件,必须加-i选项。那些带.bak的形式是生成备份后再写回。所以建议的顺序是:先不加-i运行看输出,确认无误,再加-i执行。

8.3 正则不符合预期:转义与基本正则/扩展正则的问题

sed 默认使用的是“基本正则表达式”(BRE),它和你在 grep -E、awk、Perl 里用习惯的“扩展正则表达式”(ERE)有些差异。最明显的就是:在 BRE 中,(、)、{、}、+、?、|等符号如果要作为元字符使用,必须加反斜杠转义;在 ERE 中则相反,不加转义的就是元字符。

例如:

  • BRE 写分组捕获:\(ab\)*
  • ERE 写分组捕获:(ab)*
  • BRE 写“一个或多个”:ab\+
  • ERE 写“一个或多个”:ab+
  • BRE 写“或”:a\|b
  • ERE 写“或”:a|b

如果你用 GNU sed,可以通过-E选项切换到扩展正则模式,这样写起来舒服很多:

sed -E 's/([0-9]{4})/(\1)/g' demo.txt

这一行在 BRE 下要写成:

sed 's/\([0-9]\{4\}\)/(\1)/g' demo.txt

肉眼可见,-E更利于阅读维护。我个人的习惯是:用得少、临时命令用 BRE 也行;复杂脚本一律-E,减少转义错误。

8.4 文件中有特殊分隔符导致替换失败

有时候你要替换的内容本身包含/,比如要把路径/usr/local/bin替换成/opt/bin。如果还用/当分隔符,写起来会非常痛苦而且错误百出:

sed 's//usr/local/bin//opt/bin/g' 文件

基本没法看。解决办法是改用其他字符当分隔符。sed 的分隔符是可以自定义的,常见用#或|:

sed 's#/usr/local/bin#/opt/bin#g' 文件

这样路径里的/就不用转义了,整个命令可读性翻倍。这个技巧在处理文件路径、URL 时极其常用。

8.5 总是忘记-n,输出乱糟糟

sed '/hello/p' file这个命令,初学者常以为只打印匹配到的行,实际会打印:所有行(默认输出)+ 匹配行(p命令再输出一遍)。解决办法是加-n,关闭默认输出,只让显式p的内容显示。规则再说一遍:看输出结果,用-n是常态。

8.6 大文件处理慢?检查是否误用正则

如果一条 sed 命令在大文件上跑得极慢,先检查正则是否有“灾难性回溯”的可能。比如.*.*.*这种重叠的贪婪匹配,会让正则引擎尝试大量可能性。但 sed 的正则引擎本身比较简单,更多时候慢是因为某些极端复杂的匹配表达。遇到慢问题,我的排查顺序是:先用head取小样本测试命令,确认逻辑没问题,再完整跑大文件;必要时拆成两条简单命令通过管道串联,而不是用一条复杂命令。

9. sed 与 awk、grep 的协同认知

很多文章会把三剑客放到一起讲,但很少有人把分工讲透。我的看法是:grep 负责“找”,sed 负责“改”,awk 负责“算”。它们不是互相替代的关系,而是管道流水线上各司其职的工位。

例如我需要从一个日志文件里提取某个时间段内、某个模块的错误数量并求和,这个需求用 awk 更合适,因为涉及数值累加。单用 sed 虽然能做,但要做很多符号运算,绕远路。反过来,如果只是规整一个导出文件的格式,比如去掉行尾空格、统一换行符、替换字段分隔符,用 sed 比 awk 更直接。

实际项目中,我经常这样组合:

grep "ERROR" app.log | sed 's/时间戳//' | awk '{print $2, $4}' | sort | uniq -c

用 grep 粗筛,用 sed 清洗格式,再用 awk 做字段处理,各用所长。这种管线式处理模式才是 Linux 三剑客的正确打开方式,也是为什么它们到今天仍然是运维和开发的高频工具。

10. 几条关于 sed 的个人心得

写到最后,我还是想啰嗦几句。

sed 的上手门槛不高,核心命令就那些,但真正用得好,需要对“模式空间”和“保持空间”的运行机制有深入理解。我见过不少人把 sed 当“查找替换工具”用,也没出过什么问题,这完全没问题。但如果你愿意多花半小时搞清楚它的执行流程,很多百思不得其解的输出结果都会迎刃而解。

我自己在实际使用中最大的体会是:sed 强在“快”和“省”。快是语法简单、执行快,省是内存占用极低、思维模型清晰。在处理超大文件、批量修改配置、搭管道的时候,它都是首选的“轻武器”。

另外有一条安全建议:凡是带-i的命令,最好用-i.bak先备份,或者先在测试目录里跑一遍。改生产配置的事,容错率极低,多花一分钟验证,能省下若干小时恢复现场的时间。

sed 的边界其实比很多人想象的大,多行模式、保持空间、范围定址这些能力组合起来,能解决很多脚本工程师习惯性交给 Python 去做的文本处理任务。下次遇到类似场景,先别急着写脚本,想想三剑客能不能“一发入魂”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询