【问题标题】:Data gets garbled when writing to csv with fputcsv() / fgetcsv()使用 fputcsv() / fgetcsv() 写入 csv 时数据出现乱码
【发布时间】:2017-06-08 06:13:07
【问题描述】:

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

以下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 或者在这里,为 delimiter、enclosure、escape_char 提供明确的值:https://3v4l.org/Svt7m

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

CSV 是一种完全可逆的格式吗?

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

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

【问题讨论】:

  • CSV 有很多限制,从嵌入式引号到多行数据。这就是使用 XML、JSON 和各种其他格式的原因。
  • 我在导出到 CSV 的数据中有大量嵌入的引号和换行符,直到现在从来没有出现过(可见的)问题。
  • CSV 数据中带有换行符是不可移植的,如果您只想为自己的应用程序提供数据,为什么还要使用 CSV 并编写您想要的任何旧格式 - 并且您使用各种分隔符- 你正在有效地做到这一点。
  • 1.取决于应用程序。例如。 LibreOffice Calc 对此没有任何问题。它可能是非标准的,但世界不在乎。 2. 因为原生支持 CSV(或 PHP 风格的 CSV),所以 3. 我主要使用标准分隔符。我使用不同分隔符的唯一原因是测试 PHP 的行为是否会有所不同。
  • 另外,我正在开发的应用程序已经有来自外部来源的各种 CSV,所以我更喜欢坚持这种格式而不是发明新格式。

标签: php csv escaping fgetcsv fputcsv


【解决方案1】:

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

【讨论】:

  • 嘿!我认为我的答案在这一点上是最新的。但是如果你是跟进 bug 报告的 cmb,如果 cmb 表示你创作了this commit,那么所有赞美你!我很乐意将复选框保留在您的答案上。
【解决方案2】:

2020 年 1 月更新

从 PHP 7.4 开始,将空字符串作为转义字符传递可以解决问题! https://www.php.net/manual/en/function.fgetcsv.php

Demo https://3v4l.org/33Wja - 查看 PHP 7.4 与旧版本的区别。 (这和下面的sn-p一样,只是用空字符串作为转义字符)

原答案

与其他人所说的相反,我声称这是一个 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

【讨论】:

    【解决方案3】:

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

    $enclosure = "'";
    

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

    【讨论】:

    • 这确实适用于给定的示例。但是可以简单地制作另一个字符串来打破它,就像这样:3v4l.org/LsTST
    • 您可以通过在字符串中添加 \n 来搞砸任何 CSV 导出,这就是为什么我对原始帖子的评论说其他格式更灵活。
    【解决方案4】:

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

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

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

    【讨论】:

    • 但目标是处理任意传入数据。如果我必须重写输入,我需要一个始终有效的算法..
    【解决方案5】:

    这不是 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);
    

    【讨论】:

    • 与奈杰尔的回答相同:它确实适用于给定的示例,但如果您的数组包含 "['a','\\\\','b']" 怎么办?或者更普遍的是,"[{$enclosure}a{$enclosure}{$delimiter}{$enclosure}{$escape_char}{$escape_char}{$enclosure}{$delimiter}{$enclosure}b{$enclosure}]" 或 ``
    • 您不能允许任何可能导致错误的(特殊)字符。您可以尝试在 json_encode():json_encode($value, JSON_HEX_QUOT); 中将 (") 转义为 Unicode 代码点。它会将双引号转换为 @987654328 @.
    • 或者用strtr()做临时替换..脏也很常见
    猜你喜欢
    • 1970-01-01
    • 2012-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-27
    • 1970-01-01
    • 1970-01-01
    • 2012-08-26
    相关资源
    最近更新 更多