【问题标题】:How to combine the data from two CSV files in BASH?如何在 BASH 中合并来自两个 CSV 文件的数据?
【发布时间】:2012-04-19 18:05:08
【问题描述】:

我有两个 CSV 文件,它们使用 @ 来划分每一列。第一个文件(file1.csv)有两列:

cat @ eats fish
spider @ eats insects

第二个文件(file2.csv)有四列:

info @ cat @ info @ info
info @ spider @ info @ info
info @ rabbit @ info @ info

我需要将第一个文件第二列的信息添加到第二个文件的新列中,以防第一个文件的第一列和第二个文件的第二列的详细信息匹配,例如,上面的结果会变成这样:

info @ cat @ info @ info @ eats fish
info @ spider @ info @ info @ eats insects
info @ rabbit @ info @ info @

如上所示,由于第一个文件不包含有关兔子的信息,因此在第二个文件的最后一行添加了一个新的空列。

到目前为止,我知道该怎么做:

while read line 可用于循环浏览第二个文件中的行,例如:

while read line
do
    (commands)
done < file2.csv

可以使用awk -F "@*" '{print $n}' 访问来自特定列的数据,其中n 是列号。

while read line
do
    columntwo=$(echo $line | awk -F "@*" '{print $2})
    while read line
    do
        columnone=$(echo $line | awk -F "@*" '{print $1})
        if [ “$columnone” == “$columntwo” ]
        then
            (commands)
        fi
    done < file1.csv
done < file2.csv

我的方法似乎效率低下,我不确定如何使用将file1.csv1 的第二列中的数据添加到file2.csv 的新列中。

  • file1.csv1 的第 1 列和file2.csv 的第 2 列中的项目对于这些文件是唯一的。这些文件中没有重复的条目。
  • 即使某些列是空的,生成的文件的每一行也应该正好有 5 列。
  • 该文件包含大量来自各种语言的 UTF-8 字符。
  • @ 周围有空格,但如果这导致脚本出现问题,我可以将其删除。

如何将第一个文件中的数据添加到第二个文件中的数据中?

【问题讨论】:

  • 匹配的行在两个文件中的顺序相同?
  • 写一个python脚本会不会更容易,例如,将两个文件读入可以轻松智能地搜索和修改的数据结构,然后将结果写入新文件?跨度>
  • 为什么要 bash?您已经涉及 awk,那么为什么不直接使用真正的语言呢?
  • 两个文件中匹配的行顺序不同。有些行也可能包含类似的内容,例如“@tree@”和“@tree frog@”,但只有完全匹配的行才被认为是匹配的。
  • 如果 BASH 不适合这种任务,我已经添加了一些其他语言,这些语言已经安装在我的系统上。

标签: ruby perl bash csv python-2.7


【解决方案1】:

还有一个漂亮、干净的awk 解决方案:

awk -F" *@ *" 'NR==FNR{lines[$2]=$0} NR!=FNR{if(lines[$1])lines[$1]=lines[$1] " @ " $2} END{for(line in lines)print lines[line]}' file2.csv file1.csv

一个不错的单线。不是很短,但不是我见过的最长的。请注意,file2 和 file1 是切换的。同样,作为带有解释的脚本:

#!/usr/bin/awk -f

# Split fields on @ and the whitespace on either side.
BEGIN { FS = " *@ *" }

# First file
NR == FNR {
    #Store the line
    lines[$2] = $0
}

# Second file
NR != FNR {
    # If the appropriate animal was in the first file, append its eating habits.
    # If not, it's discarded; if you want something else, let me know.
    if(lines[$1]) lines[$1] = lines[$1] " @ " $2
}

# After both files have been processed
END {
    # Loop over all lines in the first file and print them, possibly updated with eating habits.
    # No guarantees on order.
    for(line in lines) print lines[line]
}

调用为awk -f join.awk file2.csv file1.csv,或使可执行文件和./join.awk file2.csv file1.csv

【讨论】:

    【解决方案2】:

    jowdder's answer 几乎就在那里,但由于我在评论中提到的问题而不完整:字段中会有不需要的空格并且文件没有排序,它们需要排序。

    join -t@ -11 -22 -o2.1,0,2.3,2.4,1.2 <(sed 's/ *@ */@/g' file1.csv | sort -t@) <(sed 's/ *@ */@/g' file2.csv | sort -t@ -k2) | sed 's/@/ @ /g' > output-file
    

    这也可以写成 bash 脚本,我会解释其中的每一步:

    #!/bin/bash -e
    
    # Remove whitespace around the `@`s, then sort using `@` to separate fields (-t@). 
    # -k2 tells sort to use the second field.
    sed 's/ *@ */@/g' file1.csv | sort -t@ >temp-left
    sed 's/ *@ */@/g' file2.csv | sort -t@ -k2 >temp-right
    
    # Join the files. -t@ means break fields at @, 
    # -11 says use the first field in the first file,  -22 is the second field in the second file.
    # -o... controls the output format, 2.1=second file, first field; 0 is the join field.
    join -t@ -11 -22 -o2.1,0,2.3,2.4,1.2 temp-left temp-right > temp-joined
    
    # Add whitespace back in around the @s so it looks better.
    sed 's/@/ @ /g' temp-joined >output-file
    
    # Clean up temporary files
    rm temp-{left,right,joined}
    

    【讨论】:

    • 当我尝试任一脚本时,它都会报告错误:join: file 2 is not in sorted order。它必须有什么样的顺序才能起作用?
    • sort 的默认顺序应该是正确的。临时文件看起来是否正确排序?你有什么版本的sortjoin
    • 生成的 temp-lefttemp-right 文件的排序方式不同,一些较早出现在 temp-left 中的项目,后来出现在 temp-right 中。使用sort sort (GNU coreutils) 8.5join (GNU coreutils) 8.5
    • 我的排序似乎是正确的,您可以尝试查找并发布一小部分未正确排序的行吗?
    • 另外,你试过我下面的awk 解决方案了吗?它不依赖于被排序的文件。
    【解决方案3】:

    这就是 POSIX 的 join 实用程序的用途。在对file1.csvfile2.csv 进行排序(在第二个字段中对后者进行排序)之后,运行以下代码:

    join -2 2 -a 2 -t @ -e '' -o 2.1,0,2.3,2.4,1.2 file1.csv file2.csv
    

    【讨论】:

    • 差不多。 -t @@ 上严格拆分,在第二个文件的左侧留下空白,但不是第一个。此外,它们需要排序。
    【解决方案4】:

    这可能对你有用:

    sed -e '1i\s/$/ @/' -e 's|^\([^@]*\)@\(.*\)|/^[^@]*@ \1/s/$/\2/|' file1.csv |
    sed -f - file2.csv
    info @ cat @ info @ info @ eats fish
    info @ spider @ info @ info @ eats insects
    info @ rabbit @ info @ info @
    

    但是在大​​容量上它可能不是很快!

    【讨论】:

      【解决方案5】:

      您还没有说明为什么必须在 bash 中执行此操作。使用 ruby​​、python 或 perl 等功能齐全的语言要容易得多。这是一个简短的 ruby​​ 程序:

      #!/usr/bin/env ruby
      
      f1_map = Hash[ * IO.readlines('file1.csv').map {|l| l.chomp.split(/\s+@\s+/,2) }.flatten ]
      
      STDIN.each_line do |l|
        cols = l.chomp.split /\s+@\s+/
        puts ( cols << f1_map[cols[1]] ).join(' @ ')
      end
      

      【讨论】:

        【解决方案6】:

        编辑:在阅读了Text::CSV(底层解析器/编写器引擎)的文档后,我发现了quote_space 选项,它可以防止空格的存在触发场地。在您的问题中,您说您可以允许删除 @ 字符周围的空格,此方法将在此过程中为您执行此操作,但如果可以接受,那么此答案现在应该符合所有标准。

        这是一个使用 Perl 和我的Tie::Array:CSV 的简单示例。该模块允许您像处理原生 Perl 2D 数组一样处理 CSV 文件。

        #!/usr/bin/env perl
        
        use strict;
        use warnings;
        
        use Tie::Array::CSV;
        use List::Util 'first';
        
        my %opts = (
          text_csv => { 
            sep_char => '@',
            allow_whitespace => 1,
            quote_space => 0,
          }, 
        );
        
        tie my @file1, 'Tie::Array::CSV', 'file1.csv', %opts;
        tie my @file2, 'Tie::Array::CSV', 'file2.csv', %opts;
        
        foreach my $line (@file2) {
          my $animal = $line->[1];
          my $eats = first { $_->[0] eq $animal } @file1;
          if ( $eats ) {
            push @$line, $eats->[1];
          } else {
            push @$line, '';
          }
        }
        

        根据 file1.csv 的大小,最好将整个文件解析到内存中以提高搜索效率。

        反正这里是先解析file1.csv的选项

        #!/usr/bin/env perl
        
        use strict;
        use warnings;
        
        use Tie::Array::CSV;
        
        my %opts = (
          text_csv => { 
            sep_char => '@',
            allow_whitespace => 1,
            quote_space => 0,
          }, 
        );
        
        tie my @file1, 'Tie::Array::CSV', 'file1.csv', %opts;
        tie my @file2, 'Tie::Array::CSV', 'file2.csv', %opts;
        
        # parse in file1 so that it doesn't need to be searched each time
        my %eats;
        foreach my $line (@file1) {
          $eats{$line->[0]} = $line->[1];
        }
        
        foreach my $line (@file2) {
          my $animal = $line->[1];
          push @$line, $eats{$animal} || '';
        }
        

        【讨论】:

        • 文件长度会有所不同,但通常file1.csv约为5000字,file2.csv约为100000行。
        【解决方案7】:

        还可以查看 DBD::CSV perl 模块。它将每个文件视为一个表,并允许您在它们上编写 SQL 连接。 http://metacpan.org/pod/DBD::CSV

        【讨论】:

          【解决方案8】:

          我有一个可以从控制台执行的solution based in a Ruby script

          我相信您可以根据自己的具体情况进行必要的调整,例如将“@”作为字段分隔符。

          【讨论】:

            猜你喜欢
            • 2017-09-06
            • 1970-01-01
            • 2020-03-16
            • 2020-04-16
            • 2020-08-22
            • 2021-11-04
            • 1970-01-01
            • 1970-01-01
            • 2019-11-08
            相关资源
            最近更新 更多