使用SED替换多行字符串
1. 概述
在本教程中,我们将学习一些使用sed 搜索和替换包含多行的文本的高级技术。
2. 嵌套读取
默认情况下,当sed读取模式空间 中的一行时,它会丢弃终止换行符 ( \n ) 。尽管如此,我们可以通过对每个换行符进行嵌套读取来处理多行字符串。
2.1. 模式空间作为滑动窗口
在我们探索嵌套读取技术之前,让我们将模式空间可视化为一个滑动窗口:
我们可以注意到,每次显式或隐式使用n 命令时,模式空间窗口都会向下滑动一行。
2.2. 两级嵌套
假设我们有一个员工数据库emp-db.txt,其中包含员工的名字和部门信息:
$ cat emp-db.txt
Name: Alex
Department: Tech
Name: Richard
Department: Finance
Name: Alex
Department: Analytics
一位名叫Alex的员工最近从Tech团队调到了Product团队。因此,我们需要在员工数据库中更改此详细信息。 现在,由于可以有多个员工具有相同的名字,我们需要在进行任何更改之前匹配这两个详细信息。而且,在sed中,我们可以通过进行两级嵌套读取来做到这一点:
$ cat nested-read.sed
/Alex/ {
p;
n;
/Tech/ {
s/Tech/Product/;
p;
d;
}
}
p;
*我们通过将命令括在花括号{}*中来实现嵌套。**匹配名字属性后,我们将模式空间窗口滑动一行。然后,我们在进行替换之前匹配部门名称属性。
接下来,当进行替换时,我们可以清除模式空间并使用d命令重新启动读取周期。通过这样做,我们能够避免在最后一行使用print ( p ) 命令进行重复打印。
那么,让我们继续验证我们的nested-read.sed脚本是否按预期工作:
$ sed -n -f nested-read.sed emp-db.txt
Name: Alex
Department: Product
Name: Richard
Department: Finance
Name: Alex
Department: Analytics
我们可以看到我们的脚本在这个用例中运行良好。但是,嵌套级别取决于我们搜索字符串中的行数。因此,随着搜索模式中行数的增加,我们的代码将变得更难阅读并且更容易出错。
3. 多行快速滑动窗口
嵌套读取的局限性最好通过使用一组更复杂的多行兼容命令 来解决。使用这些技术,我们可以在模式空间中容纳多行,每行由换行符分隔。让我们通过编辑文件中的非重叠结构化数据来理解这一点。
3.1. 非重叠记录
现在,让我们将另一个版本的emp-db.txt员工数据库视为非重叠记录的集合,每个记录具有三个属性,即Name、Department和EmpId:
$ sed '' emp-db.txt
Name: Alex
Department: Tech
EmpId: 100
Name: George
Department: HR
EmpId: 500
这一次,我们需要将EmpId = 500的员工的Department属性更改为Finance。
3.2. 脚本
如果我们要使用嵌套读取技术编写sed 脚本,那将需要三层嵌套。因此,我们将使用相对更好的多行快速滑动窗口方法。
该技术是一个读取-编辑循环,包括三个步骤:
- 初始化固定大小的多行模式空间窗口
- 对记录进行编辑
- 按等于窗口大小的行滑动窗口
首先,让我们通过将模式空间窗口从默认的单行窗口增加到三行窗口来开始编写我们的emp.sed脚本:
此外,我们必须将窗口大小增加两行,这样我们就可以调用N命令 两次:
$ sed -n '1,2p' emp.sed
N;
N;
现在,我们在模式空间中有了完整的员工记录。因此,我们可以使用替换 ( s ) 命令进行搜索和替换:
有了模式空间中可用的新行,我们可以使用*\n字符作为正则表达式的一部分。此外,我们可以使用\1*、\2和*\3*来反向引用替换组 :
$ sed -n '3,4p' emp.sed
s/(.*\n)(.*: ).*(\n.*: 500)/\1\2Finance\3/;
p;
最后,我们可以在当前读取周期结束时使用d命令滑动模式空间窗口,然后在下一个读取周期调用两次N命令:
$ sed -n '$p' emp.sed
d;
这样,读取周期将继续,直到没有更多的员工记录可供读取。
3.3. 行动中的编辑
在我们执行脚本之前,让我们看看完整的emp.sed脚本:
$ sed '' emp.sed
N;
N;
s/(.*\n)(.*: ).*(\n.*: 500)/\1\2Finance\3/;
p;
d;
最后,让我们运行这个脚本:
$ sed -E -n -f emp.sed emp-db.txt
Name: Alex
Department: Tech
EmpId: 100
Name: George
Department: Finance
EmpId: 500
正如预期的那样,EmpId为500 的员工的Department属性更改为Finance。
4. 多行慢速滑动窗口
在本节中,我们将探索多行模式空间窗口技术的一种变体,其中窗口一次滑动一行。
4.1. 重叠记录
假设我们必须参加一个为期两天的研讨会。为了评估参加课程的最佳时间,我们有兴趣通过处理研讨会的日历数据找出所有可能的开始日期:
$ sed '' workshop_calendar.txt
Day-1:0
Day-2:1
Day-3:1
Day-4:1
Day-5:0
Day-6:1
Day-7:1
因此,一天在workshop_calendar.txt文件中标记为0或1,以指示研讨会是否在该日运行。所以,如果我们考虑这个问题,我们需要从每个可能的连续两天 的研讨会中找出第一天。
从这个角度来看,每对连续的两行都是我们需要处理的记录。此外,这些记录在本质上是重叠的。
4.2. 使用D和N的滑动窗口
与快速滑动窗口的情况一样,使用此技术进行编辑首先会初始化大小等于记录大小的模式空间:
$ sed -n '1p' workshop.sed
N;
对于这种情况,我们需要将模式空间的大小从默认的单行窗口增加到两行。因此,单次调用N命令即可完成这项工作。
然而,当涉及到我们执行主要编辑操作的第二阶段时,它就变得有点棘手了。那是因为记录是重叠的,如果我们直接改变一条记录,我们对下一个窗口的读取会受到干扰。
为了解决这个问题,我们首先将模式空间的副本保存到保持空间中,然后对模式空间的内容进行更改。在我们打印模式空间的编辑版本后,可以从保持空间恢复原始副本,下一个循环可以从那里继续:
$ sed -n -e '1! {$!p;}' workshop.sed
/.*:1\n.*:1/ {
h;
s/(.*):1(\n.*:1)/\1/p;
g;
}
我们必须注意,*1!{$!p;}*打印除第一行和最后一行之外的所有脚本行。
接下来,我们可以通过使用D命令删除第一行并使用N命令在下一个读取周期中附加下一行来滑动模式空间窗口 :
$ sed -n '$p' workshop.sed
D;
为了更加清晰,让我们想象一下模式空间窗口的缓慢滑动:
我们的脚本已准备就绪:
$ sed '' workshop.sed
N;
/.*:1\n.*:1/ {
h;
s/(.*):1(\n.*:1)/\1/p;
g;
}
D;
最后,让我们执行它并获取所需的数据:
$ sed -E -n -f workshop.sed workshop_calendar.txt
Day-2
Day-3
Day-6
5. 搜索
从理论上讲,模式空间的窗口大小没有上限。当文件较小时,我们实际上可以负担得起将整个文件加载到模式空间来替换多行字符串。因此,让我们学习这种搜索技术来解决一些用例。
5.1. 倒序排列
假设我们在employees.txt文件中有一个枚举的员工列表:
$ sed -n '' employees.txt
e9
e2
e3
e4
e8
e6
e7
e1
e5
现在,我们需要颠倒文件中行的顺序。为此,我们可以将文件的全部内容视为一个多行字符串,我们需要用它的反向对应物来替换它。
通过结合使用G和h命令,我们可以有效地为保持空间模拟类似堆栈的先进先出功能。并且,在每个循环结束时,模式空间和保持空间将具有相同的内容:
因此,让我们将此逻辑转换为我们的reverse.sed脚本:
$ sed '' reverse.sed
1 {
h;
};
2,$ {
G;
h;
}
$p;
我们必须注意,**由于保留空间最初是空的,因此我们不需要为第一个读取周期调用G命令。**并且,在最后一个读取周期结束时,我们可以打印模式空间以给出行的相反顺序。
有了我们的脚本,让我们执行它以查看它的运行情况:
$ sed -n -f reverse.sed employees.txt
e5
e1
e7
e6
e8
e4
e3
e2
e9
完美的!这正是我们所需要的。
6. 两个文件之间的映射数据
要使用来自两个文件的数据替换多行文本,我们可以使用搜索较小文件的两阶段策略,并使用较大文件的滑动窗口策略。让我们在实践中看看这个。
6.1. 两阶段战略
假设我们在projects.txt文件中有一个项目列表,我们需要从employees.txt文件中将每个项目分配给一个由三名员工组成的团队:
$ sed '' projects.txt
p1
p2
p3
为了解决这个用例,让我们通过两阶段策略有效地利用保持空间和模式空间:
- 将较小的projects.txt文件吞噬到保留空间
- 使用快速滑动窗口策略替换employees.txt文件中的文本
6.2. 搜索
在第一阶段,让我们将projects.txt文件加载到保留空间中:
对于第一个读取周期,我们可以使用h命令,之后,我们可以使用H命令。这样做将确保换行符分隔保留空间中的各个行:
$ sed -n '1,5p' mapping.sed
/^p[0-9]+/ {
1h;
2,$H;
$d;
}
我们必须注意单级嵌套的使用,因为我们希望这些命令仅针对projects.txt文件中的内容执行。并且,在最后一个读取周期结束时,我们清除模式空间,以便我们可以使用它来读取第二个文件。
6.3. 滑动窗口
在第二阶段,让我们对employees.txt文件使用快速滑动窗口方法。在每个读取周期中,模式空间窗口大小将为三行。
让我们看一下主要负责读取和编辑employees.txt文件的mapping.sed脚本片段:
$ sed -n '7,18p' mapping.sed
/^e[0-9]+/ {
N;
N;
s/\n/,/g;
s/.*/(&)/;
P;
x;
P;
s/^[^\n]*\n//;
x;
D;
}
由于滑动窗口技术中的主要编辑逻辑位于中间,让我们将其分解以更清楚地理解它:
- 第一个替换s/\n/,/g用逗号分隔每个组中的员工姓名
- 第二个替换*s/.*/(&)/*将每个组括在括号内
- 使用P命令 ,我们打印模式空间的第一行
- 使用x命令 ,我们在模式空间和保持空间之间交换内容
- 第三个替换*s/^[^\n]*\n//*从同一读取周期内的模式空间中删除第一行
6.4. 测试
最后,让我们使用projects.txt和employees.txt分别作为第一个和第二个输入文件来执行我们的mapping.sed脚本:
$ sed -E -n -f mapping.sed projects.txt employees.txt
(e9,e2,e3)
p1
(e4,e8,e6)
p3
(e7,e1,e5)
p3
因此,我们可以看到每个组包含一组三名员工。并且,输出中的第一行显示分配给该组的项目。