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


24

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

我怎样才能做到这一点?

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

我只需要替换前50个事件。



您是否需要sed或其他工具可以接受?您需要在命令行上工作还是接受文本编辑器?
evilsoup 2014年

任何在命令行上起作用的东西都是可以接受的。
narendra-choudhary 2014年

Answers:


31

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

面向行的解决方案

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

sed 's/old/new/3'

或者,您可以将所有事件替换为:

sed 's/old/new/g'

这些都不是您想要的。

GNU sed提供了一个扩展,它将更改第k个出现次数,并且此后都会更改。例如,如果k为3:

sed 's/old/new/g3'

这些可以组合起来做您想要的。要更改前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

这里的\n位置很有用,因为我们可以确保它永远不会在线发生。

说明:

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

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

    这是GNU扩展,用于替换old带有的第四个和所有后续出现的\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

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/'

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

因为上面没有使用任何GNU扩展,所以它应该可以在BSD(OSX)sed上运行。注意,请注意,这种方法要求sed可以处理长行。GNU sed应该没问题。使用非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'

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


5
这仅适用于行,并且将更改每行中的前4个事件

1
@mikeserv好主意!答案已更新。
约翰1024年

(1)您提到了GNU和非GNU sed,并提出了建议tr '\n' '|' < input_file | sed …。但是,当然,这会将整个输入转换为一行,并且某些非GNU sed无法处理任意长行。(2)您说,“…上面,带引号的字符串'|'应替换为任何字符或字符串,…”,但是您不能tr用来用长度大于1的字符串替换字符。(3)在最后一个示例中,您说-e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new。这似乎是的错字-e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'
G-Man说'Resstate Monica''Sep

@ G-Man非常感谢!我已经更新了答案。
John1024

这是如此丑陋
Louis Maddox

8

使用Awk

awk命令可用于用替换替换单词的前N个出现的位置。
仅当单词完全匹配时,命令才会替换。

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

使用子

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

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

手动替换字段

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

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

之前进行检查

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

检查行中是否包含旧行,以及计数器是否低于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

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

如果字符串“ old”在* word old 之前,则第一个(使用sub)执行错误的操作;例如,“给老人一些金子。”→“给老人一些金子。”
G-Man说'

@ G-Man是的,我忘记了$i位,对其进行了编辑,谢谢:)

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

注意:以上内容可能不适用于嵌入注释
...或者在我的示例中为'1'...

输出:

22
211
211
311

在那里,我使用两种著名的技术。首先1,用替换行中每次出现的\n1。这样,当我接下来进行递归替换时,如果替换字符串包含替换字符串,就可以确定不会两次替换出现的位置。例如,如果我替换hehey它仍然可以工作。

我这样做:

s/1/\
&/g

其次,我通过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"

和GNU sed

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'

还要记住,它sed是面向行的-它不会读入整个文件,然后像其他编辑器一样,尝试遍历该文件。sed简单高效。也就是说,执行以下操作通常很方便:

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

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

因此,我可以这样做:

seq 11 100 311 | firstn 7 1 5

...并得到...

55
555
255
311

...要么...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'

...要得到...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25

...或者,为了与您的示例相匹配(幅度较小)

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

4

Perl中的一个简短替代:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file

将$$的值更改为您喜欢的值。

怎么运行的:

  • 对于每一行,它一直试图替换newolds/old/new/)和任何时侯,它增加了变量$i++$i)。
  • 它保持工作就行了(1 while ...),只要它不是变得不那么$n总共替代,它可以使在该行至少一个取代。

4

使用shell循环和ex

{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt

是的,这有点愚蠢。

;)

注意:如果old文件中的实例少于50个,此操作可能会失败。(我尚未测试过。)如果这样,它将使文件保持不变。


更好的是,使用Vim。

vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x

说明:

q                                # Start recording macro
 q                               # Into register q
  gg                             # Go to start of file
    /old<CR>                     # Go to first instance of 'old'
            :s/old/new/<CR>      # Change it to 'new'
                           q     # Stop recording
                            49@q # Replay macro 49 times

:x  # Save and exit

:s // new <CR>应该也可以正常工作,因为空的正则表达式会重用上一次使用的搜索
eike

3

一个简单但不是很快的解决方案是遍历/programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a中描述的命令 -文件

for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/"  file.txt  ; done

如果newword不属于oldword,则此特定的sed命令可能仅适用于GNU sed 。对于非GNU sed,请参见此处如何仅替换文件中的第一个模式。


+1表示用“ bold”替换“ old”会引起问题。
G-Man说'Resstate Monica'2014/

2

随着GNU awk您可以将记录分隔符设置RS要被替换的字由字边界分隔。然后是在输出上将记录分隔符设置为第一k条记录的替换词,而保留其余部分的原始记录分隔符的情况

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, NR <= limit? replacement: RT}' file

要么

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.