使用 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 行,依此类推