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

Posted

技术标签:

【中文标题】使用 awk 或 perl 从 CSV 中提取特定列(解析)【英文标题】:Using awk or perl to extract specific columns from CSV (parsing) 【发布时间】:2012-03-06 11:07:15 【问题描述】:

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


示例输入和所需输出 - 例如,我只希望输出文件中的第 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 就可以实现我想要的输出吗?如果是这样,是否可以编辑上面的脚本来解决我遇到的问题?有人可以逐行解释脚本到底在做什么吗?

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

【问题讨论】:

建议 perl 优于 awk 的原因是前者能够进行前瞻/后瞻断言,以区分字段分隔符和内部字段值 @SiegeX - 抱歉,你错了。建议使用 Perl 而不是 awk,因为在 CPAN 上有 100% 工作、完全(或几乎)调试稳定的生产质量 CSV 解析模块,因此您不必重新发明(糟糕)自行车。具体来说,Text::CSV 通常被认为是经典。 禁止“添加额外引号”部分是否有特殊原因?此外,字段的引号是否遵守输入文件的某些 100% 非易失性标准规则? (例如“引用包含空格、逗号或引号的字段”)? @DVK,不,没有这样的规则。是否使用引号是随机的。 @DVK - 不,没有理由禁止像 ikegami 提到的那样添加额外的引号。我刚刚提到要强调我希望输出文件的格式尽可能接近原始文件 【参考方案1】:

我不会重新发明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,""

【讨论】:

感谢您的解决方案。不幸的是,我无法使用您的第一个解决方案,因为我使用的机器没有 Text::CSV_XS 模块并且我无法安装它。第二个(重新发明的)解决方案可以满足我的需要。但是,唯一的问题是它指定要打印的列的部分。有没有办法指定与第一个解决方案类似的列,您可以在其中列出列号?我的 csv 文件可能有数百列,我需要能够轻松更改要解析的列。 @yousir - 你用 Text::SCV 代替。它是纯粹的 Perl @yousir,您没有说明为什么无法安装它,因此如果我们不知道需要解决什么问题,我们既无法帮助您安装它,也无法找到解决方法。 @yousir,我没有成功,所以您可以选择其他列,因为那不是您的问题。但实际上,动态构建模式以选择其他列是微不足道的。 @ikegami - 除非我按照错误的说明进行操作,否则我确实需要额外的权限才能从 CPAN 安装模块。无论如何,我能够找到一种解决方法来按照 DVK 的建议“安装”Text:CSV,并利用您的第一个脚本来实现我想要的。我只需将 Text:CSV 源中的 CSV.pm 和 CSV_PP.pm 放入脚本工作目录中名为“Text”的文件夹中。【参考方案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])

输出.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,

【讨论】:

删除存在的双引号比添加不存在的双引号更糟糕。【参考方案3】:

以下命令将从 sample.csv 文件中提取以分隔符 ',' 分隔的所需字段(例如,第一、第三和第四),并在控制台中显示输出。 剪切 -f1,3,4 -d',' 示例.txt 如果要将输出存储在新的 csv 文件中,则将输出重定向到如下文件 cut -f1,3,4 -d',' sample.txt > newSample.csv

【讨论】:

【参考方案4】:

在我发布之前,我现在看到这是一个已经被删除的答案的老问题,但是,我想我仍然会利用这个机会炫耀Tie::Array::CSV,这使得 CSV 文件操作就像使用 Perl 一样容易数组。全面披露:我是作者。

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

#!/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;

【讨论】:

【参考方案5】:

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

见csvcut。它的命令行界面紧凑,可以处理多种 csv 格式(tsv、其他分隔符、编码、转义字符等)

您要求的可以使用:

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

【讨论】:

【参考方案6】:

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

现在,引导您完成您的脚本。基本上它试图重写你的字段的样子。起初,你用“,”分割,这显然会导致一些问题。接下来,它会查找没有被 '"' 正确关闭的字段。

BEGINOFS=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 上失败了。

我希望你同意我的观点,像这样重写字段可能会很棘手。不仅如此,它就像“哦,我喜欢 awk,但我会像 C/perl/python 一样使用它。”至少可以说,使用 FPAT 是一种更短的解​​决方案。

【讨论】:

【参考方案7】:

我犯了一些错误希望现在得到纠正。

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,""

【讨论】:

第 2 行的输出不符合 OP。

以上是关于使用 awk 或 perl 从 CSV 中提取特定列(解析)的主要内容,如果未能解决你的问题,请参考以下文章

使用 awk 命令从 csv 读取和打印第一 1000 行,然后再打印 1000 行,依此类推

awk 和 sed 文本操作(从特定组中提取大多数负值)

perl单行命令[03]-awk

Python:使用熊猫从csv文件中提取特定列(包含特殊字符)

如何 grep/perl/awk 重叠正则表达式

当我使用python从CSV文件中提取特定关键字的数据时,为什么会出现KeyError?