在perl中的一行中第n次出现后替换所有出现

Replacing all occurrence after nth occurrence in a line in perl

我需要在 Unix 文件的每一行中第 n 次出现后替换所有出现的字符串。

我的文件数据:

:account_id:12345:6789:Melbourne:Aus
:account_id:98765:43210:Adelaide:Aus

我的输出数据:

:account_id:123456789MelbourneAus
:account_id:9876543210AdelaideAus

尝试使用 sed:sed 's/://3g' test.txt

不幸的是,出现的 g 选项没有按预期工作。相反,它正在替换所有出现的地方。

您可以使用 perl 解决方案,例如

perl -pe 's~^(?:[^:]*:){2}(*SKIP)(?!)|:~~g if /^:account_id:/' test.txt

参见online demo and the regex demo

^(?:[^:]*:){2}(*SKIP)(?!)|: 正则表达式表示:

  • ^(?:[^:]*:){2}(*SKIP)(?!) - 匹配
    • ^ - 字符串的开头(这里是一行)
    • (?:[^:]*:){2} - 除了 :: 字符
    • 以外的任何零个或多个字符出现两次
    • (*SKIP)(?!) - 跳过匹配,从失败位置继续搜索下一个匹配
  • | - 或
  • : - 匹配一个 : 字符。

如果当前行以 :account_id: 开头,则只有 运行 替换(参见 if /^:account_id:/')。

或者像

这样的awk解决方案
awk 'BEGIN{OFS=FS=":"} /^:account_id:/ {result="";for (i=1; i<=NF; ++i) { result = result (i > 2 ? $i : $i OFS)}; print result}' test.txt

参见 this online demo。详情:

  • BEGIN{OFS=FS=":"} - 将 input/output 字段分隔符设置为 :
  • /^:account_id:/ - 行必须以 :account_id:
  • 开头
  • result="" - 将 result 变量设置为空字符串
  • for (i=1; i<=NF; ++i) { result = result (i > 2 ? $i : $i OFS)}; print result} - 遍历字段,如果字段编号大于 2,只需将当前字段值附加到 result,否则,附加值 + 输出字段分隔器;然后打印 result.

另一种方法使用awk

awk -v c=':' -v n=2 'BEGIN{
                       FS=OFS=""
                     }
                     {
                       j=0;
                       for(i=0; ++i<=NF;)
                         if($i==c && j++>=n)$i=""
                     }1' file 
$ cat file 
:account_id:12345:6789:Melbourne:Aus
:account_id:98765:43210:Adelaide:Aus

$ awk -v c=':' -v n=2 'BEGIN{FS=OFS=""}{j=0;for(i=0; ++i<=NF;)if($i==c && j++>=n)$i=""}1' file 
:account_id:123456789MelbourneAus
:account_id:9876543210AdelaideAus

我根据您提供给我们的有限数据进行了推断,所以这可能行不通。但我不会将正则表达式用于这项工作。你所拥有的是冒号分隔的字段。

所以我会使用 split 来提取数据,然后使用某种形式的字符串格式来重新组合您喜欢的内容:

#!/usr/bin/perl

use strict;
use warnings;

while (<DATA>) {
  chomp;
  my ( undef, $first, @rest ) = split /:/; 
  print ":$first:", join ( "", @rest ),"\n";
}

__DATA__
:account_id:12345:6789:Melbourne:Aus
:account_id:98765:43210:Adelaide:Aus

这给了你想要的结果,而 IMO 对于下一个 reader 比复杂的正则表达式要清晰得多。

如果 n 固定且等于 2 以下方式,我将使用 GNU AWK 以下方式,让 file.txt 内容为

:account_id:12345:6789:Melbourne:Aus
:account_id:98765:43210:Adelaide:Aus

然后

awk 'BEGIN{FS=":";OFS=""}{=FS  FS;print}' file.txt

输出

:account_id:123456789MelbourneAus
:account_id:9876543210AdelaideAus

说明:使用 : 作为字段分隔符,没有任何内容作为输出字段分隔符,这本身确实删除了所有 : 所以我添加了必须保留的 ::1st(之前第二列)和第二(第二列之后)。请注意,我仅针对此数据对其进行了测试,因此如果您想使用它,您应该首先使用更多可能的输入对其进行测试。

(在 gawk 4.2.1 中测试)

使用 GNU awk,使用 gensub 请尝试以下操作。这完全基于您显示的示例,其中 OP 希望从第 3 次出现后删除 : 。根据 OP 的要求,使用 gensub 分隔匹配值的各个部分,并从第二部分(从第三个冒号开始)中删除所有冒号。

awk -v regex="^([^:]*:)([^:]*:)(.*)" '
{
  firstPart=restPart=""
  firstPart=gensub(regex, "\1 \2", "1", [=10=])
  restPart=gensub(regex,"\3","1",[=10=])
  gsub(/:/,"",restPart)
  print firstPart restPart
}
' Input_file

这可能适合您 (GNU sed):

sed 's/:/\n/3;h;s/://g;H;g;s/\n.*\n//' file

用换行符替换第三次出现的 :

复制该行。

删除所有出现的 :

将修改后的行附加到副本。

通过删除从第三次出现的副本到第三次出现的修改后的行之间的所有内容来连接这两行。

N.B。在 sed 中使用换行符是最好的分隔符,因为提供给 seds 命令的行最初没有换行符。然而,分隔符的重要 属性 是它是唯一的,因此可以是任何这样的字符,只要它在数据集中的任何地方都找不到。

另一种解决方案使用循环删除前两个之后的所有 :

sed -E ':a;s/^(([^:]*:){2}[^:]*)://;ta' file

使用 GNU awk 将第 3 个参数匹配 () 和 gensub():

$ awk 'match([=10=],/(:[^:]+:)(.*)/,a){ [=10=]=a[1] gensub(/:/,"","g",a[2]) } 1' file
:account_id:123456789MelbourneAus
:account_id:9876543210AdelaideAus

并且在每个 Unix 机器上的任何 shell 中使用任何 awk:

$ awk 'match([=11=],/:[^:]+:/){ tgt=substr([=11=],1+RLENGTH); gsub(/:/,"",tgt); [=11=]=substr([=11=],1,RLENGTH) tgt } 1' file
:account_id:123456789MelbourneAus
:account_id:9876543210AdelaideAus