如何使用正则表达式在Perl中解析带引号的CSV?

12

我在解析带有引号的CSV数据时遇到了一些问题。我的主要问题是在字段内部使用引号。在以下示例中,第1-4行能够正确工作,但第5,6和7行不能。

COLLOQ_TYPE,COLLOQ_NAME,COLLOQ_CODE,XDATA
S,"BELT,FAN",003541547,
S,"BELT V,FAN",000324244,
S,SHROUD SPRING SCREW,000868265,
S,"D" REL VALVE ASSY,000771881,
S,"YBELT,"V"",000323030,
S,"YBELT,'V'",000322933,

我想避免使用Text::CSV,因为目标服务器上没有安装它。意识到CSV比看起来更复杂,我正在使用Perl Cookbook中的一个配方。

sub parse_csv {
  my $text = shift; #record containg CSVs
  my @columns = ();
  push(@columns ,$+) while $text =~ m{
    # The first part groups the phrase inside quotes
    "([^\"\\]*(?:\\.[^\"\\]*)*)",?
      | ([^,]+),?
      | ,
    }gx;
  push(@columns ,undef) if substr($text, -1,1) eq ',';
  return @columns ; # list of vars that was comma separated.
}

有没有人能提出改进正则表达式以处理上述情况的建议?


第5、6和7行不是无效的CSV吗? - MikeKulls
7个回答

35

请尝试使用 CPAN

你可以下载Text::CSV或其他基于非 XS 的 CSV 解析器的副本,并将其安装在本地目录中,或者安装在项目的 lib/ 子目录中,以便与你的项目一起发布。

如果你不能在项目中存储文本文件,那我就想知道你是如何编写你的项目的。

http://novosial.org/perl/life-with-cpan/non-root/ 可以作为一个良好的指南,告诉你如何在本地创建一个可用的环境。

不使用 CPAN 真的是灾难的配方。

在尝试编写自己的 CSV 实现之前,请考虑这一点。

Text::CSV 包括 Bug 修复和边缘情况在内,已经有超过一百行的代码了。从头开始重写这个库只会让你学习 CSV 的困难程度。

注:我自己也曾经吃过亏。在发现 PHP 中已经添加了内置的 CSV 解析器之前,我花了整整一天时间才得到一个可用的解析器。这真的是件让人头疼的事情。


@Kent,谢谢...我对Text::CSV的主要厌恶是在其他地方安装的困难。例如:它们是否有编译器(并非所有的Unix都带有编译器)等等。但是由于您的第一篇文章,我重新检查了一下,发现有一个纯Perl实现,CSV_PP。谢谢。 - Mark Nold
2
你可能还想看一下Text::xSV,它也是纯Perl的。 - Kent Fredric
我需要检查一下,因为Text:CSV_PP即使设置了allow_loose_quotes和escape_char,也不能处理第五个案例。再次感谢。 - Mark Nold
7
我曾经不得不应对初级程序员的要求,他们希望安装日志 CPAN 模块,而只需要一个简单的正则表达式就可以解决问题。Perl 社区主张“有多种方法可以做到”,因此涂上大大的粗体字,好像只有一种正确的方法是不会帮助那些可能真正想发现另一种方法的人。 - PP.
“当一个简单的正则表达式就足够时”,我不能确定没有看到例子,但我的经验通常表明这种说法是错误的。通常当你认为正则表达式足够时,你只是不了解问题域,无法知道是否有更好的解决方法。这就是为什么模块存在的原因,让那些对问题领域有专业知识的人解决问题,找到最好的解决方案,并解决你甚至没有意识到的问题。 - Kent Fredric
2
@PP,这还不及更常见的情况糟糕,那就是有些烦人的家伙毫无合理原因地拒绝使用模块,因为他们既不了解“一个简单正则表达式”无法很好地完成工作的多种方式,也没有学习的雄心壮志。 - hobbs

26
你可以使用随Perl一起发布的Text::ParseWords 来解析CSV。
use Text::ParseWords;

while (<DATA>) {
    chomp;
    my @f = quotewords ',', 0, $_;
    say join ":" => @f;
}

__DATA__
COLLOQ_TYPE,COLLOQ_NAME,COLLOQ_CODE,XDATA
S,"BELT,FAN",003541547,
S,"BELT V,FAN",000324244,
S,SHROUD SPRING SCREW,000868265,
S,"D" REL VALVE ASSY,000771881,
S,"YBELT,"V"",000323030,
S,"YBELT,'V'",000322933,

这个能够正确解析你的CSV文件...

# => COLLOQ_TYPE:COLLOQ_NAME:COLLOQ_CODE:XDATA
# => S:BELT,FAN:003541547:
# => S:BELT V,FAN:000324244:
# => S:SHROUD SPRING SCREW:000868265:
# => S:D REL VALVE ASSY:000771881:
# => S:YBELT,V:000323030:
# => S:YBELT,'V':000322933:

我使用Text::ParseWords遇到的唯一问题是,当数据中的嵌套引号没有正确转义时。然而,这是构建不良的CSV数据,会对大多数CSV解析器造成问题 ;-)

因此,您可能会注意到

# S,"YBELT,"V"",000323030,

变成了(例如,“V”周围的引号被去掉)

# S:YBELT,V:000323030:

但是如果像这样进行了转义

# S,"YBELT,\"V\"",000323030,

那么引号将被保留

# S:YBELT,"V":000323030:

FYI:Text::ParseWords 已包含在所有 Perl 5 版本中: perl -MModule::CoreList -l -e'print Module::CoreList->first_release_by_date("Text::ParseWords");' printd 5.000 - mirod
1
不幸的是,除非您有一个维护状态的解析器,否则您无法逐行解析所有CSV。一些CSV在引号字符串内部具有文字换行符,这使得解析CSV成为一场噩梦。即:如果您自己解决了换行符问题,则会得到此结果:https://gist.github.com/1329430,但是当您将代码应用于文字数据时,您会得到这个怪物https://gist.github.com/1329436。像这样的细微差别是为什么您需要一个真正的解析器;) - Kent Fredric
如果字符串连续包含2个引号,例如 "他说" 你好 "",则此方法会失败。 - MikeKulls
@MikeKulls - 这是无效的CSV,所以我不惊讶它失败了。 - draegtun
1
@MikeKulls - 我知道,但你的例子是无效的,因为应该是 - "他说" 你好"" - draegtun
显示剩余2条评论

2

测试成功:工作中

$_.=','; # fake an ending delimiter

while($_=~/"((?:""|[^"])*)",|([^,]*),/g) {
  $cell=defined($1) ? $1:$2; $cell=~s/""/"/g; 
  print "$cell\n";
}

# The regexp strategy is as follows:
# First - we attempt a match on any quoted part starting the CSV line:-
#  "((?:""|[^"])*)",
# It must start with a quote, and end with a quote followed by a comma, and is allowed to contain either doublequotes - "" - or anything except a sinlge quote [^"] - this goes into $1
# If we can't match that, we accept anything up to the next comma instead, & put it into $2
# Lastly, we convert "" to " and print out the cell.

请注意,CSV文件中的单元格可能包含带引号的换行符,因此如果要逐行读取数据,则需要这样做:

if("$pre$_"=~/,"[^,]*\z/) {
  $pre.=$_; next;
}
$_="$pre$_";

你能否将那段代码重新组织成一种可以应用于文本主体的函数形式?我很想测试一下它在我的样本数据中的表现,就像我测试其他功能一样(例如:https://gist.github.com/1329456)。=) - Kent Fredric

2

这个功能非常好用

假设行是由逗号分隔且嵌有英文逗号的文本,

我的 @columns 变量会用 Text::ParseWords::parse_line(',', 0, $line) 方法解析行中的每一列。


1
使用正则表达式查找匹配对是一项非常复杂且通常无法解决的任务。在 Jeffrey Friedl 的《精通正则表达式》一书中有很多例子。我现在手头没有这本书,但我记得他也在一些例子中使用了 CSV。

“无法解决”?您可以使用正则表达式轻松查找匹配的引号!正则表达式不能处理括号,不是因为它们匹配,而是因为它们是嵌套匹配。您通常无法嵌套引号。(您可以使用 \ ",但这不会在旧字符串内开始新字符串,对吗?) - Chris Lutz
谢谢 Eugene,有趣的是我很确定Perl Cookbook中的例子是从MRE中提取的 :) 不过我会再确认一下。 - Mark Nold
1
"/((?:[^\n,"]|"(?:[^"]|"")+")+),/g" 应该更接近 OP 所期望的,但我承认仍不完美。 - Chris Lutz
是的,我相信在引号区域内使用原样引号是无效的CSV格式,需要一些转义机制,否则仅凭猜测就无法解决问题。 - Kent Fredric

0

测试通过:


use Test::More tests => 2;

use strict;

sub splitCommaNotQuote {
    my ( $line ) = @_;

    my @fields = ();

    while ( $line =~ m/((\")([^\"]*)\"|[^,]*)(,|$)/g ) {
        if ( $2 ) {
            push( @fields, $3 );
        } else {
            push( @fields, $1 );
        }
        last if ( ! $4 );
    }

    return( @fields );
}

is_deeply(
    +[splitCommaNotQuote('S,"D" REL VALVE ASSY,000771881,')],
    +['S', '"D" REL VALVE ASSY', '000771881', ''],
    "Quote in value"
);
is_deeply(
    +[splitCommaNotQuote('S,"BELT V,FAN",000324244,')],
    +['S', 'BELT V,FAN', '000324244', ''],
    "Strip quotes from entire value"
);

我知道样本数据集中没有列出,但是在带引号的字符串字段中间有换行符怎么办?你的代码在这种情况下能正常工作吗?你意识到CSV中带引号的字符串中允许换行符吗?你会浪费多少时间来重新调整和重新测试你的代码以处理这种边缘情况?因为我之前曾经实现过CSV解析器,所以我可以列举出许多会破坏天真解析器的情况,我向你保证,当你本可以安装和使用已有的东西并开始处理其他事情时,你会很快拥有一堆难以维护的代码。 - Kent Fredric
我提供了经过测试的代码,而你没有。有些人觉得正则表达式很难,这没关系。我相当喜欢和享受正则表达式(可能是因为我使用Emacs)。 - PP.
请在包含字段中间换行符的示例数据集上运行您的代码。祝您使用愉快 =)。 - Kent Fredric

0

你可以尝试使用CPAN.pm来简单地安装/更新Text::CSV。如前所述,你甚至可以将其“安装”到家目录或本地目录,并将该目录添加到@INC中(或者,如果你不想使用BEGIN块,你可以使用use lib 'dir'; - 这可能更好)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接