封面图片源自 Pixabay
前言
前段时间在应用 str_getcsv
和 fgetcsv
解决 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_CTYPE
是 Chinese (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
。