programing

PHP의 거대 XML 파일 구문 분석

muds 2023. 11. 2. 22:07
반응형

PHP의 거대 XML 파일 구문 분석

DMOZ content/structures XML 파일을 MySQL로 구문 분석하려고 하는데, 이를 위한 기존 스크립트가 모두 오래되어 제대로 작동하지 않습니다.파싱을 위해 PHP에서 대용량(+1GB) XML 파일을 열려면 어떻게 해야 합니까?

대용량 파일 처리에 정말 적합한 php API는 2개뿐입니다.첫 번째는 오래된 expat api이고, 두 번째는 새로운 XML 리더 기능입니다.이러한 api는 전체 트리를 메모리에 로드하는 것이 아니라 연속적인 스트림을 읽습니다(simplexml과 DOM이 하는 일입니다).

예를 들어, DMOZ-카탈로그의 다음 부분 파서를 살펴볼 수 있습니다.

<?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();

이것은 PHP에서 XML을 처리하는 최선의 방법과 매우 유사한 질문이지만 DMOZ 카탈로그 파싱의 특정 문제를 해결하기 위해 매우 좋은 구체적인 답변을 제시했습니다.그러나 이것은 일반적으로 대규모 XML에 대한 Google의 좋은 히트작이므로, 다른 질문에 대한 답변도 다시 게시하겠습니다.

제 생각은 이렇습니다.

https://github.com/prewk/XmlStreamer

파일을 스트리밍하는 동안 XML 루트 요소에 모든 자식을 추출하는 단순 클래스입니다.pubmed.com 에서 108MB 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();

최근에 꽤 큰 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>

그리고 돌려주고 싶었습니다.<Object/>s

PHP:

require_once('class.chunk.php');

$file = new Chunk('complex-test.xml', array('element' => 'Object'));

while ($xml = $file->read()) {
  $obj = simplexml_load_string($xml);
  // do some parsing, insert to DB whatever
}

###########
Class File
###########

<?php
/**
 * Chunk
 * 
 * Reads a large file in as chunks for easier parsing.
 * 
 * The chunks returned are whole <$this->options['element']/>s found within file.
 * 
 * Each call to read() returns the whole element including start and end tags.
 * 
 * Tested with a 1.8MB file, extracted 500 elements in 0.11s
 * (with no work done, just extracting the elements)
 * 
 * Usage:
 * <code>
 *   // initialize the object
 *   $file = new Chunk('chunk-test.xml', array('element' => 'Chunk'));
 *   
 *   // loop through the file until all lines are read
 *   while ($xml = $file->read()) {
 *     // do whatever you want with the string
 *     $o = simplexml_load_string($xml);
 *   }
 * </code>
 * 
 * @package default
 * @author Dom Hastings
 */
class Chunk {
  /**
   * options
   *
   * @var array Contains all major options
   * @access public
   */
  public $options = array(
    'path' => './',       // string The path to check for $file in
    'element' => '',      // string The XML element to return
    'chunkSize' => 512    // integer The amount of bytes to retrieve in each chunk
  );

  /**
   * file
   *
   * @var string The filename being read
   * @access public
   */
  public $file = '';
  /**
   * pointer
   *
   * @var integer The current position the file is being read from
   * @access public
   */
  public $pointer = 0;

  /**
   * handle
   *
   * @var resource The fopen() resource
   * @access private
   */
  private $handle = null;
  /**
   * reading
   *
   * @var boolean Whether the script is currently reading the file
   * @access private
   */
  private $reading = false;
  /**
   * readBuffer
   * 
   * @var string Used to make sure start tags aren't missed
   * @access private
   */
  private $readBuffer = '';

  /**
   * __construct
   * 
   * Builds the Chunk object
   *
   * @param string $file The filename to work with
   * @param array $options The options with which to parse the file
   * @author Dom Hastings
   * @access public
   */
  public function __construct($file, $options = array()) {
    // merge the options together
    $this->options = array_merge($this->options, (is_array($options) ? $options : array()));

    // check that the path ends with a /
    if (substr($this->options['path'], -1) != '/') {
      $this->options['path'] .= '/';
    }

    // normalize the filename
    $file = basename($file);

    // make sure chunkSize is an int
    $this->options['chunkSize'] = intval($this->options['chunkSize']);

    // check it's valid
    if ($this->options['chunkSize'] < 64) {
      $this->options['chunkSize'] = 512;
    }

    // set the filename
    $this->file = realpath($this->options['path'].$file);

    // check the file exists
    if (!file_exists($this->file)) {
      throw new Exception('Cannot load file: '.$this->file);
    }

    // open the file
    $this->handle = fopen($this->file, 'r');

    // check the file opened successfully
    if (!$this->handle) {
      throw new Exception('Error opening file for reading');
    }
  }

  /**
   * __destruct
   * 
   * Cleans up
   *
   * @return void
   * @author Dom Hastings
   * @access public
   */
  public function __destruct() {
    // close the file resource
    fclose($this->handle);
  }

  /**
   * read
   * 
   * Reads the first available occurence of the XML element $this->options['element']
   *
   * @return string The XML string from $this->file
   * @author Dom Hastings
   * @access public
   */
  public function read() {
    // check we have an element specified
    if (!empty($this->options['element'])) {
      // trim it
      $element = trim($this->options['element']);

    } else {
      $element = '';
    }

    // initialize the buffer
    $buffer = false;

    // if the element is empty
    if (empty($element)) {
      // let the script know we're reading
      $this->reading = true;

      // read in the whole doc, cos we don't know what's wanted
      while ($this->reading) {
        $buffer .= fread($this->handle, $this->options['chunkSize']);

        $this->reading = (!feof($this->handle));
      }

      // return it all
      return $buffer;

    // we must be looking for a specific element
    } else {
      // set up the strings to find
      $open = '<'.$element.'>';
      $close = '</'.$element.'>';

      // let the script know we're reading
      $this->reading = true;

      // reset the global buffer
      $this->readBuffer = '';

      // this is used to ensure all data is read, and to make sure we don't send the start data again by mistake
      $store = false;

      // seek to the position we need in the file
      fseek($this->handle, $this->pointer);

      // start reading
      while ($this->reading && !feof($this->handle)) {
        // store the chunk in a temporary variable
        $tmp = fread($this->handle, $this->options['chunkSize']);

        // update the global buffer
        $this->readBuffer .= $tmp;

        // check for the open string
        $checkOpen = strpos($tmp, $open);

        // if it wasn't in the new buffer
        if (!$checkOpen && !($store)) {
          // check the full buffer (in case it was only half in this buffer)
          $checkOpen = strpos($this->readBuffer, $open);

          // if it was in there
          if ($checkOpen) {
            // set it to the remainder
            $checkOpen = $checkOpen % $this->options['chunkSize'];
          }
        }

        // check for the close string
        $checkClose = strpos($tmp, $close);

        // if it wasn't in the new buffer
        if (!$checkClose && ($store)) {
          // check the full buffer (in case it was only half in this buffer)
          $checkClose = strpos($this->readBuffer, $close);

          // if it was in there
          if ($checkClose) {
            // set it to the remainder plus the length of the close string itself
            $checkClose = ($checkClose + strlen($close)) % $this->options['chunkSize'];
          }

        // if it was
        } elseif ($checkClose) {
          // add the length of the close string itself
          $checkClose += strlen($close);
        }

        // if we've found the opening string and we're not already reading another element
        if ($checkOpen !== false && !($store)) {
          // if we're found the end element too
          if ($checkClose !== false) {
            // append the string only between the start and end element
            $buffer .= substr($tmp, $checkOpen, ($checkClose - $checkOpen));

            // update the pointer
            $this->pointer += $checkClose;

            // let the script know we're done
            $this->reading = false;

          } else {
            // append the data we know to be part of this element
            $buffer .= substr($tmp, $checkOpen);

            // update the pointer
            $this->pointer += $this->options['chunkSize'];

            // let the script know we're gonna be storing all the data until we find the close element
            $store = true;
          }

        // if we've found the closing element
        } elseif ($checkClose !== false) {
          // update the buffer with the data upto and including the close tag
          $buffer .= substr($tmp, 0, $checkClose);

          // update the pointer
          $this->pointer += $checkClose;

          // let the script know we're done
          $this->reading = false;

        // if we've found the closing element, but half in the previous chunk
        } elseif ($store) {
          // update the buffer
          $buffer .= $tmp;

          // and the pointer
          $this->pointer += $this->options['chunkSize'];
        }
      }
    }

    // return the element (or the whole file if we're not looking for elements)
    return $buffer;
  }
}

이것은 오래된 게시물이지만 구글 검색 결과에서 처음이라 이 게시물을 바탕으로 다른 해결책을 게시하는 줄 알았습니다.

http://drib.tech/프로그래밍/parse-large-xml-files-

이 솔루션은 두 가지를 모두 사용합니다.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();

저는 DOM 기반 파싱보다는 SAX 기반 파서를 사용하는 것을 제안합니다.

PHP에서 SAX 사용에 관한 정보 : http://www.brainbell.com/tutorials/php/Parsing_XML_With_SAX.htm

이것은 좋은 해결책이 아니라 단지 다른 옵션을 제안하는 것입니다.

많은 대형 XML 파일을 청크로 분할할 수 있습니다. 특히 유사한 요소의 목록일 뿐인 파일(작업 중인 파일의 경우가 그럴 것으로 생각됩니다.)

예를 들어, 귀하의 문서가 다음과 같은 경우:

<dmoz>
  <listing>....</listing>
  <listing>....</listing>
  <listing>....</listing>
  <listing>....</listing>
  <listing>....</listing>
  <listing>....</listing>
  ...
</dmoz>

한 번에 1, 2 메가 단위로 읽을 수 있고, 인위적으로 몇 개를 포장할 수 있습니다.<listing>루트 수준 태그에 로드한 다음 simplexml/domxml을 통해 로드합니다(이 접근 방식을 취할 때는 domxml을 사용했습니다).

솔직히 PHP < 5.1.2를 사용하는 경우에는 이 방법을 선호합니다.5.1.2 이상에서는 XMLReader를 사용할 수 있는데, 이것이 아마도 가장 좋은 옵션일 것입니다. 하지만 그 전에는 위의 청킹 전략이나 기존 SAX/expat lib을 사용해야 합니다.그리고 나머지 분들은 모르겠지만, 저는 Sax/expat pars를 쓰는/유지하는 것을 싫어합니다.

그러나 문서가 동일한 하위 수준 요소로 많이 구성되어 있지 않은 경우(예: 어떤 종류의 파일 목록이나 URL 등에 대해서도 잘 작동하지만 큰 HTML 문서를 구문 분석하는 것은 의미가 없습니다)에는 이 방법이 실제로 유용하지 않습니다.

이를 위해 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>

예제 코드:

// open the XML file
$reader = new XMLReader();
$reader->open('books.xml');

// prepare a DOM document
$document = new DOMDocument();
$xpath = new DOMXpath($document);

// find the first `book` element node at any depth
while ($reader->read() && $reader->localName !== 'book') {
  continue;
}

// as long as here is a node with the name "book"
while ($reader->localName === 'book') {
  // expand the node into the prepared DOM
  $book = $reader->expand($document);
  // use Xpath expressions to fetch values
  var_dump(
    $xpath->evaluate('string(title/@isbn)', $book),
    $xpath->evaluate('string(title)', $book)
  );
  // move to the next book sibling node
  $reader->next('book');
}
$reader->close();

확장된 노드는 DOM 문서에 추가되지 않습니다.가스크로마토그래피가 청소할 수 있게 해줍니다.

이 접근 방식은 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 to(IMHO)를 위해 랩퍼를 작성했습니다. 따라서 원하는 비트를 쉽게 얻을 수 있습니다.래퍼를 사용하면 데이터 요소의 경로 집합과 이 경로가 발견될 때 실행할 콜백을 연결할 수 있습니다.경로는 정규 표현식을 허용하고 콜백으로 전달할 수 있는 그룹을 캡처할 수도 있습니다.

라이브러리는 https://github.com/NigelRel3/XMLReaderReg 에 있으며 다음을 사용하여 설치할 수도 있습니다.composer require nigelrel3/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, DOMElement 또는 마지막 것이 문자열로 데이터를 전달할 수 있습니다.경로와 일치하는 데이터만 표시됩니다.

이 경로는 캡처 그룹을 사용하는 방법도 보여줍니다.(.*/person(?:\[\d*\])?)사용자 요소(요소 배열 포함)를 찾습니다.$path[1]콜백에 이 특정 인스턴스가 발견되는 경로가 표시됩니다.

도서관에는 단위 테스트뿐만 아니라 확장된 예시가 있습니다.

2GB 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);

그리고 그 후에는 한 번의 사이클만 필요합니다.

언급URL : https://stackoverflow.com/questions/911663/parsing-huge-xml-files-in-php

반응형