引言
最近在工作中需要实现一个数据导出功能。由于之前都是使用现成的工具或库,换了一家公司后,发现需要从零开始构建这个功能。最初我计划实现一个异步导出功能,但上级认为过于复杂,建议采用同步方式。于是,我开始寻找一种高效的同步导出方案。
在这个过程中,我发现了PHP中的生成器(Generator),这是一个非常强大的工具,特别适合处理大数据场景。本文将详细介绍生成器的概念、工作原理、优势以及如何利用生成器实现高效的CSV导出功能。
一、什么是生成器?
生成器(Generator)是PHP中一个非常重要的概念。它就像一个“节能引擎”,特别适合处理大数据场景。生成器允许你按需生成数据,而不是一次性将所有数据加载到内存中。
传统数组 vs 生成器对比
// 传统方式:一次性加载全部数据
function getAllUsers() {
$users = [/* 10万条数据 */]; // 瞬间占用500MB内存
return $users;
}
// 生成器:逐条生成数据
function generateUsers() {
for ($i = 0; $i < 100000; $i++) {
yield ['id' => $i, 'name' => "User$i"]; //每次只占 1KB
}
}
在上面的例子中,传统方式会一次性加载10万条数据,占用大量内存。而生成器则逐条生成数据,内存占用极低。
二、生成器的工作原理
yield
关键字——时空穿梭的魔法
每次执行到 yield
时,函数会暂停并保存当前状态。下次请求数据时,从暂停的位置继续执行。
function simpleGenerator() {
echo "开始执行\n";
yield '第一块数据';
echo "继续执行\n";
yield '第二块数据';
}
$gen = simpleGenerator();
echo $gen->current(); // 输出:开始执行 | 第一块数据
$gen->next();
echo $gen->current(); // 输出:继续执行 | 第二块数据
生成器执行流程图解
三、生成器的核心优势
1. 惰性计算(Lazy Evaluation)
生成器按需生成数据,避免提前计算。例如,读取10GB的日志文件时,传统方式可能会导致内存溢出,而生成器可以逐行获取数据。
2. 内存友好
处理10万条数据时,传统数组方式的内存峰值可能达到500MB+,而生成器仅占用2MB左右。
3. 可组合性
生成器可以链式调用,形成数据处理流水线。
function filter($generator) {
foreach ($generator as $item) {
if ($item['age'] > 18) {
yield $item;
}
}
}
$data = filter(getUserGenerator());
四、生成器使用注意事项
1. 不可逆性
生成器只能向前遍历,不能 rewind()
重置。
2. 资源释放
使用完成后及时关闭生成器。
$gen->close(); // 释放数据库连接等资源
3. 与数组的转换
需要时可通过 iterator_to_array()
转换,但会失去内存优势。
4. 性能陷阱
避免在生成器内部进行复杂计算,保持轻量。
五、面试常见问题
1. yield
和 return
有什么区别?
return
终结函数执行。yield
暂停函数,保留上下文。
2. 生成器如何实现低内存占用?
通过维持执行状态(栈帧),避免一次性加载所有数据。
3. 什么时候不该用生成器?
- 需要随机访问数据时。
- 需要多次遍历数据集时。
- 数据量较小时(反而增加复杂度)。
六、案例:基于生成器的CSV导出功能
由于在很多地方都需要导出数据,因此我将导出功能封装成一个公共函数,代码如下:
/**
* @param Generator $dataGenerator 数据生成器
* @param array $headers 表头
* @param string $filename 文件名
* @return void
* desc: 批量导出CSV文件
* author: author
* datetime: 2025/3/3下午3:46
*/
function batch_export_csv(Generator $dataGenerator, array $headers, string $filename = '')
{
try {
// 设置运行环境
set_time_limit(300); // 设置合理的超时时间
// 清理输出缓冲区
if (ob_get_level() > 0) {
ob_start();
}
// 验证和清理文件名
$fileName = basename($filename ?: 'export_' . date('YmdHis')) . '.csv';
$fileName = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $fileName);
// 设置响应头
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Cache-Control: max-age=0');
// 打开输出流
$fp = fopen('php://output', 'w');
fwrite($fp, chr(0xEF) . chr(0xBB) . chr(0xBF)); // 输出 UTF-8 头
fputcsv($fp, $headers);
// 分批处理数据
$batchCount = 0;
foreach ($dataGenerator as $batch) {
foreach ($batch as $row) {
fputcsv($fp, $row);
}
$batchCount++;
// 每处理一定数量的批次刷新一次缓冲区
if ($batchCount % 10 === 0) {
ob_flush(); // 刷新输出缓冲
flush(); // 刷新系统缓冲
}
}
fclose($fp);
exit();
} catch (\Exception $e) {
// 捕获所有类型的异常并记录日志
error_log('Export failed: ' . $e->getMessage());
throw new \Exception('导出失败: ' . $e->getMessage());
}
}
接口调用
/**
* 获取SKU列表或导出SKU数据
*
* 该方法根据请求参数获取SKU列表,并支持分页和导出功能。
* - 如果 `export` 参数为0,则返回分页的SKU列表。
* - 如果 `export` 参数为1,则导出SKU数据为CSV文件。
*
* @return \think\Response JSON响应或导出CSV文件
*/
public function skuList()
{
// 输入验证与初始化参数
$limit = intval($this->request->post('limit', 20)); // 每页显示条数,默认值为20
$page = intval($this->request->post('page', 1)); // 页数,默认值为1
$export = intval($this->request->post('export', 0)); // 是否导出,默认值为0(不导出)
// 验证分页参数是否合法
if ($limit <= 0 || $page <= 0) {
return json(['status' => 400, 'message' => 'Invalid limit or page']); // 返回错误信息,提示无效的分页参数
}
// 验证导出参数是否合法
if (!in_array($export, [0, 1])) {
return json(['status' => 400, 'message' => 'Invalid export parameter']); // 返回错误信息,提示无效的导出参数
}
// 构建查询条件
$where = [];
$memberId = $this->request->post('membe_id', $this->info['membe']['membe_id'], 'intval'); // 获取成员ID,并进行整数验证
if (empty($memberId)) {
return json(['status' => 400, 'message' => 'Invalid member ID']); // 返回错误信息,提示无效的成员ID
}
$where['membe_id'] = $memberId;
// 定义关联查询字段
$withJoin = [
'inventory' => ['product_sku_id', 'amount', 'shelves_amount'], // 关联库存表,选择特定字段
];
// 显示字段列表
$field = 'product_sku_id,company_id,membe_id,sku_No,product_title,product_describe,product_category,hs_code,univalent,weight,specifications,volume,create_time';
// 构建查询对象
$query = ProductSku::where(formatWhere($where))
->field($field)
->withJoin($withJoin, 'left') // 左连接库存表
->order('product_sku_id desc'); // 按产品SKU ID降序排列
// 判断是否需要导出数据
if (!$export) {
/*列表逻辑*/
} else {
ob_clean(); // 清除输出缓冲区
// 定义导出数据生成器函数
$dataGenerator = function ($query, $chunkSize = 2000) {
$page = 1;
while (true) {
try {
// 分页查询数据
$data = $query->page($page, $chunkSize)->select();
if ($data->isEmpty()) break; // 如果没有数据则退出循环
// 构建导出数据行
$sku_data = [];
foreach ($data as $item) {
$row = $item->toArray();
$sku_data[] = [
$row['sku_No'] ?? '', // SKU编号
$row['sku_No'] ?? '', // 卖家SKU(重复列)
$row['product_title']['zh'] ?? '', // 产品名称(中文)
$row['product_title']['en'] ?? '', // 产品名称(英文)
$row['weight'] ?? '', // 重量
$row['volume']['L'] ?? 0, // 长度
$row['volume']['W'] ?? 0, // 宽度
$row['volume']['H'] ?? 0, // 高度
$row['univalent'] ?? 0, // 单价
'启用', // 状态
$row['create_time'] // 添加时间
];
}
yield $sku_data;
// 判断是否最后一页
if (count($data) < $chunkSize) break;
$page++;
} catch (\Exception $e) {
// 记录日志并继续
error_log($e->getMessage());
continue;
}
}
};
// 定义CSV头部
$headers = ['Fnsku', 'seller_sku', '产品名称', '产品英文名称', '重量', '长', '宽', '高', '货值', '状态', '添加时间'];
// 调用批量导出CSV函数
batch_export_csv(
$dataGenerator($query),
$headers,
'sku_list'
);
}
}
执行效果
点击发送,在Postman上效果如上图所示,或者点击【发送并保存响应】,就能直接看到CSV文件了。
结语
通过本文的介绍,我们了解了生成器的基本概念、工作原理及其在大数据处理中的优势。利用生成器,我们可以实现高效、低内存占用的CSV导出功能,特别适合处理大数据场景。希望本文对你理解和应用生成器有所帮助。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。