使用awk或perl从CSV中提取特定列(解析)

时间:2012-02-15 04:11:17

标签: perl parsing csv awk

背景 - 我想从csv文件中提取特定列。 csv文件以逗号分隔,使用双引号作为text-qualifier(可选,但是当字段包含特殊字符时,限定符将在那里 - 参见示例),并使用反斜杠作为转义字符。某些字段也可能为空白。


示例输入和所需输出 - 例如,我只希望第1列,第3列和第4列位于输出文件中。 csv文件中列的最终提取应与原始文件的格式匹配。不应删除任何转义字符或添加额外的引号等。

输入

"John \"Super\" Doe",25,"123 ABC Street",123-456-7890,"M",A
"Jane, Mary","",132 CBS Street,333-111-5332,"F",B
"Smith \"Jr.\", Jane",35,,555-876-1233,"F",
"Lee, Jack",22,123 Sesame St,"","M",D

所需的输出

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,""

初级脚本(awk) - 以下是我发现的大部分工作的初步脚本,但在我注意到的一个特定实例中不起作用,可能更多我没见过还是想到了

#!/usr/xpg4/bin/awk -f

BEGIN{  OFS = FS = ","  }

/"/{
    for(i=1;i<=NF;i++){
        if($i ~ /^"[^"]+$/){
            for(x=i+1;x<=NF;x++){
                $i=$i","$x
                if($i ~ /"+$/){
                    z = x - (i + 1) + 1
                    for(y=i+1;y<=NF;y++)
                        $y = $(y + z)
                    break
                }
            }
            NF = NF - z
            i=x
        }
    }
print $1,$3,$4
}

上述内容似乎运行良好,直到遇到包含转义双引号和逗号的字段。在这种情况下,解析将关闭,输出将不正确。


问题/评论 - 我已经读过awk不是解析csv文件的最佳选择,建议使用perl。但是,我根本不懂perl。我找到了一些perl脚本的例子,但它们没有提供我想要的所需输出,而且我不知道如何根据我的需要轻松编辑脚本。

至于awk,我对它很熟悉并偶尔使用它的基本功能,但我不知道很多高级功能,比如上面脚本中使用的一些命令。只需使用awk,我想要的输出是否可行?如果是这样,是否可以编辑上面的脚本来解决我遇到的问题?有人可以逐行解释脚本究竟在做什么吗?

任何帮助将不胜感激,谢谢!

7 个答案:

答案 0 :(得分:10)

我不会重新发明wheel

use Text::CSV_XS;

my $csv = Text::CSV_XS->new({
   binary      => 1,
   escape_char => '\\',
   eol         => "\n",
});

my $fh_in  = \*STDIN;
my $fh_out = \*STDOUT;

while (my $row = $csv->getline($fh_in)) {
   $csv->print($fh_out, [ @{$row}[0,2,3] ])
      or die("".$csv->error_diag());
}

$csv->eof()
   or die("".$csv->error_diag());

输出:

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary","132 CBS Street",333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack","123 Sesame St",

它在已经没有任何地址的地址附加引号,但由于某些地址已经有引号,你显然可以处理它。


重新发明轮子:

my $field = qr/"(?:[^"\\]|\\.)*"|[^"\\,]*/s;
while (<>) {
   my @fields = /^($field),$field,($field),($field),/
      or die;
   print(join(',', @fields), "\n");
}

输出:

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,""

答案 1 :(得分:2)

我建议 python csv模块:

#!/usr/bin/env python3
import csv
rdr = csv.reader(open('input.csv'), escapechar='\\')
wtr = csv.writer(open('output.csv', 'w'), escapechar='\\', doublequote=False)
for row in rdr:
    wtr.writerow(row[0:1]+row[2:4])

output.csv

John \"Super\" Doe,123 ABC Street,123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,

答案 2 :(得分:0)

以下命令将从sample.csv文件中提取由分隔符“,”分隔的必填字段(例如,第一个,第三个和第四个),并在控制台中显示输出。 cut -f1,3,4 -d','sample.txt 如果要将输出存储在新的csv文件中,请将输出重定向到文件,如下所示 cut -f1,3,4 -d','sample.txt&gt; newSample.csv

答案 3 :(得分:0)

在发布之前,我现在看到这是一个已被删除的答案所引发的旧问题,但是,我认为我仍然会利用这个机会展示Tie::Array::CSV,这使得CSV文件操作变得像工作一样简单使用Perl数组。完全披露:我是作者。

无论如何这里是脚本。 OP的数据需要更改转义字符和从0开始的Perl索引数组,但除此之外,这应该是非常易读的。

#!/usr/bin/env perl

use strict;
use warnings;

use Tie::Array::CSV;

my $opts = { text_csv => { escape_char => '\\' } };

tie my @input,  'Tie::Array::CSV', 'data', $opts or die "Cannot open file 'data': $!";
tie my @output, 'Tie::Array::CSV', 'out',  $opts or die "Cannot open file 'out': $!";

for my $row (@input) {
  my @slice = @{ $row }[0,2,3];
  push @output, \@slice;
}

那就是说,如果我将它转换为(IMO)更令人印象深刻的形式,我认为最后一个循环不会失去太多的可读性:

push @output, [ @{$_}[0,2,3] ] for @input;

答案 4 :(得分:0)

csvkit是一个处理csv文件并允许此类操作(以及其他功能)的工具。

csvcut。它的命令行界面很紧凑,它处理大量的csv格式(tsv,其他分隔符,编码,转义字符等)。

您要求的内容可以使用:

csvcut --columns 0,2,3 input.csv

答案 5 :(得分:0)

我犯了一些错误,希望现在能够纠正。

awk '{sub(/y",""/,"y\42")sub(/,2.|,3./,"")sub(/,".",.*/,"")}1' file

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\", Jane",,555-876-1233
"Lee, Jack",123 Sesame St,""

答案 6 :(得分:0)

GNU awk 解决方案。只需将车轮用作车轮。您可以使用 FPAT 定义哪些字段应如下所示:

$ awk -vFPAT='[^,]+|"[^"]*"' -vOFS=, '{print $1, $3, $4}' file

导致:

"John \"Super\" Doe","123 ABC Street",123-456-7890
"Jane, Mary",132 CBS Street,333-111-5332
"Smith \"Jr.\",35,555-876-1233
"Lee, Jack",123 Sesame St,""

正则表达式的解释:

[^,]+           # 1 or more occurrences of anything that's not a comma, 
|               # OR
"[^"]*"         # 0 or more characters unequal to '"' enclosed by '"'

gawk manual

中了解FPAT

现在,引导您完成您的脚本。基本上它会尝试重写你的字段的样子。首先,你用&#34;,&#34;分开,这显然会导致一些问题。接下来,它会查找&#39;&#39;&#39;

未正确关闭的字段。

BEGIN{OFS=FS =","}                        # set field sep (FS) and output field 
                                          #   sep to ,
/"/{                                      # for each line matching '"'
    for(i=1;i<=NF;i++){                   # loop through fields 1 to NF
        if($i ~ /^"[^"]+$/){              # IF field $i start with '"', followed by
                                          #   non-quotes
            for(x=i+1;x<=NF;x++){         # loop through ALL following fields
                $i=$i","$x                # concatenate field $i with ALL following 
                                          #   fields, separated by ","
                if($i ~ /"+$/){           # IF field $i ends with '"'
                    z = x - (i + 1) + 1   # z is index of field we're looking at next
                    for(y=i+1;y<=NF;y++)  
                        $y = $(y + z)     # change contents of following fields to 
                                          #   contents of field, z steps further
                                          #   down the line
                    break                 # break out of for(x) loop
                }
            }
            NF = NF - z                   # reset number of fields
            i=x                           # continue loop for(i) at index x
        }
    }
 print $1,$3,$4
}

您的脚本在此输入行上失败:

"Smith \"Jr.\", Jane",35,,555-876-1233,"F",

只是因为$i ~ /^"[^"]+$/在$ 1上失败。

我希望你同意我的意见,重写这样的字段可能会很棘手。更重要的是,它就像&#34; O,我喜欢awk,但我会像C / perl / python一样使用它。&#34;至少可以说,使用FPAT是一种较短的解决方案。