我需要替换许多大型文本文件中的字符串,但我有一个异常字符串列表(200 多个项目)。例如:
# I want to replace every "dank". Except when it comes in the following form:
dankine
dankzwd
nudankip
dankphys
danko.mod
... (The list is 200+ items long)
我当前的正则表达式如下所示:
sed -e "s/dank/monk/g" /path/to/file
该文件的内容如下所示:
xdankine redankus
dankzwd
danke dankbe
testdank
这是执行后文件的内容:
xmonkine remonkus
monkzwd
monke monkbe
testmonk
但我希望内容看起来像这样:
xdankine remonkus
dankzwd
monke monkbe
testmonk
因为 dankine 和 dankzwd 在我的排除列表中。
该文件的每一行可以包含多个可能的替换。
我怎样才能做到这一点?
答案1
dank
如果像示例中那样每行只出现一次,则可以使用反转地址:
sed -E '/dankine|dankzwd|nudankip|dankphys|danko\.mod/!s/dank/monk/'
如果每行可以出现多次,您可以使用不能成为文件一部分的字符,例如#
,将全部更改dank
为#
,将单词列表更改回,并将其余更改为#
:monk
sed 's/dank/#/g;s/#ine/dankine/g;s/#zwd/dankzwd/g;s/nu#ip/nudankip/g;s/#phys/dankphys/g;s/#o\.mod/danko.mod/g;s/#/monk/g'
(如果可以出现任何字符,请使用换行符代替)
更新:从文件中读取排除列表的新要求
将您的黑名单写入文件exclusion.list
带有尾随换行符(脚本将使用它来检测第一个文件的结束位置):
sed -e '1,/^$/{H;d;}' -e 'G;s/\n/&&/;:loop' -e 's/\(.*da\)\(nk.*\)\(.*\n\1\2\n\)/\1#\2\3/;tloop' -e 's/\n.*//;s/dank/monk/g;s/da#nk/dank/g' exclusion.list file
或者,因为多行可能更容易阅读
sed '1,/^$/{H;d;}
G
s/\n/&&/
:loop
s/\(.*da\)\(nk.*\)\(.*\n\1\2\n\)/\1#\2\3/
tloop
s/\n.*//
s/dank/monk/g
s/da#nk/dank/g' exclusion.list file
无论如何,这可能仍然比阅读更容易。这个概念是
- 读取排除列表以保留空间
- 对于文件的每一行,将该列表附加到保留空间中
dank
将列表中出现的文件中的每个替换为da#nk
,以防止以后被替换- 然后删除列表,全部替换
dank
为,最后从s 中monk
删除。#
da#nk
添加l
在后面:loop
说明了工作原理。
感谢 Stéphane 提示dankfoodank
问题,现已解决。然而,对 case 的要求dankdank
仍不清楚,是dankmonk
因为只有dank
ofdankda
受到保护,还是应该保留,dankdank
因为da
第二个的 ofdank
作为 的一部分受到保护,dankda
还是超出了范围?
答案2
在每个 Unix 机器上的任何 shell 中使用任何 awk 并使用文字字符串操作,因此我们不关心输入或异常列表中的任何正则表达式或反向引用元字符:
$ cat tst.awk
NR==FNR {
mask[$0] = RS NR RS
next
}
{
delete changed
for (exception in mask) {
while ( s=index($0,exception) ) {
$0 = substr($0,1,s-1) mask[exception] substr($0,s+length(exception))
changed[exception]
}
}
gsub(/dank/,"monk")
for (exception in changed) {
while ( s=index($0,mask[exception]) ) {
$0 = substr($0,1,s-1) exception substr($0,s+length(mask[exception]))
}
}
print
}
$ awk -f tst.awk exceptions file
xdankine remonkus
dankzwd
monke monkbe
testmonk
上面假设您没有作为其他异常的子字符串的异常,例如dankfoo
和dankdankfoo
因为您没有在问题的示例中显示类似的情况。如果这样做,请确保对异常文件进行排序,以便较长的超字符串出现在较短的子字符串之前,并按照输入顺序对其进行迭代,这样在第一个循环中屏蔽异常时就不会替换为xdankdankfooy
。xdank<replacement>y
x<replacement>y
答案3
由于排除列表可能超过 200 个强,为了不使正则表达式过载,我们首先使用排除列表文件生成 sed 代码,并将生成的代码应用于数据输入。
GNU sed
sed -e '
1i\
s/dank/\\n/g
h;s:[\&/]:\\&:g
x;s/dank/\n/g
s:[][^$\/.*]:\\&:g
s/\n/\\n/g;G
s:\n:/:;s:.*:s/&/g:
$a\
s/\\n/MONK/g
' excludes.txt | sed -f - file
输出:-
xdankine reMONKus
dankzwd
MONKe MONKbe
testMONK
概念证明:-
- 首先,将所有 danks 转换为字面换行符,保证找不到一个字符。
- 然后将排除列表中的一行转为
nudankip
如下所示,同样对于排除列表中的所有行。 s/nu\nip/nudankip/g
- 复杂性在于我们需要逃避 sed s/// 表达式的 rhs 和 lhs 的排除列表。
答案4
有了perl
,你可以这样做:
perl -pe '
BEGIN{
chomp (@excl = <STDIN>);
$re = "(" . join( "|", map {qr{\Q$_\E}} @excl) . ")|dank"
}
s{$re}{$1//"monk"}ge' input < exclusion.list
这会构造一个正则表达式,例如:
(dankine|dankzwd|nudankip|dankphys|danko\.mod)|dank
我们将它的任何出现替换为如果设置了(匹配了一个排除项)或否则(匹配了匹配项) $1
(所以匹配了什么,所以基本上什么也不做)。$1
monk
dank
请注意,如果排除项同时包含dankzwd
和zwddank
,则 仍然会变成dankzwddank
,dankzwdmonk
因为它先dankzwd
用dankzwd
( $1
) 替换,然后只剩dank
下它替换。
一种解决方法是记录掩码字符串中出现任何排除的所有位置,然后在进行替换时dank
,仅在掩码表明可以的地方进行替换。
perl -spe '
BEGIN {
chomp (@excl = <STDIN>);
$word_len = length $word;
}
my $len = length;
my $mask = "-" x $len;
my $i;
for my $e (@excl) {
my $e_len = length $e;
my $hide = "#" x $e_len;
for (my $o = 0;
$o < $len && ($i = index($_, $e, $o)) >= 0;
$o = $i + 1) {
substr($mask, $i, $e_len) = $hide;
}
}
s{dank}{substr($mask, pos, $word_len) =~ /-/ ? $repl : $&}ge
' -- -word=dank -repl=monk input < exclusion.list
例如,如果输入行包含:
dodankdankdankoodankdodank
并且排除有:dankdank
,dankdo
掩码将逐步构建
--------------------------
--########---------------- # first dankdank
--############------------ # second dankdank
--############--######---- # first and only dankdo
dodankdankdankoodankdodank
^^^^
那么只剩下dank
最后不受排除影响的部分。