Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Разбор большого XML файла с помощью PHP


WeBinstruments

Инструментарий для веб-мастеров 10

 
 Меню
  PHP скрипты  
  Программы  
  Документация  
  Новости  
  Статьи  
 
 Контакты
  icq: 158325531
  email: adm.webi.ru
 
 

   Разбор большого XML файла с помощью PHP

Комментарии к статье и вопросы http://webi.ru/webi_articles/big_xml.html
Автор статьи webi.ru

Разбор большого XML файла с помощью PHP

В этой статье я покажу пример, как разобрать большой 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 на мастерхосте, но об этом не в этой статье.




   Новости CMS
1С-Битрикс: Внутренний портал учебного заведения

Компания "1С-Битрикс" выпускает новое специализированное решение – "1С-Битрикс: Внутренний портал учебного заведения", предназначенное для построения внутреннего информационно-коммуникационного ресурса образовательного учреждения.


Copyright © 2003-2009 WeBi Constructor

В избранное