在PHP中解析巨大的XML文件
我试图将DMOZ内容/结构XML文件解析为MySQL,但所有现有的脚本都非常陈旧,无法正常工作。如何在PHP中打开一个大的(+1GB)XML文件进行解析?只有两个PHP API真正适合处理大文件。第一个是旧的api,第二个是更新的函数。这些API读取连续流,而不是将整个树加载到内存中(simplexml和DOM就是这样做的) 例如,您可能希望查看DMOZ目录的这个部分解析器:在PHP中解析巨大的XML文件,php,xml,parsing,large-files,dmoz,Php,Xml,Parsing,Large Files,Dmoz,我试图将DMOZ内容/结构XML文件解析为MySQL,但所有现有的脚本都非常陈旧,无法正常工作。如何在PHP中打开一个大的(+1GB)XML文件进行解析?只有两个PHP API真正适合处理大文件。第一个是旧的api,第二个是更新的函数。这些API读取连续流,而不是将整个树加载到内存中(simplexml和DOM就是这样做的) 例如,您可能希望查看DMOZ目录的这个部分解析器: <?php class SimpleDMOZParser { protected $_stack = a
<?php
class SimpleDMOZParser
{
protected $_stack = array();
protected $_file = "";
protected $_parser = null;
protected $_currentId = "";
protected $_current = "";
public function __construct($file)
{
$this->_file = $file;
$this->_parser = xml_parser_create("UTF-8");
xml_set_object($this->_parser, $this);
xml_set_element_handler($this->_parser, "startTag", "endTag");
}
public function startTag($parser, $name, $attribs)
{
array_push($this->_stack, $this->_current);
if ($name == "TOPIC" && count($attribs)) {
$this->_currentId = $attribs["R:ID"];
}
if ($name == "LINK" && strpos($this->_currentId, "Top/Home/Consumer_Information/Electronics/") === 0) {
echo $attribs["R:RESOURCE"] . "\n";
}
$this->_current = $name;
}
public function endTag($parser, $name)
{
$this->_current = array_pop($this->_stack);
}
public function parse()
{
$fh = fopen($this->_file, "r");
if (!$fh) {
die("Epic fail!\n");
}
while (!feof($fh)) {
$data = fread($fh, 4096);
xml_parse($this->_parser, $data, feof($fh));
}
}
}
$parser = new SimpleDMOZParser("content.rdf.u8");
$parser->parse();
我建议使用基于SAX的解析器,而不是基于DOM的解析
关于在PHP中使用SAX的信息:这不是一个很好的解决方案,但只需抛出另一个选项:
您可以将许多大型XML文件分成块,尤其是那些实际上只是类似元素列表的文件(我怀疑您正在处理的文件可能是)
e、 例如,如果您的文档看起来像:
<dmoz>
<listing>....</listing>
<listing>....</listing>
<listing>....</listing>
<listing>....</listing>
<listing>....</listing>
<listing>....</listing>
...
</dmoz>
....
....
....
....
....
....
...
您可以一次在一两个meg中读取它,人工将加载到根级别标记中的少数完整的
标记打包,然后通过simplexml/domxml加载它们(采用这种方法时,我使用了domxml)
坦白地说,如果您使用的是PHP<5.1.2,我更喜欢这种方法。在5.1.2及更高版本中,XMLReader是可用的,这可能是最好的选择,但在此之前,您只能使用上面的分块策略,或者旧的SAX/expat库。我不知道你们其他人的情况,但我讨厌编写/维护SAX/expat解析器
但是,请注意,当您的文档不包含许多相同的底层元素时(例如,它适用于任何类型的文件列表或URL等,但对解析大型HTML文档没有意义),这种方法实际上并不实用。我最近不得不解析一些相当大的XML文档,需要一种一次读取一个元素的方法
如果您有以下文件complex test.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<Complex>
<Object>
<Title>Title 1</Title>
<Name>It's name goes here</Name>
<ObjectData>
<Info1></Info1>
<Info2></Info2>
<Info3></Info3>
<Info4></Info4>
</ObjectData>
<Date></Date>
</Object>
<Object></Object>
<Object>
<AnotherObject></AnotherObject>
<Data></Data>
</Object>
<Object></Object>
<Object></Object>
</Complex>
这是一个非常类似的问题,但有一个非常好的具体答案,解决了DMOZ目录解析的具体问题。
然而,由于这是谷歌对大型XML的一次成功尝试,我也将转述另一个问题的答案:
我的看法是:
一个简单的类,它将在流式传输文件时将所有子元素提取到XML根元素。
在pubmed.com的108 MB XML文件上测试
class SimpleXmlStreamer extends XmlStreamer {
public function processNode($xmlString, $elementName, $nodeIndex) {
$xml = simplexml_load_string($xmlString);
// Do something with your SimpleXML object
return true;
}
}
$streamer = new SimpleXmlStreamer("myLargeXmlFile.xml");
$streamer->parse();
这是一篇老文章,但首先出现在谷歌搜索结果中,因此我认为我发布了另一个基于这篇文章的解决方案:
此解决方案同时使用XMLReader
和SimpleXMLElement
:
$xmlFile = 'the_LARGE_xml_file_to_load.xml'
$primEL = 'the_name_of_your_element';
$xml = new XMLReader();
$xml->open($xmlFile);
// finding first primary element to work with
while($xml->read() && $xml->name != $primEL){;}
// looping through elements
while($xml->name == $primEL) {
// loading element data into simpleXML object
$element = new SimpleXMLElement($xml->readOuterXML());
// DO STUFF
// moving pointer
$xml->next($primEL);
// clearing current element
unset($element);
} // end while
$xml->close();
为此,您可以将XMLReader与DOM相结合。在PHP中,两个API(和SimpleXML)都基于同一个库——libxml2。大型XML通常是记录列表。因此,您可以使用XMLReader来迭代记录,将单个记录加载到DOM中,并使用DOM方法和Xpath来提取值。关键是方法XMLReader::expand()
。它将XMLReader实例中的当前节点及其子体作为DOM节点加载
XML示例:
<books>
<book>
<title isbn="978-0596100087">XSLT 1.0 Pocket Reference</title>
</book>
<book>
<title isbn="978-0596100506">XML Pocket Reference</title>
</book>
<!-- ... -->
</books>
请注意,扩展节点从未附加到DOM文档中。它允许GC对其进行清理
这种方法也适用于XML名称空间
$namespaceURI = 'urn:example-books';
$reader = new XMLReader();
$reader->open('books.xml');
$document = new DOMDocument();
$xpath = new DOMXpath($document);
// register a prefix for the Xpath expressions
$xpath->registerNamespace('b', $namespaceURI);
// compare local node name and namespace URI
while (
$reader->read() &&
(
$reader->localName !== 'book' ||
$reader->namespaceURI !== $namespaceURI
)
) {
continue;
}
// iterate the book elements
while ($reader->localName === 'book') {
// validate that they are in the namespace
if ($reader->namespaceURI === $namespaceURI) {
$book = $reader->expand($document);
var_dump(
$xpath->evaluate('string(b:title/@isbn)', $book),
$xpath->evaluate('string(b:title)', $book)
);
}
$reader->next('book');
}
$reader->close();
我已经为XMLReader编写了一个包装器(IMHO),以便更轻松地获取后面的内容。包装器允许您将数据元素的一组路径与找到该路径时要运行的回调关联起来。该路径允许正则表达式和捕获也可以传递到回调的组
该库位于,也可以使用composer require nigel3/xml reader reg
安装
如何使用它的示例
$inputFile = __DIR__ ."/../tests/data/simpleTest1.xml";
$reader = new XMLReaderReg\XMLReaderReg();
$reader->open($inputFile);
$reader->process([
'(.*/person(?:\[\d*\])?)' => function (SimpleXMLElement $data, $path): void {
echo "1) Value for ".$path[1]." is ".PHP_EOL.
$data->asXML().PHP_EOL;
},
'(.*/person3(\[\d*\])?)' => function (DOMElement $data, $path): void {
echo "2) Value for ".$path[1]." is ".PHP_EOL.
$data->ownerDocument->saveXML($data).PHP_EOL;
},
'/root/person2/firstname' => function (string $data): void {
echo "3) Value for /root/person2/firstname is ". $data.PHP_EOL;
}
]);
$reader->close();
从示例中可以看出,您可以获取要作为SimpleXMLElement、DomeElement或最后一个字符串传递的数据。这将仅表示与路径匹配的数据
路径还显示如何使用捕获组-(.*/person(?:\[\d*\])?)
查找任何person元素(包括元素数组),回调中的$path[1]
显示找到此特定实例的路径
库中有一个扩展示例以及单元测试。我使用2 GB xml测试了以下代码:
<?php
set_time_limit(0);
$reader = new XMLReader();
if (!$reader->open("data.xml"))
{
die("Failed to open 'data.xml'");
}
while($reader->read())
{
$node = $reader->expand();
// process $node...
}
$reader->close();
?>
我的解决方案:
$reader = new XMLReader();
$reader->open($fileTMP);
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'xmltag' && $reader->isEmptyElement === false) {
$item = simplexml_load_string($reader->readOuterXML(), null, LIBXML_NOCDATA);
//operations on file
}
}
$reader->close();
这是一种非常高性能的方法
preg_split('/(<|>)/m', $xmlString);
preg_split('/()/m',$xmlString);
在这之后,只需要一个周期。这是一个很好的答案,但我花了很长时间才弄清楚您需要使用访问XML节点数据,使用上面的代码,您只能看到节点的名称及其属性。这太棒了!谢谢一个问题:如何使用这个来获取根节点的属性?@gyaani_guy我认为目前不可能。不幸的是,这只是将整个文件加载到内存中@NickStrupat不正确,processNode方法对每个节点执行一次。因此,在任何时间内存中只有一个节点。代码中的simplexml_load_字符串仅指一个xml节点,而不是整个xml文档。@AeonOfTime感谢您的建议,因为在更积极的开发中还有其他解决方案,而且由于链接到它的继任者所在的旧XmlStreamer上的内容非常清楚,我想我将保留这个答案。在rubyThanks中处理大型xml非常简单。这真的很有帮助。它有bug,我没有调试它,但我有各种错误。有时它输出的不是一行而是两行xml,有时它会跳过它们。@John,我发现了这个错误。当结束标记的一部分位于行的第一部分,第二部分位于下一部分时,就会发生这种情况。要解决此问题,您需要执行以下操作:在$checkClose+=strlen($close)之后代码>添加if(mb_strlen($buffer)>mb_strpon($buffer.$tmp,$close))$checkClose=mb_strlen($close)-(mb_strlen($buffer)–mb_strpon
$reader = new XMLReader();
$reader->open($fileTMP);
while ($reader->read()) {
if ($reader->nodeType === XMLReader::ELEMENT && $reader->name === 'xmltag' && $reader->isEmptyElement === false) {
$item = simplexml_load_string($reader->readOuterXML(), null, LIBXML_NOCDATA);
//operations on file
}
}
$reader->close();
preg_split('/(<|>)/m', $xmlString);