sed -- 替换文件中单词的前 k 个实例

nar*_*ary 29 sed awk text-processing

我只想替换k单词的第一个实例。

我怎样才能做到这一点?

例如。说文件foo.txt包含 100 个单词 'linux' 的实例。

我只需要替换前 50 次出现。

Joh*_*024 37

下面的第一部分描述了如何使用sed改变一行上的第一个 k 次出现。第二部分扩展了这种方法以仅更改文件中的前 k 次出现,而不管它们出现在哪一行。

面向线路的解决方案

使用标准 sed,有一个命令可以替换一行中出现的第 k 个单词。如果k是 3,例如:

sed 's/old/new/3'
Run Code Online (Sandbox Code Playgroud)

或者,可以将所有出现的内容替换为:

sed 's/old/new/g'
Run Code Online (Sandbox Code Playgroud)

这些都不是你想要的。

GNUsed提供了一个扩展,可以改变第 k 次出现以及之后的所有情况。如果 k 是 3,例如:

sed 's/old/new/g3'
Run Code Online (Sandbox Code Playgroud)

这些可以结合起来做你想做的事。要更改前 3 次出现:

$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old
Run Code Online (Sandbox Code Playgroud)

where 在这里\n很有用,因为我们可以确定它永远不会出现在一行上。

解释:

我们使用三个sed替换命令:

  • s/\<old\>/\n/g4

    这是 GNU 扩展,用于替换第四次和所有后续出现的oldwith \n

    扩展正则表达式功能\<用于匹配单词的开头和\>匹配单词的结尾。这确保只匹配完整的单词。扩展正则表达式需要-E选择sed.

  • s/\<old\>/new/g

    只有前三个出现的old仍然存在,这将它们全部替换为new

  • s/\n/old/g

    在第一步old中替换了第四次和所有剩余的出现\n。这会将它们返回到原始状态。

非 GNU 解决方案

如果 GNU sed 不可用,并且您想将前 3 次出现更改oldnew,则使用三个s命令:

$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old
Run Code Online (Sandbox Code Playgroud)

这在k数字较小时效果很好,但对大的缩放效果不佳k

由于某些非 GNU sed 不支持将命令与分号组合,因此这里介绍的每个命令都有自己的-e选项。可能还需要验证您是否sed支持单词边界符号\<\>

面向文件的解决方案

我们可以告诉 sed 读取整个文件,然后执行替换。例如,要替换old使用 BSD 样式的 sed的前三个匹配项:

sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
Run Code Online (Sandbox Code Playgroud)

sed 命令H;1h;$!d;x读取整个文件。

因为上面没有使用任何 GNU 扩展,它应该适用于 BSD (OSX) sed。请注意,这种方法需要一个sed可以处理长行的方法。GNUsed应该没问题。那些使用非 GNU 版本的人sed应该测试它处理长行的能力。

使用 GNU sed,我们可以进一步使用上述g技巧,但用 ,\n替换\x00来替换前三个出现:

sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'
Run Code Online (Sandbox Code Playgroud)

这种方法可以很好地扩展k。但是,这假定它\x00不在您的原始字符串中。由于不可能将字符\x00放入 bash 字符串中,因此这通常是一个安全的假设。

  • 这仅适用于行,并将更改每行中的前 4 次出现 (5认同)
  • 这太丑了 (2认同)

小智 10

使用 awk

awk 命令可用于用替换来替换单词的前 N ​​次出现。
只有当单词完全匹配时,命令才会替换。

在下面的例子中,我代替第一27出现的oldnew

使用子

awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file
Run Code Online (Sandbox Code Playgroud)

此命令循环遍历每个字段,直到匹配为止old,它检查计数器是否低于 27,递增并替换该行上的第一个匹配项。然后移动到下一个字段/行并重复。

手动替换字段

awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file
Run Code Online (Sandbox Code Playgroud)

与之前的命令类似,但由于它已经在它所在的字段上有一个标记($i),它只是将字段的值从 更改oldnew

执行检查之前

awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file
Run Code Online (Sandbox Code Playgroud)

检查该行是否包含旧的并且计数器低于 27 会SHOULD提供一个小的速度提升,因为当这些为假时它不会处理行。

结果

例如

old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old
Run Code Online (Sandbox Code Playgroud)

new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old
Run Code Online (Sandbox Code Playgroud)


mik*_*erv 7

假设您只想替换字符串的前三个实例...

seq 11 100 311 | 
sed -e 's/1/\
&/g'              \ #s/match string/\nmatch string/globally 
-e :t             \ #define label t
-e '/\n/{ x'      \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{'   \ #if not 3 characters in hold space do
-e     's/$/./'   \ #add a new char to hold space
-e      x         \ #exchange hold/pattern spaces again
-e     's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e     'b t'      \ #branch back to label t
-e '};x'          \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g'      #end match function; remove all newline characters
Run Code Online (Sandbox Code Playgroud)

注意:上述内容可能不适用于嵌入的注释
……或者在我的示例中,“1”……

输出:

22
211
211
311
Run Code Online (Sandbox Code Playgroud)

在那里我使用了两个值得注意的技术。首先,1一行中的每个出现都替换为\n1. 这样,当我接下来进行递归替换时,如果我的替换字符串包含我的替换字符串,我可以确保不会替换出现的两次。例如,如果我hehey它替换仍然可以工作。

我这样做:

s/1/\
&/g
Run Code Online (Sandbox Code Playgroud)

其次,我通过h为每次出现向旧空间添加一个字符来计算替换次数。一旦我达到三个不再发生。如果您将其应用到您的数据并将其更改为您\{3\}想要的替换总数,并将/\n1/地址更改为您想要替换的任何内容,则您应该只替换您希望的数量。

我只是-e为了可读性做了所有的事情。POSIXly 可以这样写:

nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"
Run Code Online (Sandbox Code Playgroud)

和 GNU sed

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'
Run Code Online (Sandbox Code Playgroud)

还请记住,这sed是面向行的 - 它不会像其他编辑器中的情况那样读取整个文件然后尝试循环返回。sed简单高效。也就是说,执行以下操作通常很方便:

这是一个小的 shell 函数,它将它捆绑成一个简单执行的命令:

firstn() { sed "s/$2/\
&/g;:t 
    /\n/{x
        /.\{$(($1))"',\}/!{
            s/$/./; x; s/\n'"$2/$3"'/
            b t
        };x
};s/\n//g'; }
Run Code Online (Sandbox Code Playgroud)

因此,我可以这样做:

seq 11 100 311 | firstn 7 1 5
Run Code Online (Sandbox Code Playgroud)

……然后得到……

55
555
255
311
Run Code Online (Sandbox Code Playgroud)

...或者...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'
Run Code Online (Sandbox Code Playgroud)

...要得到...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25
Run Code Online (Sandbox Code Playgroud)

...或者,为了匹配您的示例(在较小的数量级上)

yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux
Run Code Online (Sandbox Code Playgroud)


Jos*_* R. 5

Perl 中的一个简短替代方案:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file
Run Code Online (Sandbox Code Playgroud)

根据您的喜好更改“$n$”的值。

怎么运行的:

  • 对于每一行,它都会不断尝试替换new( old) s/old/new/,并且只要有可能,它就会递增变量$i( ++$i)。
  • 1 while ...只要它所做的替换总数少于( ) 行$n,并且它可以在该行上至少进行一次替换,它就会继续在行 ( ) 上工作。