PHP - 检测CSV分隔符的最佳方法

18
我看到了很多关于如何自动检测CSV文件分隔符的讨论。其中大部分都是20-30行的函数,包含多个循环、预先确定的分隔符列表、读取前5行并匹配计数等等。以下是一个示例:(链接)
我刚刚实现了这个过程,并进行了一些修改。它非常出色。
然后我找到了下面的代码:
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'=>",");

有人能够解释一下为什么我不应该采用这种更简单的方法,以及为什么无论我去哪里寻找答案,都似乎是那种更加复杂的解决方案被认可了吗?

谢谢!


我认为这个解决方案比链接中的另一个示例更易读和更清晰。 - vaso123
1
我是不是唯一一个觉得通过神奇的方式确定分隔符在逻辑上是错误的人?如果分隔符不是逗号(如其名称所示——逗号分隔值),那么就在请求中查找指定的分隔符。如果没有找到,则终止解析,直到收到有效信息为止。 - N.B.
1
@N.B. 你说得很有道理。对于这个项目,我只需要在给用户提供选项之前让它猜测。如果使用上述详细的方法2无法提供任何合法数据,那么我将要求用户指定他们的分隔符。然而,我喜欢方法1的一点是,如果它找到2个或更多匹配的分隔符,那么我可以通知用户并让他们从我认为已经找到的选项中选择,或者建议他们自己的选项。 - simon_www
7个回答

19
这个函数非常优雅 :)
/**
* @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);
}

2
要检测是否找不到分隔符,可以在返回语句之前添加:if (array_sum($delimiters) <= count($delimiters)) return false; - Paul Naveda
1
你应该确保CSV文件的第一行包含列标签,否则它可能无法优雅地失败。建议扫描和比较几行。 - David
1
@PaulNaveda 为什么定义的分隔符数量应该有助于确定结果是否正确?首先,max($delimiters) 只需要大于0即可。比较多行文本仍然可以提供更多线索,以确定哪个候选项是正确的。 - David
2
@Braza 在单独的一行中,如果测试一些极端案例,很难可靠地检测到它。我创建了一个实用类,检查每一行并返回整个文件或至少几行的结果。我使用了这个页面上的一些代码。 您可以在这里查看:https://gist.github.com/DavidBruchmann/1215dc4fb9b7bd339253de5b6e304909 - David
1
@David 做得太棒了!谢谢你! 这完全值得成为这个问题的答案 ;) - Braza
显示剩余3条评论

9

这些方法都没适用于我的情况,因此我进行了一些微小的修改。

   /**
    * @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;
}


1
这个很好用,我正在使用laravel,但我真的找不到CSV分隔符检查器...所以我在这里复制了你的代码..非常感谢! - Jenuel Ganawed

8

修复版本。

如果一个字符串有多个分隔符,您将得到错误的结果(例如:val; string, with comma;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 = null; 更改为 $data_1 = []; $data_2 = []; - Vit

2

通常情况下,你无法检测文本文件的分隔符。如果有其他提示,你需要在检测中实现它们以确保正确性。

所提出的方法存在一个特定问题,即它将计算文件不同行中元素的数量。假设你有这样一个文件:

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

尽管看起来是由分号分隔的,但你的方法会返回“逗号”。

1
即使在常见的 CSV 阅读器(如 OpenOffice、Excel)中,同一文件中的不同分隔符也会导致晦涩的错误。 - bastien
2
确实,这正是我的观点:如果您想正确地读取csv文件,请要求用户指定分隔符。 - andy

1

以下是我从互联网上找到的答案,通过组合而成:

/**
 * 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');
}

0

好的,这段代码可以解析你的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;
    }

附言:也进行了单元测试 - 但我不想在这里粘贴100多行的测试代码 ;)


-1
这是使用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]; 
}

1
谢谢您抽出时间回答问题,但问题在于,如果您不知道分隔符,是否有更好的方法检测文件中的分隔符。 如果您已经使用了setCSVControl()或使用了默认值,那么您已经知道它是什么。所以我真的很好奇,这个答案解决了什么问题。 - pbarney

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接