sh 按匹配的列名值拆分 CSV 文件,同时保留标题

Arg*_*101 1 awk

我有一个包含许多来自表导出的 CSV 文件的目录

tblA.csv

A,B,C
1,1,1
1,2,2
2,2,2
3,3,3
Run Code Online (Sandbox Code Playgroud)

tblB.csv

C,D,A
1,1,1
1,2,2
2,2,2
3,3,3
Run Code Online (Sandbox Code Playgroud)

为了打破文件,我找到了这个脚本

   awk -F, '
      NR== 1 { hdr = $0;next}
      {out = "File" $1 ".csv"}
      printed[$1]++<1 {print hdr >out}
      {print $0 > out}
  ' tblA.csv 
Run Code Online (Sandbox Code Playgroud)

创建 3 个文件

A,B,C
1,1,1
1,2,2
Run Code Online (Sandbox Code Playgroud)
A,B,C
2,2,2
Run Code Online (Sandbox Code Playgroud)
A,B,C
3,3,3
Run Code Online (Sandbox Code Playgroud)

现在对于 tblB.csv 我仍然需要按列 A 打破文件,但该列是第 3 列而不是第 1 列

我想不出一种方法来传递参数 A 让它循环标题以找到与我传递的参数匹配的列名,然后使用该列值破坏文件。或者,如果该列名不存在,则跳过该文件。

Ed *_*ton 6

以下是按名称打印列的方法:

按名称打印列:

$ cat tst.awk
BEGIN { FS="," }
NR==1 {
    for (i=1; i<=NF; i++) {
        f[$i] = i
    }
}
{ print $(f["A"]) }
Run Code Online (Sandbox Code Playgroud)

$ awk -f tst.awk tblA.csv
A
1
1
2
3
Run Code Online (Sandbox Code Playgroud)

$ awk -f tst.awk tblB.csv
A
1
2
2
3
Run Code Online (Sandbox Code Playgroud)

以下是如何使用该习惯用法在每个 Unix 机器上的任何 shell 中使用任何 awk* 来稳健有效地执行您要求的操作:

按分组键值拆分输入文件:

$ cat tst.awk
BEGIN { FS="," }
NR==1 {
    for (i=1; i<=NF; i++) {
        f[$i] = i
    }
    hdr = $0
    next
}
!(tgt in f) { exit }
{ curr = $(f[tgt]) }
curr != prev {
    close(out)
    out = "File" curr ".csv"
    print hdr > out
    prev = curr
}
{ print > out }
Run Code Online (Sandbox Code Playgroud)

$ awk -v tgt='A' -f tst.awk tblA.csv
Run Code Online (Sandbox Code Playgroud)

$ head File*.csv
==> File1.csv <==
A,B,C
1,1,1
1,2,2

==> File2.csv <==
A,B,C
2,2,2

==> File3.csv <==
A,B,C
3,3,3
Run Code Online (Sandbox Code Playgroud)

$ awk -v tgt='A' -f tst.awk tblB.csv
Run Code Online (Sandbox Code Playgroud)

$ head File*.csv
==> File1.csv <==
C,D,A
1,1,1

==> File2.csv <==
C,D,A
1,2,2
2,2,2

==> File3.csv <==
C,D,A
3,3,3
Run Code Online (Sandbox Code Playgroud)

以上假设输入文件按示例输入中所示的键字段分组 - 如果不是,则可以在 awk 脚本中处理:

仅使用 AWK 按非分组键值拆分输入文件:

$ cat tblC.csv
C,D,A
2,2,3
1,2,2
1,1,3
3,3,1
Run Code Online (Sandbox Code Playgroud)

$ cat tst.awk
BEGIN { FS="," }
NR==1 {
    for (i=1; i<=NF; i++) {
        f[$i] = i
    }
    hdr = $0
    next
}
{ curr = $(f[tgt]) }
curr != prev {
    close(out)
    out = "File" curr ".csv"
    if ( !doneHdr[curr]++ ) {
        print hdr > out
    }
    prev = curr
}
{ print >> out }
Run Code Online (Sandbox Code Playgroud)

$ awk -v tgt='A' -f tst.awk tblC.csv
Run Code Online (Sandbox Code Playgroud)

$ head File*.csv
==> File1.csv <==
C,D,A
3,3,1

==> File2.csv <==
C,D,A
1,2,2

==> File3.csv <==
C,D,A
2,2,3
1,1,3
Run Code Online (Sandbox Code Playgroud)

但是如果您的文件很大,则对它们进行排序会更有效,以便在运行 awk 脚本之前对键值进行分组,这样 awk 就不必重复打开/关闭输出文件:

使用 sort+AWK 按非分组键值拆分输入文件(对大文件更有效):

$ cat tst.sh
#!/usr/bin/env bash

tgt="$1"
shift

awk -v tgt="$tgt" '
    BEGIN { FS=","; OFS="\t" }
    NR==1 {
        for (i=1; i<=NF; i++) {
            f[$i] = i
        }
    }
    { print (NR>1), $(f[tgt]), NR, $0 }
' "${@:--}" |
sort -k1,1n -k2,2 -k3,3n |
cut -f4- |
awk -v tgt="$tgt" '
    BEGIN { FS="," }
    NR==1 {
        for (i=1; i<=NF; i++) {
            f[$i] = i
        }
        hdr = $0
        next
    }
    { curr = $(f[tgt]) }
    curr != prev {
        close(out)
        out = "File" curr ".csv"
        print hdr > out
        prev = curr
    }
    { print > out }
'
Run Code Online (Sandbox Code Playgroud)

$ ./tst.sh 'A' tblC.csv
Run Code Online (Sandbox Code Playgroud)

$ head File*.csv
==> File1.csv <==
C,D,A
3,3,1

==> File2.csv <==
C,D,A
1,2,2

==> File3.csv <==
C,D,A
2,2,3
1,1,3
Run Code Online (Sandbox Code Playgroud)

上面的工作首先使用 awk 通过在每一行前面添加来装饰原始输入:

  1. NR>1= 是否标头,0 或 1 指示符,因此我们可以确保标头始终在sort,
  2. $(f[tgt]) = 我们要排序的键值,
  3. NR= 当前行号,因此我们得到与重复键输入相同的顺序输出(也可以不添加,然后使用 GNU sort for -s

然后我们对这些字段进行排序,然后在主 awk 脚本开始创建输出文件之前使用cut(可以在随后的 awk 脚本中这样做但cut很有效并避免混乱)再次删除它们。

在此类 AWK 脚本中需要注意的事项:

* 如果您得到其他答案以执行以下任一操作,请注意:

  1. close()一旦超过可能低至 15 的阈值,任何没有输出文件的解决方案都会在大多数 awk 中因“打开的文件太多”而失败,甚至支持无限“打开”文件的 awk,例如 GNU awk 也会失败放慢超过该阈值的速度,因为它必须通过在幕后实际需要在操作系统中打开/关闭它们来管理所有这些“打开”文件,并且
  2. 任何print > "File" $1 ".csv"在右侧的表达式周围使用或类似但没有括号的解决方案>在大多数 awks 中都将失败并出现语法错误,因为这是未定义的行为。