Parseur XML alternatif

ViPHP
ViPHP | 928 Messages

31 mai 2007, 15:39

Bonjour,
j'avais rencontré quelques problèmes avec l'extension XML fournie avec PHP, aussi j'ai décidé de me coder mon parseur XML dans le cadre de mon projet OS. L'intéret de cette classe sans prétention (se content vraiment se faire strictement du parsing XML), est de pouvoir faire du parsing XML dans un environement où l'extension XML ne sera pas activée, ou bien tout simplement si l'extension XML de PHP ne vous convient pas (pour ma part j'ai eu des bugs liés à l'encodage avec l'extension XML de PHP).

Ainsi donc au lieu d'avoir un bon vieux
		$this->xml_parser = xml_parser_create($encoding);
		xml_parser_set_option($this->xml_parser, XML_OPTION_SKIP_WHITE, 0);
		xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, 0);
		xml_set_character_data_handler($this->xml_parser, array(&$this, 'handle_cdata'));
		xml_set_element_handler($this->xml_parser, array(&$this, 'handle_element_start'), array(&$this, 'handle_element_end'));
		xml_parse($this->xml_parser, $this->xmldata);
avec la classe que je fourni vous devrez faire :
		$xml = new Xml_regexp_parser();
		$xml->obj =& $this;
		$xml->open_handler = 'handle_element_start';
		$xml->value_handler = 'handle_cdata';
		$xml->close_handler = 'handle_element_end';
		$result = $xml->parse($votre_chaine_XML);
votre ancien code restera ainsi totalement compatible, a un détail près : les handler appelé lors de l'ouverture / fermeture / valeur d'un tag ne prenne plus de premier argument (qui était avec l'extension XML l'objet du parseur XML).


Voici le code de la classe :
class Xml_regexp_parser
{
	// En faisant pointer cette propriété sur un objet, les handler seront appelés en tant que méthode de cet objet
	var $obj = NULL;

	// Fonction / méthode appelée lors de l'ouverture d'un tag
	var $open_handler = NULL;

	// Fonction / méthode appelée lors de la fermeture d'un tag
	var $close_handler = NULL;

	// Fonction / méthode appelée lors de la fermeture d'un tag, avec la valeur de celui ci
	var $value_handler = NULL;

	// Erreur lors du parsing
	var $errstr = NULL;

	/*
	** Parse la chaîne de caractère XML
	*/
	function parse($str)
	{
		// Parse du header XML
		if (preg_match('#^\s*<\?xml.*?\?>#si', $str, $m))
		{
			$str = preg_replace('#^\s*<\?xml.*?\?>#si', '', $str);
		}

		$stack = array();
		$in_cdata = FALSE;
		$last_offset = 0;
		$value = '';

		// Parse des différentes balises
		preg_match_all('#<(/)?\s*([a-zA-Z0-9_\-]*?)(\s+(.*?))?\s*(/?)>\s*(<\!\[CDATA\[)?#si', $str, $m, PREG_OFFSET_CAPTURE);
		$count = count($m[0]);
		for ($i = 0; $i < $count; $i++)
		{
			// Longueur de la balise, offset de début et offset de fin de la chaîne
			$length = strlen($m[0][$i][0]);
			$curent_offset = $m[0][$i][1] + $length;

			// Tag de la balise
			$tag = $m[2][$i][0];

			// Ouverture de balise ?
			if (!$m[1][$i][0] || $m[5][$i][0])
			{
				// On ne prend pas en compte les tags dans un CDATA
				if ($in_cdata)
				{
					continue ;
				}

				array_push($stack, $tag);

				// Appel du handler d'ouverture du tag
				if ($this->open_handler)
				{
					// Parse des attributs
					preg_match_all('#([a-zA-Z0-9_\-]*?)="(.*?)"#si', $m[3][$i][0], $a, PREG_SET_ORDER);
					$attrs = array();
					foreach ($a AS $attr)
					{
						$attrs[$attr[1]] = $attr[2];
					}

					if ($this->obj)
					{
						$this->obj->{$this->open_handler}($tag, $attrs);
					}
					else
					{
						call_user_func($this->open_handler, $tag, $attrs);
					}
				}

				// Début de CDATA ?
				if ($m[6][$i] && $m[6][$i][0])
				{
					$in_cdata = TRUE;
				}
			}

			// Fermeture de balise ?
			if ($m[1][$i][0] || $m[5][$i][0])
			{
				$value = substr($str, $last_offset, $m[0][$i][1] - $last_offset);

				// Fin de CDATA ?
				if (substr($value, -2) == ']>')
				{
					$value = substr($value, 0, -3);
					$in_cdata = FALSE;
				}

				// On ne prend pas en compte les tags dans un CDATA
				if ($in_cdata)
				{
					continue ;
				}

				// Vérification de la fermeture du tag
				$check = array_pop($stack);
				if ($check != $tag)
				{
					$this->errstr = 'XML error : tag <' . $check . '> is different of <' . $tag . '>';
					return (FALSE);
				}

				// Appel des handlers pour la fermeture des tags, et leur valeur
				if ($this->obj && $this->value_handler)
				{
					$this->obj->{$this->value_handler}($value);
				}
				else if ($this->value_handler)
				{
					call_user_func($this->value_handler, $value);
				}

				if ($this->obj && $this->close_handler)
				{
					$this->obj->{$this->close_handler}($tag);
				}
				else if ($this->close_handler)
				{
					call_user_func($this->close_handler, $tag);
				}
			}

			$last_offset = $curent_offset;
		}

		return (TRUE);
	}
}

J'en profite pour joindre au message le parseur XML complet (c'est à dire l'analyseur de balise posté ci dessus, avec la classe qui s'occupe de mettre en forme correctement sous forme d'arbre le XML).
<?php
/*
** +---------------------------------------------------+
** | Name :		~/main/class/class_xml.php
** | Begin :	31/05/2007
** | Last :		31/05/2007
** | User :		Genova
** | Project :	Fire-Soft-Board 2 - Copyright FSB group
** | License :	GPL v2.0
** +---------------------------------------------------+
*/

/*
** Parseur XML
*/
class Xml
{
	// Contenu du code XML à parser
	var $content;

	// Racine de l'arbre XML
	var $document;

	// Pile utilisée par le parseur
	var $stack = array();

	/*
	** Constructeur
	*/
	function Xml()
	{
		$this->document = new Xml_element();
	}

	/*
	** Charge le contenu d'un fichier XML
	** -----
	** $filename ::		Fichier XML à charger
	*/
	function load_file($filename)
	{
		if (!file_exists($filename))
		{
			die('Le fichier XML ' . $filename . ' n\'existe pas');
		}

		$this->content = file_get_contents($filename);
		$this->parse();
	}

	/*
	** Charge du code XML
	** -----
	** $content ::		Contenu XML
	*/
	function load_content($content)
	{
		$this->content = $content;
		$this->parse();
	}

	/*
	** Parse du contenu XML
	*/
	function parse()
	{
		$xml = new Xml_regexp_parser();
		$xml->obj =& $this;
		$xml->open_handler = 'open_tag';
		$xml->value_handler = 'value_tag';
		$xml->close_handler = 'close_tag';
		$result = $xml->parse($this->content);
		if (!$result)
		{
			die($xml->errstr);
		}
	}

	/*
	** Callback appelé lors de l'ouverture d'un tag
	*/
	function open_tag($tag, $attr)
	{
		// Déplacement vers la référence de l'élément
		$ref = &$this->document;
		foreach ($this->stack AS $i => $item)
		{
			if ($i > 0)
			{
				$ref = &$ref->$item;
				$ref = &$ref[count($ref) - 1];
			}
		}

		// Création du nouvel élément
		if (count($this->stack))
		{
			$new = $ref->createElement($tag);
			foreach ($attr AS $k => $v)
			{
				$new->setAttribute($k, $v);
			}
			$new->__data['depth'] = count($this->stack);
			$ref->appendChild($new);
		}
		else
		{
			$ref->setTagName($tag);
		}

		// Ajout du tag à la pile
		array_push($this->stack, $tag);
	}

	/*
	** Callback appelé lors de la fermeture d'un tag
	*/
	function close_tag($tag)
	{
		array_pop($this->stack);
	}

	/*
	** Callback appelé lors de la capture de texte entre les tags
	*/
	function value_tag($text)
	{
		$ref = &$this->document;
		foreach ($this->stack AS $i => $item)
		{
			if ($i > 0)
			{
				$ref = &$ref->$item;
				$ref = &$ref[count($ref) - 1];
			}
		}

		$ref->setData($text);
	}
}

/*
** Représente une node de l'arbre XML
*/
class Xml_element
{
	var $__data = array();

	/*
	** Constructeur
	*/
	function Xml_element()
	{
		$this->__data['name'] = 'newElement';
		$this->__data['value'] = NULL;
		$this->__data['attr'] = array();
		$this->__data['depth'] = 0;
	}

	/*
	** Créé un nouvel élément
	** -----
	** $name ::		Nom du tag du nouvel élément
	*/
	function createElement($name)
	{
		$new = new Xml_element();
		$new->setTagName($name);

		return ($new);
	}

	/*
	** Retourne la liste des attributs
	*/
	function attribute()
	{
		return ($this->__data['attr']);
	}

	/*
	** Créé ou met à jour un attribut
	** -----
	** $name ::		Nom de l'attribut
	** $value ::	Valeur de l'attribut
	*/
	function setAttribute($name, $value)
	{
		$this->__data['attr'][$name] = $value;
	}

	/*
	** Retourne TRUE si l'attribut existe, sinon FALSE
	** -----
	** $name ::		Nom de l'attribut
	*/
	function attributeExists($name)
	{
		return ((isset($this->__data['attr'][$name])) ? TRUE : FALSE);
	}

	/*
	** Retourne la valeur d'un attribut
	** -----
	** $name ::		Nom de l'attribut
	*/
	function getAttribute($name)
	{
		return (($this->attributeExists($name)) ? $this->__data['attr'][$name] : NULL);
	}

	/*
	** Supprime un attribut
	** -----
	** $name ::		Nom de l'attribut
	*/
	function deleteAttribute($name)
	{
		unset($this->__data['attr'][$name]);
	}

	/*
	** Modifie le nom du tag
	** -----
	** $name ::		Nom du tag
	*/
	function setTagName($name)
	{
		$this->__data['name'] = $name;
	}

	/*
	** Retourne le nom du tag
	*/
	function getTagName()
	{
		return ($this->__data['name']);
	}

	/*
	** Modifie la valeur du tag
	** -----
	** $value ::		Chaîne de caractère
	*/
	function setData($value)
	{
		$this->__data['value'] = $value;
	}

	/*
	** Retourne la valeur du tag
	*/
	function getData()
	{
		return ($this->__data['value']);
	}

	/*
	** Ajoute un enfant
	** -----
	** $node ::		Objet Xml_element
	*/
	function appendChild($node)
	{
		$name = $node->getTagName();
		if (!isset($this->$name))
		{
			$this->$name = array();
		}
		$ref = &$this->$name;
		$ref[] = $node;
	}

	/*
	** Retourne la liste des enfants
	*/
	function children()
	{
		$children = array();
		foreach ($this AS $property_name => $property_value)
		{
			if ($property_name != '__data')
			{
				$children[] = &$this->$property_name;
			}
		}

		return ($children);
	}

	/*
	** Retourne TRUE si l'enfant existe
	** -----
	** $name ::		Nom de l'enfant
	*/
	function childExists($name)
	{
		return ((isset($this->$name)) ? TRUE : FALSE);
	}

	/*
	** Retourne TRUE si l'élément a des enfants
	*/
	function hasChildren()
	{
		return ((count($this->children())) ? TRUE : FALSE);
	}

	/*
	** Supprime l'enfant
	** -----
	** $name ::		Nom de l'enfant
	*/
	function deleteChildren($name)
	{
		if ($this->childExists($name))
		{
			unset($this->$name);
		}
	}

	/*
	** Retourne l'arbre sous format XML
	*/
	function asXML()
	{
		$xml = str_repeat("\t", $this->__data['depth']) . '<' . $this->getTagName();
		foreach ($this->attribute() AS $key => $value)
		{
			$xml .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
		}
		$xml .= '>';

		if ($this->hasChildren())
		{
			foreach ($this->children() AS $childs)
			{
				foreach ($childs AS $child)
				{
					$xml .= "\n" . $child->asXML();
				}
			}
			$xml .= "\n" . str_repeat("\t", $this->__data['depth']) . '</' . $this->getTagName() . '>';
		}
		else
		{
			$xml .= '<![CDATA[' . $this->getData() . ']]></' . $this->getTagName() . '>';
		}

		return ($xml);
	}
}

/*
** Parseur XML fait maison, afin d'éviter certains problèmes liés à l'encodage des caractères
*/
class Xml_regexp_parser
{
	// En faisant pointer cette propriété sur un objet, les handler seront appelés en tant que méthode de cet objet
	var $obj = NULL;

	// Fonction / méthode appelée lors de l'ouverture d'un tag
	var $open_handler = NULL;

	// Fonction / méthode appelée lors de la fermeture d'un tag
	var $close_handler = NULL;

	// Fonction / méthode appelée lors de la fermeture d'un tag, avec la valeur de celui ci
	var $value_handler = NULL;

	// Erreur lors du parsing
	var $errstr = NULL;

	/*
	** Parse la chaîne de caractère XML
	*/
	function parse($str)
	{
		// Parse du header XML
		if (preg_match('#^\s*<\?xml.*?\?>#si', $str, $m))
		{
			$str = preg_replace('#^\s*<\?xml.*?\?>#si', '', $str);
		}

		$stack = array();
		$in_cdata = FALSE;
		$last_offset = 0;
		$value = '';

		// Parse des différentes balises
		preg_match_all('#<(/)?\s*([a-zA-Z0-9_\-]*?)(\s+(.*?))?\s*(/?)>\s*(<\!\[CDATA\[)?#si', $str, $m, PREG_OFFSET_CAPTURE);
		$count = count($m[0]);
		for ($i = 0; $i < $count; $i++)
		{
			// Longueur de la balise, offset de début et offset de fin de la chaîne
			$length = strlen($m[0][$i][0]);
			$curent_offset = $m[0][$i][1] + $length;

			// Tag de la balise
			$tag = $m[2][$i][0];

			// Ouverture de balise ?
			if (!$m[1][$i][0] || $m[5][$i][0])
			{
				// On ne prend pas en compte les tags dans un CDATA
				if ($in_cdata)
				{
					continue ;
				}

				array_push($stack, $tag);

				// Appel du handler d'ouverture du tag
				if ($this->open_handler)
				{
					// Parse des attributs
					preg_match_all('#([a-zA-Z0-9_\-]*?)="(.*?)"#si', $m[3][$i][0], $a, PREG_SET_ORDER);
					$attrs = array();
					foreach ($a AS $attr)
					{
						$attrs[$attr[1]] = $attr[2];
					}

					if ($this->obj)
					{
						$this->obj->{$this->open_handler}($tag, $attrs);
					}
					else
					{
						call_user_func($this->open_handler, $tag, $attrs);
					}
				}

				// Début de CDATA ?
				if ($m[6][$i] && $m[6][$i][0])
				{
					$in_cdata = TRUE;
				}
			}

			// Fermeture de balise ?
			if ($m[1][$i][0] || $m[5][$i][0])
			{
				$value = substr($str, $last_offset, $m[0][$i][1] - $last_offset);

				// Fin de CDATA ?
				if (substr($value, -2) == ']>')
				{
					$value = substr($value, 0, -3);
					$in_cdata = FALSE;
				}

				// On ne prend pas en compte les tags dans un CDATA
				if ($in_cdata)
				{
					continue ;
				}

				// Vérification de la fermeture du tag
				$check = array_pop($stack);
				if ($check != $tag)
				{
					$this->errstr = 'XML error : tag <' . $check . '> is different of <' . $tag . '>';
					return (FALSE);
				}

				// Appel des handlers pour la fermeture des tags, et leur valeur
				if ($this->obj && $this->value_handler)
				{
					$this->obj->{$this->value_handler}($value);
				}
				else if ($this->value_handler)
				{
					call_user_func($this->value_handler, $value);
				}

				if ($this->obj && $this->close_handler)
				{
					$this->obj->{$this->close_handler}($tag);
				}
				else if ($this->close_handler)
				{
					call_user_func($this->close_handler, $tag);
				}
			}

			$last_offset = $curent_offset;
		}

		return (TRUE);
	}
}
?>
Avec un exemple d'utilisation :
<?php
include('class_xml.php');

$string = '<bibliotheque>
	<roman type="policier">
		<nom><![CDATA[Les 10 petits nègres]]></nom>
		<description><![CDATA[bla bla bla bla]]></description>
		<magasin name="paris ouest" />
		<magasin name="paris sud" />
	</roman>
	<roman type="amour">
		<nom><![CDATA[Titanic]]></nom>
		<description><![CDATA[bla bla bla bla]]></description>
		<magasin name="saint ouen" />
		<magasin name="clichy" />
		<magasin name="new york" />
	</roman>
</bibliotheque>';


$xml = new Xml;
$xml->load_content($string);

foreach ($xml->document->roman AS $roman)
{
	echo '<ul>';
	echo '<li>Nom du roman : ' . $roman->nom[0]->getData() . '</li>';
	echo '<li>Description du roman : ' . $roman->description[0]->getData() . '</li>';
	echo '<li>Liste des magasins :<ul>';
	foreach ($roman->magasin AS $magasin)
	{
		echo '<li>' . $magasin->getAttribute('name') . '</li>';
	}
	echo '</li></ul>';
	echo '</ul>';
}
?>
J'ai essayé de la faire la plus intuitive possible. Il y a pas mal de méthodes assez utiles tel que children() qui retourne la liste des enfants, createElement() qui créé un nouvel élément, appendChild() qui permet d'ajouter l'élément créé dans l'arbre XML, etc ..

Si ça peut servir, tant mieux.

Eléphant du PHP | 199 Messages

31 mai 2007, 18:41

J'avais cru comprendre que SimpleXML fonctionnait bien (pas eu l'occasion de tester). Mais bravo pour ton travail :)
Klomac - Blog Lambda

ViPHP
ViPHP | 4674 Messages

31 mai 2007, 19:55

Les fonctions XML de PHP ne sont pas présentes (je fais allusion à xml_parser_create, xml_parser_set_option, xml_set_object, xml_set_element_handler et xml_set_character_data_handler -- pour celles de base --). Pourquoi ?

Ce serait beaucoup plus rapide qu'un parse. Nan ?
« Un handicap est le résultat d'une rencontre entre une déficience ou différence et une incapacité de la société à répondre à celle-ci. »

Hoa : http://hoa-project.net (sur @hoaproject).

ViPHP
ViPHP | 928 Messages

31 mai 2007, 21:22

J'avais cru comprendre que SimpleXML fonctionnait bien (pas eu l'occasion de tester).
Bonsoir,
SimpleXML n'existe qu'à partir de PHP5. La classe ci dessus fonctionne pour PHP4 et PHP5.

@HyWaN : c'est justement le but de la classe, pouvoir parser du XML même lorsque les fonctions xml_parser_* de PHP sont indisponibles. Donc c'est fait pour être portable en fait. A vrai dire, la principale motivation qui m'a fait coder cette classe, c'est que j'avais des problèmes avec l'UTF-8 avec les xml_parser* de PHP. Dès qu'un caractère UTF8 avait un problème (ce qui peut arriver de temps en temps - la faute au client où à la base de donnée), paf le parseur refusait de continuer. L'avantage de ma classe est que le parseur est bien plus permissif que celui de PHP. Donc en cas d'erreurs de caractère, il n'en tiendra pas compte.

ViPHP
ViPHP | 4674 Messages

31 mai 2007, 22:28

Bonsoir,

les fonctions XML de PHP sont toujours activées :
Ces fonctions sont activées par défaut, et utilisent la bibliothèque expat fournie avec la distribution.
source : http://fr.php.net/xml.

Et j'ai fais un paquetage XML qui gère tous les encodages, à l'aide de la fonction xml_parser_set_option.
xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $encoding);
Voir la documentation pour plus d'informations : http://fr.php.net/xml_parser_set_option.

Mais bien joué pour le code ;-)

Bonne soirée.
« Un handicap est le résultat d'une rencontre entre une déficience ou différence et une incapacité de la société à répondre à celle-ci. »

Hoa : http://hoa-project.net (sur @hoaproject).