1. 程式人生 > >PHP百萬級資料匯出方案(多csv檔案壓縮)

PHP百萬級資料匯出方案(多csv檔案壓縮)

概述:

最近公司專案要求把資料除了頁面輸出也希望有匯出功能,雖然之前也做過幾個匯出功能,但這次資料量相對比較大,差不多一天資料就20W條,要求導7天或者30天,那麼資料量就輕鬆破百萬了甚至破千萬,因此開發的過程中發現了一些大資料匯出的坑,在此跟大家分享一下,互相學習。

準備:

1、PHP設定坑:

  • set_time_limit – 設定指令碼最大執行時間:

此配置一般PHP預設是30秒,如果你是資料小的,可能就不會發現有該設定問題,但如果你資料達到了百萬級匯出,往往30秒是不夠的,因此你需要在你的指令碼中新增 set_time_limit(0),讓該指令碼沒有執行時間現在

  • memory_limit – PHP的記憶體限定:

此配置一般php預設是128M,如果之前做過小資料的朋友可能也會動過這個配置就能解決許多問題,或許有人想,你大資料也把這個調大不就行了嗎?那麼真的是too young too native了,你本地能設定1G或者無限制或許真的沒問題,但是正式場,你這麼搞遲早會出事的,一個PHP程式佔那麼大的記憶體的空間,如果你叫你公司運維幫忙調一下配置,估計運維一定很不情願,伺服器硬體這麼搞也是太奢侈了。所以說,我們要儘量避免調大該設定。

2、excel坑:

既然是匯出資料,大夥們當然馬上想到了excel格式了,多方便檢視資料呀,然而萬萬沒想到excel也是有脾氣的呀! 

  • 表資料限制:
Excel 2003及以下的版本。一張表最大支援65536行資料,256列。
Excel 2007-2010版本。一張表最大支援1048576行,16384列。

也就是說你想幾百萬條輕輕鬆鬆一次性匯入一張EXCEL表是不行的,你起碼需要進行資料分割,保證資料不能超過104W一張表。

  • PHPexcel記憶體溢位:

既然資料限制在104W,那麼資料分割就資料分割唄,於是你嘗試50W一次匯入表,然而PHPexcel內部有函式報記憶體溢位錯誤,然後你就不斷的調小資料量,直到5W一次匯入你都會發現有記憶體溢位錯誤。這是為什麼呢,雖然你分割資料來匯入多個數據表,但是最後PHPexcel內部還是一次性把所有表資料放進一個變數中來建立檔案……額,這幾百萬資料一個變數儲存,你想記憶體不溢位,還真有點困難。
(後來看了一些文章發現PHPExcel也有解決方案,PHPExcel_Settings::setCacheStorageMethod方法更改緩衝方式來減小記憶體的使用)

3、csv坑:

EXCEL這麼麻煩,我不用還不行嗎?我用csv檔案儲存,既不限制數量,還能直接用EXCEL來檢視,又能以後把檔案匯入資料庫,一舉幾得豈不是美哉?咦,少俠好想法!但是CSV也有坑哦!

  • 輸出buffer過多:

當你用PHP原生函式putcsv()其實就使用到了輸出快取buffer,如果你把幾百萬的資料一直用這個函式輸出,會導致輸出快取太大而報錯的,因此我們每隔一定量的時候,必須進行將輸出快取中的內容取出來,設定為等待輸出狀態。具體操作是:

ob_flush();
flush();
  • EXCEL檢視CSV檔案數量限制:

大多數人看csv檔案都是直接用EXCEL開啟的。額,這不就是回到EXCEL坑中了嗎?EXCEL有資料顯示限制呀,你幾百萬資料只給你看104W而已。什麼?你不管?那是他們開啟方式不對而已?不好不好,我們解決也不難呀,我們也把資料分割一下就好了,再分開csv檔案儲存,反正你不分割資料變數也會記憶體溢位。

4、總結做法

分析完上面那些坑,那麼我們的解決方案來了,假設資料量是幾百萬。

1、那麼我們要從資料庫中讀取要進行資料量分批讀取,以防變數記憶體溢位,

2、我們選擇資料儲存檔案格式是csv檔案,以方便匯出之後的閱讀、匯入資料庫等操作。

3、以防不方便excel讀取csv檔案,我們需要104W之前就得把資料分割進行多個csv檔案儲存

4、多個csv檔案輸出給使用者下載是不友好的,我們還需要把多個csv檔案進行壓縮,最後提供給一個ZIP格式的壓縮包給使用者下載就好。

程式碼:

 //匯出說明:因為EXCEL單表只能顯示104W資料,同時使用PHPEXCEL容易因為資料量太大而導致佔用記憶體過大,
    //因此,資料的輸出用csv檔案的格式輸出,但是csv檔案用EXCEL軟體讀取同樣會存在只能顯示104W的情況,所以將資料分割儲存在多個csv檔案中,並且最後壓縮成zip檔案提供下載
    function putCsv(array $head, $data, $mark = 'attack_ip_info', $fileName = "test.csv")
    {
        set_time_limit(0);
        $sqlCount = $data->count();
        // 輸出Excel檔案頭,可把user.csv換成你要的檔名
        header('Content-Type: application/vnd.ms-excel;charset=utf-8');
        header('Content-Disposition: attachment;filename="' . $fileName . '"');
        header('Cache-Control: max-age=0');

        $sqlLimit = 100000;//每次只從資料庫取100000條以防變數快取太大
        // 每隔$limit行,重新整理一下輸出buffer,不要太大,也不要太小
        $limit = 100000;
        // buffer計數器
        $cnt = 0;
        $fileNameArr = array();
        // 逐行取出資料,不浪費記憶體
        for ($i = 0; $i < ceil($sqlCount / $sqlLimit); $i++) {
            $fp = fopen($mark . '_' . $i . '.csv', 'w'); //生成臨時檔案
      //     chmod('attack_ip_info_' . $i . '.csv',777);//修改可執行許可權
            $fileNameArr[] = $mark . '_' .  $i . '.csv';
        // 將資料通過fputcsv寫到檔案控制代碼
            fputcsv($fp, $head);
            $dataArr = $data->offset($i * $sqlLimit)->limit($sqlLimit)->get()->toArray();
            foreach ($dataArr as $a) {
                $cnt++;
                if ($limit == $cnt) {
                    //重新整理一下輸出buffer,防止由於資料過多造成問題
                    ob_flush();
                    flush();
                    $cnt = 0;
                }
                fputcsv($fp, $a);
            }
            fclose($fp);  //每生成一個檔案關閉
        }
        //進行多個檔案壓縮
        $zip = new ZipArchive();
        $filename = $mark . ".zip";
        $zip->open($filename, ZipArchive::CREATE);   //開啟壓縮包
        foreach ($fileNameArr as $file) {
            $zip->addFile($file, basename($file));   //向壓縮包中新增檔案
        }
        $zip->close();  //關閉壓縮包
        foreach ($fileNameArr as $file) {
            unlink($file); //刪除csv臨時檔案
        }
        //輸出壓縮檔案提供下載
        header("Cache-Control: max-age=0");
        header("Content-Description: File Transfer");
        header('Content-disposition: attachment; filename=' . basename($filename)); // 檔名
        header("Content-Type: application/zip"); // zip格式的
        header("Content-Transfer-Encoding: binary"); //
        header('Content-Length: ' . filesize($filename)); //
        @readfile($filename);//輸出檔案;
        unlink($filename); //刪除壓縮包臨時檔案
    }

總結:

其實上面程式碼還是有優化的空間的,比如說用異常捕捉,以防因為某些錯誤而導致生成了一些臨時檔案又沒有正常刪除,還有PHPexcel的快取設定也許能解決記憶體溢位問題,可以生成一個EXCEL檔案多個工作表的形式,這樣對於檔案閱讀者來說更友好。

以上便是本人對PHP大資料匯出的見解,希望能幫到您們,同時不足的地方請多多指教!

————————————————————————————————————
2017年12月17日
PS:最近了解其實關於記憶體溢位的問題,用迭代器來處理會方便多了。