使用 fputcsv() / fgetcsv() 写入 csv 时数据出现乱码

Data gets garbled when writing to csv with fputcsv() / fgetcsv()

PHP 中 fputcsv() 和 fgetcsv() 似乎存在编码问题或错误。

以下PHP代码:

$row_before = ['A', json_encode(['a', '\', 'b']), 'B'];

print "\nBEFORE:\n";
var_export($row_before);
print "\n";

$fh = fopen($file = 'php://temp', 'rb+');

fputcsv($fh, $row_before);

rewind($fh);

$row_after = fgetcsv($fh);

print "\nAFTER:\n";
var_export($row_after);
print "\n\n";

fclose($fh);

给我这个输出:

BEFORE:
array (
  0 => 'A',
  1 => '["a","\\","b"]',
  2 => 'B',
)

AFTER:
array (
  0 => 'A',
  1 => '["a","\\',
  2 => 'b""]"',
  3 => 'B',
)

很明显,数据在途中损坏了。最初一行只有 3 个单元格,后来一行中有 4 个单元格。由于反斜杠也用作转义字符,中间单元格被拆分。

另请参阅 https://3v4l.org/nc1oE 或者在这里,使用分隔符、附件的显式值,escape_char:https://3v4l.org/Svt7m

有什么方法可以在写入 CSV 之前清理/转义我的数据,以保证从文件中读取的数据完全相同?

CSV 是完全可逆的格式吗?

编辑:目标是建立一种机制,以 csv 形式正确写入和读取任何数据,以便在一次往返之后数据仍然相同。

编辑:我意识到我并不真正理解 $escape_char 参数。另请参阅 fgetcsv/fputcsv $escape parameter fundamentally broken 也许对此的回答也会使我们更接近解决方案。

使用带有特定分隔符的代码但更改以下行将起作用...

$enclosure = "'";

我认为这可能与认为 \ 正在转义以下引号有关。

与php中一样,\用于转义反斜杠(link for PHP manual escape sequence),因此要使其成为字符串,您需要再使用一个单引号(' ')。

所以你的输入数组应该是...

$row_before = ['A', json_encode(['a', "'\'", 'b']), 'B'];

这不是 PHP 错误。 json_encode() 似乎使用相同的分隔符 (,)、包围符 (") 和转义符 (\),这与 fputcsv()fgetcsv() 的默认分隔符、包围符和转义符相同。可区分圈闭或转义,必要时可加定界符。

正如已经回答的那样,在这种情况下,它将通过使用 (') 指定附件来工作:

$row_before = ['A', json_encode(['a', '\', 'b']), 'B'];

print "\nBEFORE:\n";
var_export($row_before);
print "\n";

$fh = fopen($file = 'php://temp', 'rb+');

fputcsv($fh, $row_before, ',', "'");

rewind($fh);

$row_after = fgetcsv($fh, 0, ',', "'");

print "\nAFTER:\n";
var_export($row_after);
print "\n\n";

fclose($fh);

2020 年 1 月更新

自 PHP 7.4 起,传递一个空字符串作为转义字符解决了这个问题! https://www.php.net/manual/en/function.fgetcsv.php

演示 https://3v4l.org/33Wja - 查看 PHP 7.4 与旧版本的区别。 (这是与下面相同的代码段,只是将空字符串作为转义字符)

原回答

与其他人所说的相反,我声称这是一个 PHP 错误。我要报告它,并更新这个答案。

编辑:现在在这里报告,https://bugs.php.net/bug.php?id=74713

在这个回答中讨论:

  • 更改分隔符有帮助吗? -> 不是真的。
  • fputcsv()可以修好吗? -> 是的。

更改分隔符有帮助吗?

可以证明,使用定界符、封闭字符和转义字符的任意组合都可以重现这一点。

https://3v4l.org/a29kR

$delimiter = 'X';
$enclosure = 'Y';
$escape_char = "Z";

$row_before = [
  'A',
  "[{$enclosure}a{$enclosure}{$delimiter}{$enclosure}{$escape_char}{$escape_char}{$enclosure}{$delimiter}{$enclosure}b{$enclosure}]",
  'B',
];

print "\nBEFORE:\n";
var_export($row_before);
print "\n";

$fh = fopen($file = 'php://temp', 'rb+');

fputcsv($fh,$row_before,$delimiter,$enclosure, $escape_char);

rewind($fh);

$row_plain = fread($fh, 1000);

print "\nPLAIN:\n";
var_export($row_plain);
print "\n";

rewind($fh);

$row_after = fgetcsv($fh, 500,$delimiter,$enclosure, $escape_char);

print "\nAFTER:\n";
var_export($row_after);
print "\n\n";

fclose($fh);

输出:

BEFORE:
array (
  0 => 'A',
  1 => '[YaYXYZZYXYbY]',
  2 => 'B',
)

PLAIN:
'AXY[YYaYYXYYZZYXYYbYY]YXB
'

AFTER:
array (
  0 => 'A',
  1 => '[YaYXYZZ',
  2 => 'bYY]Y',
  3 => 'B',
)

可以修复 fputcsv() 吗?

为此让我们回到更常见和可读的定界符、包围符和转义符。

$delimiter = ',';
$enclosure = '"';
$escape_char = "@";

这里的结果是:

BEFORE:
array (
  0 => 'A',
  1 => '["a","@@","b"]',
  2 => 'B',
)

PLAIN:
'A,"[""a"",""@@",""b""]",B
'

AFTER:
array (
  0 => 'A',
  1 => '["a","@@',
  2 => 'b""]"',
  3 => 'B',
)

我们看到 '"@@"' 部分导出为 '""@@"',而它应该导出为 '""@@""'

事实上,使用 fwrite() 而不是 fputcsv() 手动执行此操作确实可以解决问题:https://3v4l.org/4U1CQ

罪魁祸首是 fputcsv() 使用了转义字符,这是 CSV 的非标准扩展。 (好吧,就 RFC 7111 而言,可以将其视为标准。)基本上,必须禁用此转义字符,但将空字符串作为 $escape 传递给 fputcsv() 不起作用。通常,传递一个 NUL 字符应该会得到预期的结果,但是,请参阅 https://3v4l.org/MlluN.