Análisis de enormes archivos XML en PHP

10 minutos de lectura

Analisis de enormes archivos XML en PHP
ian

Estoy tratando de analizar los archivos XML de contenido/estructuras DMOZ en MySQL, pero todos los scripts existentes para hacer esto son muy antiguos y no funcionan bien. ¿Cómo puedo abrir un archivo XML grande (+1 GB) en PHP para analizarlo?

1646958908 432 Analisis de enormes archivos XML en PHP
emilio h

Solo hay dos API de php que son realmente adecuadas para procesar archivos grandes. El primero es el viejo expatriado api, y el segundo es el más nuevo Lector XML funciones Estas API leen flujos continuos en lugar de cargar todo el árbol en la memoria (que es lo que hacen simplexml y DOM).

Por ejemplo, es posible que desee ver este analizador parcial del catálogo 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();

  • Esta es una gran respuesta, pero me tomó mucho tiempo darme cuenta de que necesita usar xml_set_default_handler() para acceder a los datos del nodo XML, con el código anterior solo puede ver el nombre de los nodos y sus atributos.

    – DirtyBirdNJ

    18 de enero de 2012 a las 17:53

1646958909 958 Analisis de enormes archivos XML en PHP
Oskarth

Esta es una pregunta muy similar a la Mejor manera de procesar XML grande en PHP, pero con una muy buena respuesta específica votada a favor que aborda el problema específico del análisis del catálogo DMOZ. Sin embargo, dado que este es un buen éxito de Google para XML grandes en general, también volveré a publicar mi respuesta de la otra pregunta:

Mi opinión al respecto:

https://github.com/prewk/XmlStreamer

Una clase simple que extraerá todos los elementos secundarios al elemento raíz XML mientras transmite el archivo. Probado en un archivo XML de 108 MB de pubmed.com.

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

  • ¡Esto es genial! Gracias. una pregunta: ¿cómo se obtiene el atributo del nodo raíz usando esto?

    – gyaani_guy

    15/10/2013 a las 10:35

  • @gyaani_guy Desafortunadamente, no creo que sea posible actualmente.

    – Oskarth

    22 de diciembre de 2013 a las 21:53

  • ¡Esto simplemente carga todo el archivo en la memoria!

    – Nick Strupat

    7 de marzo de 2014 a las 16:14

  • @NickStrupat Incorrecto, el método processNode se ejecuta una vez por nodo. Por lo tanto, solo hay un nodo en la memoria en un momento dado. simplexml_load_string en el código solo se refiere a un nodo xml, no a todo el documento xml.

    – Oskarth

    31 de marzo de 2014 a las 16:03


  • @AeonOfTime Gracias por la sugerencia, ya que hay otras soluciones en desarrollo más activo Y porque está muy claro en el enlace al antiguo XmlStreamer donde vive su sucesor, creo que dejaré esta respuesta como está.

    – Oskarth

    16 de abril de 2021 a las 14:43

Recientemente tuve que analizar algunos documentos XML bastante grandes y necesitaba un método para leer un elemento a la vez.

Si tienes el siguiente archivo 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>

Y quería devolver el <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) != "https://stackoverflow.com/") {
      $this->options['path'] .= "https://stackoverflow.com/";
    }

    // 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;
  }
}

  • Gracias. Esto fue realmente útil.

    – Ajinkya Kulkarni

    11 de noviembre de 2014 a las 16:58

  • Tiene errores, no lo depuré, pero he tenido varios errores. A veces genera no una sino dos filas xml A veces las omite.

    – Juan

    29 de junio de 2018 a las 17:57

  • @John, capté este error. Ocurre cuando parte de la etiqueta final está en la primera parte de la línea y la segunda en la siguiente. Para resolverlo, debe hacer lo siguiente: después de $checkClose += strlen($close); agregar if (mb_strlen($buffer) > mb_strpos($buffer . $tmp, $close)) $checkClose = mb_strlen($close) - (mb_strlen($buffer) - mb_strpos($buffer . $tmp, $close));

    – Trabaja

    29 de marzo de 2021 a las 5:34


Sugeriría usar un analizador basado en SAX en lugar de un análisis basado en DOM.

Información sobre el uso de SAX en PHP: http://www.brainbell.com/tutorials/php/Parsing_XML_With_SAX.htm

Esta no es una gran solución, pero solo para lanzar otra opción:

Puede dividir muchos archivos XML grandes en fragmentos, especialmente aquellos que en realidad son solo listas de elementos similares (como sospecho que sería el archivo con el que está trabajando).

por ejemplo, si su documento se parece a:

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

Puede leerlo en un mega o dos a la vez, envolver artificialmente los pocos completos <listing> etiquetas que cargó en una etiqueta de nivel raíz, y luego cárguelas a través de simplexml/domxml (utilicé domxml, al adoptar este enfoque).

Francamente, prefiero este enfoque si usa PHP <5.1.2. Con 5.1.2 y versiones posteriores, XMLReader está disponible, que probablemente sea la mejor opción, pero antes de eso, estarás atrapado con la estrategia de fragmentación anterior o con la antigua SAX/lib para expatriados. Y no sé sobre el resto de ustedes, pero ODIO escribir/mantener analizadores SAX/expatriados.

Tenga en cuenta, sin embargo, que este enfoque NO es realmente práctico cuando su documento no constan de muchos elementos de nivel inferior idénticos (p. ej., funciona muy bien para cualquier tipo de lista de archivos, direcciones URL, etc., pero no tendría sentido para analizar un documento HTML grande)

Analisis de enormes archivos XML en PHP
Székelygobe

Esta es una publicación anterior, pero primero en el resultado de búsqueda de Google, así que pensé en publicar otra solución basada en esta publicación:

http://drib.tech/programming/parse-large-xml-files-php

Esta solución utiliza ambos XMLReader y 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();

1646958910 660 Analisis de enormes archivos XML en PHP
ThW

Puede combinar XMLReader con DOM para esto. En PHP, ambas API (y SimpleXML) se basan en la misma biblioteca: libxml2. Los XML grandes suelen ser una lista de registros. Entonces, usa XMLReader para iterar los registros, cargar un solo registro en DOM y usar métodos DOM y Xpath para extraer valores. La clave es el método. XMLReader::expand(). Carga el nodo actual en una instancia de XMLReader y sus descendientes como nodos DOM.

XML de ejemplo:

<books>
  <book>
    <title isbn="978-0596100087">XSLT 1.0 Pocket Reference</title>
  </book>
  <book>
    <title isbn="978-0596100506">XML Pocket Reference</title>
  </book>
  <!-- ... -->
</books>

Código de ejemplo:

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

Tenga en cuenta que el nodo expandido nunca se agrega al documento DOM. Permite que el GC lo limpie.

Este enfoque también funciona con espacios de nombres 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();

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad