【问题标题】:PHP - Best approach to detect CSV delimiterPHP - 检测 CSV 分隔符的最佳方法
【发布时间】:2014-12-30 07:11:04
【问题描述】:

我已经看到了多个关于自动检测传入 CSV 分隔符的最佳解决方案的主题。其中大部分是长度在 20 - 30 行之间的函数,多个循环预先确定的分隔符列表,读取前 5 行并匹配计数 e.t.c e.t.c

Here is 1 example

我刚刚实现了这个过程,做了一些修改。效果很好。

然后我找到了以下代码:

private function DetectDelimiter($fh)
{
    $data_1 = null;
    $data_2 = null;
    $delimiter = self::$delim_list['comma'];
    foreach(self::$delim_list as $key=>$value)
    {
        $data_1 = fgetcsv($fh, 4096, $value);
        $delimiter = sizeof($data_1) > sizeof($data_2) ? $key : $delimiter;
        $data_2 = $data_1;
    }

    $this->SetDelimiter($delimiter);
    return $delimiter;
}

在我看来,这似乎实现了相同的结果,其中 $delim_list 是一个分隔符数组,如下所示:

static protected $delim_list = array('tab'=>"\t", 
                                     'semicolon'=>";", 
                                     'pipe'=>"|", 
                                     'comma'=>",");

任何人都可以解释为什么我不应该以这种更简单的方式来做,以及为什么在我看来更复杂的解决方案似乎是公认的答案?

谢谢!

【问题讨论】:

  • 我认为这个解决方案更具可读性,并且更清洁了链接中的另一个示例。
  • 只有我一个人认为神奇地确定分隔符在逻辑上是错误的吗?如果分隔符不是逗号(顾名思义 - 逗号分隔值),则在请求中查找指定的分隔符。如果没有,则中止解析,直到发送有效信息。
  • @N.B.你有一个有效的观点。对于这个项目,我只需要在给用户选项之前猜测。如果使用上面详述的方法 2 未能给我任何合法数据,那么我将要求用户指定他们的分隔符。然而,我喜欢方法 1 的一点是,如果它找到 2 个或更多匹配的分隔符,那么我可以通知用户并让他们从我认为我找到的选项中进行选择,或者建议他们自己的。

标签: php csv delimiter


【解决方案1】:

这个函数很优雅:)

/**
* @param string $csvFile Path to the CSV file
* @return string Delimiter
*/
public function detectDelimiter($csvFile)
{
    $delimiters = [";" => 0, "," => 0, "\t" => 0, "|" => 0];

    $handle = fopen($csvFile, "r");
    $firstLine = fgets($handle);
    fclose($handle); 
    foreach ($delimiters as $delimiter => &$count) {
        $count = count(str_getcsv($firstLine, $delimiter));
    }

    return array_search(max($delimiters), $delimiters);
}

【讨论】:

  • 不错!但它会返回找不到分隔符的错误值(因为它只是从数组中返回一个随机元素)
  • 要检测是否没有找到分隔符,可以在返回语句之前添加:if( array_sum( $delimiters ) <= count( $delimiters ) ) return false;
  • 您应该确保 CSV 文件的第一行包含 cols 的标签,否则它可能会正常失败。建议扫描和比较几行。
  • @PaulNaveda 为什么定义的分隔符的数量应该有助于确定结果是否正确?首先max($delimiters)应该大于0。比较几行仍然可以提供更多线索,哪个候选是正确的。
  • @Braza 在一行中,如果测试了一些极端的边缘情况,可能真的很难检测到它是否可靠。我创建了一个实用程序类,它检查每一行并返回整个文件或至少几行的结果。我在这里使用了此页面中的一些代码。你可以看看这里:gist.github.com/DavidBruchmann/1215dc4fb9b7bd339253de5b6e304909
【解决方案2】:

固定版本。

在您的代码中,如果一个字符串有超过 1 个分隔符,您将得到错误的结果(例如:val;字符串,带逗号;val2;val3)。此外,如果文件有 1 行(行数

这是一个固定的变体:

private function detectDelimiter($fh)
{
    $delimiters = ["\t", ";", "|", ","];
    $data_1 = null; $data_2 = null;
    $delimiter = $delimiters[0];
    foreach($delimiters as $d) {
        $data_1 = fgetcsv($fh, 4096, $d);
        if(sizeof($data_1) > sizeof($data_2)) {
            $delimiter = $d;
            $data_2 = $data_1;
        }
        rewind($fh);
    }

    return $delimiter;
}

【讨论】:

  • 在 php7.2 中出现可数错误。更改 $data_1 = null; $data_2 = 空;到 $data_1 = []; $data_2 = [];
【解决方案3】:

这些都不适用于我的用例。所以我做了一些细微的修改。

   /**
    * @param string $filePath
    * @param int $checkLines
    * @return string
    */
   public function getCsvDelimiter(string $filePath, int $checkLines = 3): string
   {
      $delimiters =[",", ";", "\t"];

      $default =",";

       $fileObject = new \SplFileObject($filePath);
       $results = [];
       $counter = 0;
       while ($fileObject->valid() && $counter <= $checkLines) {
           $line = $fileObject->fgets();
           foreach ($delimiters as $delimiter) {
               $fields = explode($delimiter, $line);
               $totalFields = count($fields);
               if ($totalFields > 1) {
                   if (!empty($results[$delimiter])) {
                       $results[$delimiter] += $totalFields;
                   } else {
                       $results[$delimiter] = $totalFields;
                   }
               }
           }
           $counter++;
       }
       if (!empty($results)) {
           $results = array_keys($results, max($results));

           return $results[0];
       }
return $default;
}

【讨论】:

  • 这很好用,我正在使用 laravel,我真的找不到 CSV 分隔符检查器......所以我在这里复制了你的代码......非常感谢!
【解决方案4】:

通常,您无法检测文本文件的分隔符。如果有额外的提示,您需要在检测中实现它们才能确定。

建议的方法的一个特殊问题是它会计算文件不同行中元素的数量。假设你有一个这样的文件:

a;b;c;d
a   b;  c   d
this|that;here|there
It's not ready, yet.; We have to wait for peter, paul, and mary.; They will know what to do

虽然这似乎用分号分隔,但您的方法将返回 comma

【讨论】:

  • 即使在普通的 csv 阅读器(即 openoffice、excel)上,同一文件中的不同分隔符也会导致一个晦涩的错误
  • 我的观点是正确的:如果您想正确读取 csv,请让用户指定分隔符。
【解决方案5】:

另一个(通过结合我在互联网上找到的大量答案构建的:

/**
 * Detects the delimiter of a CSV file (can be semicolon, comma or pipe) by trying every delimiter, then
 * counting how many potential columns could be found with this delimiter and removing the delimiter from array of
 * only one columns could be created (without a working limiter you'll always have "one" column: the entire row).
 * The delimiter that created the most columns is returned.
 *
 * @param string $pathToCSVFile path to the CSV file
 * @return string|null nullable delimiter
 * @throws \Exception
 */
public static function detectDelimiter(string $pathToCSVFile): ?string
{
    $delimiters = [
        ';' => 0,
        ',' => 0,
        "|" => 0,
    ];

    $handle = fopen($pathToCSVFile, 'r');
    $firstLine = fgets($handle);
    fclose($handle);

    foreach ($delimiters as $delimiterCharacter => $delimiterCount) {
        $foundColumnsWithThisDelimiter = count(str_getcsv($firstLine, $delimiterCharacter));
        if ($foundColumnsWithThisDelimiter > 1) {
            $delimiters[$delimiterCharacter] = $foundColumnsWithThisDelimiter;
        }else {
            unset($delimiters[$delimiterCharacter]);
        }
    }

    if (!empty($delimiters)) {
        return array_search(max($delimiters), $delimiters);
    } else {
        throw new \Exception('The CSV delimiter could not been found. Should be semicolon, comma or pipe!');
    }
}

以及相应的单元测试(您必须添加自定义 test.csv 文件):

/**
 * Test the delimiter detector
 *
 * @test
 */
public function testDetectDelimiter()
{
    $this->assertEquals(',', Helper::detectDelimiter('test1.csv'));
    $this->assertEquals(';', Helper::detectDelimiter('test-csv-with-semicolon-delimiter.csv'));
    $this->assertEquals('|', Helper::detectDelimiter('test-csv-with-pipe-delimiter.csv'));

    $this->expectExceptionMessage('The CSV delimiter could not been found. Should be semicolon, comma or pipe!');
    Helper::detectDelimiter('test-csv-with-failing-delimiter.csv');
}

【讨论】:

    【解决方案6】:

    好的,这会解析 CSV 的单行(通常您取第一行),如果可能有多个分隔符或没有一个匹配,则抛出异常。 据此,看起来您要测试的分隔符不在带引号的字符串中或已转义。

        public function getDelimiter(string $content, $throwExceptionOnNonUnique = true, $expectSingleColumn = false): string
        {
            // Would be cleaner if you pass the delimiters from outside
            // as also the order matters in the special case you've got something like "a,b;c"
            // and you don't throw the exception - then the first match is preferred
            // But for StackOverflow I put them inside
            $delimiters = ["\t", ";", "|", ","];
            $result = ',';
            $maxCount = 0;
    
            foreach ($delimiters as $delimiter) {
                // Impress your code reviewer by some badass regex ;)
                $pattern = "/(?<!\\\)(?:\\\\\\\)*(?!\B\"[^\\\"]*)\\" . $delimiter . "(?![^\"]*\\\"\B)/";
                $amount = preg_match_all($pattern, $content);
    
                if ($maxCount > 0 && $amount > 0 && $throwExceptionOnNonUnique) {
                    $msg = 'Identifier is not clear: "' . $result . '" and "' . $delimiter . '" are possible';
                    throw new \Exception($msg);
                }
    
                if ($amount > $maxCount) {
                    $maxCount = $amount;
                    $result = $delimiter;
                }
            }
    
            // If nothing matches and you don't expect that just the CSV just
            // consists of one single column without a delimeter at the end
            if ($maxCount === 0 && !$expectSingleColumn) {
                throw new \Exception('Unknown delimiter');
            }
    
            return $result;
        }
    

    P.S:也进行了单元测试——但我不想在此处粘贴 100 多行测试;)

    【讨论】:

      【解决方案7】:

      这是 shortest 版本,通过使用 SplFileObject 类和方法 getCsvControl 并利用 来检测 CSV 分隔符数组取消引用

      但有一个问题,下面的函数只有在使用 setCsvControl() 函数手动设置分隔符时才会起作用,否则,请使用评分最高的答案之一。

      // SplFileObject::getCsvControl — Get the delimiter, enclosure and escape character for CSV
      function detectDelimiter($csvFile){
          if(!file_exists($csvFile) || !is_readable($csvFile)){
              return false;
          }   
          $file = new SplFileObject($csvFile);
          return $file->getCsvControl()[0]; 
      }
      

      【讨论】:

      • 感谢您抽出宝贵时间回答,但问题是如果您还不知道,是否有更好的方法来检测文件中的分隔符。如果您使用过setCSVControl() 或使用默认值,那么您已经知道它是什么。所以我真的很好奇这个答案解决了什么问题。
      猜你喜欢
      • 2021-12-17
      • 1970-01-01
      • 2016-11-20
      • 2014-07-08
      • 2013-06-09
      • 2011-02-17
      • 1970-01-01
      • 1970-01-01
      • 2011-12-06
      相关资源
      最近更新 更多