在AWK中,是否可以指定“字段范围”?

56
在AWK中,是否可以指定“范围”字段?
例如。给定一个每行有100个字段的以制表符分隔的文件“foo”,我想仅打印每行的32到57个字段,并将结果保存在文件“bar”中。现在我所做的是:
awk 'BEGIN{OFS="\t"}{print $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52, $53, $54, $55, $56, $57}' foo > bar

问题在于这样做很繁琐且容易出错。

是否有一种语法形式可以让我更简洁、更少出错地表达相同的意思(比如“$32..$57”)?


可能是打印第三列到最后一列的重复问题。 - Ciro Santilli OurBigBook.com
9个回答

37
除了@Jerry提供的awkanswer,还有其他替代方案:

使用cut(默认情况下假定为制表符分隔符):

cut -f32-58 foo >bar

使用perl:
perl -nle '@a=split;print join "\t", @a[31..57]' foo >bar

28

稍作修改后的版本:

BEGIN { s = 32; e = 57; }

      { for (i=s; i<=e; i++) printf("%s%s", $(i), i<e ? OFS : "\n"); }

你可以通过在循环之前执行 printf "%s", $s,从而消除 printf 中的测试。在循环中始于 s+1,始终使用 OFS 作为前缀,并在循环后打印 \n - jfg956
但是如果您的字段之间有2个FS,这种解决方案就会失效:它将把它替换为单个FS。 - jfg956

8
你可以使用awk和正则表达式区间来实现。例如,要打印此文件中记录的第3到第6个字段:
$ cat file
1 2 3 4 5 6 7 8 9
a b c d e f g h i

would be:

$ gawk 'BEGIN{f="([^ ]+ )"} {print gensub("("f"{2})("f"{4}).*","\\3","")}' file
3 4 5 6
c d e f

我正在创建一个RE段f,以表示每个字段以及它后面的字段分隔符(为了方便),然后将其用于gensub中以删除其中的2个(即前两个字段),使用\3记住接下来的4个字段以备后续参考,然后删除其后的内容。对于您想要打印第32-57个字段(即第31个字段之后的26个字段)的制表符分隔文件,您需要使用以下命令:
gawk 'BEGIN{f="([^\t]+\t)"} {print gensub("("f"{31})("f"{26}).*","\\3","")}' file

上面的代码使用的是GNU awk 的gensub()函数。在其他的awk中,你需要使用sub()或match()和substr()。
编辑:下面是编写该功能的方法:
gawk '
function subflds(s,e,   f) {
   f="([^" FS "]+" FS ")"
   return gensub( "(" f "{" s-1 "})(" f "{" e-s+1 "}).*","\\3","")
}
{ print subflds(3,6) }
' file
3 4 5 6
c d e f

只需将FS设置为适当的值。请注意,如果您的输入文件以空格开头和/或在字段之间具有多个空格,则需要调整默认FS,并且仅在FS为单个字符时才有效。


1
在awk中拥有这样的功能绝对是非常好的! - fred
@fred 有很多东西在awk中都是很好的,但这会导致出现无数的语言结构,从而导致语言膨胀和每个程序中都充满了象形文字。如果有人需要这样的功能,已经有一个提供这种功能的工具/语言 - https://www.zoitz.com/archives/13。awk语言的基本思想是只有那些难以用其他语言结构实现的功能才需要语言结构 - 因此,这是一种小型语言,你可以用它做任何事情,并且易于阅读。 - Ed Morton
旧帖子,但是这样做比仅使用循环更快吗?(长行) - Jotne
2
@Jotne 我认为是这样,但我还没有测试过。我这么说是因为它不仅避免了循环的迭代,而且通过在脚本中不提及任何字段来关闭了字段拆分,并且对于每个记录只执行 print gensub(,,s,,e,,) 而不是等价于 split(<input>,$0); for (i=s; i<=e; i++) printf "%s%s", $i, (i<e ? OFS : ORS) - Ed Morton
1
@EdMorton 感谢您的回复。如果我有时间,我可能会测试它 :) - Jotne

7

虽然我有些晚了,但是我会简单明了地解释一下。在这种情况下,通常我会使用gsub删除不需要的字段并打印出剩余的内容。以下是一个简单粗暴的例子:因为您知道文件是由制表符分隔的,所以可以删除前31个字段:

awk '{gsub(/^(\w\t){31}/,"");print}'

因懒惰而删除4个字段的示例:

printf "a\tb\tc\td\te\tf\n" | awk '{gsub(/^(\w\t){4}/,"");print}'

输出:

e   f

相比于可怕的循环,这种写法更短、更易记,并且使用更少的CPU周期。


感谢提供一个简单的例子,易于转换。 - Samveen

2
您可以在awk中使用循环和printf的组合来实现这一点:
#!/bin/bash

start_field=32
end_field=58

awk -v start=$start_field -v end=$end_field 'BEGIN{OFS="\t"}
{for (i=start; i<=end; i++) {
    printf "%s" $i;
    if (i < end) {
        printf "%s", OFS;
    } else {
        printf "\n";
    }
}}'

这看起来有点hacky,但是:

  • 它根据指定的OFS正确地分隔您的输出,
  • 它确保为文件中的每个输入行打印一个新行。

好的观点(+1)- 但我认为为了实现这些目标,它不需要变得如此冗长。 - Jerry Coffin
我担心这个版本的输入甚至比原始版本更长,而且它不能像awk一行命令那样工作,因此需要创建一个中间文件 -> 更多步骤。如果我要走这条路,我可能会写一个Perl脚本。 - user438602
@gojira 实际上你完全可以一行代码解决,我只是将其分解开来,以便你能够看到发生了什么。 - sampson-chen

1

我不知道如何在awk中进行字段范围选择。我知道如何在输入的末尾删除字段(见下文),但不容易在开头删除。以下是在开头删除字段的困难方法。

如果您知道一个未包含在输入中的字符 c ,则可以使用以下awk脚本:

BEGIN { s = 32; e = 57; c = "#"; }
{ NF = e            # Drop the fields after e.
  $s = c $s         # Put a c in front of the s field.
  sub(".*"c, "")    # Drop the chars before c.
  print             # Print the edited line.
}

编辑:

我刚想到,你总是可以找到一个输入中没有的字符:使用\n


1
如果你想要一个不在输入中的字符,使用 RS 代替 "\n"。 - Ed Morton
删除 tab(或任何其他单个字符)分隔输入中的前 n 个字段,其中 n 是数字变量,可以使用 sub("([^" FS "]*" FS "){" n "}","")。这种方法的优点在于,在不设置 OFS="\t" 的情况下,不需要将输入中的所有制表符替换为空格,与您发布的解决方案不同。当然,您也需要将 FS 设置为 \t。 - Ed Morton
@EdMorton:由于RS或FS可以超过一个字符,我不认为在“sub”中使用它们是最好的通用解决方案。 - jfg956
@EdMorton:你也说得对,我的解决方案结合了FS。 - jfg956
在GNU awk中,RS只能是一个以上的字符,如果您这样做,那么不能依赖于"\n"不是记录的一部分,因此您需要另一种解决方案。最好将RS用作起始假设,然后根据需要修改脚本。是的,如果您的FS是RE,则无法完全像那样使用sub(),这就是为什么我说它仅适用于单个字符分隔字段的原因。 - Ed Morton

1
很遗憾,我好像不能再访问我的账户了,但是我也没有50个声望以添加评论。Bob的答案可以使用“seq”大大简化:
echo $(seq -s ,\$ 5 9| cut -d, -f2-)
$6,$7,$8,$9

小缺点是您必须将第一个字段编号指定为较低的数字。因此,要获取3到7个字段,我将2指定为第一个参数。 seq -s ,\$ 2 7 将 seq 的字段分隔符设置为 ',$' 并产生 2,$3,$4,$5,$6,$7 cut -d, -f2- 将字段分隔符设置为 ',' 并基本上切掉第一个逗号之前的所有内容,显示从第二个字段开始的所有内容。因此,结果为$3,$4,$5,$6,$7 与 Bob 的答案结合起来,我们得到:
    $ cat awk.txt

    1 2 3 4 5 6 7 8 9

    a b c d e f g h i

    $ awk "{print $(seq -s ,\$ 2 7| cut -d, -f2-)}" awk.txt

    3 4 5 6 7

    c d e f g

    $

0
(我知道OP要求“用AWK”,但是...)
在命令行上使用bash扩展来生成参数列表;
$ cat awk.txt

1 2 3 4 5 6 7 8 9

a b c d e f g h i

$ awk "{print $(c="" ;for i in {3..7}; do c=$c\$$i, ; done ; c=${c%%,} ; echo $c ;)}" awk.txt

3 4 5 6 7
c d e f g

说明:

c="" # var to hold args list
for i in {3..7} # the required variable range 3 - 7
do 
   # replace c's value with concatenation of existing value, literal $, i value and a comma
   c=$c\$$i, 
done 
c=${c%%,} # remove trailing/final comma
echo $c #return the list string

使用分号放置在单行中,放在$()内以进行评估/扩展。


0
我使用这个简单的函数,它并没有检查字段范围是否存在于该行中。
function subby(f,l, s) {
  s = $f
  for(i=f+1;i<=l;i++)
    s = sprintf("%s %s",s,$i)

  return s
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接