使用 SimpleXMLElement 的大型 PHP for 循环非常慢:内存问题?
Large PHP for loop with SimpleXMLElement very slow: memory issues?
我目前有一些 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 类似的行定义,但为了便于阅读而被删除。
有什么想法可以 do/read 完成吗?只要最终完成,花费的时间对我来说并不重要。
请检查以下 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.
我之前遇到过同样的问题。
将大 xml 文件分解为较小的文件,如 file1、file2、file3,而不是处理它们。
您可以使用可以打开大文件的文本编辑器来扩展您的 xml。不要在分解文件时浪费时间在 php 上。
编辑: 我为巨大的 xml 文件找到了答案。我认为这是为此目的的最佳答案。 Parsing Huge XML Files in PHP
您可以尝试增加内存限制。如果这不是一个选项,而您只需要完成一次,我个人会把它分块并一次处理 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;
}
?>
只要这不是您的用户负载(例如您网站的标准访问者),就不会有问题。
另一种选择,您是否正确尝试过preparing/binding您的查询?
更新:永远不要在 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(N2)。没有解决方法,因为不能保证当您访问项目 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'];
...您访问一个越来越远的索引 到一个简单 XML 对象 中。这可能会遇到 Schlemiel the Painter.
的问题
您的问题 "how do I get the loop to complete, no matter the time" 的直接答案是 "increase memory limit and max execution time"。
要提高性能,您可以在提要对象中使用不同的界面:
$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个元素需要多少时间。为了验证它确实是时间增量,还打印了日期。增量应该大约是时间戳之间的时间差。
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);
}
}
现在时间似乎是恒定的...我说 "seem" 因为它们似乎减少了大约一万倍,而且我很难获得可靠的测量值。
(不,我不知道。我可能从未使用过大型 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.)
这里有两个问题需要解决:
内存
目前您正在使用 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() 来增加限制。
另一种选择是优化插入,收集一些记录并将它们组合成一个插入。这减少了数据库服务器上的 roundtrips/work 但消耗了更多内存。你必须找到一个平衡点。
INSERT INTO table
(field1, field2)
VALUES
(value1_1, value1_2),
(value2_1, value2_2), ...
您甚至可以将 SQL 写入文件并使用 mysql 命令行工具插入记录。这确实很快,但有安全隐患,因为您需要使用 exec()
.
我目前有一些 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 类似的行定义,但为了便于阅读而被删除。
有什么想法可以 do/read 完成吗?只要最终完成,花费的时间对我来说并不重要。
请检查以下 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.
我之前遇到过同样的问题。
将大 xml 文件分解为较小的文件,如 file1、file2、file3,而不是处理它们。
您可以使用可以打开大文件的文本编辑器来扩展您的 xml。不要在分解文件时浪费时间在 php 上。
编辑: 我为巨大的 xml 文件找到了答案。我认为这是为此目的的最佳答案。 Parsing Huge XML Files in PHP
您可以尝试增加内存限制。如果这不是一个选项,而您只需要完成一次,我个人会把它分块并一次处理 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;
}
?>
只要这不是您的用户负载(例如您网站的标准访问者),就不会有问题。
另一种选择,您是否正确尝试过preparing/binding您的查询?
更新:永远不要在 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(N2)。没有解决方法,因为不能保证当您访问项目 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'];
...您访问一个越来越远的索引 到一个简单 XML 对象 中。这可能会遇到 Schlemiel the Painter.
的问题您的问题 "how do I get the loop to complete, no matter the time" 的直接答案是 "increase memory limit and max execution time"。
要提高性能,您可以在提要对象中使用不同的界面:
$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个元素需要多少时间。为了验证它确实是时间增量,还打印了日期。增量应该大约是时间戳之间的时间差。
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);
}
}
现在时间似乎是恒定的...我说 "seem" 因为它们似乎减少了大约一万倍,而且我很难获得可靠的测量值。
(不,我不知道。我可能从未使用过大型 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.)
这里有两个问题需要解决:
内存
目前您正在使用 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() 来增加限制。
另一种选择是优化插入,收集一些记录并将它们组合成一个插入。这减少了数据库服务器上的 roundtrips/work 但消耗了更多内存。你必须找到一个平衡点。
INSERT INTO table
(field1, field2)
VALUES
(value1_1, value1_2),
(value2_1, value2_2), ...
您甚至可以将 SQL 写入文件并使用 mysql 命令行工具插入记录。这确实很快,但有安全隐患,因为您需要使用 exec()
.