通过回调函数的方式,实现数据的csv的导入与导出。
优点是开发时不需要接触csv文件的操作,解决windows与mac下打开utf-8会乱码的问题( 通过bom头标记文档字符集为utf-8的处理),以及大数据导入或者导出可能产生的大文件写入塞爆内存的问题。
文件的导出与导入,对csv文件的操作都是分批的逐行处理,不会产生大的内存堆积消耗

样例-数据导出处理(一般为从数据库通过规则批量导出数据写csv文件)
//定义表格文件头与数据字段key
$aHeader = [
'price_tmpl_id' => '模版ID',
'ds_id' => '药库ID',
'mid' => '药材ID',
'title' => '药材名称',
'unit' => '单位',
'supply' => '原始结算价',
'supply_custom' => '自定义结算价',
'rate' => '参考系数',
];
//回调函数(若数据量较大可在此处使用$aLastNode入参,实现分批数据处理,知道返回[],导出程序自动终止)
//use()用于将上下文数据,传入回调函数内
$cbFunc = function ($aLastNode) use(&$aMList){
//再此传入需要写文件的行数据
if (!empty($aLastNode)){
return [];//回调写入终止
}
return $aMList;//抛出本次需要写入的数据
};
$aFilePath = CsvFileIoService::save2File($aHeader, $cbFunc); //导出成功则返回文件名
样例-数据从csv文件导入处理
导入可能发生失败,根据CsvFileIoService::loadFile()的返回值判定是否成功,若失败可以获取$aRet['msg']了知失败原因
导入时若数据量较大时建议定义iChunkSize入参,来限制每次分批提取的数据量,一般建议未开可能数据量成长会超过2000+行以上的,通过分批处理。若导入的数据量可控,不用设置iChunkSize则会一次将所有数据都提取出来后执行回调。
【注意】导入时可以定义需要提取哪些字段,通过sHeader方式枚举,函数会通过sHeader中的定义表头名称找到指定的列,在回调的时候转换为$sHeader对应的key,以保证提取最小数据集的剥离导入。
$sFile = 'E:\PHPRoot\workgroup\gapis_scm\storage\app\price_tmpl_20251008.csv';
$sHeader = ['price_tmpl_id' => '模版ID', 'ds_id' => '药库ID', 'mid'=>'药材ID', 'supply_custom' => '自定义结算价'];
$cbFunc = function($aRow){
$aResult = ['state'=>0, 'msg'=>'']; //返回结构
//本次处理成功返回:$aResult['state']=1
//本次处理失败,终止继续回调:$aResult['msg']='处理失败原因'
return $aResult;
};
$aRet = CsvFileIoService::loadFile($sFile, $sHeader, $cbFunc, 5);
类源码
php
<?php
namespace App\Service\Common;
/**
* Csv文件导入导出处理类
* @author: lijian
* @version 20251014
*/
final class CsvFileIoService {
/**
* @param array $aHeader 表头定义
* <p>['key_name'=>'字段名字']</p>
* @param callable $cbFunc 回调函数
* <p>func(array $aLastNode); $aLastNode 首次为[],其次则为前次返回数组的最后一个节点值</p>
* <p>返回结果:[['field_name'=>'字段值',...],...] ;若为空表示导出终止【注意】field_name必须与$sHeader的定义对应</p>
* @param string $sFilePath
* @return string 文件名
*/
static public function save2File(array $aHeader, callable $cbFunc, string $sFilePath=''): string {
//处理完成生成csv文件
if (empty($sFilePath)){ //默认生成文件名
$sCsvFile = storage_path('app/temp_'. time() . '_'. rand(100,999).'.csv'); //csv文件
}else{ //给定了文件名
$sCsvFile = $sFilePath;
}
if (file_exists($sCsvFile)){ //文件存在则删除
unlink($sCsvFile);
}
$fp = fopen($sCsvFile, 'w'); //打开文件
fputs($fp, pack('CCC',0xef,0xbb,0xbf));
fputcsv($fp, array_values($aHeader)); //输出首行
$aLastNode = []; //记录上次从回调函数返回值处获得的最后一个节点
while(true){ //存在数据则输出
$aOutDt = $cbFunc($aLastNode); //取出数据
if (empty($aOutDt)){ //终止导出io
break;
}else{ //记录本次处理的最后一个节点数据
$aLastNode = end($aOutDt);
}
//逐行输出数据(key)
$sFields = array_keys($aHeader);
foreach ($aOutDt as $aNode){ //逐行输出
//重组输出key的顺序
$aInRow = [];
foreach ($sFields as $sField){
if (!isset($aNode[$sField])){ //缺失字段
$aInRow[] = '';
}else{ //按照顺序输出列字段
$aInRow[] = $aNode[$sField];
}
}
fputcsv($fp, $aInRow); //输出数据行
unset($aInRow); $aInRow=null;
}
unset($aOutDt); $aOutDt=null; //释放空间
}
fclose($fp);//关闭文件句柄
return $sCsvFile;
/* //样例代码
$aHeader = [
'price_tmpl_id' => '模版ID',
'ds_id' => '药库ID',
'mid' => '药材ID',
'title' => '药材名称',
'unit' => '单位',
'supply' => '原始结算价',
'supply_custom' => '自定义结算价',
'rate' => '参考系数',
];
//回调函数
$cbFunc = function ($aLastNode) use(&$aMList){
//再此传入需要写文件的行数据
if (!empty($aLastNode)){
return []; //回调写入终止
}
return $aMList; //抛出本次需要写入的数据
};
$aFilePath = CsvFileIoService::save2File($aHeader, $cbFunc);
*/
}
/**
* 载入csv文件内容(回调方式提取数据块)
* @param string $sFilePath 文件名
* @param array $aHeader 提取的字段
* <p>['field'=>'field_name',...]</p>
* <p>【注意】会根据field_name提取文件找那个的列,输出时数组的key为field</p>
* @param callable $cbFunc 回调函数
* <p>func(array $aChunk):array; $aChunk 为行数据块,每行以field为ksy</p>
* <p>返回值:['state'=>'0|1', 'msg'=>'回调处理故障原因']</p>
* @param int $iChunkSize 提取块大小
* @return array
* <p>['state'=>'0|1', 'msg'=>'失败提示语', 'len'=>'总提取的记录条数']</p>
*/
static public function loadFile(string $sFilePath, array $aHeader, callable $cbFunc, int $iChunkSize=999999): array{
$aOutBuf = ['state'=>0, 'msg'=>'', 'len'=>0];
self::toUtf8File($sFilePath); //若非utf-8格式文件,强转成utf-8
$bIsBom = self::isBomFile($sFilePath);//检查是否为有bom头的文件
//打开文件-预处理
$fh = fopen($sFilePath, 'r');
if (false === $fh){ //文件打开失败
$aOutBuf['msg'] = '文件打开失败';
return $aOutBuf;
}
if ($bIsBom){
fgets($fh,4); //跳过bom文件头; bom: efbbbf; bin2hex($s)
}
//装载文件数据
$iLen = 0; //有效记录数
$iRowSite = 0; //表格行数位置
$aColSite = [];
$aChunkDt = [];
$aHeaderName = array_values($aHeader);
$aHeaderKey = array_keys($aHeader);
$bCallbackState = true;
while(! feof($fh)){ //取出整个csv数据内容
$aTmp = fgetcsv($fh);
$iRowSite++; //表格总行数计数
if (empty($aTmp)){ //跳过空行
continue;
}
$aTmp = array_map('trim', $aTmp); //清理前后空格
//表头数据位定位处理
if ($iLen++ == 0){ //第一行是表头
//取出需要的表头字段列号
foreach ($aHeaderName as $sHName) {
//取出$sHName在$aTmp中命中的下标号
$iSite = array_search($sHName, $aTmp); //搜索命中key的下标
if (false !== $iSite){ //找到下标
$aColSite[] = $iSite;
}else{
fclose($fh);//关闭文件句柄
$aOutBuf['msg'] = '未匹配到表头字段【'. $sHName .'】,请检查导入文件。';
return $aOutBuf;
}
}
}else{ //数据体列提取
if (empty($aColSite)){ //未定位到表头
fclose($fh);//关闭文件句柄
$aOutBuf['msg'] = '未匹配到表头字段,请检查。';
return $aOutBuf;
}else{
//提取$aTmp中的指定$aColSite列的内容
foreach ($aColSite as $iKey => $iNodeSite){
if (!isset($aTmp[$iNodeSite])){
fclose($fh);//关闭文件句柄
$aOutBuf['msg'] = '第 '. $iRowSite .' 行数据项【'. $aHeaderName[$iNodeSite].'】缺失,请核对数据';
return $aOutBuf;
}
$aColTmp[$aHeaderKey[$iKey]] = $aTmp[$iNodeSite];
}
$aChunkDt[] = $aColTmp??[]; //临时存储行数据
unset($aColTmp); $aColTmp=null; //释放空间
$aOutBuf['len']++;
//根据chunk数量积累后发起数据回调
if (count($aChunkDt) >= $iChunkSize){ //触发回调函数
$aCbRet = $cbFunc($aChunkDt);
if (!isset($aCbRet['state'])){ //回调函数
$aOutBuf['msg'] = '回调函数返回数组结构异常,请检查';
$bCallbackState = false; //标记回调处理失败
break;
}elseif (1 != $aCbRet['state']){ //回调函数抛出失败状态
$aOutBuf['msg'] = $aCbRet['msg']??'回调函数未知错误';
$bCallbackState = false; //标记回调处理失败
break;
}
unset($aChunkDt);$aChunkDt=null; //清理缓存
}
}
}
}
fclose($fh);//关闭文件句柄
if (!$bCallbackState){ //回到函数遇到失败
return $aOutBuf;
}
//还有数据没有回传,回传最后一次
if (!empty($aChunkDt)){
$aCbRet = $cbFunc($aChunkDt);
if (!isset($aCbRet['state'])){ //回调函数
$aOutBuf['msg'] = '回调函数返回数组结构异常,请检查';
}elseif (1 != $aCbRet['state']){ //回调函数抛出失败状态
$aOutBuf['msg'] = $aCbRet['msg']??'回调函数未知错误';
}
unset($aChunkDt);$aChunkDt=null; //清理缓存
}
//检查是否存在错误
if (empty($aOutBuf['msg'])){
$aOutBuf['state'] = 1; //无错误
}
return $aOutBuf;
/* //样例代码
$sFile = 'E:\PHPRoot\gancao_workgroup\gapis_scm\storage\app\price_tmpl_20251008.csv';
$sHeader = ['price_tmpl_id' => '模版ID', 'ds_id' => '药库ID', 'mid'=>'药材ID', 'supply_custom' => '自定义结算价'];
$cbFunc = function($aRow){
$aResult = ['state'=>0, 'msg'=>'']; //返回结构
//本次处理成功返回:$aResult['state']=1
//本次处理失败,终止继续回调:$aResult['msg']='处理失败原因'
return $aResult;
};
$aRet = CsvFileIoService::loadFile($sFile, $sHeader, $cbFunc, 5);
*/
}
/**
* 检查是否为带bom投的文件
* @param string $sFileName 文件名
* @return bool
*/
static private function isBomFile(string $sFileName): bool{
$hf = fopen($sFileName, 'r');
if (false !== $hf){
$str = fgets($hf,4);
fclose($hf);
return $str === pack('CCC',0xef,0xbb,0xbf);
}else{
return false;
}
}
/**
* 强转UTF-8文件格式
* @param string $sFileName 文件名
* @return bool
* <p>true:发生强转, false:未发生强转</p>
*/
static private function toUtf8File(string $sFileName):bool{
if (file_exists($sFileName)){
if (mb_detect_encoding(file_get_contents($sFileName)) !== 'UTF-8'){
file_put_contents($sFileName, iconv('GBK//IGNORE','UTF-8', file_get_contents($sFileName)));
return true; //强转成功
} ;
}
return false;//未发生强转
}
}
测试代码
php
$aMList[] = ['mid'=>10, 'title'=>'葛根', 'unit'=>'克', 'supply'=>1.2222];
$aMList[] = ['mid'=>11, 'title'=>'麻黄', 'unit'=>'克', 'supply'=>0.02221];
$aMList[] = ['mid'=>55, 'title'=>'桂枝', 'unit'=>'克', 'supply'=>0.00332];
//====保存文件======
$aHeader = [
'mid' => '药材ID',
'title' => '药材名称',
'unit' => '单位',
'supply' => '原始结算价',
];
//回调函数
$cbFunc = function ($aLastNode) use(&$aMList){
//再此传入需要写文件的行数据
if (!empty($aLastNode)){
return []; //回调写入终止
}
return $aMList; //抛出本次需要写入的数据
};
$aFilePath = CsvFileIoService::save2File($aHeader, $cbFunc);
//======载入文件======
$aOutBuf = [];
$sFile = $aFilePath;
$sHeader = ['mid'=>'药材ID', 'supply' => '原始结算价'];
$cbFunc = function($aRow) use(&$aOutBuf){
$aResult = ['state'=>0, 'msg'=>'']; //返回结构
$aOutBuf = $aRow; //取出数据
//本次处理成功返回:$aResult['state']=1
//本次处理失败,终止继续回调:$aResult['msg']='处理失败原因'
return $aResult;
};
$aRet = CsvFileIoService::loadFile($sFile, $sHeader, $cbFunc, 5);
unlink($sFile);
dd($aRet, $aOutBuf);