提问人:grimx 提问时间:5/17/2023 最后编辑:grimx 更新时间:5/17/2023 访问量:88
PHP 将 CSV 文件处理成 19 个较小的 CSV,速度非常慢
PHP processes a CSV file into 19 Smaller CSV's very slow
问:
我创建了这个 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.';
答:
接触文件系统具有计算机通常最慢的 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)
评论
您的执行时间源于三个来源:
- 关闭和重新打开文件的成本非常高。
- 一次写入少量数据(一行)效率不高。
- 在这种情况下,不需要解析 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
评论
explode()
explode()
评论
$f