Создание больших файлов Excel из данных MySQL с помощью PHP из корпоративных приложений

Мы разрабатываем и поддерживаем несколько систем, которым необходимо экспортировать отчеты в формате Excel для конечного пользователя. Отчеты собираются из базы данных MySQL с некоторой тривиальной обработкой и обычно приводят к примерно 40000 строк данных с 10-15 столбцами, мы ожидаем, что объем данных будет постоянно расти.

На данный момент мы используем PHPExcel для генерации Excel, но он больше не работает для нас. После того, как мы превысим 5000 строк, потребление памяти и время загрузки станут недопустимыми и не могут быть решены путем бесконечного увеличения максимальных ограничений PHP на использование памяти и время выполнения скрипта. Обработка данных настолько скудна, насколько это возможно, и вся проблема в том, что PHPExcel потребляет много памяти. Создание CSV было бы проще, но, к сожалению, мы обязаны экспортировать Excel (и только Excel) из наших сервисов из-за требований пользователей. Это связано с требованиями к форматированию и т. д., поэтому CSV не подходит.

Любые идеи/рекомендации для стороннего приложения/модуля/сервиса/чего-либо для создания больших превосходств? Не имеет значения, является ли это коммерческой лицензией, если она соответствует нашим потребностям, может быть интегрирована в существующие приложения PHP и выполняет свою работу. Наши сервисы обычно работают на linux/php/mysql, и мы можем делать с серверами практически все, что нам нужно.

Спасибо!


person lostcontrol    schedule 18.05.2012    source источник
comment
Я слишком знаком с любыми другими методами, чтобы создавать файлы xls. Как вы сказали, создание csv, а затем создание xls было бы быстрее :). Вы искали возможное исправление для phpExcel (например, обсуждение здесь: stackoverflow.com/questions/4817651/   -  person Nanne    schedule 18.05.2012
comment
Есть ли конкретная причина, по которой вы не хотите использовать CSV (несколько листов, форматирование и т. д.)? Также у вас должен быть экспорт в xls или xlsx (или это не имеет значения)?   -  person Bo.    schedule 18.05.2012
comment
Нэнн: Насколько я знаю, один из наших программистов более или менее перепробовал все, чтобы PHPExcel работал как можно легче. Я перешлю ему вашу ссылку, но я думаю, что мы уже попробовали это. Можете ли вы порекомендовать какие-либо разумные альтернативы PHPExcel, которые могли бы выполнять эту работу лучше? Бо: Требования пользователей (т.е. они не знают, как правильно импортировать CSV или не хотят возиться с этим), форматирование и целостность данных, несколько листов и многое другое. Экспорт XLS — это минимум, так как мы не контролируем версию Excel, которая есть у конечных пользователей, XLSX — это не необходимость, а приятный бонус.   -  person lostcontrol    schedule 18.05.2012
comment
Единственное, что я придумал в прошлом, но отверг сам себя по нескольким причинам (сторонние материалы, действительно немного обходной путь, отсутствие опыта работы с API и т. д.), — это отправить его в таблицу Google с использованием API. Не уверен, что вообще возможно легко загрузить его в виде XLS, но вы можете изучить его: developers.google.com/google-apps/spreadsheets   -  person Nanne    schedule 18.05.2012


Ответы (6)


Для такого большого объема данных я бы не рекомендовал такие инструменты, как PHPExcel или ApachePOI (для Java) из-за их требований к памяти. Недавно я боролся с подобной задачей, и я нашел удобный (но, может быть, немного неудобный) способ вводить данные в электронные таблицы. Создание или обновление электронных таблиц Excel на стороне сервера может быть достигнуто простым редактированием XML. У меня есть таблица XLSX на сервере, и каждый раз, когда данные собираются из dB, я распаковываю ее с помощью php. Затем я получаю доступ к определенным XML-файлам, содержащим содержимое рабочих листов, которые необходимо внедрить, и вставляю данные вручную. После этого я сжимаю папку с электронными таблицами, чтобы распространять ее как обычный файл XLSX. Весь процесс довольно быстрый и надежный. Очевидно, что есть несколько проблем и сбоев, связанных с внутренней организацией файла XLSX/Open XML (например, Excel склонен хранить все строки в отдельной таблице и использовать ссылки на эту таблицу в рабочих листах). Но при вставке только данных, таких как числа и строки, это не так сложно. Если кому интересно, могу предоставить код.

Хорошо, вот пример кода для этого. Я попытался прокомментировать, что он делает, но не стесняйтесь просить дополнительных объяснений.

<?php
/** 
 * Class for serverside spreadsheet data injecting
 * Reqs: unzip.php, zip.php (containing any utility functions able to unzip files & zip folders)
 *
 * Author: Poborak
 */
class DataInjector
{    
    //spreadsheet file, we inject data into this one
    const SPREADSHEET_FILE="datafile.xlsx";   
    // specific worksheet into which data are being injected    
    const SPREADSHEET_WORKSHEET_FILE="/xl/worksheets/sheet7.xml"; 
    //working directory, spreadsheet is extracted here
    const WSPACE_DIR="Wspace";
    // query for obtaining data from DB
    const STORE_QUERY = "SELECT * FROM stores ORDER BY store_number ASC"; 

    private $dbConn;
    private $storesData;

    /**
     * @param   mysqli  $dbConn
     */
    function __construct(mysqli $dbConn) {   
        $this->dbConn = $dbConn;
    }

    /**
     * Main method for whole injection process
     * First data are gathered from DB and spreadsheet is decompressed to workspace.
     * Then injection takes place and spreadsheet is ready to be rebuilt again by zipping.
     *
     * @return   boolean    Informace o úspěchu
     */     
    public function injectData() {

        if (!$this->getStoresInfoFromDB()) return false;        
        if (!$this->explodeSpreadsheet(self::SPREADSHEET_FILE,self::WSPACE_DIR)) return false;                      
        if (!$this->injectDataToSpreadsheet(self::WSPACE_SUBDIR.self::SPREADSHEET_WORKSHEET_FILE)) return false;            
        if (!$this->implodeSpreadsheet(self::SPREADSHEET_FILE,self::WSPACE_DIR)) return false;
        return true;
    }

    /**
     * Decompress spreadsheet file to folder
     *
     * @param   string  $spreadsheet
     * @param   string  $targetFolder
     *
     * @return   boolean    success/fail 
     */   
    private function explodeSpreadsheet($spreadsheet, $targetFolder) {
        return unzip($spreadsheet,$targetFolder);
    }

    /**
     * Compress source folder to spreadsheet file
     *
     * @param   string  $spreadsheet    
     * @param   string  $sourceFolder
     *
     * @return   boolean    success/fail 
     */   
    private function implodeSpreadsheet($spreadsheet, $sourceFolder) {
        return zip($sourceFolder,$spreadsheet);
    }

    /**
     * Loads data from DB to member variable $storesDetails (as array)
     *
     * @return   boolean    success/fail 
     */ 
    private function getStoresInfoFromDb() {
        unset($this->storesData);       

        if ($stmt = $this->dbConn->prepare(self::STORE_QUERY)) {
            $stmt->execute();
            $stmt->bind_result($store_number, $store_regional_manager, $store_manager, $store_city, $store_address);
            while ($stmt->fetch()) {
                $this->storesData[trim($store_number)] = array(trim($store_regional_manager),trim($store_manager),trim($store_address),trim($store_city));
            }           
            $stmt->close();
        }   
        return true;        
    }

    /**
     * Injects data from member variable $storesDetails to spreadsheet $ws
     *
     * @param   string  $ws target worksheet
     *
     * @return   boolean    success/fail
     */ 
    private function injectDataToSpreadsheet($ws) {
         $worksheet = file_get_contents($ws);    
         if ($worksheet === false or empty($this->storesData) return false;

         $xml = simplexml_load_string($worksheet);  
         if (!$xml) return false;

        // Loop through $storesDetails array containing rows of data
        foreach ($this->storesData as $std){

            // For each row of data create new row in excel worksheet
            $newRow = $xml->sheetData->addChild('row'); 

            // Loop through columns values in rowdata
            foreach ($std as $cbd){                      
                // Save each column value into next column in worksheets row 
                 foreach ($this->storesData as $cbd){
                    $newCell = $newRow->addChild('c'); 
                    $newCell->addAttribute('t', "inlineStr");
                    $newIs = $newCell->addChild('is');
                    // text has to be saved as utf-8 (otherwise the spreadsheet file become corrupted)
                    if (!mb_check_encoding($cbd, 'utf-8')) $cbd = iconv("cp1250","utf-8",$cbd); 
                    $newT = $newIs->addChild('t',$cbd);                     
                }
             }
         }

         // Save xml data back to worksheet file
         if (file_put_contents($ws, $xml->asXML()) !== false) return true;           
    }
}
?>   
person bazinac    schedule 31.05.2012
comment
Привет, Poborak, ваше решение звучит многообещающе, и мне интересно взглянуть на соответствующий пример кода. Можете ли вы предоставить ссылку для скачивания? - person lostcontrol; 07.06.2012
comment
Я вставил код в свой исходный ответ. У меня есть упрощенный класс, который я использую в своем проекте, чтобы показать основную идею и рабочий процесс. Вам нужно только включить функцию для распаковки и архивирования папок для этого примера. Если нужно, могу и предоставить... - person bazinac; 07.06.2012
comment
@Poborak Какую библиотеку электронных таблиц XLSX вы используете, будет ли она работать для 2 строк лака? - person Ankit Balyan; 05.02.2016

Список альтернатив для PHPExcel, которые я стараюсь обновлять, находится здесь

Если вам нужна чистая скорость/производительность памяти выше всего, что может предложить PHPExcel, то единственный, который я действительно рекомендую, это расширение-оболочка Ильи для libXL, так как библиотека по-прежнему активно поддерживается.

person Mark Baker    schedule 18.05.2012
comment
Привет Марк, спасибо за список альтернатив. На данный момент ваша рекомендация обертки Ильи для libXL кажется лучшим вариантом, но нам придется изучить ее подробнее. - person lostcontrol; 21.05.2012

Вы можете экспортировать в формате CSV, Excel справится с этим. Если у вас возникли проблемы с записью файла, вы всегда можете зациклить результаты (разбиение на страницы) и добавить их в файл CSV.

Попробуйте впоследствии преобразовать с помощью PHPExcel в формат .xsl или .odf, в противном случае оставьте его в CSV.

person Boby    schedule 18.05.2012
comment
Но он заявляет, что CSV не вариант из-за требований (возможно, необоснованных, но все же на месте). Как же тогда это ответ? - person Nanne; 18.05.2012
comment
Я предложил CSV в качестве промежуточного шага, потому что его легко создать. А затем попробуйте преобразовать его в формат Excel. - person Boby; 18.05.2012
comment
Но вы ничего не указали о преобразовании (не говоря уже о том, чтобы оставить это как примечание CSV). Поскольку у PHPExcel есть проблема с большими листами и памятью, вы столкнетесь с той же проблемой, не так ли? А если нет, вам нужно добавить, как конвертировать большие файлы .csv в .xls, чтобы это было полезным ответом, насколько я понимаю. - person Nanne; 18.05.2012
comment
Привет, Боби, раньше мы доставляли отчеты в формате CSV конечному пользователю, но проблема заключалась в том, что содержимое некоторых ячеек неправильно импортировалось в Excel, если только пользователь не использовал отдельную функцию импорта (примечание: файлы CSV были сформированы правильно) . Например, такие строки, как 000123123, потеряли начальные нули, что привело к повреждению данных. Это произошло из-за того, что Excel решил, что строка должна интерпретироваться как число, и исходное содержимое (т.е. ведущие нули) было потеряно. - person lostcontrol; 18.05.2012
comment
Это была одна из основных причин, по которой мы обратились к PHPExcel для генерации файлов, так как мы могли напрямую установить тип содержимого ячейки. Доставка CSV не будет проблемой, если наши конечные пользователи согласятся с тем, что им придется использовать функцию импорта, но, к сожалению, они этого не сделают, и мы должны предоставить Excel. - person lostcontrol; 18.05.2012
comment
Мы еще не пробовали преобразование CSV в XLS в PHPExcel, но рассмотрим его подробнее. Меня беспокоит то, что либо использование времени/памяти для преобразования останется относительно таким же, либо мы потеряем контроль над типами содержимого ячейки из-за использования функций импорта/преобразования вместо прямой настройки содержимого ячейки. - person lostcontrol; 18.05.2012
comment
Бо, вот почему я заявил, что нам нужно сгенерировать Excel (и только Excel) в моем исходном вопросе :) Я отредактирую свой вопрос, чтобы уточнить это. - person lostcontrol; 18.05.2012
comment
Извините @lostcontrol, но большинство людей, которые публикуют вопросы, не знают о преимуществах простоты и пытаются чрезмерно усложнить проблему. Основываясь на ваших комментариях, я более четко понимаю суть проблемы и рассуждаю, почему CSV не подходит. - person Bo.; 18.05.2012

Пробовали ли вы старый Pear Excel (он же Spreadsheet_Excel_Writer: http://pear.php.net/package/Spreadsheet_Excel_Writer/redirected)?

Обсуждение Checkuout относительно Pear и PHPExcel:
http://phpexcel.codeplex.com/discussions/240688

person Bo.    schedule 18.05.2012
comment
Но он заявляет, что CSV не вариант из-за требований (возможно, необоснованных, но все же на месте). Как же тогда это ответ? - person Nanne; 18.05.2012
comment
@Nanne: изначально не было ясно, что CSV не подходит. Это должно быть намеком на то, что если два человека дают одинаковый ответ (что может быть неправильным), то вопросы могут быть не такими ясными, как планировалось изначально. - person Bo.; 18.05.2012
comment
Первоначальный вопрос звучал буквально: CSV generation would be lighter, but unfortunately we're required to export Excel (and Excel alone). Мне кажется ясно. И даже если это было неясно, с вопросом было что-то не так, хорошо, но это не делает этот ответ действительным на (возможно, неясный) вопрос, теперь, когда он ясен. - person Nanne; 18.05.2012
comment
@Nanne: я отредактировал ответ, чтобы удалить ссылку на CSV. :) И что было неясно, так это ... нам нужно экспортировать Excel (и только Excel) ... поскольку excel - это не формат файла, а форматы файлов, загружаемые приложением (Excel поддерживает несколько форматов: xls, xlsx и т. д.) . - person Bo.; 18.05.2012
comment
Но предыдущая часть, где слово CSV упоминается в отбрасывающей форме, все же вполне ясна. В любом случае, это ни к чему не приводит. Не придираюсь к вам или что-то в этом роде, но приветствия и подписи постов не нужны, ваше имя и так уже есть :) - person Nanne; 18.05.2012

Попробуйте OfficeWriter. Недавно мы специально улучшили производительность больших наборов данных для финансовой компании из списка Fortune 500. Он делает гораздо больше с форматом файла, чем вам конкретно нужно (диаграммы и все, что у вас есть), но API довольно прост в использовании, и с оценкой вы можете быстро получить POC. Отказ от ответственности - я на инженерах, которые построили последнюю версию.

Еще один недостаток для вас, ребята, заключается в том, что это .NET.

person Nick Martin    schedule 18.05.2012
comment
Привет, Ник! OfficeWriter выглядит великолепно, но, поскольку большинство наших сервисов работает на linux/php, переход на .net — это то, что нам придется планировать и рассматривать в течение более длительного времени, и я не уверен, сможем ли мы в настоящее время выделить ресурсы. для этой задачи. Для существующей среды .net это кажется хорошим вариантом. - person lostcontrol; 21.05.2012

А если просто распечатать таблицу?

<?php
header("Content-Type:   application/vnd.ms-excel; charset=utf-8");
header("Content-Disposition: attachment; filename=abc.xls");  //File name extension was wrong
header("Expires: 0");
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header("Cache-Control: private",false);

echo "<table><tr><td>Test</td><td>Test2</td></table>";
person FDisk    schedule 07.06.2013