【问题标题】:How to convert a list of attribute-value pairs into a flat table whose columns are attributes如何将属性值对列表转换为列为属性的平板表
【发布时间】:2013-07-10 03:02:42
【问题描述】:

我正在尝试将包含 3 列 (ATTRIBUTE_NAME,ATTRIBUTE_VALUE,ID) 的 csv 文件转换为每行为 (ID,Attribute1,Attribute2,Attribute3,....) 的平面表。最后提供了此类表格的示例。

Python、Perl 或 SQL 都可以。非常感谢您,我非常感谢您的时间和努力!

事实上,我的问题与post 非常相似,除了在我的情况下,属性的数量非常大(~300)并且每个 ID 之间并不一致,因此对每个属性进行硬编码可能不切实际解决方案。

对我来说,具有挑战性/困难的部分是:

  • 大约有 2.7 亿行输入,输入表的总大小约为 60 GB。
  • 一些单个值(字符串)中包含逗号(,),整个字符串将用双引号(")括起来,以使读者意识到这一点。例如 ID=53 中的"JPMORGAN CHASE BANK, NA, TX"
  • ID 的属性集不同。比如整体属性个数是8,但是ID=53、17、23分别只有7、6、5个。 ID=17没有string_countrystring_address属性,所以在逗号后面输出blank/nothing

输入属性值表如下所示。在这个示例输入和输出中,我们有 3 个 ID,其属性数量可以不同,具体取决于我们是否可以从服务器获取这些属性。

ATTRIBUTE_NAME,ATTRIBUTE_VALUE,ID
num_integer,100,53
string_country,US (United States),53
string_address,FORT WORTH,53
num_double2,546.0,53
string_acc,My BankAcc,53
string_award,SILVER,53
string_bankname,"JPMORGAN CHASE BANK, NA, TX",53
num_integer,61,17
num_double,34.32,17
num_double2,200.541,17
string_acc,Your BankAcc,17
string_award,GOLD,17
string_bankname,CHASE BANK,17
num_integer,36,23
num_double,78.0,23
string_country,CA (Canada),23
string_address,VAN COUVER,23
string_acc,Her BankAcc,23

输出表应如下所示。 (列中属性的顺序不固定,可以按字母顺序排序,也可以按出现顺序排序。)

ID,num_integer,num_double,string_country,string_address,num_double2,string_acc,string_award,string_bankname
53,100,,US (United States),FORT WORTH,546.0,My BankAcc,SILVER,"JPMORGAN CHASE BANK, NA, TX"
17,61,34.32,,,200.541,Your BankAcc,GOLD,CHASE BANK
23,36,78.0,CA (Canada),VAN COUVER,,Her BankAcc,,

【问题讨论】:

  • 您似乎正在将一个 CSV 文件转换为另一个 CSV。 SQL 在哪里考虑到这一点?
  • 嗨 @runrig 我对 Python 和 R 没问题,但可以理解一点 Perl。我现在正在尝试 Python。
  • 嗨,shawnhcorey,是的,你是对的。这实际上是关于将 csv 文件转换为另一个 csv 文件。我提到 SQL 是因为我只是认为 SQL 可能会提供一些自动执行此操作的函数,但我找不到。

标签: sql database perl csv python-3.x


【解决方案1】:

这个程序会按照你的要求去做。它需要输入文件的名称作为命令行上的参数。

更新更仔细地查看数据,我发现并非所有数据字段都适用于每个 ID。如果要按照它们在文件中出现的顺序保持字段的顺序,这会使事情变得更加复杂。

这个程序通过扫描文件并将所有数据累积到哈希%data中来工作。同时它构建了一个散列%headers,保持每个标头在每个ID值的数据中出现的位置。

扫描文件后,通过查找包含两个标题信息的每对的第一个 ID 来对收集的标题进行排序。该对在完整集合中的排序顺序必须与它们在该 ID 的数据中出现的顺序相同,因此只需使用 <=> 比较两个位置值即可。

一旦创建了一组排序的标头,%data 哈希就会被转储,使用哈希切片访问每个 ID 的完整值列表。

更新 2 现在我意识到您数据的庞大规模,我可以看到我的第二次尝试也存在缺陷,因为它试图将所有信息读入 输出之前的内存。除非你有一台拥有大约 1TB 内存的巨型机器,否则这是行不通的!

你可能会从这个版本中获得一些里程。它扫描文件两次,第一次读取数据以便可以创建和排序完整的标题名称集,然后再次读取每个 ID 的数据并输出。

如果它不适合你,请告诉我,因为我仍然可以做一些事情来提高内存效率。

use strict;
use warnings;
use 5.010;

use Text::CSV;
use Fcntl 'SEEK_SET';

my $csv = Text::CSV->new;

open my $fh, '<', $ARGV[0] or die qq{Unable to open "$ARGV[0]" for input: $!};

my %headers = ();
my $last_id;
my $header_num;
my $num_ids;

while (my $row = $csv->getline($fh)) {
  next if $. == 1;

  my ($key, $val, $id) = @$row;

  unless (defined $last_id and $id eq $last_id) {
    ++$num_ids;
    $header_num = 0;
    $last_id = $id;
    print STDERR "Processing ID $id\n";
  }

  $headers{$key}[$num_ids-1] = ++$header_num;
}

sub by_position {
  for my $id (0 .. $num_ids-1) {
    my ($posa, $posb) = map $headers{$_}[$id], our $a, our $b;
    return $posa <=> $posb if $posa and $posb;
  }
  0;
}

my @headers = sort by_position keys %headers;
%headers = ();
print STDERR "List of headers complete\n";

seek $fh, 0, SEEK_SET;
$. = 0;

$csv->combine('ID', @headers);
print $csv->string, "\n";

my %data = ();
$last_id = undef;

while () {
  my $row = $csv->getline($fh);
  next if $. == 1;

  if (not defined $row or defined $last_id and $last_id ne $row->[2]) {
    $csv->combine($last_id, @data{@headers});
    print $csv->string, "\n";
    %data = ();
  }

  last unless defined $row;
  my ($key, $val, $id) = @$row;
  $data{$key} = $val;
  $last_id = $id;
}

输出

ID,num_integer,num_double,string_country,string_address,num_double2,string_acc,string_award,string_bankname
53,100,,"US (United States)","FORT WORTH",546.0,"My BankAcc",SILVER,"JPMORGAN CHASE BANK, NA, TX"
17,61,34.32,,,200.541,"Your BankAcc",GOLD,"CHASE BANK"
23,36,78.0,"CA (Canada)","VAN COUVER",,"Her BankAcc",,

【讨论】:

  • 也许应该在 ID 更改后立即刷新输出......毕竟,这是 60GB 的输入。
  • 嘘。我没有看到60GB。这意味着我的新代码也无法工作:-/ 谢谢 Slaven。
  • 非常感谢,伙计们!我将使用 60GB 输入对其进行测试并让您知道。虽然我会更多地研究如何刷新输出。
  • @user1613297:我以前的版本不适合你,因为它会占用太多内存。此外,刷新输出不会有任何区别,因为 Perl 无论如何都会每 8KB 刷新一次。试试上面的代码。读取标头列表可能需要一些时间,将一些 print STDERR 跟踪放入其中可能会很有用,以便您知道它正在做某事。
  • @Borodin:我已经测试了你的代码的新版本,它适用于中等大小的文件。我现在正在用更大的文件对其进行测试。再次感谢您在这里的贡献;我真的很感谢你的努力!
【解决方案2】:

使用Text::CSVfrom CPAN

#!/usr/bin/env perl

use strict;
use warnings;

# --------------------------------------

use charnames qw( :full :short   );
use English   qw( -no_match_vars );  # Avoids regex performance penalty

use Text::CSV;

my $col_csv     = Text::CSV->new();
my $id_attr_csv = Text::CSV->new({ eol=>"\n", });

$col_csv->column_names( $col_csv->getline( *DATA ));
while( my $row = $col_csv->getline_hr( *DATA )){

  # do all the keys but skip if ID
  for my $attribute ( keys %$row ){
    next if $attribute eq 'ID';

    $id_attr_csv->print( *STDOUT, [ $attribute, $row->{$attribute}, $row->{ID}, ]);

  }
}

__DATA__
ID,num_integer,num_double,string_country,string_address,num_double2,string_acc,string_award,string_bankname
53,100,,US (United States),FORT WORTH,546.0,My BankAcc,SILVER,"JPMORGAN CHASE BANK, NA, TX"
17,61,34.32,,,200.541,Your BankAcc,GOLD,CHASE BANK
23,36,78.0,CA (Canada),VAN COUVER,,Her BankAcc,,

【讨论】:

  • @shawnhcorey 我正在检查这个解决方案,我会尽快回复您。再次感谢您的帮助!
  • 我的输出列顺序错误。现在已经更正了。
  • 我已经测试过你的代码,看起来你的代码做了相反的事情,即从属性表转换回属性值列表文件。其实这也很有用!再次感谢您的帮助。
猜你喜欢
  • 1970-01-01
  • 2021-11-02
  • 1970-01-01
  • 2019-12-16
  • 1970-01-01
  • 1970-01-01
  • 2018-10-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多