Разбор большого XML файла с помощью PHP12.11.2009 сайт автора: http://webi.ru
публикация данной статьи разрешена только со ссылкой на сайт автора статьиВ этой статье я покажу пример, как разобрать большой XML файл. Если на вашем сервере (хостинге) не запрещено увеличение времени работы скрипта, то можно разбирать XML файл весом хоть гигабайты, сам лично разбирал только файлы от озона весом 450 мегабайт.
При разборе больших XML файлов возникает две проблемы: 1. Не хватает памяти. 2. Не хватает выделенного времени для работы скрипта. Вторую проблему с временем решить можно, если сервером это не запрещено. А вот проблему с памятью решить сложно, даже если речь идет о своем сервере, то ворочать файлы по 500 мегабайт не очень просто а уж на хостинге и на VDS увеличить память просто не получится.
В PHP существует несколько встроенных вариантов обработки XML - SimpleXML, DOM, SAX. Все эти варианты подробно описаны во многих статьях с примерами, но все примеры демонстрируют работу с полным XML документом.
Вот один из примеров, получаем объект из XML файла
<? $xml = simplexml_load_file("1.xml"); ?>
Теперь можно обрабатывать этот объект, НО... Как видно, весь XML файл считывается в память, затем все разбирается в объект. То есть все данные попадают в память и если выделенной памяти мало, то скрипт останавливается.
Для обработки больших файлов такой вариант не подходит, нужно читать файл построчно и обрабатывать эти данные по очереди. При этом проверка на валидность осуществляется так же по мере обработки данных, поэтому нужно иметь возможность для отката, например удалить все внесенные в базу данные в случае не валидного XML файла, либо осуществить два прохода по файлу, сначала читать на валидность, затем читать для обработки данных.
Вот теоретический пример разбора большого XML файла. Этот скрипт читает по одному символу из файла, собирает эти данные в блоки и отправляет в разборщик XML. Такой подход полностью решает проблему с памятью и не вызывает нагрузки, но усугубляет проблему со временем. Как попытаться решить проблему со временем, читайте ниже.
<? function webi_xml($file) { #################################################### ### функция работы с данными function data ($parser, $data) { print $data; } ############################################ #################################################### ### функция открывающих тегов function startElement($parser, $name, $attrs) { print $name; print_r($attrs); } ############################################### ################################################# ## функция закрывающих тегов function endElement($parser, $name) { print $name; } ############################################ $xml_parser = xml_parser_create(); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); // указываем какие функции будут работать при открытии и закрытии тегов xml_set_element_handler($xml_parser, "startElement", "endElement"); // указываем функцию для работы с данными xml_set_character_data_handler($xml_parser,"data"); // открываем файл $fp = fopen($file, "r"); $perviy_vxod=1; // флаг для проверки первого входа в файл $data=""; // сюда собираем частями данные из файла и отправляем в разборщик xml // цикл пока не найден конец файла while (!feof ($fp) and $fp) { $simvol = fgetc($fp); // читаем один символ из файла $data.=$simvol; // добавляем этот символ к данным для отправки // если символ не завершающий тег, то вернемся к началу цикла и добавим еще один символ к данным, и так до тех пор, пока не будет найден закрывающий тег if($simvol!='>') { continue;} // если закрывающий тег был найден, теперь отправим эти собранные данные в обработку // проверяем, если это первый вход в файл, то удалим все, что находится до тега <? // так как иногда может встретиться мусор до начала XML (корявые редакторы, либо файл получен скриптом с другого сервера) if($perviy_vxod) {$data=strstr($data, '<?'); $perviy_vxod=0;} // теперь кидаем данные в разборщик xml if (!xml_parse($xml_parser, $data, feof($fp))) { // здесь можно обработать и получить ошибки на валидность... // как только встретится ошибка, разбор прекращается echo "<br>XML Error: ".xml_error_string(xml_get_error_code($xml_parser)); echo " at line ".xml_get_current_line_number($xml_parser); break; } // после разбора скидываем собранные данные для следующего шага цикла. $data=""; } fclose($fp); xml_parser_free($xml_parser); } webi_xml('1.xml'); ?>
В этом примере я все сложил в одну функцию webi_xml() и в самом низу видно ее вызов. Сам скрипт состоит из трех основных функций: 1. Функция которая ловит открытие тега startElement() 2. Функция которая ловит закрытие тега endElement() 3. И функция получения данных data(). Предположим что содержимое файла 1.xml некий рецепт
<?xml version="1.0" encoding="windows-1251"?> <recipe name="хлеб" preptime="5" cooktime="180"> <title>Простой хлеб</title> <ingredient amount="3" unit="стакан">Мука</ingredient> <ingredient amount="0.25" unit="грамм">Дрожжи</ingredient> <ingredient amount="1.5" unit="стакан">Тёплая вода</ingredient> <ingredient amount="1" unit="чайная ложка">Соль</ingredient> <instructions> <step>Смешать все ингредиенты и тщательно замесить.</step> <step>Закрыть тканью и оставить на один час в тёплом помещении.</step> <step>Замесить ещё раз, положить на противень и поставить в духовку.</step> <step>Посетить сайт webi.ru</step> </instructions> </recipe>
Начинаем все с вызова общей функции webi_xml('1.xml'); Дальше в этой функции стартует разборщик и все имена тегов переводим в верхний регистр, чтобы все теги имели одинаковый регистр.
$xml_parser = xml_parser_create(); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true);
Теперь указываем какие функции будут работать для отлова открытия тега, закрытия и обработки данных
xml_set_element_handler($xml_parser, "startElement", "endElement"); xml_set_character_data_handler($xml_parser,"data");
Дальше идет открытие указанного файла, перебор файла по одному символу и каждый символ добавляется в строковую переменную пока не будет найден символ >. Если это самое первое обращение к файлу, то попутно будет удалено все что будет лишним в начале файла, все что стоит до <?, именно с такого тега должен начинаться XML. В первый раз строковая переменная соберет в себе строку <?xml version="1.0" encoding="windows-1251"?> И отправит ее в разборщик xml_parse($xml_parser, $data, feof($fp)); После обработки данных строковая переменная сбрасыватеся и снова начинается сбор данных в строку и во второй раз сформируется строка <recipe name="хлеб" preptime="5" cooktime="180"> В третий <title> в четвертый Простой хлеб</title>
Обратите внимание, что строковая переменная всегда формируется по законченному тегу > и не обязательно посылать разбощику открытый и закрытый тег с данными например <title>Простой хлеб</title> Данному обработчику важно получить целый не разбитый тег, хоть один открытый, а в следущий шаг закрытый тег, или сразу получить 1000 строк файла, не важно, главное чтобы тег не разрывался, например <tit le>Простой хлеб Так отправить данные обработчику нельзя, так как тег разорвался. Вы можете придумать свою методику посылания данных в обработчик, например собирать по 1 мегабайту данных и отправлять в обработчик для повышения скорости, только следите чтобы теги всегда завершались, а данные можно разрывать <title>Простой хлеб</title>
Таким образом частями, как вы пожелаете можно отправить большой файл в обработчик.
Теперь рассмотрим каким образом эти данные обрабатываются и как их получить.
Начинаем с функции открывающих тегов startElement($parser, $name, $attrs) Предположим, что обработка дошла до строки <ingredient amount="3" unit="стакан">Мука</ingredient> Тогда внутри функции переменная $name будет равна ingredient то есть название открытого тега (до закрытия тега дело еще не дошло). Так же в данном случае будет доступен массив атрибутов этого тега $attrs, в котором будут данные amount="3" и unit="стакан".
После этого пошла обработка данных открытого тега функцией data ($parser, $data) В переменной $data будет все, что находится между открывающим и закрывающим тегом, в нашем случае это текст Мука
И завершается обработка нашей строки функцией endElement($parser, $name) Это название закрытого тега, в нашем случае $name будет равна ingredient
А после этого опять пошло все по кругу.
Приведенный пример лишь демонстрирует принцип обработки XML, но для реального применения его нужно доработать. Обычно, разбирать большой XML приходится для занесения данных в базу и для правильной обработки данных нужно знать к какому открытому тегу относятся данные, какой уровень вложения тега и какие теги открыты по иерархии выше. Обладая такой информацией можно без проблем правильно обработать файл. Для этого нужно ввести несколько глобальных переменных, которые будут собирать информацию об открытых тегах, вложенности и данные. Привожу пример, который можно использовать
<? function webi_xml($file) { global $webi_depth; // счетчик, для отслеживания глубины вложенности $webi_depth = 0; global $webi_tag_open; // будет содержать массив открытых в данный момент тегов $webi_tag_open= array(); global $webi_data_temp; // этот массив будет содержать данные одного тега #################################################### ### функция работы с данными function data ($parser, $data) { global $webi_depth; global $webi_tag_open; global $webi_data_temp; // добавляем данные в массив с указанием вложенности и открытого в данный момент тега $webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['data'].=$data; } ############################################ #################################################### ### функция открывающих тегов function startElement($parser, $name, $attrs) { global $webi_depth; global $webi_tag_open; global $webi_data_temp; // если уровень вложенности уже не нулевой, значит один тег уже открыт // и данные из него уже в массиве, можно их обработать if ($webi_depth) { // здесь начинается обработка данных, например добаление в базу, сохранение в файл и т.д. // $webi_tag_open содержит цепочку открытых тегов по уровню вложенности // например $webi_tag_open[$webi_depth] содержит название открытого тега чья информация сейчас обрабатывается // $webi_depth уровень вложенности тега // $webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['attrs'] массив атрибутов тега // $webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['data'] данные тега print 'данные '.$webi_tag_open[$webi_depth].'--'.($webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['data']).'<br>'; print_r($webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['attrs']); print '<br>'; print_r($webi_tag_open); // массив открытых тегов print '<hr>'; // после обработки данных удаляем их для освобождения памяти unset($GLOBALS['webi_data_temp'][$webi_depth]); } // теперь пошло открытие следующего тега и дальше обработка его произойдет на следующем шаге $webi_depth++; // увеличиваем вложенность $webi_tag_open[$webi_depth]=$name; // добавляем открытый тег в массив информаци $webi_data_temp[$webi_depth][$name]['attrs']=$attrs; // теперь добавляем атрибуты тега } ############################################### ################################################# ## функция закрывающих тегов function endElement($parser, $name) { global $webi_depth; global $webi_tag_open; global $webi_data_temp; // здесь начинается обработка данных, например добаление в базу, сохранение в файл и т.д. // $webi_tag_open содержит цепочку открытых тегов по уровню вложенности // например $webi_tag_open[$webi_depth] содержит название открытого тега чья информация сейчас обрабатывается // $webi_depth уровень вложенности тега // $webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['attrs'] массив атрибутов тега // $webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['data'] данные тега print 'данные '.$webi_tag_open[$webi_depth].'--'.($webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['data']).'<br>'; print_r($webi_data_temp[$webi_depth][$webi_tag_open[$webi_depth]]['attrs']); print '<br>'; print_r($webi_tag_open); print '<hr>'; unset($GLOBALS['webi_data_temp']); // после обработки данных удаляем массив с данными целиком, так как произошло закрытие тега unset($GLOBALS['webi_tag_open'][$webi_depth]); // удаляем информацию об этом открытом теге... так как он закрылся $webi_depth--; // уменьшаем вложенность } ############################################ $xml_parser = xml_parser_create(); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, true); // указываем какие функции будут работать при открытии и закрытии тегов xml_set_element_handler($xml_parser, "startElement", "endElement"); // указываем функцию для работы с данными xml_set_character_data_handler($xml_parser,"data"); // открываем файл $fp = fopen($file, "r"); $perviy_vxod=1; // флаг для проверки первого входа в файл $data=""; // сюда собираем частями данные из файла и отправляем в разборщик xml // цикл пока не найден конец файла while (!feof ($fp) and $fp) { $simvol = fgetc($fp); // читаем один символ из файла $data.=$simvol; // добавляем этот символ к данным для отправки // если символ не завершающий тег, то вернемся к началу цикла и добавим еще один символ к данным, и так до тех пор, пока не будет найден закрывающий тег if($simvol!='>') { continue;} // если закрывающий тег был найден, теперь отправим эти собранные данные в обработку // проверяем, если это первый вход в файл, то удалим все, что находится до тега <? // так как иногда может встретиться мусор до начала XML (корявые редакторы, либо файл получен скриптом с другого сервера) if($perviy_vxod) {$data=strstr($data, '<?'); $perviy_vxod=0;} // теперь кидаем данные в разборщик xml if (!xml_parse($xml_parser, $data, feof($fp))) { // здесь можно обработать и получить ошибки на валидность... // как только встретится ошибка, разбор прекращается echo "<br>XML Error: ".xml_error_string(xml_get_error_code($xml_parser)); echo " at line ".xml_get_current_line_number($xml_parser); break; } // после разбора скидываем собранные данные для следующего шага цикла. $data=""; } fclose($fp); xml_parser_free($xml_parser); // удаление глобальных переменных unset($GLOBALS['webi_depth']); unset($GLOBALS['webi_tag_open']); unset($GLOBALS['webi_data_temp']); } webi_xml('1.xml'); ?>
Весь пример сопроводил комментариями, теперь тестируйте и экспериментируйте. Обратите внимание, в функции работы с данными в массив данные не просто вставляются, а именно добавляются с помощью ".=" так как данные могут поступать не в целом виде и если сделать просто присвоение, то время от времени вы будете получать данные кусками.
Ну вот и все, теперь при обработке файла любого размера памяти хватит, а вот время работы скрипта увеличить можно несколькими способами. В начало скрипта вставьте функцию set_time_limit(6000); или ini_set("max_execution_time", "6000");
Либо добавьте в файл .htaccess текст php_value max_execution_time 6000
Данные примеры увеличат время работы скрипта до 6000 секунд. Увеличить время подобным образом можно только в выключенном безопасном режиме.
Если у вас есть доступ к редактированию php.ini можете увеличить время с помощью max_execution_time = 6000
Например на хостинге мастерхост на момент написания статьи увеличение времени скрипта запрещено, не смотря на выключенный безопасный режим, но если вы профи, вы можете сделать свою сборку php на мастерхосте, но об этом не в этой статье.
Комментарии
RSS комментарии
13.11.2009 developer
проверил.
взял файл озона 450 мб с книгами.
и обработал его этим примером.
вставлял в базу данные минут 30, долго, но в итоге вставил...
28.02.2010 leksus.info
у меня хмл-файл на 25 гигов, вот думаю, сколько это времени займёт :)
Я так понимаю, если указать считываемые кусочки по 1мб, дело пойдёт быстрее?
01.03.2010 Админ
25 гигов это конечно круто.
если честно, не знаю что тут может ускорить процесс, надо экспериментировать.
разобрать конечно можно такой файл, но сколько часов это будет обрабатываться...
01.05.2010 Alex-Dnepr
PHP для разбора больших баз не очень-то подходит, причем - всего лишь в силу того, что сам процесс занимает очень много времени (450MB = 30мин. = 1800сек.), а, очень часто, настройки веб-сервера просто не позволяют его осуществить полностью из-за установленного лимита времени на работу php-скрипта (например стандартные для Апача 30сек.).
Такие вещи реально осуществлять на своем выделенном сервере (виртуальном либо реальном), так как появляется возможность изменения данного лимита времени исходя из своих потребностей.
Поэтому, в этом плане, парсер на базе perl - один из лучших вариантов.
P.S. Пробовал изменять размер куска на считывание - почти нулевое изменение производительности.
01.05.2010 админ
Alex-Dnepr, конечно, для подобных вещей perl более предпочтителен.
Скорость выполнение подобного разбора в perl должна быть быстрее, но не сильно уж быстрее.
Да и потом у perl по времени тоже существуют ограничения, такие же как в php.
02.05.2010 Alex-Dnepr
Как ни крути, но sax-парсер - медленный.
Для ускорения разбора больших баз можно попробовать использовать парсер, построенный, например на базе strpos() и substr().
02.05.2010 админ
Alex-Dnepr, на базе strpos() и substr() тоже можно, но будет ли быстрее... мне кажется, что не будет быстрее.
проверки и поиск символов по тексту создаст тоже не малую нагрузку.
Но это все уже тестировать надо.
Если прирост в скорости будет значительный, тогда наверное стоит обратить внимание на обработку строковыми функциями.
10.05.2010 Alex-Dnepr
Здравствйте, Admin.
Какая средняя скорость разбора больших XML файлов sax-парсером была зафиксирована Вами (размер файла/время обработки)?
11.05.2010 админ
Да вот тут я не отвечу. Я не проводил тестов на время.
Была задача сделать разбор файлов до 50 МБ на хостинге.
Естественно обычным способом разобрать не получалось, не хватало памяти.
А вот вариант описанный в этой статье подошел и укладывался в ресурсы хостинга, хватало и памяти и времени.
А вот сколько по времени длились разборы, я вот тут не засекал.
14.05.2010 Alex-Dnepr
Попробовал парсить XML-базу с помощью строковых функций - результат достаточно преемлемый.
По факту, разбор XML базы размером 500 Мб (только чтение - без записи в БД) с помощью SAX занял 460 сек, а строковыми функциями - 260 сек.
14.05.2010 админ
результат конечно впечатляет, почти в два раза быстрее.
но все ли вы учли? например разбор атрибутов предусмотрели?
может покажите этот пример(на почту), я его обработаю, потестирую и присоединю к этой статье.
15.05.2010 Alex-Dnepr
Как уже указывал - такие результаты были достигнуты в режиме <u>чтения</u> XML-базы - без учета затрат времени на работу с СУБД (например MySQL).
По факту, необходимость записи в базу данных - практически нивелирует относительную разницу в результатах работы парсеров.
П.С. Выделение атрибутов осуществляется и не ничем не отличается от процесса выделения значений, заключенных в теги.
По факту - это тот же sax, разница в том, что выделение тегов и атрибутов осуществляется с помощью строковых функций.
15.05.2010 админ
Про атрибуты я упомянул потому, что выделение атрибутов такое же затратное действие как и выделение значений тегов. И если опустить атрибуты, то скорость увеличится, поэтому я и спросил про атрибуты.
А зачем учитывать время работы с базой?
Тут было интересно узнать скорость самого разбора. Раз он быстрее в два раза, значит более предпочтителен.
Ведь может понадобиться не вставлять данные в базу, а лишь найти несколько значений в большом XML
26.07.2010 Виталий
А можно после считывания из xml сразу удалять строку?
т.е. считал, закинул эти данные в базу(например), удалил эту строку и так далее... а то у меня строк 400 считывает и потом 504ю ошибку выдает, приходится удалять уже добавленные строки...
27.07.2010 Никита
Виталий, просто так удалить строку из файла нельзя.
Для этого надо прочитать все оставшиеся строки и записать их в новый файл.
28.07.2010 Виталий
А если крупный файл, то оперативку сильно забивать будет...?
28.07.2010 Никита
Виталий, ну у тебя же проблема с какой-то ошибкой после прочтения 400 строк.
Скорее всего просто не хватает времени.
А для того, чтобы выкинуть обработанные строки из файла, нужно оставшиеся строки читать и складывать в новый файл и тут опять же возникнет твоя ошибка.
Если прочитать всю оставшуюся часть файла и сохранить в новый файл, то тут конечно не хватит оперативки, если файл большой.
30.07.2010 Виталий
В общем ничего сочинять не нужно... надо просто запускать такие вещи через крон, и все потихоньку за раз хоть за час хоть за два добавится...
01.04.2011 m11
А можно пример как в базу вставить полученные данные?
12.04.2011 Алексей
Может быть что-то изменилось в PHP'шном парсере... Но факт тот, что сейчас, судя по всему, пляски с поиском начала и конца тегов не нужны...
Читай хоть по одному символу - оно запоминает и парсит как надо:
while ($data = fread($fp, 1)) {
if (!xml_parse($xml_parser, $data, feof($fp))) {
* * * *
}
}
Добавить свой комментарий
|