Parser du HTML avec PHP

Eléphant du PHP | 92 Messages

06 sept. 2010, 17:46

Bonjour,
J'ai un bout de code HTML sous forme de chaîne de cette façon là :
$content='
<div id="bloc1">
Le contenu du bloc 1
</div>
<div id="bloc2">
Le contenu du bloc 2
</div>
<ul id="liste3">
<li>item 31</li>
<li>item 32</li>
</ul>
';
Je voudrais parser ce bout de code de façon à obtenir par exemple un tableau $mon_html de cette forme :

Code : Tout sélectionner

$mon_html['bloc1']='Le contenu du bloc 1' $mon_html['bloc2']='Le contenu du bloc 2' $mon_html['liste3']='<li>item 31</li><li>item 32</li>'
J'ai vu dans la doc de PHP que la classe DomDocument permettait de charger du HTML et de le parcourir, sauf que ça ne marche pas comme je le voudrais.
$dom=new DomDocument();
$dom->loadHTML($content);
foreach($dom->childNodes as $node) echo '<pre>'.$node->nodeValue.'</pre>';
Je ne comprends pas pourquoi mon objet DomDocument contient deux nœuds en parcourant childNodes : un vide et le second qui contient tout le code HTML. Je m'attendais à avoir trois nœuds (2 div et 1 ul), le troisième ayant lui-même deux nœuds enfants (les deux li)...

Est-ce que c'est moi qui m'y prends comme un manche ? J'ai loupé une subtilité quelque part ? Vous voyez une autre solution pour parser facilement un bout de HTML ?

EDIT : apparemment, c'est moi qui me perds dans la structure du DOM. En allant plus profondément, j'obtiens mes contenus au lieu du contenu global :
foreach($dom->documentElement->childNodes as $nodes)
    foreach($nodes->childNodes as $node)
        echo '<h2>'.$node->nodeName.'('.$node->getAttribute('id').')</h2><pre>'.$node->nodeValue.'</pre>';
Juste un truc qui me perturbe : entre la plupart des éléments, j'ai un nœud vide. Apparemment, ça vient des retours à la ligne dans ma chaîne (si je les supprime avec str_replace, ça marche mieux).

Est-ce qu'il y a une possibilité pour afficher l'arbre du DOM une fois qu'il est construit ? Ce serait pratique si les objet DomDocument/DomElement/DomNode définissaient une méthode __tostring() mais ce n'est apparemment pas le cas.

EDIT : c'est pas idéal mais on peut convertir le DOM en XML pour l'afficher :
print_r(htmlentities($dom->saveXML()));
Ce qui me permet de comprendre pourquoi je dois passer plusieurs niveaux pour retrouver mes éléments : la construction du DOM rajoute automatiquement les balises de base HTML et BODY (ce qui est logique, en fait)...

ViPHP
AB
ViPHP | 5818 Messages

07 sept. 2010, 00:39

Avant d'utiliser getAttribute() tu devrais tester avec hasAttributes.

Cela te permettrais de ne pas avoir à faire les remplacements avec str_replace et puis cela t'éviterais surtout des erreurs si le code est "mal" formaté ou n'est pas formaté exactement comme tu l'attends :
<?php
header('Content-type: text/html; charset=UTF-8');

$content='
<div id = "bloc1">
Le contenu du bloc 1
</div>

<div id = "bloc2">
Le contenu du bloc 2
</div>

azerty

<ul id = "liste3">
<li>item 31</li>
<li>item 32</li>
</ul>

<p>
texte non récupéré tant qu\'il n\'y a pas d\'id de défini dans la balise p
</p>
';


$dom = new DOMDocument();

$dom->loadHTML('<?xml encoding="UTF-8">' . $content);


/*foreach($dom->childNodes as $nodes) 
{
	echo '<pre>';
	echo $nodes->nodeValue;
	echo '</pre>';
}*/

foreach($dom->documentElement->childNodes as $nodes)
{
	foreach($nodes->childNodes as $node)
	{
		$attribut_id = $node->hasAttributes() ? $node->getAttribute('id') : null;
		
		if($attribut_id)
		{
			echo '<h2>';
			echo $node->nodeName;
			echo '('.$attribut_id.')';
			echo '</h2>';
			echo '<pre>'.$node->nodeValue.'</pre>';
		}
	}
}?>
En testant de la sorte je ne récupère que les éléments qui ont un id défini, et pas besoin de faire préalablement des remplacement sur les sauts de lignes, et le "azerty" ne renvoi pas d'erreur.
J'ai choisi d'encoder ma page en utf-8 uniquement pour donner la syntaxe loadHTML en cas de besoin (hack provenant du manuel)

ViPHP
ViPHP | 5462 Messages

07 sept. 2010, 01:58

nodeValue ne récupère que le texte, il faut créer un nouveau DOMDocument temporaire pour copier les nœuds dedans et sortir le HTML (en attendant le innerHTML du HTML 5 pour la libxml2), c'est plus simpel et plus optimisé d'utilise xpath pour la suite, ca évite de faire des tour de boucle pour rien
$content = <<<HEREDOC
<div id = "bloc1">
Le contenu du bloc 1
</div>

<div id = "bloc2">
Le contenu du bloc 2
</div>

azerty

<ul id = "liste3">
<li>item 31</li>
<li>item 32</li>
</ul>

<p>
texte non récupéré tant qu'il n'y a pas d'id de défini dans la balise p
</p>
HEREDOC;

$mon_html = array();

$doc = new DOMDocument();
$doc->loadHTML($content);

$xpath = new DOMXPath($doc);
$items = $xpath->query('*/*[@id]');

foreach($items as $item)
{
    $id = $item->getAttribute('id');    
    $tmp = new DOMDocument();
    
     foreach($item->childNodes as $node)
    {        
        $tmp->appendChild($tmp->importNode($node, true));       
    }

    $mon_html[$id] = $tmp->saveHTML();
}

print_r($mon_html);

Code : Tout sélectionner

Array ( [bloc1] => Le contenu du bloc 1 [bloc2] => Le contenu du bloc 2 [liste3] => <li>item 31</li> <li>item 32</li> )
sinon faut pas mélanger non plus le XML et le HTML c'est 2 choses différentes, en XML il attend l'encoding dans l'entête <?xml, en HTML il attend la balise meta avec le content type,
ducoup le $content c'est ni du HTML ni du XML :wink:

Eléphant du PHP | 92 Messages

07 sept. 2010, 12:16

Avant d'utiliser getAttribute() tu devrais tester avec hasAttributes.
Oui, je sais, j'ai simplifié le code dans mon exemple et pour mes tests (étant donné que c'est du code que je génère moi-même et qui a − normalement − un attribut "id"). ;)
Cela te permettrais de ne pas avoir à faire les remplacements avec str_replace et puis cela t'éviterais surtout des erreurs si le code est "mal" formaté ou n'est pas formaté exactement comme tu l'attends :
En fait, je stocke du code HTML (mise en forme d'articles) dans une base de données et c'est ce code que je charge et que je donne à loadHTML pour le traiter. Au niveau mise en forme, il peut être un peu aléatoire. Je préfère supprimer les caractères spéciaux du genre sauts de ligne et tabulations, des fois que dans mon usine à gaz qui n'est pas encore très bien définie je décide de traiter des balises qui n'ont pas d'attributs.

EDIT : tiens, je viens de faire un essai sur mon bout de code actuel en gardant mon contenu d'origine (sans virer les \n) et en testant hasAttribute() sur les nœuds pour ne pas traiter ceux en texte uniquement (les sauts de ligne en question) et je me reçois une erreur :

Code : Tout sélectionner

Fatal error: Call to undefined method DOMText::hasAttribute() in /media/www/projets/bureau/test.php on line 30
Mon code étant le suivant :
$dom=new DomDocument();
$dom->loadHTML('<?xml encoding="UTF-8">'.$post->content);
foreach($dom->documentElement->firstChild->childNodes as $node){
    if($node->hasAttribute('id')){
        $attribute_id=$node->getAttribute('id');
        $fp->textarea($attribute_id,$attribute_id,getNodeInnerHTML($node));
    }
}
La ligne 30 étant le if($node->hasAttribute('id')){.
(Ne vous occupez pas du $fp->textarea, c'est une classe à moi de création de formulaires.)
nodeValue ne récupère que le texte, il faut créer un nouveau DOMDocument temporaire pour copier les nœuds dedans et sortir le HTML (en attendant le innerHTML du HTML 5 pour la libxml2), c'est plus simpel et plus optimisé d'utilise xpath pour la suite, ca évite de faire des tour de boucle pour rien
Oui, je m'en suis aperçu après, le coup du texte. J'ai trouvé une bidouille dans les commentaires de php.net qui consiste (si je comprends bien) à exporter le nœud en XML pour le récupérer tel quel :
function getNodeInnerHTML($elem) {
	return simplexml_import_dom($elem)->asXML();
}
On file le nœud en paramètre à la fonction et hop ! on a le contenu d'origine. Je ne sais pas si c'est très sûr mais ça a l'air de marcher.
Je vais quand même regarder du côté de xpath.
sinon faut pas mélanger non plus le XML et le HTML c'est 2 choses différentes, en XML il attend l'encoding dans l'entête <?xml, en HTML il attend la balise meta avec le content type,
ducoup le $content c'est ni du HTML ni du XML ;)
Oh, je sais bien mais c'est la meilleure solution que j'ai trouvée pour parser du code HTML basique...

ViPHP
ViPHP | 5462 Messages

07 sept. 2010, 16:33

function getNodeInnerHTML($elem) {
        return simplexml_import_dom($elem)->asXML();
}
ca marche pas non plus puisque prend tout le nœud et pas le contenu

Eléphant du PHP | 92 Messages

07 sept. 2010, 16:48

ca marche pas non plus puisque prend tout le nœud et pas le contenu
Pour ce que je veux en faire, ça va aussi. C'est même peut-être mieux, en fait. ;)

Je cherche à séparer un ensemble de "blocs" HTML (div et ul) en plusieurs blocs indépendants. Soit j'ai tout le bloc d'un coup, soit j'ai les infos (avec le type de nœud) et le contenu.

ViPHP
ViPHP | 5462 Messages

07 sept. 2010, 16:58

ca marche pas non plus puisque prend tout le nœud et pas le contenu
Pour ce que je veux en faire, ça va aussi. C'est même peut-être mieux, en fait. ;)

Je cherche à séparer un ensemble de "blocs" HTML (div et ul) en plusieurs blocs indépendants. Soit j'ai tout le bloc d'un coup, soit j'ai les infos (avec le type de nœud) et le contenu.
si tu veux reste en html, comme ca suffira
foreach($items as $item)
{
    $id = $item->getAttribute('id'); 
       
    $tmp = new DOMDocument();       
    $tmp->appendChild($tmp->importNode($item, true));
    
    $mon_html[$id] = $tmp->saveHTML();
}

Eléphant du PHP | 92 Messages

07 sept. 2010, 17:21

Merci, je vais tester un peu tout ça. :)

ViPHP
AB
ViPHP | 5818 Messages

07 sept. 2010, 19:35

[quote="abelthorne"]
$dom=new DomDocument();
$dom->loadHTML('<?xml encoding="UTF-8">'.$post->content);
foreach($dom->documentElement->firstChild->childNodes as $node){
    if($node->hasAttribute('id')){
        $attribute_id=$node->getAttribute('id');
        $fp->textarea($attribute_id,$attribute_id,getNodeInnerHTML($node));
    }
}
Trois remarques :

- hasAttribute() ne prend pas de paramètre.

- Si tu utilises le hack '<?xml encoding="UTF-8">' dans loadHTML, n'oublies pas au paravant de spécifier l'entête header('Content-type: text/html; charset=UTF-8');

- Aères ton code et indentes-le, ce sera plus lisible :wink:

Eléphant du PHP | 92 Messages

07 sept. 2010, 19:51

- hasAttribute() ne prend pas de paramètre.
Il y a effectivement une méthode de DOMNode qui est hasAttributes(void) (au pluriel) et qui ne prend pas de paramètres (on vérifie si le nœud a des attributs), mais il y a aussi hasAttribute(string) (au singulier) qui vérifie la présence de l'attribut en paramètre. Celle-ci fait partie des méthodes de DomElement.
- Si tu utilises le hack '<?xml encoding="UTF-8">' dans loadHTML, n'oublies pas au paravant de spécifier l'entête header('Content-type: text/html; charset=UTF-8');
Je l'avais rajouté pour tester, mais ça m'arrange pas dans l'immédiat de rajouter des headers (mon code est un peu bordélique). Est-ce que je peux faire l'impasse sur l'encoding et le content-type, sachant que mes fichiers sont en UTF-8, ma base MySQL aussi, et ma connexion à cette dernière aussi ?
- Aères ton code et indentes-le, ce sera plus lisible :wink:
Mais il est parfaitement indenté, mon code... 8-)

ViPHP
AB
ViPHP | 5818 Messages

07 sept. 2010, 20:10

Il y a effectivement une méthode de DOMNode qui est hasAttributes(void) (au pluriel) et qui ne prend pas de paramètres (on vérifie si le nœud a des attributs), mais il y a aussi hasAttribute(string) (au singulier) qui vérifie la présence de l'attribut en paramètre. Celle-ci fait partie des méthodes de DomElement.
Ah oui j'ai cité ton code (hasAttribute) en pensant au miens (hasAttributes) :?
- Si tu utilises le hack '<?xml encoding="UTF-8">' dans loadHTML, n'oublies pas au paravant de spécifier l'entête header('Content-type: text/html; charset=UTF-8');
Je l'avais rajouté pour tester, mais ça m'arrange pas dans l'immédiat de rajouter des headers (mon code est un peu bordélique). Est-ce que je peux faire l'impasse sur l'encoding et le content-type, sachant que mes fichiers sont en UTF-8, ma base MySQL aussi, et ma connexion à cette dernière aussi ?
Pas certain que tu puisse t'en passer. Apache risque de t'envoyer par défaut des entêtes au format ISO... à tester.
Mais il est parfaitement indenté, mon code... 8-)
Décidément je suis un peu approximatif aujourd'hui :? Je voulais parler des accolades... :)

Eléphant du PHP | 92 Messages

07 sept. 2010, 20:20

Pas certain que tu puisse t'en passer. Apache risque de t'envoyer par défaut des entêtes au format ISO... à tester.
Ok, je verrai ça. De toute façon, pour l'instant, ce sont surtout des tests un peu empiriques. Mon génial système de publication n'est pas encore bien défini.
Décidément je suis un peu approximatif aujourd'hui :? Je voulais parler des accolades... :)
De vieilles habitudes de programmation qui datent de mon apprentissage du C il y a 15 ans. Mon prof les mettait comme ça. Je suis complètement paumé et je trouve le code illisible si je change les accolades de place, si je mets des espaces, etc. ;)

ViPHP
AB
ViPHP | 5818 Messages

08 sept. 2010, 20:38

De vieilles habitudes de programmation ...
Je faisais comme toi aussi avant... :)

Puis à force de voir du code et les accolades bien indentées et donc plus lisibles (surtout à l'intérieur des fonctions ou méthodes), je me suis laissé tenter par plus de clarté et le petit temps supplémentaire que ça me prend est amorti dès la première relecture...

Oui bon chacun fait comme il veut, c'était juste pour causer :)

Eléphant du PHP | 92 Messages

27 sept. 2010, 09:22

Bonjour,
Je suis de nouveau sur mon problème de "parseage" HTML (je l'avais un peu mis de côté) et j'ai un questionnement qui se rajoute.

Voici mon code actuel :
function explode_content($content){
    $table=array();

    $dom=new DomDocument();
    $dom->loadHTML('<?xml encoding="UTF-8">'.$content);
    foreach($dom->documentElement->firstChild->childNodes as $node){
        if($node->nodeType===XML_ELEMENT_NODE){
            if($node->hasAttribute('id')){
                $tmp=new DomDocument();
                foreach($node->childNodes as $subnode) $tmp->appendChild($tmp->importNode($subnode,true));
                $id=$node->getAttribute('id');
                $table[$id]['id']=$id;
                $table[$id]['type']=$node->nodeName;
                $table[$id]['content']=$tmp->saveHTML();
            }
        }
    }
    return $table;
}
Je passe un contenu HTML (récupéré depuis une BdD) à ma fonction qui me le rend "découpé" et stocké dans un tableau.

À la base, mon contenu est tout en UTF-8, stocké "en clair" (pas d'entités HTML). Dans le tableau que je récupère après la fonction, le contenu de mes balises a été converti (caractères accentués remplacés par des entités mais pas les balises elles-mêmes). Je suppose que ça vient du passage par le DomDocument temporaire, soit au moment des appendChild ou du saveHTML. De plus, si je décode le contenu avec html_entity_decode($tmp->saveHTML()); je me retrouve avec du texte encodé en ISO.

Est-ce que c'est normal ? Le DOM travaille forcément en convertissant les caractères spéciaux en entités ? Comment modifier mon code pour que le DOM temporaire (qui sert à récupérer le contenu de mes balises avec le code, suggéré par stealth35 ci-dessus) travaille en UTF-8 ?

Au passage, j'ai dû rajouter un test sur le type des nœuds lorsque je traite le contenu : pour une raison qui m'échappe, depuis que j'ai corrigé le contenu des champs de ma BdD pour virer toutes les entités HTML (j'ai donc du texte en UTF-8), je me retrouve avec des nœuds domText qui viennent je ne sais d'où. Avant, si mon contenu était "&eacute;t&eacute;", le charger avec loadHTML me donnait un domElement. Maintenant que mon contenu est "été", je me retrouve avec un domElement + un domText.

ViPHP
ViPHP | 5462 Messages

27 sept. 2010, 09:51

a quoi correspond $content ?