关于php:Windows-下-PHP-7-中-getcsv-函数解析-CSV-错误的问题记录

111次阅读

共计 3145 个字符,预计需要花费 8 分钟才能阅读完成。

封面图片源自 Pixabay

前言

前段时间在应用 str_getcsvfgetcsv 解决 CSV 文件的时候遇到的一个问题:

测试中, 文,foo,bar,123

预期状况下,应该返回一个数组。["测试中", "文", "foo", "bar", "123"],而理论却失去了 ["测试中, 文,foo", "bar", "123"],是的,测试中, 文 竟然没有被离开,通过一番测试和查证,最初发现,这个问题默认状况下只会在 Windows 上的 PHP 7 版本(5 测试的时候没有问题,然而会乱码)中呈现(还跟字符长度无关),Linux 下默认没问题。

问题起源

因为是间接从文件进行获取解决,共事一开始间接应用的 explode(',', $row) 进行解决,一开始是好的,然而当 CSV 列中呈现了 , 号的时候,就会被意外离开了,至于源数据,不便做批改。为了解决这个问题,我将其改为 str_getcsv 进行解决,却引发了这个问题。

简略说一下 CSV 格局,个别状况下,应用逗号 (,) 宰割列,用换行来示意新行,而共事一开始就是以 explode 的形式来解析单行的数据,而这种状况下,如果有一列的数据中呈现了 逗号(,) 就会导致被意外宰割,多处一列数据来,显然这是不合理的,为此就须要引入本义解决。

为了在单列数据中应用逗号(,),那就须要应用英文的双引号(")把这一列数据包起来(对于须要换行的数据也须要这样解决),而当咱们须要示意一个双引号时,就须要双写这一个双引号,就像这样子。

"php,composer",foo,bar"","
say"

下面的例子该当被解析为:

array(4) {[0]=>
  string(12) "php,composer"
  [1]=>
  string(3) "foo"
  [2]=>
  string(5) "bar"""
  [3]=>
  string(4) "say"
}

解决问题

通过多个环境验证,发现在 Linux 下没有问题,在 PHP 8 也没问题,就只有 PHP 7 上有这个问题。

当搜寻过一番时,发现遇到过最多的问题,都是乱码,偶有人提到过这个问题。

因为这里编码解析失常,天然不认为是编码的问题,所以持续找材料,顺带还问了问 ChatGPT,一开始他也文不对题的说,是分隔符的问题,最初再疏导下,他提到,能够增加 UTF-8 BOM(字节程序标记(英语:byte-order mark,BOM))来解决。

于是便调整代码,大抵如下:

$str = '';
$str .= "\xEF\xBB\xBF";
$str .= '测试中, 文,foo,bar,123';
var_dump(str_getcsv($str));

当尝试增加 BOM 之后,后果从原先的 ["测试中, 文,foo", "bar", "123"] 变成了 ["测试中", "文,foo", "bar", "123"] 🤔。

然而有些状况下就会正确了,假如去掉第二列的 字,就能够合乎预期,然而这显然不行,因为这样(增加 BOM)不能解决所有状况,所以还是不合时宜的。

通过在 PHP 的 Change Log 外面一番搜寻 csv,找到了一条。

  • Fixed bug #72330 (CSV fields incorrectly split if escape char followed by UTF chars).

在这个 bug 中,有人遇到了同样的问题,并且提供了残缺的复现步骤给出了。

其中有人给出了一个解决方案,就是通过设置 setlocale(LC_ALL, ‘C’) 办法设置本机运行的 locale 信息,从而解决。

既然要设置,无妨先看看,以后的 locale 是什么,在我的 Windows 平台上,执行 setlocale(LC_ALL, 0),其返回为:

LC_COLLATE=C;LC_CTYPE=Chinese (Simplified)_China.936;LC_MONETARY=C;LC_NUMERIC=C;LC_TIME=C

而当在 Linux 上执行时,这里返回 C

留神这里,在咱们 Windows 平台上 PHP 7.x 这里的 LC_CTYPEChinese (Simplified)_China.936,而自 PHP 8 开始,在 Windows 平台上 LC_CTYPE,将默认为 C,所以在 PHP 8 上没有了这个问题。

  • PHP: Other Changes – Manual
setlocale(LC_ALL, 'C');
$str = '测试中,foo,bar,123';
var_dump(str_getcsv($str));

当初这个后果将合乎预期,输入:["测试中", "文", "foo", "bar", "123"]

看起来所有都很好,问题被实打实的解决,然而,在后续的探讨中,PHP 官网回复指出,因为 str_getcsv 思考了 locale,所以是能够通过设置 locale 来解决这个问题。

然而这并不是一个好的解决方案,正如 setlocale 在文档中所写的。

区域信息是按过程保护的,而不是线程。如果在多线程服务器 API 上运行 PHP,区域设置可能在脚本运行时忽然变动,只管脚本自身并没有调用 setlocale()。这是因为其它脚本在同一时刻的同一过程的不同线程中运行,应用 setlocale() 扭转了过程级别的区域。在 Windows 上,自 PHP 7.0.5 起,每个线程都保护本人的区域信息。

而给出的另一个计划是,将源字符串转为 CSV 能够辨认并解决的编码,解决当前,再转回去。🤔

在 中文环境下的 Windows 平台上,将会是这样,后果合乎预期。

$str = '测试中, 文,foo,bar,123';
$str = mb_convert_encoding($str, 'gb2312', 'UTF-8');
$arr = str_getcsv($str);
$arr = array_map(function ($v) {return mb_convert_encoding($v, 'UTF-8', 'gb2312');
}, $arr);

var_dump($arr);

总之,就是最好的实现形式就是提供一个不依赖用户 locale 设置的办法来解决。

问了问 ChatGPT,TA 给出了一份答案:

function user_str_getcsv($input, $delimiter = ',', $enclosure = '"', $escape ='\\')
{$output = array();
    $string = '';
    $quote = false;

    $strlen = mb_strlen($input);
    for ($i = 0; $i < $strlen; $i++) {$char = mb_substr($input, $i, 1);

        if ($char === $enclosure) {$quote = !$quote;} elseif (!$quote && (($char === $delimiter) || ($char === "\n"))) {$output[] = $string;
            $string = '';
            if ($char === "\n") {break;}
        } elseif ($char === $escape) {
            $i++;
            $string .= ($i < $strlen) ? mb_substr($input, $i, 1) : '';
        } else {$string .= $char;}
    }

    $output[] = $string;
    return $output;
}

然而这样的性能或者不肯定高。


这份回复后,PHP 文档中,将本来在上面的“此函数思考区域设置。如果 LC_CTYPE 是相似 en_US.UTF-8 的值,此函数将谬误的读取单字节编码的字符串。”

总结

解决这个问题的计划有几个:

  • 1、应用 setlocale 办法设置 locale 为 C。能够仅设置 LC_CTYPE。
  • 2、手动对传入的数据进行编码转换解决
  • 3、实现自行实现一个 CSV 办法[1]
  • 4、应用 PHP8

locale 的设置影响内置函数的行为比拟多的,所以请审慎处理 LC_ALL

正文完
 0