【问题标题】:Move column to start by header keyword in bash将列移动到 bash 中的 header 关键字开头
【发布时间】:2019-06-05 17:17:20
【问题描述】:

我有一个格式如下的文件(data.rdb):

date    star    jdb texp
2013-11-22  epsInd      2400000.23551544    100.
2013-11-22  epsInd      2400000.23551544    100.
2013-11-22  epsInd      2400000.23551544    100.
2013-11-22  HD217987    2400000.23551544    900.
2013-11-22  TOI-134     2400000.23551544    900.
2013-11-22  tauCet      2400000.23551544    60. 
2013-11-22  BD+01316    2400000.23551544    300.
2013-11-22  BD+01316    2400000.23551544    300.
2013-11-22  BD+01316    2400000.23551544    300.
2013-11-22  BD+01316    2400000.23551544    300.

一些属性:

  • 所有列都用制表符分隔
  • 列的宽度不同
  • 单元格的长度可能不同
  • 该文件的列数将比显示的多得多,并且有数百行
  • 列名可以是任何单词,不能有制表符、空格或特殊字符

如何将标题为 jdb 的列移动为第一列?

一些限制:

  • 这将应用于多个文件,jdb 列不会总是出现在同一位置
  • 理想情况下,其余列的顺序不应改变
  • jdb 将始终是最后的第一列。

谢谢!

更新

这是我目前使用的awk 块:

BEGIN {
    numCols = split(column_list,cols)
    OFS="\t"
}
{ sub(/\r$/,"") }
NR==1 {
    for (fldNr=1; fldNr<=NF; fldNr++) {
        f[$fldNr] = fldNr
    }
}
{
    for (colNr=1; colNr<=numCols; colNr++) {
        colName = cols[colNr]
        colVal  = (colNr=1 ? $(f["jdb"]): (colNr <= $(f["jdb"] ? 
$(f[colName] -1) : $(f[colName]))))
        printf "%s%s", colVal, (colNr<numCols ? OFS : ORS)
    }
}

但它没有给我任何输出......我(认为我)做了什么:

  1. 为每个列标题值分配一个数字

  2. 遍历一个范围

    2.1 if iterator = 0 -> 打印列jdb

    2.2 if iterator 打印列号iterator - 1

    2.3 if iterator > jdb的列号->打印列号iterator

(这是我在https://stackoverflow.com/questions/56132249/extract-columns-from-tab-separated-file)提出的问题的延续@


结束结果

最后我最终使用了@Ed Morton 的解决方案:

$ cat move_to_first.awk
BEGIN { FS=OFS="\t" }
NR==1 {
    cols[++numCols] = tgt
    for (fldNr=1; fldNr<=NF; fldNr++) {
        f[$fldNr] = fldNr
        if ($fldNr != tgt) {
            cols[++numCols] = $fldNr
        }
    }
}
{
    for (colNr=1; colNr<=numCols; colNr++) {
        colName = cols[colNr]
        printf "%s%s", $(f[colName]), (colNr<numCols ? OFS : ORS)
    }
}

出于好奇,要将列移动到最后一个位置,上面的代码只需要以下修改:

$ cat move_to_last.awk
BEGIN { 
    FS=OFS="\t" 
    }
NR==1 {
    for (fldNr=1; fldNr<=NF; fldNr++) {
        f[$fldNr] = fldNr
        if ($fldNr != target) {
            cols[++numCols] = $fldNr
        }
    }
    cols[++numCols] = target
}
{
    for (colNr=1; colNr<=numCols; colNr++) {
        colName = cols[colNr]
        printf "%s%s", $(f[colName]), (colNr<numCols ? OFS : ORS)
    }
}

【问题讨论】:

  • 也许考虑安装专门的实用程序来处理类似 csv 的文件,而不是用 bash 和 awk 重新发明轮子,比如 csvkit
  • @EdMorton:我的第一次尝试是两次使用您之前回答中建议的方法,一次选择jdb 列,第二次选择所有其他列。然后我会连接。但这很难看,所以我仍在寻找更优雅的解决方案。我要做的是选择列jdb,将其索引存储在变量中,然后在打印时重新排序...
  • = 是赋值,== 是比较。请参阅 colNr=1 作为脚本中三元组中的条件。

标签: bash awk


【解决方案1】:

嗯,我真的很希望在这里有一个“教人钓鱼”的时刻,但无论如何你都会得到答案,所以......这里是如何调整 the previous answer 来做你现在想做的事情:

$ cat tst.awk
BEGIN { FS=OFS="\t" }
NR==1 {
    cols[++numCols] = tgt
    for (fldNr=1; fldNr<=NF; fldNr++) {
        f[$fldNr] = fldNr
        if ($fldNr != tgt) {
            cols[++numCols] = $fldNr
        }
    }
}
{
    for (colNr=1; colNr<=numCols; colNr++) {
        colName = cols[colNr]
        printf "%s%s", $(f[colName]), (colNr<numCols ? OFS : ORS)
    }
}

$ awk -v tgt=jdb -f tst.awk data.rdb
jdb     date    star    texp
2400000.23551544        2013-11-22      epsInd  100.
2400000.23551544        2013-11-22      epsInd  100.
2400000.23551544        2013-11-22      epsInd  100.
2400000.23551544        2013-11-22      HD217987        900.
2400000.23551544        2013-11-22      TOI-134 900.
2400000.23551544        2013-11-22      tauCet  60.
2400000.23551544        2013-11-22      BD+01316        300.
2400000.23551544        2013-11-22      BD+01316        300.
2400000.23551544        2013-11-22      BD+01316        300.
2400000.23551544        2013-11-22      BD+01316        300.

请注意循环是多么简单,在您希望效率达到的每个输入行中执行一次,因为 所有 确定输出顺序的艰苦工作是在 NR==1 块中完成的对整个文件执行一次。

在这种特殊情况下,您实际上并不关心其他列名,您可以更简洁有效地编写如下:

$ cat tst.awk
BEGIN { FS=OFS="\t" }
NR==1 {
    numOutFlds = 1
    for (inFldNr=1; inFldNr<=NF; inFldNr++) {
        out2inFldNrs[$inFldNr == tgt ? 1 : ++numOutFlds] = inFldNr
    }
}
{
    for (outFldNr=1; outFldNr<=numOutFlds; outFldNr++) {
        inFldNr = out2inFldNrs[outFldNr]
        printf "%s%s", $inFldNr, (outFldNr<numOutFlds ? OFS : ORS)
    }
}

$ awk -v tgt=jdb -f tst.awk data.rdb
jdb     date    star    texp
2400000.23551544        2013-11-22      epsInd  100.
2400000.23551544        2013-11-22      epsInd  100.
2400000.23551544        2013-11-22      epsInd  100.
2400000.23551544        2013-11-22      HD217987        900.
2400000.23551544        2013-11-22      TOI-134 900.
2400000.23551544        2013-11-22      tauCet  60.
2400000.23551544        2013-11-22      BD+01316        300.
2400000.23551544        2013-11-22      BD+01316        300.
2400000.23551544        2013-11-22      BD+01316        300.
2400000.23551544        2013-11-22      BD+01316        300.

【讨论】:

  • 抱歉,有一个演示文稿,所以之前无法回答,但我绝对喜欢teach a man how to fish approach。我现在已经发布了我的尝试。谢谢你的回答,晚上我会努力解决这个问题:)
  • 完美运行,现在是时候理解它了。 :) 谢谢!
  • 此解决方案在处理单个文件时效果很好。如果您想通过单个 awk 调用运行多个文件。将 NR 替换为 FNR。然后记录计数器将在每个文件处重新启动。
  • @DudiBoy 它计算的是字段,而不是记录,所以如果文件都具有相同数量的字段,那么您唯一需要做的就是添加 -i inplace (GNU awk) 来实现工作,而如果它们的字段数量不同,那么您需要删除 FNR==1 部分中的数组,而不仅仅是将 NR 更改为 FNR。多个文件似乎不是 OP 正在处理的内容。
  • 对不起,我暂时没有其他问题,只是检查一下我明白你的意思。感谢所有的帮助! :)
【解决方案2】:

这有点冗长,但确实有效:

awk 'NR==1{for(i=1;i<=NF;i++){if ($i=="jdb") break;}} {printf "%s\t",$i; for (j=1;j<=NF;j++){if (i!=j){printf j==NF||(j==NF-1&&j+1==i)?"%s\n":"%s\t", $j}}}' yourfile.txt

根据 Ed Morton 的出色建议。这是带有适当空格、缩进和换行符的脚本:

    NR == 1 {
            for (i = 1; i <= NF; i++) {
                    if ($i == "jdb") {
                            break
                    }
            }
    }

    {
            printf "%s\t", $i
            for (j = 1; j <= NF; j++) {
                    if (i != j) {
                            printf (j == NF || j == NF - 1 && j + 1 == i ? "%s\n" : "%s\t"), $j
                    }
            }
    }

您可以将其粘贴到它自己的文件中(比如... script.awk),然后调用它:awk -f script.awk yourfile.txt

【讨论】:

  • 谢谢@EdMorton 我不知道gawk -o- 甚至是一件事。我总是格式化手动失控的单行。你就是男人。
【解决方案3】:

所以任务是双重的:

  • 首先确定哪一列是我们要排在第一位的列
  • 然后更改列的顺序

所以:

# our testing input file
cat <<EOF >file
date    star    jdb texp
2013-11-22  epsInd      2400000.23551544    100.
2013-11-22  epsInd      2400000.23551544    100.
2013-11-22  epsInd      2400000.23551544    100.
2013-11-22  HD217987    2400000.23551544    900.
2013-11-22  TOI-134     2400000.23551544    900.
2013-11-22  tauCet      2400000.23551544    60. 
2013-11-22  BD+01316    2400000.23551544    300.
2013-11-22  BD+01316    2400000.23551544    300.
2013-11-22  BD+01316    2400000.23551544    300.
2013-11-22  BD+01316    2400000.23551544    300.
EOF

# my copy+paste messed up tabs with spaces, fix it
sed 's/[[:space:]]\+/\t/g' -i file


# first we need header count.
# I could remove all characters except tabs and use wc -c
# but was lazy, this will not affect performance anyway
hdrcnt=$(
    head -n1 file |
    tr '\t' '\n' |
    wc -l
)

# get the column number that has jdb
# I get the first line
# substitute tab with newlines
# and get the line number with "jdb"
num=$(
    head -n1 file |
    tr '\t' '\n' |
    grep -n jdb | 
    cut -d: -f1
)

# ten I generate the awk script
# so it's like '{print $num, $1, $2 ... except $num ... $hdrcnt }'
awkarg='{print $'"$num"', '"$(
    seq $hdrcnt |
    grep -v "$num" |
    sed 's/\(.*\)/$\1, /' |
    sed '$s/, //' |
    tr -d '\n'
)"'}'

# finally run awk
awk -vIFS='\t' -vOFS='\t' "$awkarg" file

【讨论】:

    【解决方案4】:

    在 Perl 中,您可以从 Text::CSV_XS 库中获益:

    #! /usr/bin/perl
    use warnings;
    use strict;
    
    use Text::CSV_XS;
    
    open my $fh, '<', shift or die $!;
    
    my $csv = 'Text::CSV_XS'->new({sep_char => "\t"});
    
    my $row = $csv->getline($fh);
    
    my ($jdb) = grep $row->[$_] eq 'jdb', 0 .. $#$row;
    
    do {
        unshift @$row, splice @$row, $jdb, 1;
        $csv->say(*STDOUT, $row);
    } while $row = $csv->getline($fh);
    

    【讨论】:

      猜你喜欢
      • 2022-08-03
      • 1970-01-01
      • 1970-01-01
      • 2011-01-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-27
      • 2011-05-09
      相关资源
      最近更新 更多