【问题标题】:str_getcsv to separate lines removes enclosing characters in linesstr_getcsv 分隔行删除行中的封闭字符
【发布时间】:2020-02-27 02:28:28
【问题描述】:

str_getcsv () 有一些奇怪的行为。它会删除与封闭字符匹配的所有字符,而不是只删除封闭字符。我正在尝试分两步解析 CSV 字符串(上传文件的内容):

  1. 将 CSV 字符串拆分为行数组
  2. 将每一行拆分为一个字段数组

使用此代码:

$whole_file_string = file_get_contents($file);
$array_of_lines = str_getcsv ($whole_file_string, "\n", "\""); // step 1. split csv into lines
foreach ($array_of_lines as $one_line_string) {
    $splitted_line = str_getcsv ($one_line_string, ",", "\""); // step 2. split line into fields
};

为了清晰起见,在代码示例中,$splitted_line 没有做任何事情

然后我为这个脚本提供一个包含以下内容的文件:"text,with,delimiter",secondfield。 当执行第 1 步时,$array_of_lines 的第一个(也是唯一的)元素是 text,with,delimiter,secondfield。因此,当执行第 2 步时,它会将行拆分为 4 个字段,但需要为 2。

我不能使用fgetcsv(),因为在读取文件之后并在步骤 1 中将其拆分为行之前,已经完成了一些字符串转换(检查 BOM、相应地转换编码等)。

我正在编写自己的字符串解析器(这对于 CSV 格式来说并不复杂),但在我这样做之前,我想确保这是最好的方法。我有点失望,PHP 函数让我对这个简单(我猜很常见)的用例感到失望:处理上传的具有不同编码的 csv 文件。

有什么建议吗?

【问题讨论】:

    标签: php csv parsing


    【解决方案1】:

    您一次只能在一行上调用str_getcsv(),而不是整个文件。

    $array_of_lines = file($file, FILE_IGNORE_NEW_LINES); // split CSV into lines
    foreach($array_of_lines as $one_line_string) {
        $splitted_line = str_getcsv($one_line_string, ",", "\""); // split line into fields
    }
    

    【讨论】:

    • file() 方法也不起作用。它在换行符处机械地中断,因此当 $file 具有以下内容时:"Enclosed text\r\n with a line break","foo"\r\n foo,bar 它分成 3 行而不是 2 行,因为它没有得到第一个换行符是封闭字符串的一部分。这不是我想要的,当然也不符合 CSV 标准(这 file() 从来没有打算成为)。
    • 您可以将fgetcsv()php://memory 流一起使用吗?首先读取文件,进行转换,然后将其写入php://memory
    • 没有。 fgetcsv 和 str_getcsv 都错误地解释了 csv 格式!不管你用哪一个。他们在换行时会弄乱封闭的数据,然后在封闭的字段数据中的分隔符上切割字段。自己检查:$a = str_getcsv("\"one piece, of text\"", "\n"); $b = str_getcsv($a[0], ","); echo $b[0]; // "one piece" 但应该是echo $b[0]; // "one piece, of text"
    • 嗯。奇怪的。我在摆弄,这似乎有效:$a = str_getcsv("\"@ne piece, @f text\"", "\n","@");。将 @ 作为封闭字符而不是 " 传递(或将其排除在外并使用默认值,即 " 也是如此)的处理方式不同,并且不会被剥离。即使您将其用作字段的第一个和最后一个字符也不行。离奇...
    【解决方案2】:

    这是我自己的 CSV 解析器,完全符合 IETF rfc 4180。 我很想知道这是否可以在正则表达式中完成,这不是我的强项。

    /**
     * Parse a string according to CSV format (https://tools.ietf.org/html/rfc4180), with variabele delimiter (default ,).
     * @param string $string    String to be parsed as csv
     * @param string $delimiter character to be used as field delimiter  
     * @return array            Array with for each line an array with csv field values
     */
     function csv_parse ($string, $delimiter = ",", $line_mode = true) {
        // This function parses on line-level first ($line_mode = true) and calls itself recursively to parse each line on field-level ($line_mode = false).
        // when in line mode, the delimiter is eol (\n, \r\n and \n\r). 
        // when in field mode, the delimiter is the passed $delimiter. 
    
        $delimiter = substr ($delimiter,0,1);                           // delimiter is one character       
        $length = strlen ($string);
        $parsed_array = array();
        $end_of_line_state = false;
        $enclosed_state = false;
        $i = 0;
        $field = "";
        do {
            switch (true) {
                case (!$enclosed_state && $end_of_line_state && ($string[$i] == "\r" && $string[$i-1] == "\n")) :
                case (!$enclosed_state && $end_of_line_state && ($string[$i] == "\n" && $string[$i-1] == "\r")) :
                    // ...found second character of eol (\r\n of \n\r). Ignore
                    $end_of_line_state = false;
                    break;
                case (!$enclosed_state && !$end_of_line_state && ($string[$i] == "\n" || $string[$i] == "\r")) :
                        // ... found first character of eol \n, \r\n of \n\r
                    $end_of_line_state = true;                  // eol can be two characters, so prepare for the second
                    if ($field != "") {                     // ignore empty lines. Prohibited in csv
                        $parsed_array [] = csv_parse ($field, $delimiter, false); // recursive call to parse on field-level. Flush result
                    };
                    $field = "";                                // prepare for next one
                    break;
                case (!$enclosed_state && $string[$i] == $delimiter && !$line_mode) : 
                    // ...delimiter found
                    $parsed_array [] = $field;              // flush field as new array element
                    $field = "";                                // prepare for next one
                    break;
                case ($string[$i] == "\"") :
                    // ...encloser found
                    if ($enclosed_state) {
                        if ($i < $length && $string[$i+1] == "\"") {
                            // ... escaped " found
                            if (!$line_mode) {
                                $field .= "\"";                     // when parsing fieldlevel, only " is part of the line
                            } else {
                                $field .= "\"\"";                   // when parsing line level, the escaping " is also part of the line
                            };
                            $i++;
                        } else {
                            // ...closing encloser found
                            $enclosed_state = false;
                            if ($line_mode) {
                                $field .= $string[$i];              // when parsing line level, the enclosing " are part of the line
                            };
                        };
                    } else {
                        // ... opening encloser found 
                        $enclosed_state = true;
                        if ($line_mode) {
                            $field .= $string[$i];                  // when parsing line level, the enclosing " are part of the line
                        };
                    };
                    break;
                default:
                    // ...regular character found
                    $field .= $string[$i];
            };
            $i++;
    
            if ($i >= $length) {                                    // end of string
                if ($line_mode) {
                    $parsed_array [] = csv_parse ($field, $delimiter, false); // recursive call to parse on field-level. Flush result.
                } else {
                    $parsed_array [] = $field;                      // flush last field
                };
            };
        } while ($i < $length); 
        return $parsed_array;    
    };
    

    【讨论】:

      【解决方案3】:

      嗯。奇怪的。我摆弄了一下,发现了一些有趣的东西:

      $a = str_getcsv("\"@ne piece, @f text\"", "\n");
      $b = str_getcsv("\"@ne piece, @f text\"", "\n","\"");
      $c = str_getcsv("\"@ne piece, @f text\"", "\n","@");
      echo $a; // @ne piece, @f text
      echo $b; // @ne piece, @f text
      echo $c; // "@ne piece, @f text"
      

      因此,将@ 作为封闭字符而不是" 传递(或将其省略并使用默认值,也就是")在语义上是牛逼的,但它可以完成工作。它在字段周围留下封闭的",所以如果你然后str_getcsv($c, ",") 它会产生一个应该的值。如果你有一个包含在 @ 中的字段,它会保持不变。我测试过。

      很明显 str_getcsv 有它的缺陷。在拆分为行时,它不应该去掉引号,就像它不去掉 @ 时那样是封闭参数。但遗憾的是它确实如此,因此不符合 CSV 标准。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-02-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-28
        • 2021-02-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多