最近接到一個需求,經過選擇的時間段導出對應的用戶訪問日誌到excel中, 因爲用戶量較大,常常會有導出50萬加數據的狀況。而經常使用的PHPexcel包須要把全部數據拿到後才能生成excel, 在面對生成超大數據量的excel文件時這顯然是會形成內存溢出的,因此考慮使用讓PHP邊寫入輸出流邊讓瀏覽器下載的形式來完成需求。php
咱們經過以下的方式寫入PHP輸出流web
$fp = fopen('php://output', 'a'); fputs($fp, 'strings'); .... .... fclose($fp)
php://output
是一個可寫的輸出流,容許程序像操做文件同樣將輸出寫入到輸出流中,PHP會把輸出流中的內容發送給web服務器並返回給發起請求的瀏覽器sql
另外因爲excel數據是從數據庫裏逐步讀出而後寫入輸出流的因此須要將PHP的執行時間設長一點(默認30秒)set_time_limit(0)
不對PHP執行時間作限制。數據庫
注:如下代碼只是闡明生成大數據量EXCEL的思路和步驟,而且在去掉項目業務代碼後程序有語法錯誤不能拿來直接運行,請根據本身的需求填充對應的業務代碼!瀏覽器
/** * 文章訪問日誌 * 下載的日誌文件一般很大, 因此先設置csv相關的Header頭, 而後打開 * PHP output流, 漸進式的往output流中寫入數據, 寫到必定量後將系統緩衝沖刷到響應中 * 避免緩衝溢出 */ public function articleAccessLog($timeStart, $timeEnd) { set_time_limit(0); $columns = [ '文章ID', '文章標題', ...... ]; $csvFileName = '用戶日誌' . $timeStart .'_'. $timeEnd . '.xlsx'; //設置好告訴瀏覽器要下載excel文件的headers header('Content-Description: File Transfer'); header('Content-Type: application/vnd.ms-excel'); header('Content-Disposition: attachment; filename="'. $fileName .'"'); header('Expires: 0'); header('Cache-Control: must-revalidate'); header('Pragma: public'); $fp = fopen('php://output', 'a');//打開output流 mb_convert_variables('GBK', 'UTF-8', $columns); fputcsv($fp, $columns);//將數據格式化爲CSV格式並寫入到output流中 $accessNum = '1000000'//從數據庫獲取總量,假設是一百萬 $perSize = 1000;//每次查詢的條數 $pages = ceil($accessNum / $perSize); $lastId = 0; for($i = 1; $i <= $pages; $i++) { $accessLog = $logService->getArticleAccessLog($timeStart, $timeEnd, $lastId, $perSize); foreach($accessLog as $access) { $rowData = [ ......//每一行的數據 ]; mb_convert_variables('GBK', 'UTF-8', $rowData); fputcsv($fp, $rowData); $lastId = $access->id; } unset($accessLog);//釋放變量的內存 //刷新輸出緩衝到瀏覽器 ob_flush(); flush();//必須同時使用 ob_flush() 和flush() 函數來刷新輸出緩衝。 } fclose($fp); exit(); }
好了, 其實很簡單,就是用逐步寫入輸出流併發送到瀏覽器讓瀏覽器去逐步下載整個文件,因爲是逐步寫入的沒法獲取文件的整體size因此就沒辦法經過設置header("Content-Length: $size");
在下載前告訴瀏覽器這個文件有多大了。不過不影響總體的效果這裏的核心問題是解決大文件的實時生成和下載。服務器
更新: 說一下我數據庫查詢這裏的思路,由於逐步寫入EXCEL的數據實際上來自Mysql的分頁查詢,你們知道其語法是LIMIT offset, num
不過隨着offset
愈來愈大Mysql在每次分頁查詢時須要跳過的行數就越多,這會嚴重影響Mysql查詢的效率(包括MongoDB這樣的NoSQL也是不建議skip掉多條來取結果集),因此我採用LastId的方式來作分頁查詢。 相似下面的語句:併發
SELECT columns FROM `table_name` WHERE `created_at` >= 'time range start' AND `created_at` <= 'time range end' AND `id` < LastId ORDER BY `id` DESC LIMIT num