PHP 将 CSV 文件处理成 19 个较小的 CSV,速度非常慢

PHP processes a CSV file into 19 Smaller CSV's very slow

提问人:grimx 提问时间:5/17/2023 最后编辑:grimx 更新时间:5/17/2023 访问量:88

问:

我创建了这个 php 脚本,它需要很长时间才能将 CSV 文件过滤成 19 个较小的文件。CSV 的链接位于此 google 驱动器行中

https://drive.google.com/drive/folders/1Bju4kkVgo21xu3IFeyKWNJ1uClTn534J?usp=share_link

我让进程逐行运行以节省内存,但这超过了 PHP 脚本中的格言计数时间。有没有改进的方法来实现此文件分解?

<?php

@date_default_timezone_set("GMT");

ini_set('memory_limit', '512M');
ini_set('max_execution_time', '300');
ini_set('auto_detect_line_endings', TRUE);
ini_set('display_errors', 1);

$inputFile = '/Users/declanryan/Desktop/UNI3.0/year3/WEB/workshop/assgienment/air-quality-data-2003-2022.csv';
$outputDirectory = 'output';

// create the output directory if it doesn't exist
if (!file_exists($outputDirectory)) {
    mkdir($outputDirectory);
}

$handle = fopen($inputFile, 'r');

if ($handle) {
    $headerRow = null;
    $siteIDs = array();

    while (($data = fgets($handle)) !== false) {
        $row = str_getcsv($data, ";");

        if ($headerRow === null) {
            $headerRow = $row;
            continue; // Skip processing the header row
        }

        $siteID = $row[array_search('SiteID', $headerRow)];
        if (!in_array($siteID, $siteIDs)) {
            $siteIDs[] = $siteID;

            $newCSVFilename = $outputDirectory . '/data-' . $siteID . '.csv';
            $f = fopen($newCSVFilename, 'w');

            fputs($f, implode(';', $headerRow) . PHP_EOL);
        }

        $currentCSVFilename = $outputDirectory . '/data-' . $siteID . '.csv';
        $f = fopen($currentCSVFilename, 'a');

        fputs($f, implode(';', $row) . PHP_EOL);

        fclose($f);
    }

    fclose($handle);
}

echo "Done.";
echo "<br>";
echo 'Script took ' . round((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 2) . ' seconds to run.';


?>

甚至花了足够长的时间才能进行文件处理。我打算将格式更改为 getcsv,但我的讲座告诉我这种方法实际上更慢?

在回复 Sammath 时,这样的事情会更符合必要条件吗?


@date_default_timezone_set("GMT");

ini_set('memory_limit', '512M');
ini_set('max_execution_time', '300');
ini_set('auto_detect_line_endings', TRUE);
ini_set('display_errors', 1);

$inputFile = '/Users/declanryan/Desktop/UNI3.0/year3/WEB/workshop/assgienment/air-quality-data-2003-2022.csv';
$outputDirectory = 'output';

// create the output directory if it doesn't exist
if (!file_exists($outputDirectory)) {
    mkdir($outputDirectory);
}

$source = fopen($inputFile, 'r');
if (!$source) {
    exit('Unable to open input file.');
}

$headerRow = fgetcsv($source, 0, ';');
if (!$headerRow) {
    exit('Unable to read header row.');
}

$columnIndexes = array_flip($headerRow);
$siteIDColumn = $columnIndexes['SiteID'];

$handles = [];

while (($row = fgetcsv($source, 0, ';')) !== false) {
    $siteID = $row[$siteIDColumn];
    if (!isset($handles[$siteID])) {
        $newCSVFilename = $outputDirectory . '/data-' . $siteID . '.csv';
        $handles[$siteID] = fopen($newCSVFilename, 'w');
        if (!$handles[$siteID]) {
            exit('Unable to open output file for SiteID: ' . $siteID);
        }
        fputcsv($handles[$siteID], $headerRow, ';');
    }

    fputcsv($handles[$siteID], $row, ';');
}

foreach ($handles as $handle) {
    fclose($handle);
}

fclose($source);

echo "Done.";
echo "<br>";
echo 'Script took ' . round((microtime(true) - $_SERVER["REQUEST_TIME_FLOAT"]), 2) . ' seconds to run.';


php csv fgets fgetcsv fput

评论

1赞 Sammitch 5/17/2023
如果你不断地在循环中重新打开文件句柄,你的代码肯定会很慢,例如:通过不断重新分配。打开每个手柄一次,将其存放起来,必要时稍后参考。$f
0赞 grimx 5/17/2023
fclose();像那样工作?或者你的意思是存储在数组中?

答:

1赞 Sammitch 5/17/2023 #1

接触文件系统具有计算机通常最慢的 IO 数量,并且 PHP 抽象出许多优化。但是,当你反复打开一个文件,写入非常少量的数据,然后关闭它时,你不仅使这些优化变得毫无意义,而且你也在做你能做的最糟糕的事情:不断地将微小的写入刷新到磁盘。

对于这样的事情,您应该打开这些句柄一次,这可能大致如下所示:

$source = fopen('somefile.csv', 'r');
$handles = [];

$header = fgetcsv($source, ';');

while( $row = fgetcsv($source) ) {
  $some_id = $row[X];
  if( ! key_exists($some_id, $handles) ) {
    $handles[$some_id] = fopen("foo/$some_id.csv", w);
  }
  fputcsv($handles[$some_id], $row, ';');
}

foreach($handles as $handle) {
  fclose($handle);
}
fclose($source);

此外,和 之间在功能上几乎没有区别,但不是合适的 CSV 编码方法,因为它不会执行任何字符串引用/转义等。fgetcsv()str_getcsv(fgets())implode(';', $row)

评论

0赞 grimx 5/17/2023
我在底部添加了一个新部分,我认为你的意思是什么?
0赞 grimx 5/17/2023
stream_set_chunk_size也会让它走得更快吗?
0赞 LSerni 5/18/2023
@grimx坦率地说,虽然我自己的解决方案可能快了几个百分点,但这个解决方案是最正确和可移植的(想想“CSV结构的变化”)。你可以投票支持我的解决方案,但你应该接受这个解决方案,以防其他人有机会。
0赞 grimx 5/18/2023
我明白了,更多的是你在编译答案时付出的努力,在我的情况下,它是根据 php 脚本的速度标记的
1赞 grimx 5/18/2023
但对于一般用例,我明白了,它对其他人投了赞成票
0赞 LSerni 5/17/2023 #2

您的执行时间源于三个来源:

  • 关闭和重新打开文件的成本非常高。
  • 一次写入少量数据(一行)效率不高。
  • 在这种情况下,不需要解析 CSV,因为您只想知道每一行的 siteId,并且文件格式是已知的。

例如:

define('BUFFERSIZE', 1048576);

$buffers = [];
$handles = [];

$start  = microtime(true);
$memory = memory_get_peak_usage(true);

$fp     = fopen("air-quality-data-2003-2022.csv", "r");
fgets($fp, 10240);
while(!feof($fp)) {
    $line = fgets($fp, 10240);
    if (empty($line)) {
        break;
    }
    [ , , , , $siteId ] = explode(';', $line);
    if (isset($handles[$siteId])) {
        if (strlen($buffers[$siteId]) > BUFFERSIZE) {
            fwrite($handles[$siteId], $buffers[$siteId]);
            $buffers[$siteId] = '';
        }
    } else {
        $handles[$siteId] = fopen("air-quality-{$siteId}.csv", "w");
        $buffers[$siteId] = '';
    }
    $buffers[$siteId] .= $line;
}
fclose($fp);

foreach ($handles as $siteId => $fp) {
    fwrite($fp, $buffers[$siteId]);
    fclose($fp);
}

print "Time elapsed: " . (microtime(true) - $start) . " seconds, memory = " . (memory_get_peak_usage(true) - $memory) . " bytes \n";

产量(在我的系统上):

Time elapsed: 0.9726489448547 seconds, memory = 20971520 bytes

我已经使用不同的 BUFFERSIZE 进行了一些实验(报告的内存是超出脚本已经分配的内存)。

Buffer = 4096, time elapsed: 1.3162 seconds, memory = 0 bytes
Buffer = 32768, time elapsed: 1.0094 seconds, memory = 0 bytes
Buffer = 131072, time elapsed: 0.9834 seconds, memory = 2097152 bytes
Buffer = 262144, time elapsed: 0.9104 seconds, memory = 4194304 bytes
Buffer = 500000, time elapsed: 0.9812 seconds, memory = 10485760 bytes
Buffer = 400000, time elapsed: 0.9854 seconds, memory = 8388608 bytes
Buffer = 300000, time elapsed: 0.9675 seconds, memory = 6291456 bytes
Buffer = 262144, time elapsed: 1.0102 seconds, memory = 4194304 bytes
Buffer = 262144, time elapsed: 0.9599 seconds, memory = 4194304 bytes

请注意可变性(我可能应该在测试之间重新启动或至少运行并刷新缓存),以及它不需要太多缓冲区来提高速度的事实(在某个点之后,效率将再次开始下降,因为 PHP 难以处理非常大的字符串连接)。缓冲区的实际大小将取决于底层文件系统:如果它像我一样是缓存支持的,那么大的 BUFFERSIZE 可能不会有太大的改变。sync

评论

0赞 grimx 5/17/2023
你是大师,与我实施的任何方法相比,这都很快。我知道它很慢的原因,但尽管我进行了研究,但我没有确切的改进来加快这个过程。我收集到中间的一些地方是您测试中最好的增益大小。我已经好几个星期没有关掉我的裤子了,它仍然管理了 21 秒。非常好的工作
1赞 Sammitch 5/17/2023
我对这个答案有两个问题:1.不适合作为CSV解析器,因为没有考虑引用和/或转义的分隔符,并且会抛弃你的列索引。2.PHP 当调用文件写入函数时,PHP 不一定写入数据,PHP 将缓冲该数据本身以供以后进行更大的写入。将缓冲区复制到用户空间可能会导致其自身的问题。explode()
0赞 LSerni 5/17/2023
@Sammitch您在这两个帐户上都是正确的;我确实说过,在这种情况下,不需要完整的CSV解析。在一般情况下,这将是一个糟糕的选择explode()
0赞 grimx 5/18/2023
occ 1,2,3 对于 data-481.xml 中的所有行都是空的,尽管它有一个包含内容的输出 csv 文件,所以当我通过 xml 转换脚本运行它时它是空的?
0赞 LSerni 5/18/2023
@grimx对不起,我不关注。XML格式?什么是OCC?