Matze: Element und Kinder aus XHTML extrahieren und zu XML

Hallo!

Ich schlag mich nun schon einige Zeit damit herum eine oder mehrere Listen aus einer XHTML-Datei zu extrahieren und deren Listenpunkte in ein Array zu übergeben.

Die Liste wird dynamisch erstellt und kann bei mehreren Einträgen mehrmals nacheinander im Quelltext stehen. Sie sieht/sehen dabei so aus

  
<ul class="foo">  
    <li class="bar">abc</li>  
    <li class="bla">123</li>  
    <li class="blub">a1b2c3</li>  
</ul>  

Daraus hätte ich gern ein Array

  
array => foo[0]  
         foo[0] => bar => abc  
         foo[0] => bla => 123  
         foo[0] => blub => a1b2c3  
// bei mehreren Listen:  
      => foo[1]  
         foo[1] =>.....  

Das umschreiben in ein Array krieg ich wahrscheinlich noch einfach mit den SimpleXML-Funktionen hin.
Ich weiß aber nicht, wie ich die Listen aus dem XHTML extrahieren kann.

Hat jemand eine Idee? Kann mir bitte jemand helfen?

Danke und Grüße, Matze

  1. Lieber Matze,

    Das umschreiben in ein Array krieg ich wahrscheinlich noch einfach mit den SimpleXML-Funktionen hin.
    Ich weiß aber nicht, wie ich die Listen aus dem XHTML extrahieren kann.

    wahrscheinlich könnte man mit ein paar regulären Ausdrücken und preg_replace() den relevanten XHTML-Code isolieren, um ihn dann dem XML-Parser (SimpleXML?) zu füttern.

    Mein Vorschlag:

    1.) Alle inline-Elemente belassen, aber deren Tags entfernen.
    $html = preg_replace('~(?s)</?(a|b|br|em|i|q|strong|u)( ?/)?>~', '', $html);

    2.) Alternativ-Text bei Bildern erhalten
    $html = preg_replace('~(?s)<img .*?alt="([^"]*)"[^>^]*>~', '\\1', $html)

    3.) Alle Block-Elemente entfernen, die kein <ol>, <ul> oder <li> sind (aus Deinem Beispiel-Code geht anscheinend hervor, dass <li>-Elemente keine weiteren Block-Elemente enthalten):
    $html = preg_replace('~(?s)<(blockquote|center|dl|dt|dd|head|hr|p|table|td|th)>.*?</\\1>~', '', $html);

    4.) Prüfen, ob noch weitere unerwünschte Block-Elemente zu entfernen sind (mitsamt ihrem Inhalt, oder dann doch diesen zumindest erhalten?):
    ... jetzt darfst Du. Denke an <div>-Elemente, und an <body>...

    Hoffentlich hilft das etwas weiter?

    Liebe Grüße,

    Felix Riesterer.

    --
    ie:% br:> fl:| va:) ls:[ fo:) rl:° n4:? de:> ss:| ch:? js:) mo:} zu:)
    1. Hallo Felix!

      Ich bin dir sehr dankbar.
      Mit den regulären Ausdrücken komm ich weniger als schlecht zurecht.

      wahrscheinlich könnte man mit ein paar regulären Ausdrücken und preg_replace() den relevanten XHTML-Code isolieren, um ihn dann dem XML-Parser (SimpleXML?) zu füttern.

      Mein erster Gedanke war, die komplette XHTML als XML zu übernehmen und die Listen(n) über ihre Klasse anzusprechen. Nur leider hab ich da gar keinen Ansatz denn SimpleXML kommt mit den XHTML-Dokumenten nicht zurecht.

      3.) Alle Block-Elemente entfernen, die kein <ol>, <ul> oder <li> sind (aus Deinem Beispiel-Code geht anscheinend hervor, dass <li>-Elemente keine weiteren Block-Elemente enthalten):

      Genau genommen enthalten die <li>-Elemente immer ausschließlich Text. Und der ist auch noch sehr kurz. Etwa in der Form "Name, Alter, Geschlecht...".

      Reicht es in dem Fall nicht, alle Elemente zu entfernen die nicht <ul> oder <li> sind? Inklusive HTML, Body und Doctype?

      Also:

        
      $html = preg_replace('(?s<  
      (!DOCTYPE|html|body|address|blockquote|del|div|dl|fieldset|form|h1|h2|h3|h4|h5|h6|  
      hr|ins|noscript|ol|p|pre|table|center|dir|isindex|menu|noframes|  
      a|abbr|acronym|applet|b|basefont|bdo|big|br|button|cite|code|del|dfn|em| font|i|img|ins|input|iframe|kbd|label|map|object|q|samp|script|select| small|span|strong|sub|sup|textarea|tt|var)  
      
      >.*?</\\1>~', '', $html);  
      
      

      Hoffentlich hilft das etwas weiter?

      Weiß ich noch nicht. Grad ein bisschen viel input ;)

      Danke und Grüße, Matze

      1. Lieber Matze,

        Reicht es in dem Fall nicht, alle Elemente zu entfernen die nicht <ul> oder <li> sind? Inklusive HTML, Body und Doctype?

        das "Entfernen" ist so eine Sache. Du willst zwar manche Elemente _komplett_ entfernen, bei anderen Elementen (z.B. <body>) möchtest Du nur die Tags entfernen, den Inhalt aber erhalten.

        $html = preg_replace('(?s<

        Das ist ein Syntaxfehler. (?s) ist ein Schalter in der PCRE-Schreibweise, der Zeilenumbrüche mit einschließt, sodass der String in Gänze behandelt wird.

        (!DOCTYPE|html|body|address|...

        Hier haben wir eine ODER-verknüpfte Liste mit festen (alternativen) Zeichenfolgen, die nach diesem Schema aufgebaut ist: (erstens|zeitens|drittens)
        Die Klammerung bedeutet nicht nur, dass der Klammerninhalt als "ODER-Liste" zu verstehen ist, sondern bewirkt auch, dass der Inhalt "gemerkt" wird, um ihn später als Referenz wieder zu benutzen.

        ~Er (war|ist) davon überzeugt, dass auch sie davon in Kenntnis gesetzt \1.~

        Dieses Muster passt auf folgende zwei Sätze:

        • Er ist davon überzeugt, dass auch sie davon in Kenntnis gesetzt ist.
        • Er war davon überzeugt, dass auch sie davon in Kenntnis gesetzt war.

        Das Muster passt aber nicht auf folgende Sätze:

        • Er ist davon überzeugt, dass auch sie davon in Kenntnis gesetzt war.
        • Er war davon überzeugt, dass auch sie davon in Kenntnis gesetzt ist.

        Der Ausdruck in Klammern wird "gemerkt". Da es die erste Klammer im Muster ist, kann man ihren Inhalt mit \1 referenzieren.

        <(!DOCTYPE|html|body|...|textarea|tt|var)>.*?</\1>

        Das wird schiefgehen. Du ignorierst mit dieser Schreibweise, dass Elemente im öffnenden Tag auch Attribute haben können (denke an Deine Klassen!). Und Du setzt ein schließendes Tag ebenfalls voraus, was nicht in allen Fällen (denke an den Doctype!) existiert. Gerade bei <br />/<hr />, <meta /> und <img /> hast Du in XHTML auch selbstschließende Elemente, die per Definition inhaltsleer sind. Auch diese bekommst Du so nicht entfernt.

        Besser (aber noch immer nicht gut):
        <(!DOCTYPE|html|...)( [^>]*)?>.*?</\1>

        Durch das Fragezeichen nach der zweiten Klammer wird eventuell Folgendes mit berücksichtigt. Wenn es fehlt ist das trotzdem ein Treffer. Der Inhalt der zweiten Klammer wird auch "gemerkt", auch wenn wir ihn nirgendwo mehr referenzieren. Bei unserem Muster ist das von der Performance her auch OK. Wenn Du das abstellen wolltest, um z.B. bei komplexeren Mustern Speicher zu sparen, dann könntest Du an den Anfang der zweiten Klammer ?: notieren, was das "Merken" abstellt:

        <(!DOCTYPE|html|...)(?: [^>]*)?>.*?</\1>

        Das Muster wird Dich aber mit seiner Wirkungsweise enttäuschen. Zum Beispiel würde <html>.*?</html> das komplette Dokument leeren, da Du den Inhalt des <html>-Elementes ebenso wie seine umschließenden Tags durch einen leeren String ersetzen lässt. Damit hast Du dann Dein Ziel endgültig nicht erreicht... Vergiss nicht, was Du erhalten willst, und wie und wo es im XHTML-Dokument notiert ist (Elternelemente usw.)!

        Meine mehrstufige Vorgehensweise hatte durchaus ihren Grund - der Dir jetzt vielleicht auch einleuchtet. Dieses "tag soup parsing" ist nicht trivial. Der Gebrauch von regulären Ausdrücken ist zum Parsen auch nicht das geeignete Werkzeug. Jedoch wollten wir ja nur einen bestehenden String von unerwünschten Bestandteilen säubern. Aber da das eben nicht trivial ist, wir aber aufgrund diverser Rahmenbedingungen gewisse Möglichkeiten von vorneherein ausschließen können (z.B. <li>-Elemente enthalten nur Textknoten und keine weiteren HTML-Elemente als Kindelemente), ist ein Vorgehen mit regulären Ausdrücken tatsächlich überhaupt sinnvoll.

        Liebe Grüße,

        Felix Riesterer.

        --
        ie:% br:> fl:| va:) ls:[ fo:) rl:° n4:? de:> ss:| ch:? js:) mo:} zu:)
        1. Hallo Felix!

          Ich versteh es immernoch nicht ganz.

          Wenn mir das:
          <(!DOCTYPE|html|...)(?: [^>]*)?>.*?</\1>

          den Inhalt von <html> löscht, müsste ich doch mit der selben regexp
          <ul class="foo"> </ul> finden können?
          also
          '~(?s)</?(ul)( class="foo")( [^>]*)?>.*?</\\1>'

          oder?

          Ich kann dein Vorgehen nicht nachvollziehen.
          Wieso soll ich die Tags von inline-Elementen entfernen aber den Inhalt belassen? Inline-Elemente dürfen doch sowieso keine block-Elemente enthalten und können somit komplett weg oder?

          Gibt es keine einfach Pattern die sagt "alle <ul class="foo"> dazwischen irgendwas bis zum nächsten </ul> speichern"?

          Ich müsste doch nur alles vor dem ersten <ul... löschen, warten bis </ul> kommt, den Teil speichern, schauen ob noch eine <ul... kommt, wenn nicht fertig.
          Ich glaub wenn ich den String in einer Schleife zerschneide komm ich schneller ans Ziel oder?

          Ich komm auch überhaupt nicht mit den regexp hinterher.

          1.) Alle inline-Elemente belassen, aber deren Tags entfernen.

          » $html = preg_replace('~(?s)</?(a)( [^>]*)?>~', '', $html);

          Das entfernt mir Links z.B. komplett, nicht nur die Tags.?

          Dank und Grüße, Matze

          1. Hallo Felix,

            »» 1.) Alle inline-Elemente belassen, aber deren Tags entfernen.
            $html = preg_replace('~(?s)</?(a)( [^>]*)?>~', '', $html);

            Das entfernt mir Links z.B. komplett, nicht nur die Tags.?

            Oh da habe ich mich geirrt. Bei meiner Test-Datei bestand das Menü nur aus Bildern und die habe ich gleich mitentfernt^^

            Grüße, Matze

        2. Hallo,

          Die Klammerung bedeutet nicht nur, dass der Klammerninhalt als "ODER-Liste" zu verstehen ist, sondern bewirkt auch, dass der Inhalt "gemerkt" wird, um ihn später als Referenz wieder zu benutzen.

          Der Ausdruck in Klammern wird "gemerkt". Da es die erste Klammer im Muster ist, kann man ihren Inhalt mit \1 referenzieren.

          ich möchte erwähnen, dass man den Inhalt (seit etlichen Jahren) besser mit $1 bzw. ${1} statt mit \1 referenziert, ich zitiere aus dem Handbuch zu preg_replace():

          <zitat>
              replacement darf Referenzen in der Form \n oder (ab PHP 4.0.4) $n
              enthalten, wobei Letztere vorzuziehen ist.
          </zitat>

          Freundliche Grüße

          Vinzenz

    2. Hallo nochmal,

      Hoffentlich hilft das etwas weiter?

      Da keine deiner regulären Ausdrücke funktioniert, also rein gar nichts am Inhalt der Variable ändert, leider auch nicht wirklich.

      Schon der erste regex sollte mir doch die z.B. Links entfernen.
      Tut er aber nicht :(

      Grüße, Matze

      1. Lieber Matze,

        Da keine deiner regulären Ausdrücke funktioniert

        ja, die waren relativ schnell hingeschrieben und ungetestet. Das hätte ich erwähnen sollen. Im Grunde waren sie auch nur dazu gedacht, Dir eine Idee zu vermitteln.

        Schon der erste regex sollte mir doch die z.B. Links entfernen.
        Tut er aber nicht :(

        Ja, ich habe mögliche Attribute vergessen zu berücksichtigen.

        1.) Alle inline-Elemente belassen, aber deren Tags entfernen.

        $html = preg_replace('~(?s)</?(a|b|br|em|i|q|strong|u)( [^>]*)?>~', '', $html);

        Liebe Grüße,

        Felix Riesterer.

        --
        ie:% br:> fl:| va:) ls:[ fo:) rl:° n4:? de:> ss:| ch:? js:) mo:} zu:)
  2. Ich würde das mittels preg_match_all() lösen

      
    $items = array();  
    preg_match_all('~<ul class="foo">(.*)</ul>~iUs', $html, $aul, PREG_PATTERN_ORDER);  
    foreach($aul[1] as $value) {  
      preg_match_all('~<li class="([^"]+)">([^<]+)</li>~iUs', $value, $ali, PREG_PATTERN_ORDER);  
      $items['foo'][] = array_combine($ali[1], $ali[2]);  
    }  
    
    

    Wenn du nur Text innerhalb der li hast, dürften die Pattern das gewünschte ergeben.

    1. Liebe(r) DiBo33,

      preg_match_all('~<ul class="foo">(.*)</ul>~iUs', $html, $aul, PREG_PATTERN_ORDER);

      das setzt voraus, dass nur eine einzige <ul> auf der Seite existiert, da Dein Muster gierig ist und den Fall, dass zwischen zwei <ul>-Elementen andere Inhalte stehen, keine Vorkehrungen trifft. Außerdem würde ich die Klasse nicht fest codieren, sondern lieber <ul(?: [^>]*)?> notieren...

      Liebe Grüße,

      Felix Riesterer.

      --
      ie:% br:> fl:| va:) ls:[ fo:) rl:° n4:? de:> ss:| ch:? js:) mo:} zu:)
      1. Hallo Felix, hallo DiBo33,

        das setzt voraus, dass nur eine einzige <ul> auf der Seite existiert (..)

        Das habe ich auch gerade bemerkt.
        Schade, beim ersten Test mit 1 <ul> sah es so schön einfach aus.

        Meine Idee, basierend auf deinen regex ist jetzt folgende:
        Ich lösche alle Tags über und inkl. <body>.
        Also <!DOCTYPE..., <html>, <meta>, <style>, <script>, <title>, <head>, <link>, <body>.
        Hab ich was vergessen?

        Der Inhalt der Tags bleibt stehen.

        Somit hab ich nur noch den Inhalt von <body> umgeben von Text. Oder?
        Dann kodier ich das Ganze in UTF-8 und kleb vorn einfach ein <xml... ran.

        Zack, schon müsste ich einen schönen DOM-Baum haben der XML-Verwertbar ist. Oder? Hab ich was vergessen?

        Danke und Grüße, Matze

        1. Lieber Matze,

          Meine Idee, basierend auf deinen regex ist jetzt folgende:
          Ich lösche alle Tags über und inkl. <body>.
          Also <!DOCTYPE..., <html>, <meta>, <style>, <script>, <title>, <head>, <link>, <body>.

          wenn Du <head> mitsamt inhalt löschst, dann sparst Du Dir 'ne Menge Schreibarbeit und Rechenleistung.

          Der Inhalt der Tags bleibt stehen.

          Von allen oben genannten Elementen? Dann steht da Titel und eventuelles CSS oder JavaScript!

          Somit hab ich nur noch den Inhalt von <body> umgeben von Text. Oder?

          Kommt auf Deine Muster und ihre Ersetzungen an!

          Dann kodier ich das Ganze in UTF-8 und kleb vorn einfach ein <xml... ran.

          Kannst Du dafür garantieren, dass der Inhalt im <body> valides XML ist?

          Zack, schon müsste ich einen schönen DOM-Baum haben der XML-Verwertbar ist. Oder? Hab ich was vergessen?

          Außer der Validitätsprüfung nichts... vermute ich.

          Liebe Grüße,

          Felix Riesterer.

          --
          ie:% br:> fl:| va:) ls:[ fo:) rl:° n4:? de:> ss:| ch:? js:) mo:} zu:)
          1. Hallo Felix,

            wenn Du <head> mitsamt inhalt löschst, dann sparst Du Dir 'ne Menge Schreibarbeit und Rechenleistung.

            Stimmt, jetzt muss ich nur noch mit deinen Beispielen und ein bisschen herumprobieren die regex dafür finden :)

            »» Der Inhalt der Tags bleibt stehen.

            Von allen oben genannten Elementen? Dann steht da Titel und eventuelles CSS oder JavaScript!

            Ja und stimmt. Also doch komplett <head> und auch <script> löschen. Nur <body> kann ich als Element ja behalten wenn

            Kannst Du dafür garantieren, dass der Inhalt im <body> valides XML ist?

            ich das getestet hab. Aber eigentlich schon.
            Wenn nicht, muss ich wohl doch nochmal drüber nachdenken.

            Und ich dachte XHTML wär so schön XML-konform :(

            Danke und Grüße, Matze

          2. Kannst Du dafür garantieren, dass der Inhalt im <body> valides XML ist?

            Wieso sollte er dafür garantieren?
            Damit ein XML-Dokument geparst werden kann, muss es wohlgeformt, aber nicht valide sein!

            Außer der Validitätsprüfung nichts... vermute ich.

            Es ist keine Validitätsprüfung notwendig.

            Mathias

        2. Somit hab ich nur noch den Inhalt von <body> umgeben von Text. Oder?
          Dann kodier ich das Ganze in UTF-8 und kleb vorn einfach ein <xml... ran.

          Zack, schon müsste ich einen schönen DOM-Baum haben der XML-Verwertbar ist. Oder? Hab ich was vergessen?

          Häh? Wieso der Aufwand?
          Du hast auch mit diesem ganzen Drumherum ein XML-verwertbares Dokument und kannst darin gezielt mit den Kindelementen von body arbeiten.
          Wo ist das Problem?

          Mathias

      2. das setzt voraus, dass nur eine einzige <ul> auf der Seite existiert, da Dein Muster gierig ist und den Fall, dass zwischen zwei <ul>-Elementen andere Inhalte stehen, keine Vorkehrungen trifft. Außerdem würde ich die Klasse nicht fest codieren, sondern lieber <ul(?: [^>]*)?> notieren...

        Dafür wird der Modifier U eingesetzt

          
        <pre><?php  
        $html = <<<EOTHTML  
        <html>  
        <head>  
        <title>Titel</title>  
        </head>  
        <body>  
        <div>text</div>  
        <ul class="foo">  
            <li class="bar">abc</li>  
            <li class="bla">123</li>  
            <li class="blub">a1b2c3</li>  
        </ul>  
        <div>text</div>  
        <ul class="foo">  
            <li class="bar1">abc</li>  
            <li class="bla1">123</li>  
            <li class="blub1">a1b2c3</li>  
        </ul>  
        <div>text</div>  
        <ul class="foo">  
            <li class="bar2">abc</li>  
            <li class="bla2">123</li>  
            <li class="blu2b">a1b2c3</li>  
        </ul>  
        <div>text</div>  
        </body>  
        </html>  
        EOTHTML;  
        $items = array();  
        preg_match_all('~<ul class="foo">(.*)</ul>~iUs', $html, $aul, PREG_PATTERN_ORDER);  
        foreach($aul[1] as $value) {  
          preg_match_all('~<li class="([^"]+)">([^<]+)</li>~iUs', $value, $ali, PREG_PATTERN_ORDER);  
          $items['foo'][] = array_combine($ali[1], $ali[2]);  
        }  
        print_r($items);  
        
        

        Ergibt:

          
        Array  
        (  
            [foo] => Array  
                (  
                    [0] => Array  
                        (  
                            [bar] => abc  
                            [bla] => 123  
                            [blub] => a1b2c3  
                        )  
          
                    [1] => Array  
                        (  
                            [bar1] => abc  
                            [bla1] => 123  
                            [blub1] => a1b2c3  
                        )  
          
                    [2] => Array  
                        (  
                            [bar2] => abc  
                            [bla2] => 123  
                            [blu2b] => a1b2c3  
                        )  
          
                )  
          
        )  
        
        

        Aber der Einwand mit der class ist schon richtig.

        1. Liebe(r) DiBo33,

          Dafür wird der Modifier U eingesetzt

          und wie reagiert das Teil auf verschachtelte Listen?

          Liebe Grüße,

          Felix Riesterer.

          --
          ie:% br:> fl:| va:) ls:[ fo:) rl:° n4:? de:> ss:| ch:? js:) mo:} zu:)
          1. Liebe(r) DiBo33,

            Lieber DiBo33

            und wie reagiert das Teil auf verschachtelte Listen?

            Nun, hier ist doch die Aufgabe relativ simpel, es handelt sich um einfache Listen, welche nicht verschachtelt sind und es sollen alle Listen, welche class="foo" beinhalten erkannt werden.
            Somit denke ich ist mein Ansatz für diese Aufgabe eher geeignet als die ganze Ausschneiderei.
            Deine Lösung versucht halt jede erdenkliche Möglichkeit mit einzubeziehen, was aber nicht immer der Fall sein muss.
            Ich lasse mich aber immer gerne vom Gegenteil überzeugen.

  3. Das umschreiben in ein Array krieg ich wahrscheinlich noch einfach mit den SimpleXML-Funktionen hin.
    Ich weiß aber nicht, wie ich die Listen aus dem XHTML extrahieren kann.

    Das geht über die DOM-Schnittstelle.
    $uls = $doc->getElementsByTagName('ul');
    Die kannst du dann durchlaufen, die Klasse über getAttribute abfragen, die Kind-Knoten (li-Elemente) durchlaufen und diese verarbeiten (die Klasse und den Wert des Text-Kindknotens abfragen).

    Das Herum-GeRegExpe hier verstehe ich nicht ganz...

    Mathias

    1. Die kannst du dann durchlaufen, die Klasse über getAttribute abfragen, die Kind-Knoten (li-Elemente) durchlaufen und diese verarbeiten (die Klasse und den Wert des Text-Kindknotens abfragen).

      Den Teil kann man auch nach Gusto mit SimpleXML umsetzen, ist mglw. einfacher. Wenn man einmal ein Knoten über das DOM zu fassen bekommen hat, kann man die Schnittstelle auf SimpleXML wechseln und umgekehrt.

      Mathias

      1. Hallo Mathias,

        Den Teil kann man auch nach Gusto mit SimpleXML umsetzen, ist mglw. einfacher. Wenn man einmal ein Knoten über das DOM zu fassen bekommen hat, kann man die Schnittstelle auf SimpleXML wechseln und umgekehrt.

        Ja, so hatte ich mir das vorgestellt. Leider komm ich grad zeitlich nicht dazu mich weiter damit zu befassen -> wichtigeres Projekt hat sich vergeschoben.

        Aber dein Vorschlag scheint mir im Moment der sinnvollste und wie du schon meintest, man muss nicht immer jede Eventualität berücksichtigen.

        Ich werde bestimmt nochmal drauf zurückkommen wenn die Zeit es wieder erlaubt :)

        Danke und Grüße, Matze