【问题标题】:Merge CSV files using join/awk/sed使用 join/awk/sed 合并 CSV 文件
【发布时间】:2015-02-20 10:49:00
【问题描述】:

您能帮我找到 bash 命令,它将加入/合并以下 cvs 文件“template.csv + file1.csv + file2.csv + file3.csv + ... + fileX.csv”到“输出.csv”。

对于 template.csv 中的每一行,连接 fileX.csv 中列出的相关值(如果存在),如下所示:

模板.csv:

header
1
2
3
4
5
6
7
8
9

file1.csv:

header,value1
2,value12
3,value13
7,value17
8,value18
9,value19

file2.csv:

header,value2
1,value21
2,value22
3,value23
4,value24

file3.csv:

header,value3
2,value32
4,value34
6,value36
7,value37
8,value38

输出.csv:

header,value1,value2,value3
1,,value21,
2,value12,value22,value32
3,value13,value23,
4,,value24,value34
5,,,
6,,,value36
7,value17,,value37
8,value18,,value38
9,value19,,

我的模板文件包含 35137 行。
我已经开发了一个执行此合并的 bash 脚本(基于“do while”等),但性能一点也不好。制作 output.csv 的时间太长。我确信可以使用 join、awk 来做同样的事情……但我不明白如何……

重要更新

我的真实文件的第一列包含一个日期时间而不是一个简单的数字......所以脚本必须考虑日期和时间之间的空间......抱歉更新!

现在应该使用以下 csv 文件设计脚本:

模板.csv:

header
2000-01-01 00:00:00
2000-01-01 00:15:00
2000-01-01 00:30:00
2000-01-01 00:45:00
2000-01-01 01:00:00
2000-01-01 01:15:00
2000-01-01 01:30:00
2000-01-01 01:45:00
2000-01-01 02:00:00

file1.csv:

header,value1
2000-01-01 00:15:00,value12
2000-01-01 00:30:00,value13
2000-01-01 01:30:00,value17
2000-01-01 01:45:00,value18
2000-01-01 02:00:00,value19

file2.csv:

header,value2
2000-01-01 00:00:00,value21
2000-01-01 00:15:00,value22
2000-01-01 00:30:00,value23
2000-01-01 00:45:00,value24

file3.csv:

header,value3
2000-01-01 00:15:00,value32
2000-01-01 00:45:00,value34
2000-01-01 01:15:00,value36
2000-01-01 01:30:00,value37
2000-01-01 01:45:00,value38

输出.csv:

header,value1,value2,value3
2000-01-01 00:00:00,,value21,
2000-01-01 00:15:00,value12,value22,value32
2000-01-01 00:30:00,value13,value23,
2000-01-01 00:45:00,,value24,value34
2000-01-01 01:00:00,,,
2000-01-01 01:15:00,,,value36
2000-01-01 01:30:00,value17,,value37
2000-01-01 01:45:00,value18,,value38
2000-01-01 02:00:00,value19,,

【问题讨论】:

    标签: bash csv join awk sed


    【解决方案1】:
    $ cat tst.awk
    BEGIN { FS=OFS="," }
    NR == FNR { key[++numRows] = $1 }
    { fld[$1,ARGIND] = $NF }
    END {
        for (rowNr=1; rowNr<=numRows; rowNr++) {
            for (colNr=1; colNr<=ARGIND; colNr++) {
                printf "%s%s", fld[key[rowNr],colNr], (colNr<ARGIND ? OFS : ORS)
            }
        }
    }
    
    $ awk -f tst.awk template.csv file1.csv file2.csv file3.csv
    header,value1,value2,value3
    2000-01-01 00:00:00,,value21,
    2000-01-01 00:15:00,value12,value22,value32
    2000-01-01 00:30:00,value13,value23,
    2000-01-01 00:45:00,,value24,value34
    2000-01-01 01:00:00,,,
    2000-01-01 01:15:00,,,value36
    2000-01-01 01:30:00,value17,,value37
    2000-01-01 01:45:00,value18,,value38
    2000-01-01 02:00:00,value19,,
    

    上面使用 GNU awk 表示 ARGIND,其他 awk 只需添加一行 FNR==1 { ++ARGIND }

    【讨论】:

    • 这个解决方案也很完美......比 user43791 提出的命令复杂一点
    • 您真的认为仅填充和打印由列和行索引的字段数组比使用模糊选项的管道连接链更复杂吗?这对我来说似乎很直接,但我想是 YMMV。
    • @EdMorton 在 Adrien 的辩护中,使用三个易于理解的选项(通过man)的文档化且非常严格的工具对我来说听起来确实比 awk 中专门制作的程序更简单。话虽这么说,我确实喜欢你的回答,因为它只使用一个进程,写得非常优雅,而且可能运行得也很快。投赞成票! ;)
    • 哦,好吧,我想当然是马,也许我只是停留在一个范式中,因为在 30 多年的 UNIX 编程中,我从未遇到过 -o auto 加入选项。话虽如此,当其中一个文件包含不在模板文件中的日期或任何其他意外的日期时,尝试修改连接链以打印警告 - 当您的陈述点是 awk 命令时,这很简单,但否则...... ;-)。
    【解决方案2】:

    您可以多次调用join

    join -t , -a 1 -o auto template.csv file1.csv | join -t , -a 1 -o auto - file2.csv | join -t , -a 1 -o auto - file3.csv
    

    或者更清楚:

    alias myjoin='join -t , -a 1 -o auto'
    myjoin template.csv file1.csv | myjoin - file2.csv | myjoin - file3.csv
    

    说明:

    • -t , 指定字段分隔符 (,)
    • -a 1 指示打印来自第一个文件的不可配对的行(假设头文件包含所有可能的头)
    • -o auto 控制格式,是打印空字段所必需的

    证明:

    $ join -t , -a 1 -o auto template.csv file1.csv | join -t , -a 1 -o auto - file2.csv | join -t , -a 1 -o auto - file3.csv
    header,value1,value2,value3
    2000-01-01 00:00:00,,value21,
    2000-01-01 00:15:00,value12,value22,value32
    2000-01-01 00:30:00,value13,value23,
    2000-01-01 00:45:00,,value24,value34
    2000-01-01 01:00:00,,,
    2000-01-01 01:15:00,,,value36
    2000-01-01 01:30:00,value17,,value37
    2000-01-01 01:45:00,value18,,value38
    2000-01-01 02:00:00,value19,,
    

    注意:

    为此,文件必须按连接字段(在您的情况下为标题)排序。如果不是这种情况,您可以使用sort 命令。

    【讨论】:

    • 完美!它就像一个魅力,它非常快:-)谢谢你
    【解决方案3】:

    这应该有效(解释阅读 cmets):

    #!/bin/sh
    
    awk -F, -v file=0 '
      FNR == 1 {                     # first line in the file
        if(file == 0) {              # if in first file (template.csv):
          header = $1                # init header
        } else {
          header = header "," $2     # else append field name
        }
        next                         # forward to next line.
      }
      file == 0 {                    # if in first file:
        key[FNR] = $1                # remember key
        next                         # next line.
      }
      {
        field[$1][file] = $2         # otherwise: remember field
      }
      ENDFILE {                      # at the end of a file:
        file = file + 1              # increase counter
      }
      END {                          # in the end, assemble and
        print header                 # print lines.
        asort(key)
        for(k in key) {
          line = ""
          for(i = 1; i < file; ++i) {
            line = line "," field[key[k]][i]
          }
          print key[k] line
        }
      }
      ' template.csv file1.csv file2.csv file3.csv
    

    【讨论】:

    • 工作正常(也适用于上次更新),但结果不像模板中那样按日期排序。
    • @Adrien 如果这对您有用,您始终可以通过在命令行末尾添加 |sort 来对输出进行排序。
    • 我尝试对输出进行排序,但这并不容易(我没有找到好的排序命令):标题(第一行)不应该排序,日期应该分两次排序(第一种:日期+第二种:时间)...我放弃了!
    • 在最后一次小改动之前它已经工作了(我没有注意到两个版本之间有任何变化)。日期和时间仍未作为模板排序。
    【解决方案4】:

    我会选择这个,但它肯定不是运行速度最快的解决方案,但对于您的数据,它返回正确的结果并且代码很短:

    #!/bin/bash
    CONTENT=$(cat template.scv)
    for line in $CONTENT; do
        TMP=$(echo $line)
        for file in file1.csv file2.csv file3.csv; do
            RESULT=$(grep "^$line," $file | cut -d',' -f2)
            TMP=$(echo $TMP,$RESULT)
        done
        echo $TMP
    done
    

    输出:

    header,value1,value2,value3
    1,,value21,
    2,value12,value22,value32
    3,value13,value23,
    4,,value24,value34
    5,,,
    6,,,value36
    7,value17,,value37
    8,value18,,value38
    9,value19,,
    

    编辑: 我的代码缺少逗号 (,),因此对于较长的 ID,它无法正常工作 编辑2: 好吧,这不是“不是最快的解决方案”,它真的很慢

    【讨论】:

    • 类似于我已经开发的脚本。 csv 文件中的一个月行太慢了。
    • 同意 - 幸运的是,其他答案提供了有用的解决方案。
    猜你喜欢
    • 2014-07-02
    • 2011-04-06
    • 1970-01-01
    • 2015-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-24
    • 1970-01-01
    相关资源
    最近更新 更多