利用perl来操作csv /分号数据

时间:2013-11-14 18:46:28

标签: perl csv awk

我正在寻找有关如何操作1行AWK命令不再足够的数据的建议。我正在处理多达1000多行的数据集。列。我遇到了定义太多列变量的问题。我在想有一种方法可以使用一个循环迭代一个数组,以便可能定义我想要计算的列数。和。我想提出计数&基于键值的行的总和类似于Excel COUNTIF& SUMIF。

Data Set Example:
Store_Location;Person;Adult_Child;Age;Weight...
LocationA;PersonA;0;50;200
LocationB;PersonB;1;10;100
LocationA;PersonC;1;12;90
LocationA;PersonA;0;50;200

Desired Output: (delimiter is not important)
Store_Location;Count_Of_Adults;Count_of_Children;Sum_of_Age;Sum_of_Weight
LocationA;2;1;112;490
LocationB;0;1;10;100

这是我正在使用的示例AWK脚本:

BEGIN {FS=";"} {print "Store_Location;Count_Of_Adults;Count_of_Children;Sum_of_Age;Sum_of_Weight"}

{
n[$1]++;
C1_[$1] += ($3 == "1" ? 0 : 1);S1_[$1] += $4;column_sum3+=$4
C2_[$1] += ($3 == "0" ? 0 : 1);S2_[$1] += $5;column_sum4+=$5
}
END {
for (i in n) {
  print i,C1_[i],C2_[i],S1_[i],S2_[i]
}
}

我使用a2p将语法转换为perl并进行了一些修改(基于使用不同的列):

$base = 20;
while (<>){
    @array = split(/$FS/, $_, -1);


    $n{$array[$base]}++;

    $C1_{$array[$base]} += ($array[21] eq '' ? 0 : 1);
    $C2_{$array[$base]} += ($array[34] eq '' ? 0 : 1);
    $column_count1 += ($array[21] eq '' ? 0 : 1);
    $column_count2 += ($array[34] eq '' ? 0 : 1);
    $S1_{$array[$base]} += $array[21];
    $S2_{$array[$base]} += $array[34];
    $column_sum1 += $array[21];
    $column_sum2 += $array[34];
}
@sorted_keys = sort { $a <=> $b} keys %n;
foreach $i (@sorted_keys){
    print $i,$C1_{$i},$C2_{$i},$S1_{$i},$S2_{$i};

我希望能够做类似的事情,但我试图将我想要的列和我想要计数的列放入不同的数组中。例如:@ sum_array = [1,6,10,15,30]&amp; @count_array = [1,10,20]。并使用循环来创建总和&amp;计数,而不必声明每个输出列。我可以只对每列进行求和并计算,然后打印出我需要的列。我试图使用散列/数组在Perl中编写代码时遇到了困难。我试图使用哈希值,但后来无法获得输出格式,所以我不确定这是否是我想要构建数据的方式。

$n{$array[$base]}{Adult}{count}+= ($array[21] eq 0 ? 0 : 1);
$n{$array[$base]}{Child}{count}+= ($array[21] eq 1 ? 0 : 1);
$n{$array[$base]}{Weight}{sum} += $array[21];
$n{$array[$base]}{Age}{sum}+= $array[34];

编辑: 我认为我的逻辑问题是我不想调出字段名称/列。因为我想要执行总和&amp;算上很多领域。成人儿童比较只是一个例子。我只想在一个地方列出我想要使用的列。也许解释它的简单方法是,假设输入数据有100列。我希望能够灵活地识别我想要分析的列。例如:第15-30栏我想要总和&amp;基于第1列中的唯一值计算每列的数量。然后能够修改相同的代码以获取列15-20和15的总和。 30-40。使用AWK我可以调用我想要使用的列($ 2,$ 3,$ 4,...)但是当列数太多时很难管理。

2 个答案:

答案 0 :(得分:1)

目前还不完全清楚你想要什么,而且“我在定义太多列变量时遇到问题”肯定不清楚你是什么意思“但是这就是我认为你想要做的事情,希望它能帮到你在正确的道路上:

$ cat file
Store_Location;Person;Adult_Child;Age;Weight
LocationA;PersonA;0;50;200
LocationB;PersonB;1;10;100
LocationA;PersonC;1;12;90
LocationA;PersonA;0;50;200

$ cat tst.awk         
BEGIN{ FS=OFS=";" }

NR==1 {
    split($0,nr2nm)
    for (nr=1;nr in nr2nm;nr++) {
        nm2nr[nr2nm[nr]] = nr
    }
    next
}

{
    stores[$nm2nr["Store_Location"]]

    for (nr=3; nr<=NF; nr++) {
        fldName = nr2nm[nr]
        if ( fldName == "Adult_Child" ) {
            fldName = ($nr == 1 ? "Child" : "Adult")
        }
        fldNames[fldName]
        cnt[$nm2nr["Store_Location"],fldName]++
        sum[$nm2nr["Store_Location"],fldName] += $nr
    }
}

END {
    printf "%s", "Store_Location"
    for (fldName in fldNames) {
        printf ";cnt[%s];sum[%s]", fldName, fldName
    }
    print ""
    for (store in stores) {
        printf "%s", store
        for (fldName in fldNames) {
            printf ";%d;%d", cnt[store,fldName], sum[store,fldName]
        }
        print ""
    }
}

$ awk -f tst.awk file
Store_Location;cnt[Weight];sum[Weight];cnt[Child];sum[Child];cnt[Adult];sum[Adult];cnt[Age];sum[Age]
LocationA;3;490;1;1;2;0;3;112
LocationB;1;100;1;1;0;0;1;10

答案 1 :(得分:1)

Text::CSV是在Perl中解析和输出分隔数据的绝佳工具。让我们运行一个使用Text :: CSV来解决问题的脚本。

设置

在我们解析任何内容之前,我们需要创建一个新的CSV对象并告诉它该分隔符是什么:

use strict; use warnings;
use Text::CSV;

my $csv = Text::CSV->new( { sep_char => ";", eol => $/ } )
    or die "Cannot use CSV: " . Text::CSV->error_diag();

我们还需要打开输入文件进行阅读:

open my $fh, "<", "file.csv" or die "Failed to open file for reading: $!";

设置列名

Text :: CSV可以将每行数据作为hashref获取,列名称为键。例如,我们可以读取行

LocationA;PersonA;0;50;200

进入以下Perl数据结构:

{
    'Age' => '50',
    'Adult_Child' => '0',
    'Person' => 'PersonA',
    'Store_Location' => 'LocationA',
    'Weight' => '200'
}

这使我们可以使用人类可读的字符串而不是列号。要使用此功能,我们首先需要告诉解析器每列使用什么名称。由于我们的数据包含带有列名的标题行,我们可以使用它:

$csv->column_names( $csv->getline($fh) );

指定要汇总的列

我们只需要计算某些列的总和。在您的示例数据中,我们希望计算AgeWeight列的总计,但不计算Store_LocationAdult_Child的总计(Adult_Child本质上是布尔标志所以简单的总和不是我们想要的。让我们创建一个我们想要计算总和的列名数组:

# Use columns 3-4 (zero-indexed)
my @cols_to_sum = @{ [ $csv->column_names() ] }[3..4];

如果您的输入有100列,并且您只想对15-20和30-40列求和,则可以执行以下操作:

my @cols_to_sum = @{ [ $csv->column_names() ] }[15..20,30..40];

这需要我们在上一部分中设置的array slice列名称。请记住,列号从零开始。

一旦我们拥有了数组,我们就不必再次引用列号了。这意味着如果我们想要改变我们要计算总和的列,我们只需要更改这一行。

我们的输入包含列Age,但我们希望相应的输出列名称为Sum_of_Age。我们将前缀Sum_of_放在一个变量中,以便我们稍后可以转换输出:

my $col_prefix = "Sum_of_";

获取CSV数据

现在我们已准备好获取数据。由于我们希望按位置对结果进行分组,因此我们将计算的总计存储在一个哈希中,并将位置作为键:

my %totals;
while (my $row = $csv->getline_hr($fh)) {
    my $location = $row->{Store_Location};

    # Add numeric columns to the totals, prepending prefix to each key
    foreach my $col (@cols_to_sum) {
        my $col_name = $col_prefix . $col;
        $totals{$location}{$col_name} += $row->{$col};
    }

    # Set counts of adults and children to zero if not set for this location
    $totals{$location}{Count_of_Adults}   //= 0;
    $totals{$location}{Count_of_Children} //= 0;

    # Handle the adult/child flag
    if ($row->{Adult_Child}) {
        $totals{$location}{Count_of_Children}++;
    }
    else {
        $totals{$location}{Count_of_Adults}++;
    }
}
$csv->eof or $csv->error_diag();

close $fh;

请注意,我们必须以不同方式处理Adult_Child列,因为我们将单个输入列映射到两个输出列(Count_of_AdultsCount_of_Children)。最后,我们的%totals哈希看起来像这样:

{
    'LocationA' => {
        'Count_of_Adults' => 2,
        'Count_of_Children' => 1,
        'Sum_of_Weight' => 490,
        'Sum_of_Age' => 112
    },
    'LocationB' => {
        'Count_of_Adults' => 0,
        'Count_of_Children' => 1,
        'Sum_of_Weight' => 100,
        'Sum_of_Age' => 10
    }
}

打印结果

现在我们已经计算了所有总数,我们可以输出结果。首先,我们需要构造标题行来设置列顺序:

# Construct output header, prepending prefix to each "totals" column
my @header = qw(Store_Location Count_of_Adults Count_of_Children);
push @header, $col_prefix . $_ for @cols_to_sum;

我们可以使用相同的Text::CSV对象将结果打印到stdout。这样我们就可以使用与输入文件相同的分号分隔格式。首先我们打印标题:

$csv->print(\*STDOUT, [ @header ]);

如果要打印到文件而不是stdout,可以这样做:

open my $fh, ">", "output.csv" or die "Failed to open file for writing: $!";
$csv->print(\*$fh, [ @header ]);

我们将使用@header数组以正确的列顺序从%totals哈希中获取总计。但是,Store_Location列是特殊的,因为它是%totals中的顶级键。我们会将其从@header数组中删除,以便更轻松地打印结果:

shift @header;

现在我们可以按位置对结果进行排序并打印出来:

foreach my $location (sort keys %totals) {

    # Use a hash slice to put result columns in the same order as the header
    my $row = [ $location, @{ $totals{$location} }{ @header } ];

    $csv->print(\*STDOUT, $row);
}

输出结果为:

Store_Location;Count_of_Adults;Count_of_Children;Sum_of_Age;Sum_of_Weight
LocationA;2;1;112;490
LocationB;0;1;10;100