Обработка на големи XML файлове с PHP


Обработката на XML файлове в PHP е сравнително лесно за документи с малък размер. Нека разгледаме следния каталог с продукти:

<?xml version="1.0" encoding="UTF-8"?>
<products>
    <product id="1">
        <name>product1</name>
        <price>10.5</price>
    </product>
    <product id="2">
        <name>product2</name>
        <price>9.5</price>
    </product>
    <product id="3">
        <name>product3</name>
        <price>11.5</price>
    </product>

</products>

Прочитането със SimpleXML е ... лесно:

function print_product($product) {
    $attrs = $product->attributes();
    echo "\n" . $attrs['id'] . ": " . $product->name .
        " (" . $product->price . ")";        
}

$xml = simplexml_load_file('../resources/xml-sample1.xml');

foreach ($xml->product as $product) {
    print_product($product);
}

Резултат:

1: product1 (10.5)
2: product2 (9.5)
3: product3 (11.5)

Бихме могли също да претърсим документа за продукт с дадено име или номер:

echo "Products with id=2:\n";
foreach ($xml->product as $product) {
    $attrs = $product->attributes();
    if ($attrs['id'] == 2) {
        print_product($product);
    }
}

Резултат:

Products with id=2:

2: product2 (9.5)


echo "Products with name=product3:\n";
foreach ($xml->product as $product) {
    if ($product->name == 'product3') {
        print_product($product);
    }
}

Резултат:

Products with name=product3:

3: product3 (11.5)


Нека сега генерираме малко повече продукти - да кажем 1000000

$fp = fopen('../resources/xml-sample2.xml', 'w');
fwrite($fp, '<?xml version="1.0" encoding="UTF-8"?><products>');
for ($i=0; $i<1000000; $i++) {
    fwrite($fp, "<product id=\"$i\">
            <name>product$i</name>
            <price>" . rand(10, 20). "</price>
        </product>");
}
fwrite($fp, '</products>');
fclose($fp);

$xml = simplexml_load_file('../resources/xml-sample2.xml');
print_r($xml);

Резултат:

PHP Fatal error:  Allowed memory size of 33554432 bytes exhausted (tried to allocate 12 bytes) in xml-sample1.php on line 45

И тук паметта свършва - xml-sample2.xml е около 88M. Забележете, че следния код не генерира тази грешка, но въпреки това PHP процеса заема почти 600M от паметта на системата, което в повечето случаи води до същия резултат:

$xml = simplexml_load_file('../resources/xml-sample2.xml');

foreach ($xml->product as $product) {
    print_product($product);
}

За обработка на големи XML документи с минимално изразходване на памет се използват поточните XML парсери. Те обработват документа като един поток, като зареждат минимален фрагмент от него в паметта. При всяко срещане на отварящ или затварящ таг, както и на текстов фрагмент (CDATA) се генерира събитие, което бива обработено от дадена функция. За нас остава единствено да реализираме подходящата функция за всяко събитие. По-долу е примерен код, който чете нашия каталог:

class ProductsParser {
    var $product;
    var $product_elem;
    
    // извиква се при срещане на отварщ таг
    // $tag е името на тага $attributes е масив от атрибутите
    function startElement($parser, $tag, $attributes) {
        switch($tag) {
            case 'product':
                $this->product=array('id'=>$attributes['id'], 'name'=>'', 'price'=>'');
                break;
            case 'name':
            case 'price':
                if ($this->product) {
                    $this->product_elem = $tag;
                }
                break;
        }
    }
    
    // извиква се при затварящ таг
    function endElement($parser, $tag) {
        switch($tag) {
            case 'product':
                if ($this->product) {
                    $this->handle_product();
                    $this->product = null;
                }
                break;
            case 'name':
            case 'price':
                $this->product_elem = null;
                break;
        }
    }
    
    // извиква се при среща на фрагмент с текст (CDATA)
    // имайте предвид, че всеки CDATA елемент може да
    // предизвика няколко извиквания на този метод
    function cdata($parser, $cdata) {
        if ($this->product && $this->product_elem) {
            $this->product[$this->product_elem] .= $cdata;
        }
    }
    
    // извиква се при прочетен продукт от документа
    function handle_product() {
        $this->print_product();
    }

    // отпечатва продукт
    function print_product() {
        echo "\n" . $this->product['id'] . ": " . $this->product['name'] .
            " (" . $this->product['price'] . ")";        
    }
    
}

$xml_handler = new ProductsParser();
$parser = xml_parser_create();

xml_set_object($parser, $xml_handler);
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
xml_set_element_handler($parser, "startElement", "endElement");
xml_set_character_data_handler($parser, "cdata");

$fp = fopen('../resources/xml-sample2.xml', 'r');
while ($data = fread($fp, 4096)) {
    xml_parse($parser, $data, feof($fp));
    flush();
}
fclose($fp);

Ето няколко пояснения. Файлът се чете на парчета от по 4k. Всеки отварящ таг предизвиква извикването на startElement(), като с еподават неговото име и атрибути. Ако тагът е <product> инициализираме променливата за текущ продукт с атрибутите на текущия продукт - в случая номера му. Следващия отварящ таг е name. Вдигаме флага, който показва че очакваме да прочетем стойността на този елемент. Следват едно или няколко извиквания на cdata(), като добавяме текста към името на текущия продукт. След това срещаме затварящ таг name и сваляме флага - прочели сме цялото име. По същия начин протичат нещата и с цената на продукта. При прочитане на </product> се извиква endElement(), където вече предаваме напълно прочетения елемент на функцията за обработка, а именно го отпечатваме.

Добре е максимална част от логиката за обработка на продуктите да е във функцията handle_product() - филтриране, операции с база данни и други. Например в случая отпечатваме всеки продукт, а не ги акумулираме, за да бъдат отпечатани заедно. По този начин се постига минимално използване на памет.

 

Няма коментари

Обратно към списъка със статиите

Тази страница последно е променяна на 2024-04-29 02:50:28