PHP分割大CSV为19个小文件速度过慢,求优化方案
大型CSV按SiteID拆分的PHP脚本优化方案
我编写了一段PHP脚本,用于将大型CSV文件按SiteID分割为19个小型CSV文件,但脚本运行耗时极长,甚至超出了PHP的最大执行时间。为节省内存我采用逐行处理的方式,原本考虑改用fgetcsv方法,但讲师告知该方法速度更慢。
初始脚本
<?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'; // 创建输出目录(如果不存在) 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; // 跳过表头行 } $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.'; ?>
改进后的脚本
<?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'; // 创建输出目录(如果不存在) if (!file_exists($outputDirectory)) { mkdir($outputDirectory); } $source = fopen($inputFile, 'r'); if (!$source) { exit('无法打开输入文件。'); } $headerRow = fgetcsv($source, 0, ';'); if (!$headerRow) { exit('无法读取表头行。'); } $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('无法为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.'; ?>
脚本分析与优化建议
改进后脚本的优势
改进后的脚本完全解决了初始脚本的核心性能问题:
- 避免频繁的文件打开/关闭:初始脚本每处理一行都要打开、关闭对应SiteID的CSV文件,文件IO的打开/关闭操作是极其耗时的。改进脚本通过维护
$handles数组,每个SiteID的文件仅打开一次,直到所有行处理完成后再统一关闭,这会大幅降低IO开销,是性能提升的关键。 - 减少重复索引查找:初始脚本每次循环都调用
array_search('SiteID', $headerRow)获取列索引,改进脚本通过array_flip仅做一次索引映射,后续直接通过$siteIDColumn获取索引,减少了重复计算。 - 更可靠的CSV处理:
fgetcsv和fputcsv是PHP专门为CSV设计的函数,能正确处理带引号的特殊字段(比如字段内容包含分号或换行的情况),而初始脚本的str_getcsv + implode组合在处理这类特殊场景时容易出错。讲师关于fgetcsv更慢的说法并不准确——在减少了文件IO开销的前提下,改进脚本的整体性能会远优于初始版本。
更优的文件拆分方案
如果改进后的脚本仍存在超时或性能问题,可以尝试以下方案:
- 切换到命令行环境运行:Web环境通常有服务器层面的超时限制,而命令行环境可以通过
set_time_limit(0)取消执行时间限制,更适合处理大文件。 - 使用原生命令行工具:Linux的
awk或sed工具处理文本文件的速度远快于PHP,比如用awk实现拆分:# 假设SiteID是第3列,实际使用时替换为对应列数 awk -F';' 'NR==1{header=$0; next} {file="output/data-"$3".csv"; if(!seen[$3]++) print header > file; print >> file}' air-quality-data-2003-2022.csv - 批量写入优化:可以积累100-1000行数据后再一次性写入文件,减少
fputcsv的调用次数,但这个优化的提升幅度远低于减少文件打开/关闭的操作。
内容的提问来源于stack exchange,提问作者grimx
相关产品推荐
相关产品推荐

