【问题标题】:Bash: Set theoryBash:集合论
【发布时间】:2017-07-28 13:43:48
【问题描述】:

我有以下制表符分隔的表格:

    A   B   C   D   E   F   G   H   I   J
ZO1     X1  X2  X3          X4      X5  X6
ZO2 X7  X8  X9  X10     X11 X12 X13 X14 X15
ZO3 X16 X17 X18 X19         X20     X21 X22
ZO4     X23 X24 X25         X26     X27 X28
ZO5     X29 X30                         
ZO6     X31 X32 X33 X34 X35 X36 X37 X38 X39
ZO7 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49
ZO8     X50 X51 X52         X53     X54 X55

(X##是一个随机字符串)

我想提取第 1 列中满足特定条件的值。示例性条件是:检索所有值(第 1 列),在 B、C、D、G、I、J 列中具有非空值,在其余列 A、E、F、H 中具有空值。

所以一个示例输出是:

Z01
Z04
Z08

编辑:抱歉输入不佳。在分号分隔的表格下方;真正的输入是制表符分隔的

;A;B;C;D;E;F;G;H;I;J
ZO1;;X1;X2;X3;;;X4;;X5;X6
ZO2;X7;X8;X9;X10;;X11;X12;X13;X14;X15
ZO3;X16;X17;X18;X19;;;X20;;X21;X22
ZO4;;X23;X24;X25;;;X26;;X27;X28
ZO5;;X29;X30;;;;;;;
ZO6;;X31;X32;X33;X34;X35;X36;X37;X38;X39
ZO7;X40;X41;X42;X43;X44;X45;X46;X47;X48;X49
ZO8;;X50;X51;X52;;;X53;;X54;X55

【问题讨论】:

  • 放入正确的嵌入示例。
  • 在您的示例中放置分号或其他我们可以看到/使用的标签,而不是标签,然后我们将有一些我们可以测试的东西,因此能够帮助您。
  • @EdMorton 但我的示例是制表符分隔的,我应该如何给出真实的输入?
  • 您的真实输入由单个字符分隔,即制表符。您发布的示例输入未由单个字符分隔,也无法通过此站点上的选项卡分隔,因此您无法在此站点上发布“现实”输入,因此需要妥协。发布逗号分隔的输入并简单地声明“在我的真实文件中逗号是制表符”比发布由多个空格分隔的输入(这需要不同的工具)要有用得多,因此我们有一个由单个分隔的输入char 进行测试,并且可以对作为选项卡的真实输入进行任何必要的调整/陈述。

标签: bash awk set-theory


【解决方案1】:

我喜欢这个,如果你将它整个复制并粘贴到 bash、cmets 等中,它就会运行。

tail -n +2 file              `# Grab the bit of the file you car about` \
|  sed 's/;/|;/'           `# Protect the first column`               \
|  sed 's/;[^;][^;]*/1/g' `# Change all the filled values to 1`      \
|  sed 's/;/0/g'            `# Change the empty values to 0`

该命令的输出如下所示:

 ZO1|0111001011
 ZO2|1111011111
 ZO3|1111001011
 ZO4|0111001011
 ZO5|0110000000
 ZO6|0111111111
 ZO7|1111111111
 ZO8|0111001011

所以现在我可以设置我正在寻找的模式。

tail -n +2 file              `# Grab the bit of the file you car about` \
|  sed 's/;/|;/'           `# Protect the first column`               \
|  sed 's/;[^;][^;]*/1/g' `# Change all the filled values to 1`      \
|  sed 's/;/0/g'            `# Change the empty values to 0`           \
|  grep "|0111001011"        `# Grab the match you want`                \
|  sed  's/|.*//'            `# Clear out the garbage`

然后用一个函数对其进行泛化

>> function table_match () {
    cat                          `# Grab the stdin`                     \
    |  sed 's/;/|;/'           `# Protect the first column`           \
    |  sed 's/;[^;][^;]*/1/g' `# Change all the filled values to 1`  \
    |  sed 's/;/0/g'            `# Change the empty values to 0`       \
    |  grep "|${1}"              `# Grab the match you want`            \
    |  sed  's/|.*//'            `# Clear out the garbage`;
}


>> tail -n +2 file | table_match 0111001011
ZO1
ZO4
ZO8

我也可以做其他事情...点通配符...kleene star...漂亮。

>> tail -n +2 file | table_match .......011
ZO1
ZO2
ZO3
ZO4
ZO5
ZO6
ZO7
ZO8

>> tail -n +2 file | table_match 01*
ZO1 
ZO4 
ZO5 
ZO6 
ZO8 

【讨论】:

  • 您可以将前三个 sed 替换组合成一个命令:sed -e 'first' -e 'second' -e 'third'...
  • @EdMorton 是的,看起来像。我不得不伪造输入,因为我无法复制和粘贴,而且我有一些错误的字符。
  • 奇怪的是,用 0/1 替换字段的两行不起作用
  • 如果文件实际上不是制表符分隔的,我不会。你确定它的制表符分隔并且没有设置宽度空格吗?
  • 我更新了它以使用您提供的分号分隔文件。不确定标签文件的问题。
【解决方案2】:

你会想要这样的东西:

awk -v pres='B,C,D,G,I,J' '
    BEGIN { FS="\t" }
    FNR==1 {
        split(pres,tmp,/,/)
        for (i in tmp) {
            presNames[tmp[i]]
        }
        for (i=2; i<=NF; i++) {
            if ($i in presNames) {
                mustBePresent[i]
            }
        }
        next
    }
    {
        pass = 1
        for (i=1; i<=NF; i++) {
            if ( ($i == "") &&  (i in mustBePresent) ) { pass = 0 }
            if ( ($i != "") && !(i in mustBePresent) ) { pass = 0 }
        }
        if (pass) {
            print $1
        }
    }
' file

未经测试,因为您没有提供我们可以轻松复制/粘贴以进行测试的示例输入。

我实际上喜欢 @gbtimmon's approach 创建字段的位图比上面的要好一些,所以这里是你在 awk 中的做法:

awk -v pres='B,C,D,G,I,J' '
    BEGIN { FS="\t" }
    FNR==1 {
        split(pres,tmp,/,/)
        for (i in tmp) {
            presNames[tmp[i]]
        }
        req = 1
        for (i=2; i<=NF; i++) {
            req = req ($i in presNames ? 1 : 0)
        }
        next
    }
    {
        act = 1
        for (i=2; i<=NF; i++) {
            act = act ($i == "" ? 0 : 1)
        }
        if (act == req) {
            print $1
        }
    }
' file

【讨论】:

    【解决方案3】:

    给定:

    $ printf "\tA\tB\tC\tD\tE\tF\tG\tH\tI\tJ
    ZO1\t\tX1\tX2\tX3\t\t\tX4\t\tX5\tX6
    ZO2\tX7\tX8\tX9\tX10\t\tX11\tX12\tX13\tX14\tX15
    ZO3\tX16\tX17\tX18\tX19\t\t\tX20\t\tX21\tX22
    ZO4\t\tX23\tX24\tX25\t\t\tX26\t\tX27\tX28
    ZO5\t\tX29\tX30\t\t\t\t\t\t\t
    ZO6\t\tX31\tX32\tX33\tX34\tX35\tX36\tX37\tX38\tX39
    ZO7\tX40\tX41\tX42\tX43\tX44\tX45\tX46\tX47\tX48\tX49
    ZO8\t\tX50\tX51\tX52\t\t\tX53\t\tX54\tX55\n" > file
    

    在 Ruby 中:

    $ sed -E '1 s/^(.*)$/hdr\1/' /tmp/file | 
      ruby -e 'require "csv"
               options={:col_sep=>"\t", :headers=>true}
               CSV.parse($<, options){ |r| 
                   puts r["hdr"] if ("B|C|D|G|I|J".split("|").map{ |e| r[e]!=nil }.all? \
                                 && "A|E|F|H".split("|").map { |e| r[e]==nil }.all?) } '
    ZO1
    ZO4
    ZO8
    

    或者,不那么简洁:

    $ sed -E '1 s/^(.*)$/hdr\1/' /tmp/file | 
    ruby -e 'require "csv"
             options={:col_sep=>"\t", :headers=>true}
             CSV.parse($<, options)
                .select { |r| "B|C|D|G|I|J".split("|").map{ |e| r[e]!=nil }.all? }
                .select { |r| "A|E|F|H".split("|").map { |e| r[e]==nil }.all? }
                .map { |r| puts r["hdr"] } '
    

    两种情况:

    1. 使用sed 插入hdr 字段,因为标题行比下面的数据少一个字段;
    2. 使用CSV模块读取修改后的文件;
    3. 在 CSV 模块中为空白字段分配了nil。使用它来选择您描述的逻辑。

    在 Ruby 中使用 gbtimmon used 的真值表方法:

    $ sed -E '1 s/^(.*)$/hdr\1/' file |
    ruby -e 'require "csv"
            options={:col_sep=>"\t", :headers=>true}
            tt=CSV.parse($<, options)
                .map { |r| [r[0], r[1..-1].map { |e| e==nil ? "0" : "1" }.join ] }
                .group_by { |hdr, bits| bits }
                .map { |bits,lol| [bits, lol.map(&:first)] }.to_h 
            tt.map { |k, a| puts "#{k} => #{a.join(%q(, ))}" if k=~/^./ } '
    0111001011 => ZO1, ZO4, ZO8
    1111011111 => ZO2
    1111001011 => ZO3
    0110000000 => ZO5
    0111111111 => ZO6
    1111111111 => ZO7
    

    您可以在正则表达式文字k=~/^./ 中添加任何正则表达式以产生所需的结果。

    awk:

    $ awk 'BEGIN { FS="\t"; OFS=", " }
          NR==1 { next } 
                { ind=""
                  for (i=2;i<=NF;i++)
                     ind=ind ($i=="" ? "0" : "1")
                map[ind]=map[ind] ? map[ind] OFS $1 : $1
                }  
          END   { for( e in map) printf "%s => %s\n", e, map[e] }' file
    0111111111 => ZO6
    0111001011 => ZO1, ZO4, ZO8
    0110000000 => ZO5
    1111111111 => ZO7
    1111011111 => ZO2
    1111001011 => ZO3
    

    表格条目将以无序的结果出现,但随后通过管道将其传送到sedgrep 以选择所需的行(或行的一部分)(或在末尾的awk 循环内)。

    最好的

    【讨论】:

    • 这个awk解决方案规则!
    【解决方案4】:

    有几种简单的方法可以做到这一点。这是一个更像 C 的语法:

    awk -F'\t' '{if( $2=="" && $3!="" && $4!="" && $5!="" && $6=="" && $7=="" && $8!="" && $9=="" && $10!="" && $11!="" ) print $1}' table_file
    

    还有另一个更压缩的 awk 原生语法版本,如下面的 cmets 中的 karakfa 所建议:

    awk -F'\t' '$3!="" && $4!="" && $5!="" && $8!="" && $10!="" && $11!="" && $2$6$7$9 == "" {print $1}' table_file
    

    【讨论】:

    • 为什么这个答案被否决了?只是因为缺少大括号?
    • @tobi,感谢您发现丢失的括号!不知道为什么它被否决了。这不是一个动态的解决方案,但可以完成工作。
    • 好吧,列数是错误的,这应该可以:awk -F'\t' '{if( $2=="" &amp;&amp; $3!="" &amp;&amp; $4!="" &amp;&amp; $5!="" &amp;&amp; $6=="" &amp;&amp; $7=="" &amp;&amp; $8!="" &amp;&amp; $9=="" &amp;&amp; $10!="" &amp;&amp; $11!="" ) print $1}'
    • 您可以通过将条件移出块来删除if。您也可以将空值合并为一个 $2$6$7$9==""
    • 显示类似 C 或类似 Lisp 或类似 Prolog 或任何其他类似的语法是没有用的。这是一个 awk 脚本,所以只显示 awk 语法。
    【解决方案5】:

    “直接”awk 解决方案的问题是解析空间以及 awk 看不到空字段这一事实,因此我们必须将 sed 与 awk 一起使用。

    sed -rn 's/([[:alpha:]]+)|([[:blank:]]{4})/,&/gp' filename | sed -rn 's/[[:blank:]]//gp' filename | awk -F , 'NR > 1 { if ( $3 == "" && $4 != "" && $5 != "" && $6 != "" && $7 == "" && $8 == "" && $9 != "" && $10 == "" && $11 != "" && $12 != "" ) { print $2 } }'
    
    
    
    sed -rn 's/([[:alpha:]]+)|([[:blank:]]{4})/,&/gp' filename | sed -rn 's/[[:blank:]]//gp' filename
    

    首先使用 sed,在任何字符前添加逗号或 4 个空格。然后运行第二个 sed 语句来删除空格。

    这让你有

    ,,A,B,C,D,E,F,G,H,I,J
    ,ZO1,,X1,X2,X3,,,X4,,X5,X6
    ,ZO2,X7,X8,X9,X10,,X11,X12,X13,X14,X15
    ,ZO3,X16,X17,X18,X19,,,X20,,X21,X22
    ,ZO4,,X23,X24,X25,,,X26,,X27,X28
    ,ZO5,,X29,X30,,,,,,
    ,ZO6,,X31,X32,X33,X34,X35,X36,X37,X38,X39
    ,ZO7,X40,X41,X42,X43,X44,X45,X46,X47,X48,X49
    ,ZO8,,X50,X51,X52,,,X53,,X54,X55
    

    然后使用awk来处理这个数据:

    awk -F , 'NR > 1 { if ( $3 == "" && $4 != "" && $5 != "" && $6 != "" && $7 == "" && $8 == "" && $9 != "" && $10 == "" && $11 != "" && $12 != "" ) { print $2 } }'
    

    使用 , 作为字段分隔符,然后根据某些条件检查分隔字段。

    输出:

    ZO1
    ZO4
    ZO8
    

    【讨论】:

      猜你喜欢
      • 2023-03-28
      • 1970-01-01
      • 2012-01-17
      • 1970-01-01
      • 1970-01-01
      • 2020-07-29
      • 1970-01-01
      • 2017-04-18
      • 1970-01-01
      相关资源
      最近更新 更多