Привет, Хабр!
Мне по работе часто приходится импортировать Excel-документы средствами PHP.
Для этого я использую библиотеку PHPExcel, которая на сегодняшний день является весьма удобным инструментом.
Но есть несколько «скользких» моментов, связанных с чтением данных из ячеек, о которых я хочу рассказать хабрачитателям, а также разобрать функцию, решающую эти проблемы.
Адресовать ячейки при работе с excel можно разными способами:
Первый способ удобнее для статических ячеек, а второй и третий для циклов.
Но в PHPExcel нет универсальной функции получения ячейки любым из этих способов, есть только отдельные функции. Что ж, исправляем это упущение:
Сразу оговорюсь, что в приводимых мной примерах кода есть ссылки на $this, т.к. это методы моего класса-обертки над PHPExcel. В этом кусочке реализуются все три способа получения объекта ячейки.
При чтении объединенных ячеек, PHPExcel возращает пустое значение для всех, кроме первой.
Т.е. для рисунка ниже значения B3 и C3 будут пустыми строками:

Мне всегда было неудобно такое поведение.
Гораздо удобнее (и логичнее!) для любой объединенной ячейки возвращать общее для них значение «mergedvalue».
Для этого при запросе ��начения нужно пройтись по всем объединенным диапазонам листа, и если заданная ячейка попадает в диапазон, то вернуть первую:
Как известно, Excel хранит даты как число дней с 1 января 1900 года. Поэтому при чтении ячейки B2 на скриншоте выше мы получим бесполезное 41044. Но есть и хорошая новость — в PHPExcel присутствует удобная функция PHPExcel_Shared_Date::ExcelToPHP(), которая превращает дату в формат php.
Осталось только применить эту функцию в нужный момент:
В большинстве случаев стандартная функция $cell->getValue() корректно обрабатывает формулы и возвращает рассчитанное значение. Но бывают ситуации, когда формула ссылается на несуществующий лист или другой файл, который локально хранится у того, кто отправил вам excel-документ. Тогда getValue() возвратит ошибку, хотя визуально в экселе вы можете увидеть верное значение, если не было пересчета листа. Дело в том, что Excel сохраняет oldCalculatedValue, которое и используется, если не пересчитывать лист. На рисунке выше я показал это в ячейке B4 — она отображает старое значение, хотя ссылка в ней нерабочая.
PHPExcel, к счастью, тоже умеет хранить старое значение формулы. Это удобно использовать, когда getValue() не смогла отработать и вернула не значение, а саму формулу (первый символ "="):
В итоге мы получили функцию, которая позволяет универсально считывать значения ячеек:
Для проверки прочитаем эксель со скриншота двумя способами: стандартной getValue (#1) и с использованием вышеописанной функции (#2):

Тест #1:

Тест #2:

Как видно, во втором случае все считалось корректно.
Важно отметить, что использование пунктов 2, 3 и 4 работает только в режиме ReadDataOnly = false. Это режим PHPExcel по умолчанию, когда он считывает всю мета-информацию о книге. Подходит для стандартных небольших документов, например счета, накладные и.т.д.
Включение ReadDataOnly = true может потребоваться для громоздких файлов, когда нужны только значения ячеек. По моей практике, такие файлы содержат отформатированные таблицы и там такой функционал не нужен.
Установка режима чтения в PHPExcel делается так:
Спасибо за внимание!
Мне по работе часто приходится импортировать Excel-документы средствами PHP.
Для этого я использую библиотеку PHPExcel, которая на сегодняшний день является весьма удобным инструментом.
Но есть несколько «скользких» моментов, связанных с чтением данных из ячеек, о которых я хочу рассказать хабрачитателям, а также разобрать функцию, решающую эти проблемы.
1. Адресация ячеек
Адресовать ячейки при работе с excel можно разными способами:
- колонка и ряд в виде строки: «A1»
- колонка буквой, ряд числом: («A», 1)
- колонка и ряд числом: (1, 1)
Первый способ удобнее для статических ячеек, а второй и третий для циклов.
Но в PHPExcel нет универсальной функции получения ячейки любым из этих способов, есть только отдельные функции. Что ж, исправляем это упущение:
public function getCellValue($cellOrCol, $row = null)
{
//column set by index
if(is_numeric($cellOrCol)) {
$cell = $this->activeSheet->getCellByColumnAndRow($cellOrCol, $row);
} else {
$lastChar = substr($cellOrCol, -1, 1);
if(!is_numeric($lastChar)) { //column contains only letter, e.g. "A"
$cellOrCol .= $row;
}
$cell = $this->activeSheet->getCell($cellOrCol);
}
$val = $cell->getValue();
return $val;
}
Сразу оговорюсь, что в приводимых мной примерах кода есть ссылки на $this, т.к. это методы моего класса-обертки над PHPExcel. В этом кусочке реализуются все три способа получения объекта ячейки.
2. Объединение ячеек
При чтении объединенных ячеек, PHPExcel возращает пустое значение для всех, кроме первой.
Т.е. для рисунка ниже значения B3 и C3 будут пустыми строками:

Мне всегда было неудобно такое поведение.
Гораздо удобнее (и логичнее!) для любой объединенной ячейки возвращать общее для них значение «mergedvalue».
Для этого при запросе ��начения нужно пройтись по всем объединенным диапазонам листа, и если заданная ячейка попадает в диапазон, то вернуть первую:
$this->mergedCellsRange = $this->activeSheet->getMergeCells();
foreach($this->mergedCellsRange as $currMergedRange) {
if($cell->isInRange($currMergedRange)) {
$currMergedCellsArray = PHPExcel_Cell::splitRange($currMergedRange);
$cell = $this->activeSheet->getCell($currMergedCellsArray[0][0]);
break;
}
}
3. Даты
Как известно, Excel хранит даты как число дней с 1 января 1900 года. Поэтому при чтении ячейки B2 на скриншоте выше мы получим бесполезное 41044. Но есть и хорошая новость — в PHPExcel присутствует удобная функция PHPExcel_Shared_Date::ExcelToPHP(), которая превращает дату в формат php.
Осталось только применить эту функцию в нужный момент:
$val = $cell->getValue();
if(PHPExcel_Shared_Date::isDateTime($cell)) {
$val = date($format, PHPExcel_Shared_Date::ExcelToPHP($val));
}
4. Формулы
В большинстве случаев стандартная функция $cell->getValue() корректно обрабатывает формулы и возвращает рассчитанное значение. Но бывают ситуации, когда формула ссылается на несуществующий лист или другой файл, который локально хранится у того, кто отправил вам excel-документ. Тогда getValue() возвратит ошибку, хотя визуально в экселе вы можете увидеть верное значение, если не было пересчета листа. Дело в том, что Excel сохраняет oldCalculatedValue, которое и используется, если не пересчитывать лист. На рисунке выше я показал это в ячейке B4 — она отображает старое значение, хотя ссылка в ней нерабочая.
PHPExcel, к счастью, тоже умеет хранить старое значение формулы. Это удобно использовать, когда getValue() не смогла отработать и вернула не значение, а саму формулу (первый символ "="):
$val = $cell->getValue();
if((substr($val,0,1) === '=' ) && (strlen($val) > 1)){
$val = $cell->getOldCalculatedValue();
}
Результат
В итоге мы получили функцию, которая позволяет универсально считывать значения ячеек:
public function getCellValue($cellOrCol, $row = null, $format = 'd.m.Y')
{
//column set by index
if(is_numeric($cellOrCol)) {
$cell = $this->activeSheet->getCellByColumnAndRow($cellOrCol, $row);
} else {
$lastChar = substr($cellOrCol, -1, 1);
if(!is_numeric($lastChar)) { //column contains only letter, e.g. "A"
$cellOrCol .= $row;
}
$cell = $this->activeSheet->getCell($cellOrCol);
}
//try to find current coordinate in all merged cells ranges
//if find -> get value from head cell
foreach($this->mergedCellsRange as $currMergedRange){
if($cell->isInRange($currMergedRange)) {
$currMergedCellsArray = PHPExcel_Cell::splitRange($currMergedRange);
$cell = $this->activeSheet->getCell($currMergedCellsArray[0][0]);
break;
}
}
//simple value
$val = $cell->getValue();
//date
if(PHPExcel_Shared_Date::isDateTime($cell)) {
$val = date($format, PHPExcel_Shared_Date::ExcelToPHP($val));
}
//for incorrect formulas take old value
if((substr($val,0,1) === '=' ) && (strlen($val) > 1)){
$val = $cell->getOldCalculatedValue();
}
return $val;
}
Тест
Для проверки прочитаем эксель со скриншота двумя способами: стандартной getValue (#1) и с использованием вышеописанной функции (#2):

Тест #1:

Тест #2:

Как видно, во втором случае все считалось корректно.
Ложка дегтя
Важно отметить, что использование пунктов 2, 3 и 4 работает только в режиме ReadDataOnly = false. Это режим PHPExcel по умолчанию, когда он считывает всю мета-информацию о книге. Подходит для стандартных небольших документов, например счета, накладные и.т.д.
Включение ReadDataOnly = true может потребоваться для громоздких файлов, когда нужны только значения ячеек. По моей практике, такие файлы содержат отформатированные таблицы и там такой функционал не нужен.
Установка режима чтения в PHPExcel делается так:
$objReader = PHPExcel_IOFactory::createReaderForFile($filename);
$objReader->setReadDataOnly(false);
$this->PHPExcel = $objReader->load($filename);
Спасибо за внимание!
