Perché non è possibile utilizzare regex per analizzare HTML / XML: una spiegazione formale in termini semplici


117

Non c'è giorno in SO che trascorra senza una domanda sull'analisi (X) HTML o XML con espressioni regolari.

Sebbene sia relativamente facile trovare esempi che dimostrino la non fattibilità di regex per questo compito o con una raccolta di espressioni per rappresentare il concetto, non sono riuscito ancora a trovare su SO una spiegazione formale del motivo per cui ciò non è possibile fatto in parole semplici termini.

Le uniche spiegazioni formali che ho potuto trovare finora su questo sito sono probabilmente estremamente accurate, ma anche abbastanza criptiche per il programmatore autodidatta:

il difetto qui è che l'HTML è una grammatica Chomsky di tipo 2 (grammatica libera dal contesto) e RegEx è una grammatica di Chomsky di tipo 3 (espressione regolare)

o:

Le espressioni regolari possono corrispondere solo alle lingue regolari, ma l'HTML è un linguaggio privo di contesto.

o:

Un automa finito (che è la struttura dati alla base di un'espressione regolare) non ha memoria a parte lo stato in cui si trova, e se hai un annidamento arbitrariamente profondo, hai bisogno di un automa arbitrariamente grande, che collide con la nozione di automa finito.

o:

Il lemma Pumping per le lingue normali è il motivo per cui non puoi farlo.

[Per essere onesti: la maggior parte delle spiegazioni di cui sopra rimandano alle pagine di wikipedia, ma queste non sono molto più facili da capire delle risposte stesse].

Quindi la mia domanda è: qualcuno potrebbe fornire una traduzione in termini profani delle spiegazioni formali fornite sopra del motivo per cui non è possibile utilizzare regex per analizzare (X) HTML / XML?

EDIT: Dopo aver letto la prima risposta ho pensato che avrei dovuto chiarire: Sto cercando una "traduzione" che anche briefely spiega i concetti che cerca di tradurre: alla fine di una risposta, il lettore dovrebbe avere una vaga idea - per esempio - cosa significano "linguaggio normale" e "grammatica libera dal contesto" ...


19
Sii consapevole del fatto che in termini di informatica, le "espressioni regolari" differiscono notevolmente dalle moderne "implementazioni di espressioni regolari" (gli strumenti / API che usi in un linguaggio di programmazione). Questi ultimi possono "ricordare" le cose che hanno incontrato e possono anche abbinare modelli (sotto) definiti in modo ricorsivo, facendoli abbinare / analizzare / riconoscere molto più delle "espressioni regolari" teoriche.
Bart Kiers

1
@Bart: Questo vale solo per le lingue che abusano del termine "espressione regolare. POSIX ERE è puramente regolare.
R .. GitHub STOP HELPING ICE

2
@R .., quindi chiami POSIX una "implementazione moderna": P. In tutta serietà però: sì, hai ragione quelli sono veramente regolari. Avrei dovuto dire "... molte delle moderne implementazioni di regex ..." o "... implementazioni di espressioni regolari PCRE ..." .
Bart Kiers

4
Ho difficoltà a prendere sul serio i linguaggi di programmazione che fondamentalmente usano in modo improprio un linguaggio rigoroso per il bene di farsi pubblicità per programmatori ignoranti ...
R .. GitHub STOP HELPING ICE

3
@R .., è un peccato che le implementazioni PCRE siano indicate come "espressioni regolari", ma non prendere sul serio il linguaggio significa fare un passo in più, IMO. Voglio dire, non stai prendendo Perl, Java, Python, Ruby, JavaScript, .NET, ecc. Non sul serio per questo motivo?
Bart Kiers

Risposte:


117

Concentrati su questo:

Un automa finito (che è la struttura dati alla base di un'espressione regolare) non ha memoria a parte lo stato in cui si trova, e se hai un annidamento arbitrariamente profondo, hai bisogno di un automa arbitrariamente grande, che collide con la nozione di automa finito.

La definizione di espressioni regolari è equivalente al fatto che un test per verificare se una stringa corrisponde al modello può essere eseguito da un automa finito (un automa diverso per ogni modello). Un automa finito non ha memoria: nessuno stack, nessun mucchio, nessun nastro infinito su cui scarabocchiare. Tutto ciò che ha è un numero finito di stati interni, ognuno dei quali può leggere un'unità di input dalla stringa da testare, e usarla per decidere a quale stato passare successivamente. Come casi speciali, ha due stati di terminazione: "sì, corrispondente" e "no, non corrispondente".

HTML, d'altra parte, ha strutture che possono annidarsi arbitrariamente in profondità. Per determinare se un file è HTML valido o meno, è necessario verificare che tutti i tag di chiusura corrispondano a un tag di apertura precedente. Per capirlo, devi sapere quale elemento viene chiuso. Senza alcun mezzo per "ricordare" quali tag di apertura hai visto, nessuna possibilità.

Si noti tuttavia che la maggior parte delle librerie "regex" in realtà consentono più della semplice definizione di espressioni regolari. Se riescono ad abbinare i riferimenti a ritroso, allora sono andati oltre un linguaggio normale. Quindi il motivo per cui non dovresti usare una libreria regex su HTML è un po 'più complesso del semplice fatto che l'HTML non è regolare.


C'è anche una spiegazione piuttosto buona degli automi a stati finiti qui: youtube.com/watch?v=vhiiia1_hC4
GDP2

55

Il fatto che l'HTML non rappresenti una lingua normale è una falsa pista. L'espressione regolare e le lingue regolari suonano in qualche modo simili , ma non lo sono: condividono la stessa origine, ma c'è una notevole distanza tra le "lingue regolari" accademiche e l'attuale potenza di corrispondenza dei motori. In effetti, quasi tutti i moderni motori di espressioni regolari supportano funzionalità non regolari: un semplice esempio è (.*)\1. che utilizza il backreferencing per abbinare una sequenza ripetuta di caratteri, ad esempio 123123o bonbon. L'abbinamento di strutture ricorsive / bilanciate le rende ancora più divertenti.

Wikipedia lo mette bene, in una citazione di Larry Wall :

Le "espressioni regolari" [...] sono solo marginalmente correlate alle espressioni regolari reali. Tuttavia, il termine è cresciuto con le capacità dei nostri motori di pattern matching, quindi non cercherò di combattere la necessità linguistica qui. Tuttavia, le chiamerò generalmente "regex" (o "regexen", quando sono di umore anglosassone).

"L'espressione regolare può corrispondere solo alle lingue regolari", come puoi vedere, non è altro che un errore comunemente affermato.

Allora perché no?

Una buona ragione per non abbinare l'HTML con l'espressione regolare è che "solo perché puoi non significa che dovresti". Sebbene sia possibile, esistono semplicemente strumenti migliori per il lavoro . Considerando:

  • L'HTML valido è più difficile / più complesso di quanto potresti pensare.
  • Esistono molti tipi di HTML "valido": ciò che è valido in HTML, ad esempio, non è valido in XHTML.
  • La maggior parte dell'HTML in formato libero trovato su Internet non è comunque valido . Anche le librerie HTML fanno un buon lavoro nell'affrontare questi problemi e sono state testate per molti di questi casi comuni.
  • Molto spesso è impossibile abbinare una parte dei dati senza analizzarli nel loro insieme. Ad esempio, potresti cercare tutti i titoli e finire per trovare una corrispondenza all'interno di un commento o di una stringa letterale. <h1>.*?</h1>potrebbe essere un audace tentativo di trovare il titolo principale, ma potrebbe trovare:

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

    O anche:

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

L'ultimo punto è il più importante:

  • Usare un parser HTML dedicato è meglio di qualsiasi regex che puoi inventare. Molto spesso, XPath consente un modo più espressivo di trovare i dati necessari e l' utilizzo di un parser HTML è molto più semplice di quanto la maggior parte delle persone creda .

Un buon riassunto dell'argomento, e un commento importante su quando mescolare Regex e HTML può essere appropriato, può essere trovato nel blog di Jeff Atwood: Parsing Html The Cthulhu Way .

Quando è meglio usare un'espressione regolare per analizzare l'HTML?

Nella maggior parte dei casi, è meglio usare XPath sulla struttura DOM che una libreria può darti. Tuttavia, contro l'opinione popolare, ci sono alcuni casi in cui consiglio vivamente di utilizzare una regex e non una libreria parser:

Date alcune di queste condizioni:

  • Quando hai bisogno di un aggiornamento una tantum dei tuoi file HTML e sai che la struttura è coerente.
  • Quando hai uno snippet molto piccolo di HTML.
  • Quando non si ha a che fare con un file HTML, ma con un motore di creazione di modelli simile (può essere molto difficile trovare un parser in questo caso).
  • Quando vuoi cambiare parti dell'HTML, ma non tutto, un parser, per quanto ne so, non può rispondere a questa richiesta: analizzerà l'intero documento e salverà un intero documento, cambiando parti che non avresti mai voluto cambiare.

4
Questo è un pezzo molto chiaro e ben scritto su quando (non usare) le espressioni regolari per analizzare l'HTML, ma non è certo una risposta alla mia domanda. Posso suggerirti di spostarlo invece su questa domanda ? Penso che ti farebbe guadagnare più reputazione lì ma - soprattutto - penso che sarebbe un posto dove i futuri visitatori lo troverebbero più pertinente (c'è un commento di @Bart Kiers alla mia domanda che ricorda ai visitatori il "potere extra" dei moderni motori regex).
mac

1
@mac - Grazie mille. In realtà, ci ho pensato un po '. So di non aver risposto alla tua domanda, ma non credo che la domanda sia fondamentalmente corretta - chiedi di spiegare il motivo sbagliato ... Hai una buona idea però, forse l'altra domanda è più adatta ...
Kobi

19

Perché HTML può avere un annidamento illimitato <tags><inside><tags and="<things><that><look></like></tags>"></inside></each></other>e regex non può davvero farcela perché non può tenere traccia di una cronologia di ciò in cui è disceso e da cui proviene.

Un semplice costrutto che illustra la difficoltà:

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

Il 99,9% delle routine di estrazione basate su espressioni regolari generalizzate non sarà in grado di fornirmi correttamente tutto all'interno di divcon l'ID foo, perché non possono distinguere il tag di chiusura per quel div dal tag di chiusura per il bardiv. Questo perché non hanno modo di dire "okay, ora sono sceso nel secondo di due div, quindi la chiusura del div successiva che vedo mi riporta fuori uno, e quello dopo è il tag di chiusura del primo" . I programmatori in genere rispondono ideando regex di casi speciali per la situazione specifica, che poi si interrompono non appena vengono introdotti più tag all'interno fooe devono essere smontati a un costo enorme in termini di tempo e frustrazione. Questo è il motivo per cui le persone si arrabbiano per l'intera faccenda.


1
Apprezzo la risposta, ma la mia domanda non è "perché non posso usare regex ...". La mia domanda riguarda la "traduzione" delle spiegazioni formali che ho fornito! :)
mac

5
Questa è una traduzione di tutti loro in un certo senso, più approssimativamente "Le espressioni regolari possono corrispondere solo a linguaggi regolari ma HTML è un linguaggio libero dal contesto" e quello sugli automi finiti. È davvero lo stesso motivo.
Ianus Chiaroscuro

Scusa, forse non sono stato chiaro nella mia domanda (i suggerimenti per migliorarla sono ben accetti!). Ma cerco una risposta che spieghi anche la "traduzione". La tua risposta non chiarisce né i concetti di "linguaggio normale" né di "linguaggio senza contesto" ...
mac

5
Spiegare questi termini sarebbe tanto tecnico quanto il gergo stesso, e una distrazione dal significato reale a cui sta arrivando tutto il linguaggio di precisione, essendo quello che ho pubblicato.
Ianus Chiaroscuro

4
<(\w+)(?:\s+\w+="[^"]*")*>(?R)*</\1>|[\w\s!']+corrisponde al codice di esempio.
Kobi

9

Una lingua normale è una lingua che può essere abbinata a una macchina a stati finiti.

(Comprendere le macchine a stati finiti, le macchine push-down e le macchine di Turing è fondamentalmente il curriculum di un corso CS del quarto anno del college.)

Considera la seguente macchina, che riconosce la stringa "hi".

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

Questa è una semplice macchina per riconoscere un linguaggio normale; Ogni espressione tra parentesi è uno stato e ogni freccia è una transizione. Costruire una macchina come questa ti permetterà di testare qualsiasi stringa di input con un linguaggio normale, quindi un'espressione regolare.

L'HTML richiede che tu sappia più del solo stato in cui ti trovi: richiede una cronologia di ciò che hai visto prima, per abbinare la nidificazione dei tag. Puoi farlo se aggiungi uno stack alla macchina, ma poi non è più "normale". Questa è chiamata macchina push-down e riconosce una grammatica.


2
"Comprendere le macchine a stati finiti, le macchine push-down e le macchine di Turing è fondamentalmente il programma di un corso CS di 300 livelli". Capisco che questo sia un tentativo di affermare quanto sia difficile / avanzato l'argomento, ma non ho familiarità con il sistema scolastico a cui ti riferisci, potresti per favore chiarire in un modo non specifico del paese? Grazie! :)
mac

1
L'ho aggiornato. Non so se sia troppo difficile da capire, solo da spiegare in un post di overflow dello stack.
Sean McMillan

6

Un'espressione regolare è una macchina con un numero finito (e in genere piuttosto piccolo) di stati discreti.

Per analizzare XML, C o qualsiasi altro linguaggio con annidamento arbitrario di elementi del linguaggio, è necessario ricordare quanto sei profondo. Cioè, devi essere in grado di contare parentesi graffe / parentesi / tag.

Non puoi contare con memoria finita. Potrebbero esserci più livelli di rinforzo rispetto agli stati! Potresti essere in grado di analizzare un sottoinsieme della tua lingua che limita il numero di livelli di nidificazione, ma sarebbe molto noioso.


6

Una grammatica è una definizione formale di dove possono andare le parole. Ad esempio, gli aggettivi precedono i nomi in English grammar, ma seguono i nomi en la gramática española. Context-free significa che la grammatica universalmente in tutti i contesti. Sensibile al contesto significa che ci sono regole aggiuntive in determinati contesti.

In C #, ad esempio, usingsignifica qualcosa di diverso using System;all'inizio dei file rispetto a using (var sw = new StringWriter (...)). Un esempio più rilevante è il codice seguente all'interno del codice:

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

Questa è una risposta comprensibile
Una persona il

Ma senza contesto non significa regolare. Il linguaggio delle parantesi abbinate è privo di contesto, ma non regolare.
Taemyr

Ciò che dovrebbe essere aggiunto è che le espressioni regolari (a meno che non si aggiungano estensioni come quelle presenti in Perl) sono equivalenti alle grammatiche regolari , il che significa che non possono descrivere strutture arbitrariamente nidificate profondamente come parentesi arbitrariamente bilanciate o tag di apertura e chiusura di elementi HTML.
reinierpost

4

C'è un'altra ragione pratica per non usare le espressioni regolari per analizzare XML e HTML che non ha nulla a che fare con la teoria dell'informatica: la tua espressione regolare sarà orribilmente complicata o sbagliata.

Ad esempio, va benissimo scrivere un'espressione regolare da abbinare

<price>10.65</price>

Ma se il tuo codice deve essere corretto, allora:

  • Deve consentire spazi bianchi dopo il nome dell'elemento sia nel tag iniziale che in quello finale

  • Se il documento si trova in uno spazio dei nomi, dovrebbe consentire l'utilizzo di qualsiasi prefisso dello spazio dei nomi

  • Probabilmente dovrebbe consentire e ignorare qualsiasi attributo sconosciuto che appare nel tag di inizio (a seconda della semantica del particolare vocabolario)

  • Potrebbe essere necessario consentire spazi bianchi prima e dopo il valore decimale (di nuovo, a seconda delle regole dettagliate del particolare vocabolario XML).

  • Non dovrebbe corrispondere a qualcosa che assomiglia a un elemento, ma in realtà si trova in un commento o in una sezione CDATA (questo diventa particolarmente importante se esiste la possibilità che dati dannosi tentino di ingannare il tuo parser).

  • Potrebbe essere necessario fornire la diagnostica se l'input non è valido.

Ovviamente parte di questo dipende dagli standard di qualità che stai applicando. Vediamo molti problemi su StackOverflow con le persone che devono generare XML in un modo particolare (ad esempio, senza spazi vuoti nei tag) perché viene letto da un'applicazione che richiede di essere scritto in un modo particolare. Se il tuo codice ha un qualche tipo di longevità, è importante che sia in grado di elaborare l'XML in entrata scritto in qualsiasi modo consentito dallo standard XML, e non solo l'unico documento di input di esempio su cui stai testando il tuo codice.


2

In senso puramente teorico, è impossibile per le espressioni regolari analizzare XML. Sono definiti in un modo che non consente loro di ricordare alcuno stato precedente, impedendo così la corretta corrispondenza di un tag arbitrario, e non possono penetrare a una profondità arbitraria di annidamento, poiché l'annidamento dovrebbe essere integrato nell'espressione regolare.

I moderni parser regex, tuttavia, sono costruiti per la loro utilità per lo sviluppatore, piuttosto che per la loro aderenza a una definizione precisa. In quanto tali, abbiamo elementi come riferimenti a ritroso e ricorsione che fanno uso della conoscenza degli stati precedenti. Usandoli, è straordinariamente semplice creare un'espressione regolare in grado di esplorare, convalidare o analizzare XML.

Considera ad esempio

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

Questo troverà il successivo tag o commento XML formato correttamente e lo troverà solo se l'intero contenuto è formato correttamente. (Questa espressione è stata testata utilizzando Notepad ++, che utilizza la libreria regex di Boost C ++, che si avvicina molto a PCRE.)

Ecco come funziona:

  1. La prima parte corrisponde a un commento. È necessario che questo venga prima in modo che possa gestire qualsiasi codice commentato che altrimenti potrebbe causare blocchi.
  2. Se non corrisponde, cercherà l'inizio di un tag. Nota che usa le parentesi per catturare il nome.
  3. Questo tag terminerà con a />, completando così il tag, oppure terminerà con a >, nel qual caso continuerà esaminando il contenuto del tag.
  4. Continuerà l'analisi fino a quando non raggiunge a <, a quel punto tornerà all'inizio dell'espressione, permettendogli di gestire un commento o un nuovo tag.
  5. Continuerà attraverso il ciclo fino a quando non arriva alla fine del testo o in un punto <che non può analizzare. La mancata corrispondenza, ovviamente, farà ricominciare il processo. Altrimenti, <è presumibilmente l'inizio del tag di chiusura per questa iterazione. Utilizzando il riferimento all'indietro all'interno di un tag di chiusura <\/\1>, corrisponderà al tag di apertura per l'iterazione corrente (profondità). C'è solo un gruppo di cattura, quindi questa partita è una questione semplice. Ciò lo rende indipendente dai nomi dei tag utilizzati, sebbene sia possibile modificare il gruppo di acquisizione per acquisire solo tag specifici, se necessario.
  6. A questo punto o uscirà dalla ricorsione corrente, fino al livello successivo o terminerà con una corrispondenza.

Questo esempio risolve i problemi relativi agli spazi bianchi o all'identificazione di contenuti rilevanti attraverso l'uso di gruppi di caratteri che si limitano a negare <o >, o nel caso dei commenti, utilizzando [\S\s], che corrisponderà a qualsiasi cosa, inclusi i ritorni a capo e le nuove righe, anche in una riga singola modalità, continuando fino a raggiungere a -->. Quindi, tratta semplicemente tutto come valido fino a quando non raggiunge qualcosa di significativo.

Per la maggior parte degli scopi, una regex come questa non è particolarmente utile. Convaliderà che XML è formato correttamente, ma è tutto ciò che farà davvero e non tiene conto delle proprietà (anche se questa sarebbe un'aggiunta facile). È solo così semplice perché esclude problemi del mondo reale come questo, così come le definizioni dei nomi dei tag. Adattarlo per un uso reale lo renderebbe molto più di una bestia. In generale, un vero parser XML sarebbe di gran lunga superiore. Questo è probabilmente il più adatto per insegnare come funziona la ricorsione.

Per farla breve: usa un parser XML per un lavoro reale e usalo se vuoi giocare con le espressioni regolari.


3
L'affermazione che questa regex corrisponderà solo se l'input è ben formato non è corretta. Non controlla che i nomi siano nomi XML validi, non controlla gli attributi, non controlla i riferimenti di entità e caratteri, non gestisce CDATA o istruzioni di elaborazione. Quando dici che è stato testato, dubito fortemente che sia stato testato su qualcosa di simile alla suite di test di conformità XML. Questo è il problema con tutti i tentativi di elaborare XML con regex che io abbia mai visto: funzionano con un piccolo numero di input, ma non con alcun XML che può essere legalmente passato alla tua applicazione.
Michael Kay

2
Inoltre, ci sono input ben formati che la regex non corrisponde. Ad esempio, non consente spazi vuoti dopo il nome nel tag di fine. La maggior parte di questi problemi possono essere facilmente risolti, ma una volta risolti TUTTI i problemi, si ottiene qualcosa di totalmente inutilizzabile. E ovviamente il vero problema è che non vuoi solo che un parser ti dia una risposta sì / no, ma vuoi che passi le informazioni a un'applicazione che fa qualcosa di utile con esso.
Michael Kay

0

Non analizzare XML / HTML con regex, usa un parser XML / HTML appropriato e un potente query.

teoria :

Secondo la teoria della compilazione, XML / HTML non può essere analizzato utilizzando regex basato su macchina a stati finiti . A causa della costruzione gerarchica di XML / HTML è necessario utilizzare un automa pushdown e manipolare la grammatica LALR utilizzando strumenti come YACC .

strumento quotidiano realLife © ® ™ in a :

Puoi utilizzare uno dei seguenti:

xmllint viene spesso installato di default con libxml2xpath1 (controlla il mio wrapper per avere un output delimitato da newline

xmlstarlet può modificare, selezionare, trasformare ... Non installato di default, xpath1

xpath installato tramite il modulo XML :: XPath, xpath1 di perl

xidel xpath3

saxon-lint mio progetto, wrapper sulla libreria Java Saxon-HE di @Michael Kay, xpath3

oppure puoi usare linguaggi di alto livello e librerie appropriate, penso a:

's lxml( from lxml import etree)

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

, controlla questo esempio

DOMXpath, controlla questo esempio


Verifica: utilizzo di espressioni regolari con tag HTML

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.