5

我有两个@用于划分每一列的 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.csv1file2.csv.

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

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

4

8 回答 8

5

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

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}
于 2012-04-06T03:22:57.213 回答
5

还有一个不错的、干净的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]
}

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

于 2012-04-06T04:04:24.137 回答
3

这就是 POSIXjoin实用程序的用途。在排序file1.csvfile2.csv(在第二个字段上对后者排序)之后,按照以下方式运行:

join -2 2 -a 2 -t @ -e '' -o 2.1,0,2.3,2.4,1.2 file1.csv file2.csv
于 2012-04-06T01:26:49.127 回答
2

这可能对您有用:

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 @

然而,在大容量上它可能不是很快!

于 2012-04-06T08:15:36.467 回答
1

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

这是一个使用 Perl 和我的Tie::Array:CSV. 该模块允许您像处理原生 Perl 二维数组一样处理 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} || '';
}
于 2012-04-06T01:37:13.777 回答
1

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

于 2012-04-06T01:57:19.847 回答
1

你还没有说为什么你必须在 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
于 2012-04-06T02:00:31.980 回答
1

我有一个基于 Ruby 脚本的解决方案,可以从控制台执行。

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

于 2014-08-01T17:45:31.997 回答