背景:接手的項目中支持導出一批數據,全數量在50W左右。在接手的時候看代碼是直接一次查詢MySQL獲得數據,然后用header函數直接寫入csv,用戶開始導出則自動下載。但是,在全導出的時候,功能出現了BUG問題。
1.數據量大導致PHP處理腳本運行時間,超過默認限制。
2.數據量過大,導致內存溢出,流程中止。
初版解決方案:
1.通過函數set_time_limit(0); 取消執行時間限制(在導出的函數入口設置,這是合理的,導出的數據量過大了)
2.關於數據過大,內存溢出的解決辦法,開始是想到了php動態變量(先由sql語句獲得總記錄數,然后每2W條切分,查詢2w條數據存入一個變量)
- $total_export_count = $db->getOne("select count(1) from ($sql) t2");
- for ($i=0;$i<intval($total_export_count/20000)+1;$i++){
- $export_data = "exportdata".$i;
- $$export_data = $db->getAll($sql." limit ".strval($i*20000).",20000");
- }
然后通過相應的代碼取出變量的信息,echo到csv文件中,這種方式在本地測試的時候是通過的,但在服務器上依舊會內存溢出。
- header ( "Content-type:application/vnd.ms-excel" );
- header ( "Content-Disposition:filename=" . iconv ( "UTF-8", "GB18030", "查詢用戶列表" ) . ".csv" );
- $out = $column_name;
- echo iconv ( "UTF-8", "GB18030", $out );
- for ($i=0;$i<intval($total_export_count/20000)+1;$i++){
- $dynamic_name = "exportdata".$i;
- foreach ( $$dynamic_name as $key => $item ) {
- echo iconv ( "UTF-8", "GB18030", "\n".implode(',',$item) );
- }
- // 將已經寫到csv中的數據存儲變量銷毀,釋放內存占用
- unset($$dynamic_name);
- }
- exit ();
因為上面的方法在服務器上沒有通過,所以只能將分割數據量的操作放到寫文件的流程中,相比上面的思路這種會慢一些。
- header ( "Content-type:application/vnd.ms-excel" );
- header ( "Content-Disposition:filename=" . iconv ( "UTF-8", "GB18030", "查詢用戶列表" ) . ".csv" );
- $out = $column_name;
- echo iconv ( "UTF-8", "GB18030", $out );
- $pre_count = 20000;
- for ($i=0;$i<intval($total_export_count/$pre_count)+1;$i++){
- $export_data = $db->getAll($sql." limit ".strval($i*$pre_count).",{$pre_count}");
- foreach ( $export_data as $key => $item ) {
- echo iconv ( "UTF-8", "GB18030", "\n".implode(',',$item) );
- }
- // 將已經寫到csv中的數據存儲變量銷毀,釋放內存占用
- unset($export_data);
- }
- exit ();
經測試之后是可行的,服務器上也可以導出,就是時間會慢一些,而且會是一直下載狀態。
關於這個場景整理了一些資料:
1.csv文件的條數是好像沒有限制的,可以一直寫(網上的博文里面看的,沒證實過)
2.excel 2010版本以上,是可以讀取100多W行數據的(驗證過,新建一個excel,ctrl+下箭頭 到文件末尾可以看到行數)
理想的解決方案(沒有具體實施,想的)
1.數據分割肯定是必須的步驟,防止內存溢出。
2.將分割后的數據寫入到一個excel或者一個csv文件中,被分割了多少次,寫多少個文件。這樣可以防止達到文件行數的最大限制。
3.將2中寫的文件進行壓縮處理,壓縮成一個壓縮包,然后進行自動下載。
補充:
在上面的方案正式運行的時候發現導出的數據,總是比查詢的總記錄數要少1000多條,幾次看數據后發現有些數據並沒有換行,而是寫到上一行去了。在有了這個覺悟后,重新看了遍之前轉別人的帖子,發現還是用fputcsv()函數比較靠譜,傳入一個數組,作為一行的數據,由該函數自己去寫換行和控列。感覺有些時候還是不要偷懶的好啊,雖然自己寫","完成列分割,寫"\n"完成空格,貌似是可行的,但是對於一些數據,並不一定能控制的好。
修改后的導出代碼:
- header ( "Content-type:application/vnd.ms-excel" );
- header ( "Content-Disposition:filename=" . iconv ( "UTF-8", "GB18030", "query_user_info" ) . ".csv" );
- // 打開PHP文件句柄,php://output 表示直接輸出到瀏覽器
- $fp = fopen('php://output', 'a');
- // 將中文標題轉換編碼,否則亂碼
- foreach ($column_name as $i => $v) {
- $column_name[$i] = iconv('utf-8', 'GB18030', $v);
- }
- // 將標題名稱通過fputcsv寫到文件句柄
- fputcsv($fp, $column_name);
- $pre_count = 10000;
- for ($i=0;$i<intval($total_export_count/$pre_count)+1;$i++){
- $export_data = $db->getAll($sql." limit ".strval($i*$pre_count).",{$pre_count}");
- foreach ( $export_data as $item ) {
- $rows = array();
- foreach ( $item as $export_obj){
- $rows[] = iconv('utf-8', 'GB18030', $export_obj);
- }
- fputcsv($fp, $rows);
- }
- // 將已經寫到csv中的數據存儲變量銷毀,釋放內存占用
- unset($export_data);
- ob_flush();
- flush();
- }
- exit ();