awk / sed:如果任何字段与模式匹配,则替换所有字段

Posted

技术标签:

【中文标题】awk / sed:如果任何字段与模式匹配,则替换所有字段【英文标题】:awk / sed: replace all fields if any field matches a pattern 【发布时间】:2018-06-27 09:03:11 【问题描述】:

我有一个至少有 16 列(但可能更多)的制表符分隔文件,其中第一列是唯一标识符;和 >10,000 行(示例中仅显示 6x6),如下所示:

ID  VAR1  VAR2  VAR3  VAR4  VAR5
1    1    1     1     1     1
2    -9   -9    -9    -9    -9
3    3    3     3     3     3
4    4    4     4     -9    4
5    5    5     5     5     5
6    6    -9    6     6     6

如果其中一个值已经是“-9”,我需要将 VAR1-5 的所有值更改为“-9”

因此,所需的输出将是:

ID  VAR1  VAR2  VAR3  VAR4  VAR5
1    1    1     1     1     1
2    -9   -9    -9    -9    -9
3    3    3     3     3     3
4    -9   -9    -9    -9    -9
5    5    5     5     5     5
6    -9   -9    -9    -9    -9

到目前为止,我已经尝试在 awk 中这样做:

awk -F'\t' '
BEGINOFS="\t"
for(i=2;i<=NF;i++)if ($i=="-9")for(j=2;j<=NF;j++)$j="-9";continue;1
' < file1.tab

这很有效,但在应用于实际数据集时非常慢。有没有更快的方法来做到这一点?也许是grepsed 的组合?

【问题讨论】:

如果您已经在使用 Awk,它会做所有sedgrep 可以做的事情,而且通常更快。 VAR1-5 的所有值 - 它应该只处理前 5 列吗? @RomanPerekhrest OP 自己的尝试似乎证实了它实际上应该替换所有列的怀疑。我更新了标题以反映这一点,所以如果这是一个不正确的解释,请回滚。 【参考方案1】:

关注awk 可能对您有同样的帮助,我已经用您提供的样本对其进行了测试。

awk 'FNR==1print;next /(^|\t)-9(\t|$)/print $1,"-9   -9    -9    -9    -9";next 1' OFS="    "   Input_file

如果 OP 在 Input_file 中有超过 5 个字段左右,那么以下可能会有所帮助,逻辑与 Triple Sir 的解决方案相同,我正在遍历字段但尽管打印了 -9 我将字段的值分配给 @ 987654324@.

awk 'FNR==1print;next /(^|\t)-9(\t|$)/for(i=2;i<=NF;i++)$i=-9; 1' OFS="\t\t"   Input_file

输出如下。

ID  VAR1  VAR2  VAR3  VAR4  VAR5
1    1    1     1     1     1
2    -9   -9    -9    -9    -9
3    3    3     3     3     3
4    -9   -9    -9    -9    -9
5    5    5     5     5     5
6    -9   -9    -9    -9    -9

解释:现在也为上面的代码添加解释。

awk '
FNR==1                ##Checking condition here if line number is 1 then do following:
  print;               ##Printing the current line then which will be very first line of Input_file.
  next                 ##next is awk out of the box keyword which will skip all further statements for program.

/(^|\t)-9(\t|$)/        ##Checking here if -9 is coming in a line either with spaces or without spaces, if yes then do following:
  print $1,"-9   -9    -9    -9    -9";  ##printing the first field of current line along with 5 -9 values as per OPs request to do so.
  next                 ##next will skip all further statements.

1                      ##awk works on method of condition then action, so I am making condition TRUE here by mentioning 1 here and not mentioning action here so by default print of the current line will happen.
' OFS="    " Input_file   ##Setting OFS(output field separator) value to spaces and mentioning the Input_file name here.

【讨论】:

/-9/ 本身就已经匹配了 / +-9 +/ 匹配的所有内容,所以这个条件看起来有点复杂。我的答案中的正则表达式在一个字段中单独搜索 -9;也许改用那个正则表达式。 另外,特殊大小写 FNR==1 可能不是绝对必要的,尽管它是一个有用的提醒,至少要考虑是否应该特别对待第一行。 (没有列标题通常是我们 Unix-heads 更喜欢的。) 1) 错字 - awk 命令中的额外单引号... 2) 由于输入是制表符分隔的,为什么不使用 OFS="\t" 3) OP 提到输入的列比示例中显示的多,所以需要通用解决方案 @Sundeep,很抱歉' 单引号在复制我忽略的三重正则表达式时出现。对于通用解决方案,让我看看我是否可以创建一个然后发布它,谢谢您的反馈。 那么你最终会复制我的答案(-:【参考方案2】:

这是一个没有硬编码列数的变体。

awk -F '\t' '/(^|\t)-9(\t|$)/ 
    printf $1; for(i=2; i<=NF; ++i) printf "\t-9"; printf "\n"
    next 
  1' file1 file2

这里的主要优化是 Awk 一次扫描整行并立即在正则表达式上触发,无需遍历所有字段,除非它已经知道存在匹配项。

因为我们知道我们会丢弃除第一个以外的所有字段,因此无需让 Awk 替换这些字段以便随后打印它们。只需生成我们想要打印的输出并继续前进,而无需触及 awk 的行的内部表示。这也应该购买几个周期,尽管这是一个非常小的性能改进。

【讨论】:

在这个具体案例中为什么会有多个文件file1 file2 只是为了表明它确实可以做到。你觉得这很混乱吗? 对我和你来说不是很关键,但可能会混淆一些不太“有经验的眼睛” 感谢您的评论 -- 希望把 cmets 留在这里就足够了 caveat emptor (-:【参考方案3】:

更多使用 GNU awk

的方法

单线:

awk '/(^|[ \t]+)-9([ \t]+|$)/for(i=2; i<=NF; i++)$0=gensub (/[^[:blank:]]+/,-9,i)1' infile

更好的可读性:

awk '/(^|[ \t]+)-9([ \t]+|$)/
       for(i=2; i<=NF; i++)
            $0=gensub (/[^[:blank:]]+/,-9,i)
     1
    ' infile

测试结果:

输入:

$ cat infile
ID  VAR1  VAR2  VAR3  VAR4  VAR5
1    1    1     1     1     1
2    -9   -9    -9    -9    -9
3    3    3     3     3     3
4    4    4     4     -9    4
5    5    5     5     5     5
6    6    -9    6     6     6

输出:

(因为-间距偏移)

$ awk '/(^|[ \t]+)-9([ \t]+|$)/for(i=2; i<=NF; i++)$0 = gensub (/[^[:blank:]]+/, -9 , i)1' infile  
ID  VAR1  VAR2  VAR3  VAR4  VAR5
1    1    1     1     1     1
2    -9   -9    -9    -9    -9
3    3    3     3     3     3
4    -9    -9     -9     -9    -9
5    5    5     5     5     5
6    -9    -9    -9     -9     -9

如果你想让输出看起来更好,可以试试这个:(不推荐)

awk '/(^|[ \t]+)-9([ \t]+|$)/for(i=2; i<=NF; i++) if($i==-9)continue; $0 = gensub (/[^[:blank:]]+/, "\b-9" , i)1' infile  
ID  VAR1  VAR2  VAR3  VAR4  VAR5
1    1    1     1     1     1
2    -9   -9    -9    -9    -9
3    3    3     3     3     3
4   -9   -9    -9     -9   -9
5    5    5     5     5     5
6   -9    -9   -9    -9    -9

上述更好的可读版本:

awk '/(^|[ \t]+)-9([ \t]+|$)/
          for(i=2; i<=NF; i++)
           
            if($i==-9)continue; 
            $0 = gensub(/[^[:blank:]]+/, "\b-9" , i)
          
     1
    ' infile 

【讨论】:

为什么是循环?只需gsub(/\t[^\t]*/, "\t-9") 即可。 @tripleee 你是对的,但不确定 OP 的原始输入,因为 OP 说 VAR1-5 我正要制作 i&lt;=6 而不是 i&lt;=NF,感觉当前上下文很好,使用了 gensub在循环中保留原始间距,并使用/(^|\t)-9(\t|$)/ 和示例 i/p 我没有收到预期的 o/p 该示例看起来像是以空格分隔的,但 OP 将其描述为制表符分隔,因此您可能需要稍微调整示例。 是的,谢谢,我回到座位后会检查并调整。【参考方案4】:
sed -r '/-9/s/[^ ]+/-9/2g' input.txt

输出

ID  VAR1  VAR2  VAR3  VAR4  VAR5
1    1    1     1     1     1
2    -9   -9    -9    -9    -9
3    3    3     3     3     3
4    -9    -9     -9     -9    -9
5    5    5     5     5     5
6    -9    -9    -9     -9     -9

【讨论】:

也许请注意-r 是一个不可移植的扩展,它启用扩展正则表达式(的变体);类似的选项-E 可能在其他一些平台上可用。这可以很容易地改写为一个可移植的脚本,尽管它会有点笨拙(你必须用xx* 或者x\+ 替换任何x+)。 [^...] 之间的东西应该是一个文字标签。在许多 shell 中,tab 在交互使用中必须完成,但您可以键入 以在命令行上获取文字制表符。【参考方案5】:
awk 'BEGINIFS=OFS="    "/-9/for(i=2;i<=NF;i++)$i=-91' filename

【讨论】:

虽然这可能是正确的并解决了问题,但最好在此处包含答案的基本部分,以便将来的访问者可以从 OP 的错误中吸取教训。

以上是关于awk / sed:如果任何字段与模式匹配,则替换所有字段的主要内容,如果未能解决你的问题,请参考以下文章

如果一行匹配条件,则 sed 替换与模式范围匹配的行

009day--grep和sed作业及awk作业

文本处理工具之:grep sed awk

sed 或 awk 替换前 14 个匹配项

sed / awk 匹配文件中第二次出现的正则表达式,并替换整行

Linux系列:grep过滤awk拆分sed替换的使用方法与区别