fgetcsv 编码问题 (PHP)
fgetcsv encoding issue (PHP)
我收到了一个以制表符分隔的 csv 文件。这是我看到的示例:
Invoice: Invoice Date Account: Name Bill To: First Name Bill To: Last Name Bill To: Work Email Rate Plan Charge: Name Subscription: Device Serial Number
2021-03-10 Test Company Wally Kolcz test@test.com Sample plan A0H1234567890A
我写了一个脚本来打开、读取和循环这些值,但之后我得到了奇怪的东西:
if (($handle = fopen($user_file, "r")) !== FALSE) {
while (($data = fgetcsv($handle, 1000, "\t")) !== FALSE) {
if($line >1 && isset($data[1])){
$user = [
'EmailAddress' => $data[4],
'Name' => $data[2].' '.$data[3],
];
}
$line++;
}
fclose($handle);
}
这是我转储第一行时得到的结果。
array:7 [▼
0 => b"ÿþI\x00n\x00v\x00o\x00i\x00c\x00e\x00:\x00 \x00I\x00n\x00v\x00o\x00i\x00c\x00e\x00 \x00D\x00a\x00t\x00e\x00"
1 => "\x00A\x00c\x00c\x00o\x00u\x00n\x00t\x00:\x00 \x00N\x00a\x00m\x00e\x00"
2 => "\x00B\x00i\x00l\x00l\x00 \x00T\x00o\x00:\x00 \x00F\x00i\x00r\x00s\x00t\x00 \x00N\x00a\x00m\x00e\x00"
3 => "\x00B\x00i\x00l\x00l\x00 \x00T\x00o\x00:\x00 \x00L\x00a\x00s\x00t\x00 \x00N\x00a\x00m\x00e\x00"
4 => "\x00B\x00i\x00l\x00l\x00 \x00T\x00o\x00:\x00 \x00W\x00o\x00r\x00k\x00 \x00E\x00m\x00a\x00i\x00l\x00"
5 => "\x00R\x00a\x00t\x00e\x00 \x00P\x00l\x00a\x00n\x00 \x00C\x00h\x00a\x00r\x00g\x00e\x00:\x00 \x00N\x00a\x00m\x00e\x00"
6 => "\x00S\x00u\x00b\x00s\x00c\x00r\x00i\x00p\x00t\x00i\x00o\x00n\x00:\x00 \x00D\x00e\x00v\x00i\x00c\x00e\x00 \x00S\x00e\x00r\x00i\x00a\x00l\x00 \x00N\x00u\x00m\x00b\x00e\x00r\x00 ◀"
]
我尝试添加:
header('Content-Type: text/html; charset=UTF-8');
$data = array_map("utf8_encode", $data);
setlocale(LC_ALL, 'en_US.UTF-8');
当我转储 mb_detect_encoding($data[2])
时,我得到 'ASCII'...
有什么方法可以解决这个问题,这样我就不必在每次收到文件时都手动更新文件了吗?谢谢!
看起来文件是 UTF-16 格式的(每隔一个字节都是空的)。
您可能需要用 mb_convert_encoding($data, "UTF-8", "UTF-16");
之类的东西转换整个文件
但在那种情况下你不能真正使用 fgetcsv()…
正如@Andrea 已经提到的,您的数据编码为 UTF-16LE,您需要将其转换为与您想要执行的操作兼容的编码。也就是说, 可以在飞行中使用 PHP 流过滤器。
abstract class TranslateCharset extends php_user_filter {
protected $in_charset, $out_charset;
private $buffer = '';
private $total_consumed = 0;
public function filter($in, $out, &$consumed, $closing) {
$output = '';
while ($bucket = stream_bucket_make_writeable($in)) {
$input = $this->buffer . $bucket->data;
for( $i=0, $p=0; ($c=mb_substr($input, $i, 1, $this->in_charset)) !== ""; ++$i, $p+=strlen($c) ) {
$output .= mb_convert_encoding($c, $this->out_charset, $this->in_charset);
}
$this->buffer = substr($input, $p);
$consumed += $p;
}
// this means that there's unconverted data at the end of the bridage.
if( $closing && strlen($this->buffer) > 0 ) {
$this->raise_error( sprintf(
"Likely encoding error at offset %d in input stream, subsequent data may be malformed or missing.",
$this->total_consumed += $consumed)
);
$consumed += strlen($this->buffer);
// give it the ol' college try
$output .= mb_convert_encoding($this->buffer, $this->out_charset, $this->in_charset);
}
$this->total_consumed += $consumed;
if ( ! isset($bucket) ) {
$bucket = stream_bucket_new($this->stream, $output);
} else {
$bucket->data = $output;
}
stream_bucket_append($out, $bucket);
return PSFS_PASS_ON;
}
protected function raise_error($message) {
user_error( sprintf(
"%s[%s]: %s",
__CLASS__, get_class($this), $message
), E_USER_WARNING);
}
}
class UTF16LEtoUTF8 extends TranslateCharset {
protected $in_charset = 'UTF-16LE';
protected $out_charset = 'UTF-8';
}
stream_filter_register('UTF16LEtoUTF8', 'UTF16LEtoUTF8');
// properly-encoded UTF-16BE example input "Invoice:,a"
$in = "\xFE\xFFI\x00n\x00v\x00o\x00i\x00c\x00e\x00:\x00,\x00a\x00";
// prep example pipe, in practice this would simple be your fopen() call.
$fh = fopen('php://memory', 'rwb+');
fwrite($fh, $in);
rewind($fh);
// skip BOM
fseek($fh, 2);
stream_filter_append($fh, 'UTF16LEtoUTF8', STREAM_FILTER_READ);
var_dump(fgetcsv($fh, 4096));
输出:
array(2) {
[0]=>
string(8) "Invoice:"
[1]=>
string(1) "a"
}
实际上,没有“灵丹妙药”来检测输入文件或字符串的编码。在这种情况下,有一个 0xFF 0xFE
的字节顺序标记 [BOM] 表示它在 UTF-16LE 中,但 BOM 经常被省略,或者可能只是自然地出现在任意字符串的开头,或者根本不是大多数编码都需要,或者根本不被编码数据的人使用。
最后一点正是为什么每个人都应该像瘟疫一样避免使用 utf8_encode()
和 utf8_decode()
函数的确切原因,因为它们只是假设您只想在 UTF-8 和 ISO 之间切换-8859-1 [西欧],并且在使用不当时不努力避免损坏您的数据,因为他们不可能知道得更好。
TLDR:您必须明确知道输入数据的编码方式,否则您会遇到麻烦。
编辑: 因为我已经离开并在上面放了一个适当的 spitshine,所以我把它作为一个 Composer 包,以防其他人需要这样的东西。
我最终得到的是工作代码:
$f = file_get_contents($user_file);
$f = mb_convert_encoding($f, 'UTF8', 'UTF-16LE');
$f = preg_split("/\R/", $f);
$f = array_map('str_getcsv', $f);
$line = 0;
foreach($f as $record){
if($line !== 0 && isset($record[0])){
$pieces = preg_split('/[\t]/',$record[0]);
//My work here
}
}
谢谢大家的例子和建议!
我收到了一个以制表符分隔的 csv 文件。这是我看到的示例:
Invoice: Invoice Date Account: Name Bill To: First Name Bill To: Last Name Bill To: Work Email Rate Plan Charge: Name Subscription: Device Serial Number
2021-03-10 Test Company Wally Kolcz test@test.com Sample plan A0H1234567890A
我写了一个脚本来打开、读取和循环这些值,但之后我得到了奇怪的东西:
if (($handle = fopen($user_file, "r")) !== FALSE) {
while (($data = fgetcsv($handle, 1000, "\t")) !== FALSE) {
if($line >1 && isset($data[1])){
$user = [
'EmailAddress' => $data[4],
'Name' => $data[2].' '.$data[3],
];
}
$line++;
}
fclose($handle);
}
这是我转储第一行时得到的结果。
array:7 [▼
0 => b"ÿþI\x00n\x00v\x00o\x00i\x00c\x00e\x00:\x00 \x00I\x00n\x00v\x00o\x00i\x00c\x00e\x00 \x00D\x00a\x00t\x00e\x00"
1 => "\x00A\x00c\x00c\x00o\x00u\x00n\x00t\x00:\x00 \x00N\x00a\x00m\x00e\x00"
2 => "\x00B\x00i\x00l\x00l\x00 \x00T\x00o\x00:\x00 \x00F\x00i\x00r\x00s\x00t\x00 \x00N\x00a\x00m\x00e\x00"
3 => "\x00B\x00i\x00l\x00l\x00 \x00T\x00o\x00:\x00 \x00L\x00a\x00s\x00t\x00 \x00N\x00a\x00m\x00e\x00"
4 => "\x00B\x00i\x00l\x00l\x00 \x00T\x00o\x00:\x00 \x00W\x00o\x00r\x00k\x00 \x00E\x00m\x00a\x00i\x00l\x00"
5 => "\x00R\x00a\x00t\x00e\x00 \x00P\x00l\x00a\x00n\x00 \x00C\x00h\x00a\x00r\x00g\x00e\x00:\x00 \x00N\x00a\x00m\x00e\x00"
6 => "\x00S\x00u\x00b\x00s\x00c\x00r\x00i\x00p\x00t\x00i\x00o\x00n\x00:\x00 \x00D\x00e\x00v\x00i\x00c\x00e\x00 \x00S\x00e\x00r\x00i\x00a\x00l\x00 \x00N\x00u\x00m\x00b\x00e\x00r\x00 ◀"
]
我尝试添加:
header('Content-Type: text/html; charset=UTF-8');
$data = array_map("utf8_encode", $data);
setlocale(LC_ALL, 'en_US.UTF-8');
当我转储 mb_detect_encoding($data[2])
时,我得到 'ASCII'...
有什么方法可以解决这个问题,这样我就不必在每次收到文件时都手动更新文件了吗?谢谢!
看起来文件是 UTF-16 格式的(每隔一个字节都是空的)。
您可能需要用 mb_convert_encoding($data, "UTF-8", "UTF-16");
但在那种情况下你不能真正使用 fgetcsv()…
正如@Andrea 已经提到的,您的数据编码为 UTF-16LE,您需要将其转换为与您想要执行的操作兼容的编码。也就是说, 可以在飞行中使用 PHP 流过滤器。
abstract class TranslateCharset extends php_user_filter {
protected $in_charset, $out_charset;
private $buffer = '';
private $total_consumed = 0;
public function filter($in, $out, &$consumed, $closing) {
$output = '';
while ($bucket = stream_bucket_make_writeable($in)) {
$input = $this->buffer . $bucket->data;
for( $i=0, $p=0; ($c=mb_substr($input, $i, 1, $this->in_charset)) !== ""; ++$i, $p+=strlen($c) ) {
$output .= mb_convert_encoding($c, $this->out_charset, $this->in_charset);
}
$this->buffer = substr($input, $p);
$consumed += $p;
}
// this means that there's unconverted data at the end of the bridage.
if( $closing && strlen($this->buffer) > 0 ) {
$this->raise_error( sprintf(
"Likely encoding error at offset %d in input stream, subsequent data may be malformed or missing.",
$this->total_consumed += $consumed)
);
$consumed += strlen($this->buffer);
// give it the ol' college try
$output .= mb_convert_encoding($this->buffer, $this->out_charset, $this->in_charset);
}
$this->total_consumed += $consumed;
if ( ! isset($bucket) ) {
$bucket = stream_bucket_new($this->stream, $output);
} else {
$bucket->data = $output;
}
stream_bucket_append($out, $bucket);
return PSFS_PASS_ON;
}
protected function raise_error($message) {
user_error( sprintf(
"%s[%s]: %s",
__CLASS__, get_class($this), $message
), E_USER_WARNING);
}
}
class UTF16LEtoUTF8 extends TranslateCharset {
protected $in_charset = 'UTF-16LE';
protected $out_charset = 'UTF-8';
}
stream_filter_register('UTF16LEtoUTF8', 'UTF16LEtoUTF8');
// properly-encoded UTF-16BE example input "Invoice:,a"
$in = "\xFE\xFFI\x00n\x00v\x00o\x00i\x00c\x00e\x00:\x00,\x00a\x00";
// prep example pipe, in practice this would simple be your fopen() call.
$fh = fopen('php://memory', 'rwb+');
fwrite($fh, $in);
rewind($fh);
// skip BOM
fseek($fh, 2);
stream_filter_append($fh, 'UTF16LEtoUTF8', STREAM_FILTER_READ);
var_dump(fgetcsv($fh, 4096));
输出:
array(2) {
[0]=>
string(8) "Invoice:"
[1]=>
string(1) "a"
}
实际上,没有“灵丹妙药”来检测输入文件或字符串的编码。在这种情况下,有一个 0xFF 0xFE
的字节顺序标记 [BOM] 表示它在 UTF-16LE 中,但 BOM 经常被省略,或者可能只是自然地出现在任意字符串的开头,或者根本不是大多数编码都需要,或者根本不被编码数据的人使用。
最后一点正是为什么每个人都应该像瘟疫一样避免使用 utf8_encode()
和 utf8_decode()
函数的确切原因,因为它们只是假设您只想在 UTF-8 和 ISO 之间切换-8859-1 [西欧],并且在使用不当时不努力避免损坏您的数据,因为他们不可能知道得更好。
TLDR:您必须明确知道输入数据的编码方式,否则您会遇到麻烦。
编辑: 因为我已经离开并在上面放了一个适当的 spitshine,所以我把它作为一个 Composer 包,以防其他人需要这样的东西。
我最终得到的是工作代码:
$f = file_get_contents($user_file);
$f = mb_convert_encoding($f, 'UTF8', 'UTF-16LE');
$f = preg_split("/\R/", $f);
$f = array_map('str_getcsv', $f);
$line = 0;
foreach($f as $record){
if($line !== 0 && isset($record[0])){
$pieces = preg_split('/[\t]/',$record[0]);
//My work here
}
}
谢谢大家的例子和建议!