web-dev-qa-db-fra.com

Pourquoi il n'est pas possible d'utiliser des expressions rationnelles pour analyser HTML / XML: une explication formelle en termes simples

Il n'y a pas de jour sur SO qui passe sans une question sur l'analyse syntaxique (X) HTML ou XML avec des expressions régulières posées.

Bien qu'il soit relativement facile de trouver exemples qui démontrent la non-viabilité des expressions rationnelles pour cette tâche ou avec un collection d'expressions pour représenter le concept, je n'ai toujours pas pu trouver on SO a explication formelle des raisons pour lesquelles cela n'est pas possible en termes simples).

Les seules explications formelles que j'ai pu trouver jusqu'à présent sur ce site sont probablement extrêmement précises, mais aussi assez cryptiques pour le programmeur autodidacte:

la faille ici est que HTML est une grammaire Chomsky Type 2 (grammaire sans contexte) et RegEx est une grammaire Chomsky Type 3 (expression régulière)

ou:

Les expressions régulières ne peuvent correspondre qu'à des langues normales, mais HTML est un langage sans contexte.

ou:

Un automate fini (qui est la structure de données sous-jacente à une expression régulière) n'a pas de mémoire en dehors de l'état dans lequel il se trouve, et si vous avez une imbrication arbitrairement profonde, vous avez besoin d'un automate arbitrairement grand, qui entre en collision avec la notion d'automate fini.

ou:

Le lemme de pompage pour les langues régulières est la raison pour laquelle vous ne pouvez pas faire cela.

[Pour être juste: la majorité des explications ci-dessus renvoient vers des pages wikipedia, mais elles ne sont pas beaucoup plus faciles à comprendre que les réponses elles-mêmes].

Donc ma question est: quelqu'un pourrait-il s'il vous plaît fournir une traduction en termes simples des explications formelles données ci-dessus pour expliquer pourquoi il n'est pas possible d'utiliser des expressions rationnelles pour analyser (X) HTML/XML?

EDIT: Après avoir lu la première réponse, j'ai pensé que je devais clarifier: je recherche une "traduction" qui aussi brièvement explique les concepts qu'il essaie de traduire: à la fin d'une réponse, le lecteur devrait avoir une idée approximative - par exemple - de ce que "langage régulier" et "grammaire sans contexte" signifient ...

107
mac

Concentrez-vous sur celui-ci:

Un automate fini (qui est la structure de données sous-jacente à une expression régulière) n'a pas de mémoire en dehors de l'état dans lequel il se trouve, et si vous avez une imbrication arbitrairement profonde, vous avez besoin d'un automate arbitrairement grand, qui entre en collision avec la notion d'automate fini.

La définition des expressions régulières équivaut au fait qu'un test de conformité d'une chaîne au modèle peut être effectué par un automate fini (un automate différent pour chaque modèle). Un automate fini n'a pas de mémoire - pas de pile, pas de tas, pas de bande infinie sur laquelle griffonner. Tout ce qu'il a, c'est un nombre fini d'états internes, chacun pouvant lire une unité d'entrée de la chaîne testée, et l'utiliser pour décider de l'état dans lequel passer. Comme cas particulier, il a deux états de terminaison: "oui, cela correspond" et "non, cela ne correspond pas".

Le HTML, en revanche, possède des structures qui peuvent s'imbriquer de façon arbitraire en profondeur. Pour déterminer si un fichier est HTML valide ou non, vous devez vérifier que toutes les balises de fermeture correspondent à une balise d'ouverture précédente. Pour le comprendre, vous devez savoir quel élément est fermé. Sans aucun moyen de "se souvenir" des balises d'ouverture que vous avez vues, aucune chance.

Notez cependant que la plupart des bibliothèques "regex" autorisent en fait plus que la simple définition stricte des expressions régulières. S'ils peuvent correspondre à des références arrières, alors ils sont allés au-delà d'un langage normal. Donc, la raison pour laquelle vous ne devriez pas utiliser une bibliothèque regex sur HTML est un peu plus complexe que le simple fait que HTML n'est pas régulier.

103
Steve Jessop

Le fait que le HTML ne représente pas une langue régulière est un hareng rouge. Expression régulière et langues régulières une sorte de son similaire, mais ne le sont pas - elles partagent la même origine, mais il existe une distance notable entre les "langues régulières" académiques et la puissance de correspondance actuelle des moteurs. En fait, presque tous les moteurs d'expression régulière modernes prennent en charge des fonctionnalités non régulières - un exemple simple est (.*)\1. qui utilise la référence arrière pour faire correspondre une séquence répétée de caractères - par exemple 123123 ou bonbon. L'association de structures récursives/équilibrées les rend encore plus amusantes.

Wikipedia le dit bien, dans une citation de Larry Wall :

Les "expressions régulières" [...] ne sont que marginalement liées aux expressions régulières réelles. Néanmoins, le terme a grandi avec les capacités de nos moteurs de correspondance de motifs, donc je ne vais pas essayer de lutter contre la nécessité linguistique ici. Je les appellerai cependant généralement "regexes" (ou "regexen", quand je suis d'humeur anglo-saxonne).

"L'expression régulière ne peut correspondre qu'à des langues régulières", comme vous pouvez le voir, n'est rien de plus qu'un sophisme communément déclaré.

Alors pourquoi pas?

Une bonne raison de ne pas faire correspondre HTML à une expression régulière est que "ce n'est pas parce que vous pouvez" que vous devez. Bien que cela soit possible - , il existe simplement de meilleurs outils pour le travail . Considérant:

  • Le HTML valide est plus difficile/plus complexe que vous ne le pensez.
  • Il existe de nombreux types de code HTML "valide" - ce qui est valide en HTML, par exemple, n'est pas valide en XHTML.
  • Une grande partie du HTML de forme libre trouvée sur Internet est non valide de toute façon. Les bibliothèques HTML font également un bon travail pour les gérer et ont été testées pour bon nombre de ces cas courants.
  • Très souvent, il est impossible de faire correspondre une partie des données sans les analyser dans leur ensemble. Par exemple, vous pouvez rechercher tous les titres et finir par correspondre dans un commentaire ou un littéral de chaîne. <h1>.*?</h1> peut être une tentative audacieuse pour trouver le titre principal, mais il peut trouver:

    <!-- <h1>not the title!</h1> -->
    

    Ou même:

    <script>
    var s = "Certainly <h1>not the title!</h1>";
    </script>
    

Le dernier point est le plus important:

  • L'utilisation d'un analyseur HTML dédié est meilleure que toute expression régulière que vous pouvez trouver. Très souvent, XPath permet une meilleure façon expressive de trouver les données dont vous avez besoin, et l'utilisation d'un analyseur HTML est beaucoup plus facile que la plupart des gens ne le pensent .

Un bon résumé du sujet, et un commentaire important sur le moment où mélanger Regex et HTML peut être approprié, peuvent être trouvés dans le blog de Jeff Atwood: Parsing Html The Cthulhu Way .

Quand est-il préférable d'utiliser une expression régulière pour analyser HTML?

Dans la plupart des cas, il est préférable d'utiliser XPath sur la structure DOM qu'une bibliothèque peut vous donner. Pourtant, contre l'opinion populaire, il y a quelques cas où je recommanderais fortement d'utiliser une regex et non une bibliothèque d'analyseur:

Compte tenu de quelques-unes de ces conditions:

  • Lorsque vous avez besoin d'une mise à jour unique de vos fichiers HTML et que vous savez que la structure est cohérente.
  • Lorsque vous disposez d'un tout petit extrait de code HTML.
  • Lorsque vous n'avez pas affaire à un fichier HTML, mais à un moteur de modèle similaire (il peut être très difficile de trouver un analyseur dans ce cas).
  • Lorsque vous souhaitez modifier des parties du code HTML, mais pas tout - à ma connaissance, un analyseur ne peut pas répondre à cette demande: il analysera le document entier, et enregistrez un document entier, en changeant les parties que vous n'avez jamais voulu changer.
53
Kobi

Parce que HTML peut avoir une imbrication illimitée de <tags><inside><tags and="<things><that><look></like></tags>"></inside></each></other> et regex ne peuvent pas vraiment faire face à cela car ils ne peuvent pas suivre l'historique de ce dans quoi ils sont descendus et sortis.

Une construction simple qui illustre la difficulté:

<body><div id="foo">Hi there!  <div id="bar">Bye!</div></div></body>

99,9% des routines d'extraction basées sur des expressions rationnelles généralisées ne seront pas en mesure de me donner tout correctement à l'intérieur du div avec l'ID foo, car elles ne peuvent pas dire la balise de fermeture pour ce div de la fermeture pour la balise bar div. C'est parce qu'ils n'ont aucun moyen de dire "d'accord, je suis maintenant descendu dans la deuxième des deux divisions, donc la prochaine fermeture de division que je vois me ramène à une, et celle qui suit est la balise de fermeture pour la première" . Les programmeurs réagissent généralement en concevant des expressions rationnelles de cas spécial pour la situation spécifique, qui se cassent ensuite dès que plus de balises sont introduites dans foo et doivent être débrouillées à un coût énorme en temps et en frustration. C'est pourquoi les gens deviennent fous de tout cela.

18
Ianus Chiaroscuro

Un langage normal est un langage auquel peut correspondre une machine à états finis.

(Comprendre les machines à états finis, les machines à enfoncer et les machines de Turing est fondamentalement le programme d'un cours CS de quatrième année.)

Considérez la machine suivante, qui reconnaît la chaîne "hi".

(Start) --Read h-->(A)--Read i-->(Succeed)
  \                  \
   \                  -- read any other value-->(Fail) 
    -- read any other value-->(Fail)

Il s'agit d'une machine simple pour reconnaître une langue régulière; Chaque expression entre parenthèses est un état et chaque flèche est une transition. Construire une machine comme celle-ci vous permettra de tester n'importe quelle chaîne d'entrée par rapport à un langage normal - donc une expression régulière.

Le HTML exige que vous sachiez plus que simplement dans quel état vous êtes - il nécessite un historique de ce que vous avez vu auparavant, pour correspondre à l'imbrication des balises. Vous pouvez accomplir cela si vous ajoutez une pile à la machine, mais alors elle n'est plus "régulière". C'est ce qu'on appelle une machine déroulante et reconnaît une grammaire.

8
Sean McMillan

Une expression régulière est une machine avec un nombre fini (et généralement assez petit) d'états discrets.

Pour analyser XML, C ou tout autre langage avec une imbrication arbitraire d'éléments de langage, vous devez vous souvenir de votre profondeur. Autrement dit, vous devez pouvoir compter les accolades/crochets/balises.

Vous ne pouvez pas compter avec une mémoire finie. Il peut y avoir plus de niveaux d'accolades que vous n'en avez! Vous pourriez être en mesure d'analyser un sous-ensemble de votre langue qui restreint le nombre de niveaux d'imbrication, mais ce serait très fastidieux.

6
n.m.

Une grammaire est une définition formelle de l'endroit où les mots peuvent aller. Par exemple, les adjectifs précèdent les noms in English grammar, Mais suivent les noms en la gramática española. Sans contexte signifie que la grammaire est universelle dans tous les contextes. Sensible au contexte signifie qu'il existe des règles supplémentaires dans certains contextes.

En C #, par exemple, using signifie quelque chose de différent dans using System; En haut des fichiers, que using (var sw = new StringWriter (...)). Un exemple plus pertinent est le code suivant dans le code:

void Start ()
{
    string myCode = @"
    void Start()
    {
       Console.WriteLine (""x"");
    }
    ";
}
6
agent-j

Il y a une autre raison pratique de ne pas utiliser d'expressions régulières pour analyser XML et HTML qui n'a rien à voir avec la théorie informatique: votre expression régulière sera soit affreusement compliquée, soit erronée.

Par exemple, il est très bien d'écrire une expression régulière pour correspondre

<price>10.65</price>

Mais si votre code doit être correct, alors:

  • Il doit autoriser les espaces après le nom de l'élément dans les balises de début et de fin

  • Si le document se trouve dans un espace de noms, il doit autoriser l'utilisation de tout préfixe d'espace de noms

  • Il devrait probablement autoriser et ignorer tous les attributs inconnus apparaissant dans la balise de début (selon la sémantique du vocabulaire particulier)

  • Il peut être nécessaire d'autoriser les espaces avant et après la valeur décimale (là encore, en fonction des règles détaillées du vocabulaire XML particulier).

  • Il ne doit pas correspondre à quelque chose qui ressemble à un élément, mais se trouve en fait dans un commentaire ou une section CDATA (cela devient particulièrement important s'il y a une possibilité de données malveillantes essayant de tromper votre analyseur).

  • Il peut être nécessaire de fournir des diagnostics si l'entrée n'est pas valide.

Bien sûr, cela dépend en partie des normes de qualité que vous appliquez. Nous voyons beaucoup de problèmes sur StackOverflow avec des personnes devant générer du XML d'une manière particulière (par exemple, sans espace dans les balises) car il est lu par une application qui nécessite qu'il soit écrit d'une manière particulière. Si votre code a une longévité quelconque, il est important qu'il puisse traiter le XML entrant écrit de la manière autorisée par la norme XML, et pas seulement le seul exemple de document d'entrée sur lequel vous testez votre code.

4
Michael Kay

Dans un sens purement théorique, il est impossible pour les expressions régulières d'analyser XML. Ils sont définis de manière à ne laisser aucune mémoire d'aucun état antérieur, empêchant ainsi la correspondance correcte d'une balise arbitraire, et ils ne peuvent pas pénétrer à une profondeur arbitraire d'imbrication, car l'imbrication devrait être intégrée à l'expression régulière.

Les analyseurs d'expressions rationnelles modernes, cependant, sont construits pour leur utilité pour le développeur, plutôt que pour leur adhérence à une définition précise. En tant que tel, nous avons des choses comme les références arrières et la récursivité qui utilisent la connaissance des états précédents. À l'aide de ceux-ci, il est remarquablement simple de créer une expression régulière qui peut explorer, valider ou analyser XML.

Considérez par exemple,

(?:
    <!\-\-[\S\s]*?\-\->
    |
    <([\w\-\.]+)[^>]*?
    (?:
        \/>
        |
        >
        (?:
            [^<]
            |
            (?R)
        )*
        <\/\1>
    )
)

Cela trouvera la prochaine balise ou commentaire XML correctement formé, et il ne le trouvera que si tout son contenu est correctement formé. (Cette expression a été testée à l'aide de Notepad ++, qui utilise la bibliothèque regex de Boost C++, qui se rapproche étroitement de PCRE.)

Voici comment cela fonctionne:

  1. Le premier morceau correspond à un commentaire. Il est nécessaire que cela vienne en premier afin qu'il traite tout code commenté qui autrement pourrait provoquer des blocages.
  2. Si cela ne correspond pas, il recherchera le début d'une balise. Notez qu'il utilise des parenthèses pour capturer le nom.
  3. Cette balise se terminera par un />, complétant ainsi la balise, ou elle se terminera par un >, auquel cas il continuera en examinant le contenu de la balise.
  4. Il continuera l'analyse jusqu'à ce qu'il atteigne un <, auquel cas il reviendra au début de l'expression, lui permettant de traiter un commentaire ou une nouvelle balise.
  5. Il continuera dans la boucle jusqu'à ce qu'il arrive à la fin du texte ou à un < qu'il ne peut pas analyser. Le fait de ne pas égaler entraînera bien sûr le redémarrage du processus. Sinon, le < est probablement le début de la balise de fermeture pour cette itération. Utilisation de la référence arrière dans une balise de fermeture <\/\1>, il correspondra à la balise d'ouverture de l'itération actuelle (profondeur). Il n'y a qu'un seul groupe de capture, donc ce match est simple. Cela le rend indépendant des noms des balises utilisées, bien que vous puissiez modifier le groupe de capture pour capturer uniquement des balises spécifiques, si vous en avez besoin.
  6. À ce stade, il sera soit éliminé de la récursion en cours, jusqu'au niveau suivant, soit terminé par un match.

Cet exemple résout les problèmes liés aux espaces blancs ou à l'identification du contenu pertinent grâce à l'utilisation de groupes de caractères qui annulent simplement < ou >, ou dans le cas des commentaires, en utilisant [\S\s], qui correspondra à tout, y compris les retours chariot et les nouvelles lignes, même en mode unifilaire, jusqu'à ce qu'il atteigne -->. Par conséquent, il traite simplement tout comme valide jusqu'à ce qu'il atteigne quelque chose de significatif.

Dans la plupart des cas, une expression régulière comme celle-ci n'est pas particulièrement utile. Il validera que XML est correctement formé, mais c'est tout ce qu'il fera vraiment, et il ne tient pas compte des propriétés (bien que ce soit un ajout facile). C'est aussi simple que cela car il laisse de côté les problèmes du monde réel comme celui-ci, ainsi que les définitions des noms de balises. L'adapter à une utilisation réelle en ferait beaucoup plus une bête. En général, un véritable analyseur XML serait bien supérieur. Celui-ci est probablement le mieux adapté pour enseigner le fonctionnement de la récursivité.

Pour faire court: utilisez un analyseur XML pour un travail réel, et utilisez-le si vous voulez jouer avec des expressions rationnelles.

2
bükWyrm

Ne pas analyser XML/HTML avec regex, utilisez un analyseur XML/HTML approprié et une puissante requête xpath .

théorie :

Selon la théorie de la compilation, XML/HTML ne peut pas être analysé en utilisant l'expression régulière basée sur machine à états finis . En raison de la construction hiérarchique de XML/HTML, vous devez utiliser un automate déroulant et manipuler LALR grammaire en utilisant un outil comme YACC .

l'outil quotidien realLife © ® ™ dans un Shell :

Vous pouvez utiliser l'une des options suivantes:

xmllint souvent installé par défaut avec libxml2, xpath1 (cochez mon wrapper pour avoir une sortie délimitée par des sauts de ligne

xmlstarlet peut éditer, sélectionner, transformer ... Non installé par défaut, xpath1

xpath installé via le module Perl XML :: XPath, xpath1

xidel xpath3

saxon-lint mon propre projet, envelopper sur Saxon-HE de @Michael Kay Java, xpath3

ou vous pouvez utiliser des langages de haut niveau et des bibliothèques appropriées, je pense à:

python 's lxml (from lxml import etree)

Perl s XML::LibXML , XML::XPath , XML::Twig::XPath , HTML::TreeBuilder::XPath

Rubynokogiri , vérifiez cet exemple

phpDOMXpath, vérifiez cet exemple


Vérifiez: en utilisant des expressions régulières avec des balises HTML

0
Gilles Quenot