php的CSV大数据导入导出的通用处理类

通过回调函数的方式,实现数据的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);
相关推荐
Web3_Daisy2 小时前
如何在市场波动中稳步推进代币发行
大数据·人工智能·物联网·web3·区块链
yumgpkpm2 小时前
Hadoop大数据平台在中国AI时代的后续发展趋势研究CMP(类Cloudera CDP 7.3 404版华为鲲鹏Kunpeng)
大数据·hive·hadoop·python·zookeeper·oracle·cloudera
WZTTMoon2 小时前
从 “完整对象” 视角看Spring 循环依赖
java·spring boot·后端·spring
一瓢一瓢的饮 alanchan2 小时前
Flink原理与实战(java版)#第1章 Flink快速入门(第一节IDE词频统计)
java·大数据·flink·kafka·实时计算·离线计算·流批一体化计算
间彧2 小时前
如何在CI/CD流水线中自动化实现镜像扫描和推送到Harbor?
后端
Elastic 中国社区官方博客2 小时前
Elasticsearch:相关性在 AI 代理上下文工程中的影响
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
9ilk3 小时前
【基于one-loop-per-thread的高并发服务器】--- 自主实现HttpServer
linux·运维·服务器·c++·笔记·后端
间彧3 小时前
Kubernetes无缝集成Harbor,实现CI/CD流水线
后端
会编程的吕洞宾3 小时前
Java中的“万物皆对象”:一场编程界的哲学革命
java·后端