使用SimpleXMLElement的大型PHP循环非常慢:内存问题?

时间:2015-04-20 18:53:13

标签: php xml loops mysqli simplexml

我目前有一些PHP代码基本上从xml文件中提取数据并使用$products = new SimpleXMLElement($xmlString);创建简单的xml对象然后我使用for循环遍历此代码,在其中我为每个设置产品详细信息XML文档中的产品。然后将其保存到mySql数据库。

在运行此脚本时,添加的产品会降低频率,直到它们最终在达到最大值之前停止。我试过间隔运行垃圾收集,但无济于事。以及取消设置似乎不起作用的各种变量。

部分代码如下所示:

<?php
$servername = "localhost";
$username = "database.database";
$password = "demwke";
$database = "databasename";
$conn = new mysqli($servername, $username, $password, $database);

$file = "large.xml";
$xmlString = file_get_contents($file);
$products = new SimpleXMLElement($xmlString);
unset($xmlString, $file);
$total = count($products->datafeed[0]);

echo 'Starting<br><br>';

for($i=0;$i<$total;$i++){
    $id = $products->datafeed->prod[$i]['id'];
etc etc
    $sql = "INSERT INTO products (id, name, uid, cat, prodName, brand, desc, link, imgurl, price, subcat) VALUES ('$id', '$store', '$storeuid', '$category', '$prodName', '$brand', '$prodDesc', '$link', '$image', '$price', '$subCategory')";
}
echo '<br>Finished';
?>

php变量全部使用与$ id类似的行定义,但删除后更容易阅读。

关于我可以做什么/阅读以完成此任务的任何想法?只要它最终完成,所花费的时间对我来说并不重要。

5 个答案:

答案 0 :(得分:2)

您可以尝试增加内存限制。如果这不是一个选项,你只需要完成一次,我个人只需将其组合起来并一次处理5k值。

<?php
$servername = "localhost";
$username = "database.database";
$password = "demwke";
$database = "databasename";
$conn = new mysqli($servername, $username, $password, $database);

$file = "large.xml";
$xmlString = file_get_contents($file);
$products = new SimpleXMLElement($xmlString);
unset($xmlString, $file);

$total = count($products->datafeed[0]);

//get your starting value for this iteration
$start = isset($_GET['start'])?(int)$_GET['start']:0;

//determine when to stop
//process no more than 5k at a time
$step = 5000;
//where to stop, either after our step (max) or the end
$limit = min($start+$step, $total);

echo 'Starting<br><br>';

//modified loop so $i starts at our start value and stops at our $limit for this load.
for($i=$start;$i<$limit;$i++){
    $id = $products->datafeed->prod[$i]['id'];
etc etc
    $sql = "INSERT INTO products (id, name, uid, cat, prodName, brand, desc, link, imgurl, price, subcat) VALUES ('$id', '$store', '$storeuid', '$category', '$prodName', '$brand', '$prodDesc', '$link', '$image', '$price', '$subCategory')";
}

if($limit >= $total){
    echo '<br>Finished';
} else {
    echo<<<HTML
<html><head>
<meta http-equiv="refresh" content="2;URL=?start={$limit}">
</head><body>
Done processing {$start} through {$limit}. Moving on to next set in 2 seconds.
</body><html>
HTML;
}
?>

只要这不是用户负载(例如您网站的标准访问者),就不会出现问题。

另一种选择,您是否尝试过正确准备/绑定查询?

答案 1 :(得分:2)

更新:永远不要使用SimpleXML的索引,除非你有非常少的对象。请改用foreach

// Before, with [index]:
for ($i=0;$i<$total;$i++) {
    $id = $products->datafeed->prod[$i]['id'];
    ...

// After, with foreach():
$i = 0;
foreach ($products->datafeed->prod as $prod) {
    $i++; // Remove if you don't actually need $i
    $id = $prod['id'];
    ...

通常,...->node[$i]将访问数组node[]并将其全部读取到所需的索引,因此迭代节点数组不是o(N),而是 o(N 2 即可。没有解决方法,因为无法保证当您访问项目K时,您刚刚访问了项目K-1(以递归方式等)。 foreach保存指针,因此工作在o(N)。

出于同样的原因,即使你真的只需要几个已知的项目(除非它们很少并且非常接近数组的开头),使用foreach整个数组进行迭代可能是有利的:

    $a[0] = $products->datafeed->prod[15]['id'];
    ...
    $a[35] = $products->datafeed->prod[1293]['id'];

// After, with foreach():
$want = [ 15, ... 1293 ];
$i = 0;
foreach ($products->datafeed->prod as $prod) {
    if (!in_array(++$i, $want)) {
        continue;
    }
    $a[] = $prod['id'];
}

首先应验证增加的延迟是由MySQLi还是由XML处理引起的。您可以从循环中删除(注释掉)SQL查询执行,而不是其他任何内容,以验证速度(授予它现在将高得多...... :-))现在保持不变,或者显示相同的减少。

我怀疑XML处理是罪魁祸首,在这里:

for($i=0;$i<$total;$i++){
    $id = $products->datafeed->prod[$i]['id'];

...在哪里访问一个更远,更远的索引到SimpleXMLObject 。这可能会遇到Schlemiel the Painter的问题。

对你的问题的直接回答是“如何让循环完成,无论时间如何”,都是“增加内存限制和最大执行时间”。

要提高效果,您可以在Feed对象中使用不同的界面

$i = -1;
foreach ($products->datafeed->prod as $prod) {
    $i++;
    $id = $prod['id'];
    ...
}

进行实验

我使用这个小程序来读取大型XML并迭代其内容:

// Stage 1. Create a large XML.
$xmlString = '<?xml version="1.0" encoding="UTF-8" ?>';
$xmlString .= '<content><package>';
for ($i = 0; $i < 100000; $i++) {
    $xmlString .=  "<entry><id>{$i}</id><text>The quick brown fox did what you would expect</text></entry>";
}
$xmlString .= '</package></content>';

// Stage 2. Load the XML.
$xml    = new SimpleXMLElement($xmlString);

$tick   = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    $id = $xml->package->entry[$i]->id;
    if (0 === ($id % 5000)) {
        $t = microtime(true) - $tick;
        print date("H:i:s") . " id = {$id} at {$t}\n";
        $tick = microtime(true);
    }
}

生成XML后,循环会解析它并打印迭代5000个元素需要多少时间。为了验证它确实是时间增量,还会打印日期。 delta应该大约是时间戳之间的时间差。

21:22:35 id = 0 at 2.7894973754883E-5
21:22:35 id = 5000 at 0.38135695457458
21:22:38 id = 10000 at 2.9452259540558
21:22:44 id = 15000 at 5.7002019882202
21:22:52 id = 20000 at 8.0867099761963
21:23:02 id = 25000 at 10.477082967758
21:23:15 id = 30000 at 12.81209897995
21:23:30 id = 35000 at 15.120756149292

这就是发生的事情:处理XML数组的速度越来越慢

这主要是使用foreach的相同程序:

// Stage 1. Create a large XML.
$xmlString = '<?xml version="1.0" encoding="UTF-8" ?>';
$xmlString .= '<content><package>';
for ($i = 0; $i < 100000; $i++) {
    $xmlString .=  "<entry><id>{$i}</id><text>The quick brown fox did ENTRY {$i}.</text></entry>";
}
$xmlString .= '</package></content>';

// Stage 2. Load the XML.
$xml    = new SimpleXMLElement($xmlString);

$i      = 0;
$tick   = microtime(true);
foreach ($xml->package->entry as $data) {
    // $id = $xml->package->entry[$i]->id;
    $id = $data->id;
    $i++;
    if (0 === ($id % 5000)) {
        $t = microtime(true) - $tick;
        print date("H:i:s") . " id = {$id} at {$t} ({$data->text})\n";
        $tick = microtime(true);
    }
}

现在时间似乎不变......我说“似乎”因为它们似乎减少了大约一万倍,而且我在获得可靠的测量方面遇到了一些困难。

(不,我不知道。我可能从未使用过大型XML数组的索引)。

21:33:42 id = 0 at 3.0994415283203E-5 (The quick brown fox did ENTRY 0.)
21:33:42 id = 5000 at 0.0065329074859619 (The quick brown fox did ENTRY 5000.)
...
21:33:42 id = 95000 at 0.0065121650695801 (The quick brown fox did ENTRY 95000.)

答案 2 :(得分:1)

以下是需要解决的两个问题:

内存

目前,您正在使用file_get_contents()将完整文件读入内存,并使用SimpleXML将其解析为对象结构。这两个操作都会将整个文件加载到内存中。

更好的解决方案是使用XMLReader:

$reader = new XMLReader;
$reader->open($file);
$dom = new DOMDocument;
$xpath = new DOMXpath($dom);

// look for the first product element
while ($reader->read() && $reader->localName !== 'product') {
  continue;
}

// while you have an product element
while ($reader->localName === 'product') {
  // expand product element to a DOM node
  $node = $reader->expand($dom);
  // use XPath to fetch values from the node
  var_dump(
    $xpath->evaluate('string(@category)', $node),
    $xpath->evaluate('string(name)', $node),
    $xpath->evaluate('number(price)', $node)
  );
  // move to the next product sibling
  $reader->next('product');
}

性能

处理大量数据需要花费时间,甚至更多地以连续方式执行。

将脚本移动到命令行可以处理超时。也可以通过`set_time_limit()来增加限制。

另一种选择是优化插入,收集一些记录并将它们组合到单个插入中。这减少了数据库服务器上的往返/工作,但消耗了更多内存。你必须找到一个平衡点。

INSERT INTO table 
   (field1, field2) 
VALUES 
   (value1_1, value1_2), 
   (value2_1, value2_2), ...

您甚至可以将SQL写入文件并使用mysql命令行工具插入记录。这非常快,但有安全隐患,因为您需要使用exec()

答案 3 :(得分:0)

请您检查以下2个步骤是否可以帮助您。

1) Increase the default PHP execution time from 30 sec to a bigger one.
   ini_set('max_execution_time', 300000);

2) If fails please try to execute your code though cron job/back end.

答案 4 :(得分:0)

之前我遇到过同样的问题。

将大型xml文件分解为较小的文件,如file1,file2,file3,而不是处理它们。

您可以使用文本编辑器来展开您的xml,它可以打开大文件。在爆炸文件时不要浪费你的时间。

编辑:我找到了大量xml文件的答案。我认为这是达到这个目的的最佳答案。 Parsing Huge XML Files in PHP