引言
最近在工作中需要實現一個數據導出功能。由於之前都是使用現成的工具或庫,換了一家公司後,發現需要從零開始構建這個功能。最初我計劃實現一個異步導出功能,但上級認為過於複雜,建議採用同步方式。於是,我開始尋找一種高效的同步導出方案。
在這個過程中,我發現了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導出功能,特別適合處理大數據場景。希望本文對你理解和應用生成器有所幫助。