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
这很有效,但在应用于实际数据集时非常慢。有没有更快的方法来做到这一点?也许是grep
和sed
的组合?
【问题讨论】:
如果您已经在使用 Awk,它会做所有sed
和grep
可以做的事情,而且通常更快。
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<=6
而不是 i<=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 在交互使用中必须完成,但您可以键入 awk 'BEGINIFS=OFS=" "/-9/for(i=2;i<=NF;i++)$i=-91' filename
【讨论】:
虽然这可能是正确的并解决了问题,但最好在此处包含答案的基本部分,以便将来的访问者可以从 OP 的错误中吸取教训。以上是关于awk / sed:如果任何字段与模式匹配,则替换所有字段的主要内容,如果未能解决你的问题,请参考以下文章