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 包,以防其他人需要这样的东西。

https://packagist.org/packages/wrossmann/costrenc

我最终得到的是工作代码:

 $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
    }
   }

谢谢大家的例子和建议!