2009-05-26 18 views
48

Sto cercando di analizzare i file XML di contenuti/strutture DMOZ in MySQL, ma tutti gli script esistenti per farlo sono molto vecchi e non funzionano bene. Come posso aprire un file XML (+ 1GB) di grandi dimensioni in PHP per l'analisi?Analisi di file XML enormi in PHP

+0

http://amolnpujari.wordpress.com/2012/03/31/reading_huge_xml-rb/ il suo modo semplice di trattare con grande XML in rubino –

risposta

74

Esistono solo due API php che sono particolarmente adatte per l'elaborazione di file di grandi dimensioni. La prima è la vecchia API expat e la seconda è la più recente XMLreader funzioni. Questi apis leggono flussi continui piuttosto che caricare l'intero albero nella memoria (che è ciò che fa simplexml e DOM).

Per un esempio, si potrebbe desiderare di guardare a questo parser parziale del DMOZ-Catalogo:

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

Sicuramente la migliore risposta – Evert

+9

Questa è una grande risposta, ma mi ci è voluto molto tempo per capire che è necessario utilizzare [xml_set_default_handler()] (http://php.net/manual/en/function.xml-set-default-handler.php) per accedere ai dati del nodo XML con il codice sopra puoi vedere solo il nome dei nodi e i loro attributi. – DirtyBirdNJ

4

Questa non è una grande soluzione, ma solo per gettare un'altra opzione là fuori:

È possibile rompere molti file XML di grandi dimensioni in blocchi, in particolare quelli in realtà sono solo elenchi di elementi simili (come sospetto sarebbe il file con cui stai lavorando).

ad esempio, se il documento si presenta come:

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

Lo si può leggere in un meg o due alla volta, artificialmente avvolgere i pochi complete <listing> tag caricati in un tag livello principale, e quindi del carico loro tramite simplexml/domxml (ho usato domxml, quando ho adottato questo approccio).

Francamente, preferisco questo approccio se si utilizza PHP < 5.1.2. Con 5.1.2 e versioni successive, XMLReader è disponibile, che è probabilmente l'opzione migliore, ma prima di questo, sei bloccato con la strategia chunking sopra, o con la vecchia SAX/expat lib. E non so per il resto di voi, ma odio scrivere/mantenere parser SAX/expat.

Si noti, tuttavia, che questo approccio NON è davvero pratico quando il documento non è costituito da molti elementi di livello inferiore identici (ad esempio, funziona perfettamente per qualsiasi tipo di elenco di file o URL, ecc. , ma non avrebbe senso per l'analisi di un documento HTML di grandi dimensioni)

9

Recentemente ho dovuto analizzare alcuni documenti XML piuttosto grandi e avevo bisogno di un metodo per leggere un elemento alla volta.

Se avete il seguente file 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> 

e voleva restituire il <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; 
    } 
} 
+0

Grazie. Questo è stato davvero utile. –

12

Questa è una domanda molto simile a Best way to process large XML in PHP ma con una ottima risposta specifica in aumento per affrontare il problema specifico dell'analisi del catalogo DMOZ. Tuttavia, dal momento che questo è un buon Google ha colpito per le grandi XMLs in generale, io pubblicare la mia risposta dall'altro domanda così:

mio prendere su di esso:

https://github.com/prewk/XmlStreamer

Una classe semplice che estrae tutti i bambini all'elemento radice XML durante lo streaming del file. Testato su 108 MB file XML da 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(); 
+0

Questo è fantastico! Grazie. una domanda: come si ottiene l'attributo del nodo radice usando questo? –

+0

@gyaani_guy Non penso che sia attualmente possibile purtroppo. – oskarth

+4

Questo carica solo l'intero file in memoria! –