четверг, 19 июля 2012 г.

PHPExcel и кодировка

У меня были проблемы с русской кодировкой при чтение xls файла при помощи PHPExcel. Кракозябры и почему-то iconv выдавал какую-то белиберду. Долго не мог найти в чем проблема, даже гугл не помог.

Потом полез ручками и оказалось, что оптимальное решение проблемы - это в файле Classes/PHPExcel/Reader/Excel5.php выставить нужную кодировку:

$this->_codepage            = 'CP1251';

Вот и все.

PHPExcel и большие файлы

PHPExcel - отличная библиотека с огромным функционалом по работе с форматами xls, xlsx. Можно считывать, записывать, менять форматирование, задавать формулы, а из xlsx можно и картинки вытаскивать.

Один минус у PHPExcel - вечно памяти не хватает, все время сыпятся ошибки "Fatal error: Out of memory". Этот пост о том, как это обойти.


Для чтения большого файла (~25 000 строк) я использовал своеобразное решение.

Считываем файл не целиком, а по несколько строк. Это делается так:

import_xls.php
require_once 'path/to/PHPExcel/IOFactory.php';

class chunkReadFilter implements PHPExcel_Reader_IReadFilter
{
    private $_startRow = 0;
    private $_endRow = 0;

    public function setRows($startRow, $chunkSize) {
        $this->_startRow    = $startRow;
        $this->_endRow      = $startRow + $chunkSize;
    }

    public function readCell($column, $row, $worksheetName = '') {
        //  Only read the heading row, and the rows that are configured in $this->_startRow and $this->_endRow
        if (($row == 1) || ($row >= $this->_startRow && $row < $this->_endRow)) {
            return true;
        }
        return false;
    }
}

session_start();

if ($_SESSION['startRow']) $startRow = $_SESSION['startRow'];
else $startRow = 13;

$fileName = "file.xls";
$inputFileType = 'Excel5';
$objReader = PHPExcel_IOFactory::createReader($inputFileType);
$chunkSize = 20;
$chunkFilter = new chunkReadFilter();

while ($startRow <= 65000) {
 $chunkFilter->setRows($startRow,$chunkSize);
 $objReader->setReadFilter($chunkFilter);
 $objReader->setReadDataOnly(true);
 $objPHPExcel = $objReader->load($fileName);
 //Что-то с этими строками делаем
 $startRow += $chunkSize;
 $_SESSION['startRow'] = $startRow;
unset($objReader);
unset($objPHPExcel);
}

echo "The End";
unset($_SESSION['startRow']);

Собственно класс chunkReadFilter - это то, что нам нужно. Устанавливаем его в качестве фильтра для чтения файла, и файл будет загружаться не целиком, а лишь определенное количество строк.

Кроме него еще используем такую полезную опцию как ReadDataOnly. Как ясно из названия, она позволяет не загружать форматирование документа, высвобождая место для данных.

И конечно же используем unset. Это так же поможет высвободить память.

Но помимо нехватки памяти возникает другая проблема. На большинстве хостингов у php-скриптов помимо ограничения на использование памяти, еще стоит ограничение на время выполнения. И крайне вероятно, что этого времени хватать не будет. Лично я обошел эту проблему при помощи сессий и повторяющихся ajax запросов. В коде представленном выше вы уже увидели использование сессий и завершающее "The End".

А вот код клиентской части

import_xls.html
<html>
<head>
<title>Импорт прайс-листа</title>
  <script src="/media/js/jquery.js" type="text/javascript"></script>
  <script src="/media/js/import-xls.js" type="text/javascript"></script>
</head>
<body>
<h1>Импорт прайс-листа</h1>
Подождите завершения импорта, не закрывайте данную страницу!
<div id="progress-bar">
</div>
<div id="content">
</div>
</body>
</html>

import-xls.js
function repeat_import() {
 $.get("import_xls.php", function(data){
  $("#progress-bar").append("I");
  if (data == "The End") {
   $("#content").html("<h2>Импорт завершен!</h2>");
  }
  else {
   $("#content").html(data);
   repeat_import();
  }
 });
}

$(function (){
 /*$(document).ajaxError(function (e, jqxhr, settings, exception) {
  error_message = jqxhr.status;
  alert(error_message);
 });*/
 repeat_import();
});

Т.е. мы отправляем ajax-запрос нашему скрипту, ждем ответа, и, если ответ нас не устраивает, посылаем новый ajax-запрос. Есть еще решение без использования AJAX - при помощи редиректа в самом php (header ("Location: import_xls.php");). Но лично мне больше нравится решение с AJAX, потому что тут можно легко и просто добавить прогресс-бар и какие-нибудь другие рюшечки. Кстати, внимательный читатель заметил, что в моем коде простой прогресс-бар уже реализован.



Для записи в формат xls так же около 25 000 строк крайне полезно использовать следующий код
$cacheMethod = PHPExcel_CachedObjectStorageFactory::cache_to_phpTemp;
$cacheSettings = array( 'memoryCacheSize ' => '256MB');
PHPExcel_Settings::setCacheStorageMethod($cacheMethod, $cacheSettings);
Можно еще поиграться с методами кеширования. Поддерживается

memcache
$cacheMethod = PHPExcel_CachedObjectStorageFactory::cache_to_memcache; $cacheSettings = array( 'memcacheServer' => 'localhost', 'memcachePort' => 11211, 'cacheTime' => 600 );


а так же cache_to_discISAM.