CSV 的难点不在逗号,而在记录边界和引号。Text::CSV_XS 的 binary => 1 允许字段中出现换行等字节;双引号字段内部的 "" 表示一个真实的 " 字符;未闭合引号必须被拒绝。课程若只用 join ',' 或只测 a,b,无法证明报表能处理接收方真正会遇到的数据。

累计实验 examples/perl-language/experiments/course-csv-xs.pl 固定加载 Text::CSV_XS 1.64,解析 id,"two\nlines","a""b"。测试断言字段结果是 id、带真实换行的 two\nlines、以及 a"b,同时把 id,"unterminated 判为 rejected。t/course-dependencies.t 又在同一数据上运行 Text::CSV_PP,要求字段结果一致,但后端身份不同。XS 和 PP 的行为可以比较,不能互相代验。

1
2
3
4
use Text::CSV_XS ();
my $csv = Text::CSV_XS->new({ binary => 1, eol => "\n" });
$csv->parse(qq{id,"two\nlines","a""b"}) or die $csv->error_diag;
my @fields = $csv->fields; # id, newline field, q{a"b}

本篇把 parse 和 getline 的边界分开:parse 接收的是一条已经完整的 CSV 记录字符串;getline($fh) 从文件句柄读取一条 CSV 记录,遇到带引号换行字段时,一条逻辑记录可以跨越多个物理行。实验用 parse 固定最小样本,课程正文说明文件入口时不能把“读一行”当成“读一条 CSV 记录”。

反例限定为本例中的 split /,/。它看不到 CSV 状态机,所以无法知道逗号当前是在引号字段里还是字段之间;"a,b" 会被拆错,"two\nlines" 的记录边界也会被普通按行读取拆错。答案不是写更长的正则,而是交给 CSV 解析器,并用未闭合引号反例验证失败路径。

来源固定在 Text::CSV_XS 1.64 POD 与源码包 Text-CSV_XS-1.64.tgz,通用前端参考 Text::CSV 2.06 POD。练习:为什么默认 Text::CSV 不能替代本篇证据?答案是本机存在 XS 时 Text::CSV 2.06 会自动选择 Text::CSV_XS,课程另用 PERL_TEXT_CSV=Text::CSV_PP 强制纯 Perl 后端,二者身份必须分别记录。