【问题标题】:In AWK, is it possible to specify "ranges" of fields?在 AWK 中,是否可以指定字段的“范围”?
【发布时间】:2012-11-03 16:35:48
【问题描述】:

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

示例。给定一个制表符分隔的文件“foo”,每行有 100 个字段,我只想打印每行的字段 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”)?

【问题讨论】:

标签: awk


【解决方案1】:

除了@Jerry 的awk answer,还有其他选择:

使用cut(默认采用制表符分隔符):

cut -f32-58 foo >bar

使用perl

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

【讨论】:

    【解决方案2】:

    轻微修改的版本:

    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
    • 但是如果您的字段之间有 2 个 FS,则此解决方案会中断:它将用单个 FS 替换它。
    【解决方案3】:

    您可以在 awk 中使用 RE 间隔来执行此操作。例如,要打印此文件中记录的第 3-6 字段:

    $ cat file
    1 2 3 4 5 6 7 8 9
    a b c d e f g h i
    

    应该是:

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

    我正在创建一个 RE 段 f 来表示每个字段加上它的后续字段分隔符(为方便起见),然后我在 gensub 中使用它来删除其中的 2 个(即前 2 个字段),记住下一个4 以供稍后使用 \3 参考,然后删除它们之后的内容。对于要打印字段 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 进行调整。

    【讨论】:

    • 在 awk 中拥有绝对不错的东西!
    • @fred 在 awk 中有一百万种东西会很好,但随后会导致一百万种额外的语言结构,这导致语言膨胀和每个程序中的象形文字完全混乱。如果有人想要这样,那么已经有一种工具/语言可以完全提供 - zoitz.com/archives/13。 awk 语言基于这样一种思想,即应该只存在语言结构来完成其他语言结构难以完成的事情 - 因此,您可以使用一种小巧的语言来做任何易于阅读的事情。
    • 旧帖子,但这会比仅使用循环更快吗? (长线)
    • @Jotne 我希望如此,但我尚未对此进行测试。我之所以这么说,是因为它不仅避免了循环的迭代,而且通过在脚本中不提及任何字段,它关闭了字段拆分,并且对于每条记录仅执行 print gensub(,,s,,e,,) 而不是相当于 split(&lt;input&gt;,$0); for (i=s; i&lt;=e; i++) printf "%s%s", $i, (i&lt;e ? OFS : ORS)
    • @EdMorton 感谢您的回复。如果我有时间,我可能会测试一下:)
    【解决方案4】:

    我迟到了,但是这很快就说到点子上了,所以我将把它留在这里。在这种情况下,我通常只使用 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 周期。

    【讨论】:

    • TY 是一个简单的例子,有助于轻松转换。
    【解决方案5】:

    您可以在 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";
        }
    }}'
    

    不过,这看起来有点老套:

    • 它根据指定的OFS 正确界定您的输出,并且
    • 它确保在文件中的每个输入行的末尾打印一个新行。

    【讨论】:

    • 好点(+1)——但我认为完成这些目标不需要那么长时间。
    • 恐怕这比原始版本需要更长的时间来输入,而且它不能作为 awk 单行,所以需要创建一个中间文件 -> 更多步骤.如果我走这条路,我也可以写一个 Perl 脚本。
    • @gojira 实际上你可以用 1 行这个,我只是把它分解了,所以你可以看看发生了什么
    【解决方案6】:

    我不知道如何在 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

    【讨论】:

    • 如果您想要输入中没有的字符,请使用 RS 而不是“\n”。
    • FYI 删除制表符(或任何其他单个字符)分隔的输入中的前 n 个字段,其中 n 是数字变量将是 sub("([^" FS "]*" FS "){" n "}","")。在这种情况下,这样做的好处是不会像您发布的解决方案那样用空格替换输入中的所有选项卡,除非您设置OFS="\t"。当然,您也需要将 FS 设置为 \t。
    • @EdMorton:因为 RS 或 FS 可以不止一个字符,我不认为在 sub 中使用它们是最好的通用解决方案。
    • @EdMorton:你对我结合 FS 的解决方案也是正确的。
    • RS 在 GNU awk 中只能是一个以上的字符,如果你这样做,那么你不能依赖 "\n" 不是记录的一部分,所以你需要一个无论如何,不​​同的解决方案。最好使用 RS 作为起始假设,然后在证明有必要时修改脚本。是的,如果你的 FS 是 RE,你不能完全像那样使用 sub(),这就是为什么我说它只适用于单字符分隔的字段。
    【解决方案7】:

    不幸的是,我似乎无法再访问我的帐户,但也没有 50 个代表来添加评论。

    使用 'seq' 可以大大简化 Bob 的答案:

    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
    
        $
    

    【讨论】:

      【解决方案8】:

      我使用这个简单的函数,它不会检查字段范围是否存在于行中。

      function subby(f,l, s) {
        s = $f
        for(i=f+1;i<=l;i++)
          s = sprintf("%s %s",s,$i)
      
        return s
      }
      

      【讨论】:

        【解决方案9】:

        (我知道 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
        

        使用分号放在单行上,在 $() 内进行评估/展开。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-05-19
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多