Robert.BrainUsers.net

PHP: parsowanie wielu dokumentów XML ze strumienia

Problem: otwieram połączenie socket (TCP) jako klient i co minutę otrzymuję ze strumienia dokument XML, bez zamykania tego połączenia. Dokumenty te nie są rozdzielone żadnym separatorem.

Jeśli serwer wysyła dokumenty XML wraz z deklaracją, czyli ze strumienia otrzymamy ciąg znaków <?xml, to co prawda można wykryć początek dokumentu XML, ale nie da się w prosty sposób stwierdzić końca dokumentu, zanim nie otrzymamy następnego (co potrwa minutę).

Propozycja rozwiązania: interfejs SAX (Simple API for XML), parser strumieniowy sterowany zdarzeniami. Za zwyczaj używa się go do parsowania bardzo dużych dokumentów XML, których parsowanie metodą DOM byłoby zbyt zasobożerne (dokument musiałby być wczytany w całości do pamięci operacyjnej). PHP udostępnia kilka funkcji wspomagających parsowanie strumieniowe XML:

Mój pomysł na rozdzielenie dokumentów wygląda następująco. Licznik ustawić na zero i zacząć parsowanie otrzymanych liter ze strumienia (ważne aby jeden krok pętli dotyczył pojedynczej litery, warto korzystać z fgetc). Podczas otwierania tagu inkrementować licznik, a podczas zamykania dekrementować. Jeśli podczas zamykania tagu pomniejszony licznik ma wartość 0, to znaczy że dokument XML właśnie się skończył - zamykany element jest korzeniem a korzeń jest tylko jeden. Krótkie tagi <item/> też wywołają callback zamknięcia tagu, więc licznik będzie się zerował wtedy i tylko wtedy, gdy napotkamy koniec dokumentu XML.

Poniżej przykłady, które uruchamiam na linuksie w konsoli poleceniem php.

Przykładowy kod klienta

<?php

interface StreamParser {
   function reset();
   function parse($data);
}


class XMLStreamParser implements StreamParser {

   public $documents = array();
   public $currentDocument = '';
   public $documentDepth = 0;
   public $documentEnd = false;
   public $xmlParser = null;
   
   
   function __construct() {
      $this->reset();
   }
   
   function reset() {
      if (!empty($this->xmlParser)) xml_parser_free($this->xmlParser);
      $this->xmlParser = xml_parser_create();
      xml_set_element_handler($this->xmlParser,
         array($this, 'start_element'), array($this, 'end_element'));
      xml_set_character_data_handler($this->xmlParser,
         array($this, 'character_data'));
      xml_parser_set_option($this->xmlParser,
         XML_OPTION_CASE_FOLDING, 0);
      $this->documentDepth = 0;
      $this->documentEnd = false;
      $this->currentDocument = '';
   }
   
   
   function parse($data) {
      xml_parse($this->xmlParser, $data);
      if ($this->documentEnd) {
         $this->saveResponse();
         $this->reset();
      }
   }
   
   
   function saveResponse() {
      $this->documents[] = $this->currentDocument;
      echo $this->currentDocument;
      echo PHP_EOL . '-------------------------------' . PHP_EOL;
   }
   
   
   
   protected function start_element($parser, $name, $attrs) {
      $this->currentDocument .= ' '. $name;
      foreach ($attrs as $attr => $val) {
         $this->currentDocument .= ' '. $attr .'="'. $val .'"';
      }
      $this->currentDocument .= '>';
      $this->documentDepth++;
   }
   
   protected function end_element($parser, $name) {
      $this->documentDepth--;
      $this->currentDocument .= '</'. $name .'>';
      if ($this->documentDepth == 0) $this->documentEnd = true;
   }
   
   
   protected function character_data($parser, $data) {
      $this->currentDocument .= $data;
   }
   
   
}



interface StreamSocketClient {
   function __construct($uri, StreamParser $parser);
   function run();
}


class XMLStreamSocketClient implements StreamSocketClient {
   
   public $uri;
   public $socket;
   public $parser;
   
   function __construct($uri, StreamParser $parser) {
      $this->uri = $uri;
      $this->parser = $parser;
   }
   
   function run() {
      if ($this->connect()) {
         while (!feof($this->socket)) {
            $this->tick();
         }
         $this->parser->reset();
         fclose($this->socket);
      }
   }
   
   
   protected function connect() {
      $this->socket = stream_socket_client($this->uri,
         $errno, $errstr, 30);
      return ($this->socket !== false && is_resource($this->socket));
   }
   
   
   protected function tick() {
      $data = fgetc($this->socket);
      $this->parser->parse($data);
   }
   
}


$uri = 'tcp://127.0.0.1:8000';
$client = new XMLStreamSocketClient($uri, new XMLStreamParser);
$client->run();

Przykładowy kod serwera

Prymitywny serwer do naszych testów, obsługuje tylko jedno połączenie na raz.

<?php

function generateDocument($nr) {
   echo 'Send '. $nr . PHP_EOL;
   $doc = '<root><head>'. $nr .'</head><body>';
   for ($i=1; $i<5; $i++) {
      $doc .= '<itemElement name="'. $i .'" />' . PHP_EOL;
   }
   $doc .= '</body></root>' . PHP_EOL;
   return $doc;
}

$socket = stream_socket_server("tcp://127.0.0.1:8000", $errno, $errstr);
stream_set_timeout($socket, 3600);
if (!$socket) {
   echo "$errstr ($errno)" . PHP_EOL;
} else {
   $i=0;
   while ($conn = stream_socket_accept($socket)) {
      while ($conn) {
         $i++;
         $doc = generateDocument($i);
         $send = fwrite($conn, $doc);
         if (!$send) break;
         sleep(2);
      }
   }
   fclose($conn);
   fclose($socket);
}

Komentarze

Na razie brak komentarzy, Twój będzie pierwszy.

Dodaj komentarz