【问题标题】:Renumbering a column based on occurrence of a string根据字符串的出现对列重新编号
【发布时间】:2018-12-18 04:04:58
【问题描述】:

对 linux 相当陌生,我很抱歉。

我有一个这样的文件:

1   C   foo   C     bar
2   C   foo   C     bar
3   C   foo   C     bar
4   H   foo   H     bar
5   H   foo   H     bar
6   O   foo   O     bar

我需要让它成为:

1   C01 foo   C     bar
2   C02 foo   C     bar
3   C03 foo   C     bar
4   H01 foo   H     bar
5   H02 foo   H     bar
6   O01 foo   O     bar

**遗憾的是 foo 和 C 之间的间距以及 C 和 bar 之间的间距必须保持。

我以分段方式尝试过,我从中提取出包含不同标识符 C、H 和 O 的行,并将它们放在一个临时文件中。然后我尝试按出现顺序排列它们,然后将原始文件重新拼接在一起。

    #!/bin/bash

    sed -i -e "/ C /w temp1.txt" -e "//d" File.txt
    sed -i -e "/ H /w temp2.txt" -e "//d" File.txt
    sed -i -e "/ O /w temp3.txt" -e "//d" File.txt


    `awk -i '{print NR $2}' temp1.txt
    awk -i '{print NR $2}' temp2.txt
    awk -i '{print NR $2}' temp3.txt

    cat temp1.txt >> File.txt
    cat temp2.txt >> File.txt
    cat temp3.txt >> File.txt

但是我很确定我的语法很糟糕,因为我真的只熟悉 sed 而不是 awk。

任何帮助将不胜感激,谢谢。

【问题讨论】:

    标签: awk sed seq


    【解决方案1】:

    相同的解决方案,同时保留初始字段位置

    $ awk '{r=sprintf("%02d",++a[$2]); sub($2"  ",$2r)}1' file
    
    1   C01 foo   C     bar
    2   C02 foo   C     bar
    3   C03 foo   C     bar
    4   H01 foo   H     bar
    5   H02 foo   H     bar
    6   O01 foo   O     bar
    

    请注意,这假设第一个字段值不与所示的第二个字段值重叠,否则您需要注意仅保留对第二个字段的更改。对于第二个字段,可以通过使用单个空格为匹配和替换值添加前缀来轻松完成。

    【讨论】:

    • 非常感谢!这为我节省了大量时间。
    • @WagnerAG 希望你能用节省的时间做一些有用的事情:)
    • .... 例如阅读 Arnold Robbins 的“Effective AWK Programming”,第 4 版 :-)。
    【解决方案2】:

    编辑: 这是一个使用 GNU awk 的解决方案,它保留了实际空间。如果您的 split 支持 4 个参数。阅读手册页后我得到了它,即使我很高兴找到它,它也会有所帮助。

    awk '
    {
      n=split($0,array," ",b)
      array[2]=sprintf("%s%02d",array[2],++a[array[2]])
      line=b[0]
      for(i=1;i<=n;i++){
        line=(line array[i] b[i])
      }
      print line
    }'  Input_file
    1   C01   foo   C     bar
    2   C02   foo   C     bar
    3   C03   foo   C     bar
    4   H01   foo   H     bar
    5   H02   foo   H     bar
    6   O01   foo   O     bar
    

    关于 GNU awk 手册页中的 split 4 个参数:

       split(s, a [, r [, seps] ])
                               Split the string s into the array a and the separators array seps on the regular expression r, and return the
    

    字段数。如果 r 被省略,使用 FS 代替。首先清除数组 a 和 seps。 seps[i] 是字段 由 r 匹配的分隔符 a[i] 和 a[i+1]。如果 r 是单个空格,则 s 中的前导空格进入额外的数组元素 seps[0] 和尾随白色- 空间进入额外的数组元素 seps[n],其中 n 是 split(s, a, r, seps) 的返回值。 拆分行为相同 字段拆分,如上所述。



    第一种解决方案:请您尝试以下方法,

    awk '{$2=sprintf("%s%02d",$2,++a[$2])} 1' Input_file
    

    输出如下。

    1 C01 bar C
    2 C02 bar C
    3 C03 bar C
    4 H01 bar H
    5 H02 bar H
    6 O01 bar O
    

    第二个解决方案:如果您想在 $2 和 $4 两个地方都有值,请执行以下操作。

    awk '{$2=$4=sprintf("%s%02d",$2,++a[$2])} 1'  Input_file
    1 C01 bar C01
    2 C02 bar C02
    3 C03 bar C03
    4 H01 bar H01
    5 H02 bar H02
    6 O01 bar O01
    

    第三种解决方案:如果您想在最后一行添加/插入新列,请执行以下操作。

    awk '{$(NF+1)=sprintf("%s%02d",$2,++a[$2])} 1'  Input_file
    1 C bar C C01
    2 C bar C C02
    3 C bar C C03
    4 H bar H H01
    5 H bar H H02
    6 O bar O O01
    

    【讨论】:

    • 这太棒了!谢谢!现在,如果我需要将列更改为第 4 列,我只需将出现的 $2 替换为 $4?
    • @WagnerAG,你的意思是 2 和 4 应该有相同的值?
    • @WagnerAG,请检查我的 EDIT 解决方案,如果有帮助,请告诉我。
    • 非常感谢您的帮助@RavinderSingh13 我已经编辑了我上面的帖子以反映我的意思
    • @WagnerAG,请立即检查我的 EDIT 解决方案并告诉我?
    【解决方案3】:

    使用 GNU awk 将第三个 arg 转换为 match()\S/\s 简写为 [^[:space]:]]/[[:space:]]

    $ awk 'match($0,/(\S+\s+)(\S+)(.*)/,a){ printf "%s%s%02d%s\n", a[1], a[2], ++cnt[a[2]], a[3] }' file
    1   C01   foo   C     bar
    2   C02   foo   C     bar
    3   C03   foo   C     bar
    4   H01   foo   H     bar
    5   H02   foo   H     bar
    6   O01   foo   O     bar
    

    上述内容适用于 ALL 输入,即使前面的字段与目标字段具有相同的值,或者目标字段包含 RE 元字符或其他任何内容。

    以上是修改第二个字段。一般来说,要修改 n=4 的第 n 个字段,例如,硬编码将是:

    $ awk 'match($0,/((\S+\s+){3})(\S+)(.*)/,a){ printf "%s%s%02d%s\n", a[1], a[3], ++cnt[a[3]], a[4]}' file
    1   C   foo   C01     bar
    2   C   foo   C02     bar
    3   C   foo   C03     bar
    4   H   foo   H01     bar
    5   H   foo   H02     bar
    6   O   foo   O01     bar
    

    如果它作为参数而不是硬编码传递:

    $ awk -v n=4 'match($0,"((\\S+\\s+){"n-1"})(\\S+)(.*)",a){ printf "%s%s%02d%s\n", a[1], a[3], ++cnt[a[3]], a[4]}' file
    1   C   foo   C01     bar
    2   C   foo   C02     bar
    3   C   foo   C03     bar
    4   H   foo   H01     bar
    5   H   foo   H02     bar
    6   O   foo   O01     bar
    

    【讨论】:

      【解决方案4】:

      使用简单的 awk 脚本:

      $ awk '{$2=sprintf("%s%02d",$2,++a[$2]);}1' file
      1 C01 foo C
      2 C02 foo C
      3 C03 foo C
      4 H01 foo H
      5 H02 foo H
      6 O01 foo O
      

      【讨论】:

      • 就是这个!谢谢!但是,我有一个问题;在我使用的实际文件中,有不同间距的列,使用它后,格式会分崩离析。因此,如果上面的文件也有 5 个空格之外的单词 bar,它现在变成了单行距。
      【解决方案5】:

      虽然 Perl 没有被标记,但它似乎很适合这些情况。如果您正在考虑使用 Perl,请查看此内容。

      > cat wagner.txt
      1   C   foo   C     bar
      2   C   foo   C     bar
      3   C   foo   C     bar
      4   H   foo   H     bar
      5   H   foo   H     bar
      6   O   foo   O     bar
      > perl -pe 's/(\s+)(\S+)(\s+)/sprintf("%s%s%02d%s",$1,$2,++$kv{$2},$3)/e ' wagner.txt
      1   C01   foo   C     bar
      2   C02   foo   C     bar
      3   C03   foo   C     bar
      4   H01   foo   H     bar
      5   H02   foo   H     bar
      6   O01   foo   O     bar
      >
      

      感谢 Karakfa,可以通过移除 $3 来进一步缩短答案

      >  perl -pe 's/(\s+)(\S+)/sprintf("%s%s%02d",$1,$2,++$kv{$2})/e ' wagner.txt
      1   C01   foo   C     bar
      2   C02   foo   C     bar
      3   C03   foo   C     bar
      4   H01   foo   H     bar
      5   H02   foo   H     bar
      6   O01   foo   O     bar
      >
      

      另一种方法是进一步删除一个组

      > perl -pe 's/([^^]\S+)/sprintf("%s%02d",$1,++$kv{$1})/e ' wagner.txt
      1   C01   foo   C     bar
      2   C02   foo   C     bar
      3   C03   foo   C     bar
      4   H01   foo   H     bar
      5   H02   foo   H     bar
      6   O01   foo   O     bar
      >
      

      或使用环视

      perl -pe 's/([^?!]\S+)/sprintf("%s%02d",$1,++$kv{$1})/e ' wagner.txt
      

      【讨论】:

      • @karakfa 如果我这样做,那么 %02d 格式将导致 C 01 在 C 和 01 之间有一个空格
      • 好的,你必须用数字替换两个空格。 sprintf 需要相应更改。
      • 是的。我认为这里不需要 3 美元。让我检查并更新
      • 刚刚找到了另一种方式[^^]\S+
      【解决方案6】:
      $ awk 'BEGIN{FS=OFS=""}{$6="";$7=((b=++a[$5])>9?"":0) b}1' file file file file
      1   C01 foo   C     bar
      2   C02 foo   C     bar
      3   C03 foo   C     bar
      4   H01 foo   H     bar
      ...
      6   O03 foo   O     bar
      1   C10 foo   C     bar
      2   C11 foo   C     bar
      

      解释:

      $ awk 'BEGIN {
          FS=OFS=""                 # empty field separators
      }
      {
          $6=""                     # null $6
          $7=((b=++a[$5])>9?"":0) b # $7 carries the count, with leading 0 if below 10
      }1' file
      

      【讨论】:

      • $6=int(++a[$5]/10); $7=a[$5]%10
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-01
      相关资源
      最近更新 更多