使用 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()
可以修好吗? -> 是的。
更改分隔符有帮助吗?
可以证明,使用定界符、封闭字符和转义字符的任意组合都可以重现这一点。
$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.
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()
可以修好吗? -> 是的。
更改分隔符有帮助吗?
可以证明,使用定界符、封闭字符和转义字符的任意组合都可以重现这一点。
$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.