Чтение файла XLSX на PHP без сторонних библиотек
Чтение файла XLSX на PHP без сторонних библиотек
На работе возникла задача читать данные из таблиц формата XLSX. Много лет для этого я использовал замечательную библиотеку PHPExcel, но по ряду технических причин дальнейшее ее использование не представляется возможным. При этом задачу никто не отменял. Поэтому пришлось рисовать свое решение, максимально простое и быстрое.
Начнем с теории. Файлы формата XLSX представляют собой обычный ZIP-архив, в котором, во вложенной папке \xl, содержится информация о документе. Первое, что надо знать, это количество листов в книге. В файле \xl\workbook.xml об этом есть вся информация. Особое значение имеет файл \xl\sharedStrings.xml, в нем содержится список строк приблизительно в таком виде:
[si] => Array (
[0] => SimpleXMLElement Object (
[t] => Тип упаковки
)
[1] => SimpleXMLElement Object (
[t] => Вес груза
)
[2] => SimpleXMLElement Object (
[t] => Количество
)
У каждой строки получается собственный номер в порядке следования. Файл имеет простейшую структуру, разобрать его не представляет труда.
Теперь самое интересное. В папке \xl\worksheets находятся файлы вида sheet1.xml, sheet2.xml и так далее по количеству листов в книге. Если такой файл загрузить в память и разобрать при помощи функции simplexml_load_string, то в поле sheetData будет содержаться вся информация о строках и столбцах, а также об ячейках. Структура примерно следующая:
[sheetData] => SimpleXMLElement Object (
[row] => Array (
[0] => SimpleXMLElement Object (
[@attributes] => Array (
[r] => 1
[spans] => 1:15
[ht] => 46.5
[customHeight] => 1
)
[c] => Array (
[0] => SimpleXMLElement Object (
[@attributes] => Array (
[r] => A1
[s] => 36
[t] => s
)
[v] => 11
)
[1] => SimpleXMLElement Object (
[@attributes] => Array (
[r] => B1
[s] => 37
[t] => s
)
[v] => 1
)
[2] => SimpleXMLElement Object (
[@attributes] => Array (
[r] => C1
[s] => 38
)
[v] => 1178
)
[3] => SimpleXMLElement Object (
[@attributes] => Array (
[r] => D1
[s] => 38
)
[v] => 2.2
)
В теге @attributes в поле r находится экселевское название ячейки, в поле v записано содержимое ячейки. При этом, если установлено поле t и оно имеет значение "s", то числовое значение ячейки является индексом строки из файла sharedStrings.xml. Если этого поля нет или оно имеет другое значение, то значение поля v рассматривается как конечное значение этой ячейки. Это самый простейший вариант структуры XLSX-файла, без формул, без объединений ячеек и сложной структуры, без перекрестных ссылок и всякого такого прочего. Обычные статичные данные. Со структурой разобрались.
Теперь о том, как извлечь XML-файлы из контейнера. Проще всего для этого использовать штатный PHP-класс ZipArchive. Открываем XLSX-файл как ZIP-архив, при необходимости запрашиваем информацию о количестве листов в книге, обязательно извлекаем все строки из sharedStrings.xml, затем загружаем первый или нужное количество листов sheetN.xml.
Минимальный код для преобразования содержимого первого листа книги в двумерный массив у меня получился следующий:
Code (PHP) : Убрать нумерацию
- <?
- $z=new ZipArchive();
- $z->open('file.xlsx');
- $str_values=array();
- // Прочитать строковые значения
- if ($fp=$z->getStream('xl/sharedStrings.xml')) {
- $data='';
- while (!feof($fp)) {
- $data.=fread($fp, 1024);
- }
- fclose($fp);
- $xml=simplexml_load_string($data);
- if (isset($xml->si) && count($xml->si)) {
- foreach ($xml->si as $data) {
- $data=(array)$data;
- $str_values[]=$data['t'];
- }
- }
- }
- $xls_values=array();
- // Прочитать значения из первой страницы документа
- if ($fp=$z->getStream('xl/worksheets/sheet1.xml')) {
- $data='';
- while (!feof($fp)) {
- $data.=fread($fp, 1024);
- }
- fclose($fp);
- $xml=simplexml_load_string($data);
- if (isset($xml->sheetData)) {
- $sheetData=(array)($xml->sheetData);
- if (isset($sheetData['row']) && count($sheetData['row'])>0) {
- foreach($sheetData['row'] as $row) {
- $row=(array)$row;
- // Особый случай для одноколоночной страницы
- if (!is_array($row['c'])) {
- $row['c']=array($row['c']);
- }
- foreach ($row['c'] as $col) {
- $col=(array)$col;
- // Столбец и колонка
- preg_match('/([A-Z]+)(\d+)/',$col['@attributes']['r'],$matches);
- // Строка из списка
- if (isset($col['@attributes']['t'])
- && $col['@attributes']['t']=='s'
- && isset($str_values[$col['v']]))
- {
- $xls_values[$matches[2]][$matches[1]]=$str_values[$col['v']];
- }
- // Непосредственное значение
- elseif (isset($col['v'])) {
- $xls_values[$matches[2]][$matches[1]]=$col['v'];
- }
- }
- }
- }
- }
- }
- $z->close();
- // Массив со значениями ячеек
- print_r($xls_values);
О том, как создавать файлы формата XSLX без использования сторонних библиотек, вы можете прочитать в этой статье.
Просмотров: 997 | Комментариев: 7
Метки: PHP
Комментарии
Отзывы посетителей сайта о статье
ID
(13.06.2024 в 06:41):
ManHunter, Олдскул ;)
ManHunter
(26.04.2024 в 12:45):
в общих случаях моя схема следующая:
if (file_exists($file) && is_file($file) && is_readable($file)) {
if ($f=fopen($file,'r')) {
// известные читаем по filesize(), неизвестные до eof()
fclose($f);
}
else {
// что-то не так с доступностью файла
}
}
else {
// что-то не так с наличием файла
}
в частных, как тут, меняется только начальная проверка доступности.
if (file_exists($file) && is_file($file) && is_readable($file)) {
if ($f=fopen($file,'r')) {
// известные читаем по filesize(), неизвестные до eof()
fclose($f);
}
else {
// что-то не так с доступностью файла
}
}
else {
// что-то не так с наличием файла
}
в частных, как тут, меняется только начальная проверка доступности.
Чик
(26.04.2024 в 12:36):
Мне просто для работы надо, я же не спорю.) Думал, вдруг я что-то упущу, если не воспользуюсь усложненным для меня вариантом. Спасибо.
ManHunter
(26.04.2024 в 12:28):
Вот, "легче" уже начинает утяжеляться проверками :) В остальном спорить не буду, мне работать с файловыми сущностями удобнее именно так.
Чик
(26.04.2024 в 12:23):
Если не сложно, можете, пожалуйста, объяснить, про какие именно ресурсы тут идет речь? Файл xl/sharedStrings.xml можно проверить с условием
$sharedStrings = $obZip->getFromName('xl/sharedStrings.xml');
if ($sharedStrings !== false) {
$sharedStringsXml = simplexml_load_string($sharedStrings);
Или тут речь про содержание файла? Что может произойти не так в таком случае?
$sharedStrings = $obZip->getFromName('xl/sharedStrings.xml');
if ($sharedStrings !== false) {
$sharedStringsXml = simplexml_load_string($sharedStrings);
Или тут речь про содержание файла? Что может произойти не так в таком случае?
ManHunter
(26.04.2024 в 11:54):
"легче" != "лучше". У меня есть хорошая привычка проверять доступность ресурса перед чтением из него, а не тупо тянуть на удачу, что вытянется.
Чик
(26.04.2024 в 11:01):
В конечном коде не легче было использовать:
$sharedStrings = $obZip->getFromName('xl/sharedStrings.xml');
$xml = simplexml_load_string($sharedStrings);
Вместо:
if ($fp=$z->getStream('xl/sharedStrings.xml')) {
$data='';
while (!feof($fp)) {
$data.=fread($fp, 1024);
}
fclose($fp);
$xml=simplexml_load_string($data);
$sharedStrings = $obZip->getFromName('xl/sharedStrings.xml');
$xml = simplexml_load_string($sharedStrings);
Вместо:
if ($fp=$z->getStream('xl/sharedStrings.xml')) {
$data='';
while (!feof($fp)) {
$data.=fread($fp, 1024);
}
fclose($fp);
$xml=simplexml_load_string($data);
Добавить комментарий
Заполните форму для добавления комментария