XSLT, HTML zu XML, externe Webseite "verstehen"

arne123

Grünschnabel
Hallo Leute,

Ich suche gerade nach einer Idee, wie ich aus einer Webseite bestimmte Informationen herauslese und weiter nutzbar mache. Es geht hier erstmal nur um eine Webseite eines Restaurants das einen wöchentlich wechselnden Mittagstisch anbietet. Ich möchte die Informationen der Webseite automatisiert in mein xslt/php basiertes Sammelbestellsystem überführen.

Mein Ansatz ist die Informationen mit xslt soweit es geht heruaszufiltern, später evtl. mit php weiter verfeinern um da raus ein XML basiertes Menu zu erstellen.

Problem an dieser Stelle, die Webseite ist vermutlich über ein webbasiertes CMS gepflegt und nicht sehr sauber. Ein wichtiger Informationsbestandteil sind z.b. die Zeilenumbrüche im Text.
Und genau daran scheitere ich gerade, wie ich den Zeilenumbruch erkenne, andere Formatierungen aber ignoriere.

Ein einfaches template welches mir die ganzen überflüssigen leeren Nodes aus dem unten angehängten Ausschnitt der Website herausfiltert wäre folgendes:

HTML:
<xsl:template match="table/tr/td/div">
  <xsl:if test=". != ''">
    DIV:<xsl:value-of select="."  /> <br/>
 </xsl:if>
</xsl:template>

jetzt möchte ich aber noch folgen Funktionen ergänzen:
- Das Template soll nur die Tabelle bearbeiten in dem der Text "Mittagstischkarte" vorkommt
- Die Zeilenumbrüche im Textfluss sollen erkannt werden
- Die Gerichte werden eindeutig nur durch die Preise getrennt, d.h. einen Nummer in der Form X.XX soll erkannt werden
- Zeilen die keinen echten Text (A-Z a-z 0-9) enthalten sollen ignoriert werden.

Ist dies mit xslt lösbar ?
es sind auch mehrere templates mit mehreren Aufrufen denkbar.


Hier ein Ausschnitt aus dem Original Menu:
HTML:
<table width="100%" border="0" cellpadding="0" cellspacing="0">
                      <tr> 
                        <td width="30" height="552"></td>
                        <td width="529" valign="top">
							<div align="center"><font size="4"><b>Mittagstischkarte</b></font><br><br><font size="4"><font size="3">Unser wöchentlich wechselnder Mittagstisch</font></font> <br><font size="4"><font size="3">von 12.00 bis 14.00 Uhr</font></font></div>
<div align="center"></div>
<div align="center"></div>
<div align="center"></div>
<div align="center"></div>
<div align="center"><font size="3"></font></div>
<div align="center"><font size="3"></font></div>
<div align="center"><font size="3"></font></div>
<div align="center"><font size="3"></font></div>
<div align="center"><font size="3"></font></div>
<div align="center"><font size="3"></font>&nbsp;</div>
<div align="center"><b><font size="3">"Eintopf der Woche"</font></b><br>Linseneintopf mit Bockwurst<br>€ 5,50 <br></div>
<div align="center"><font size="3"></font>&nbsp;</div>
<div align="center"><font size="6">Tagessuppe &nbsp; 1,50 €<br><br></font>&nbsp;<br></div>
<div align="center"><font size="3">Kasseler mit Sauerkraut und Kartoffelpüree<br><b>5,50 €</b><br></font><br><font size="4"><font size="2">__________</font></font><font size="4"><br>kl. Schnitzel mit Sauce nach Wahl,<br>Bratkartoffeln und Gemüse<br><br></font><font size="4"><b>5,50 €<br><br></b></font></div>
<div align="center"></div>
<div align="center"></div>
<div align="center"></div>
<div align="center">______________</div>
<div align="center"></div>
<div align="center"></div>
<div align="center"></div>
<div align="center"><font size="4"></font></div>
<div align="center"><font size="4"></font></div><div align="center"><font size="4" color="#f0f090">frische Bratwurst<br>mit Bratkartoffeln und Gemüse<br></font></div><div align="center"></div>
<div align="center"><font size="4"></font></div>
<div align="center"><font size="4"></font></div>

<div align="center"><font size="4">5,50 €</font></div>
<div align="center">____________</div>
<div align="center"><font size="4"></font></div>
<div align="center"><font size="4">fruchtiges Hähnchengeschnetzeltes<br>im Reisrand mit Salat<br>5,50 €<br>---------<br></font></div>
<div align="center"></div>
<div align="center"><font size="4">2 Spiegeleier<br>&nbsp;mit Salzkartoffeln und Blattspinat<br>5,50 €</font></div>
<div align="center"><font size="4"></font></div>
<div align="center"></div>
<div align="center"><font size="4">_________</font><br><font size="5"><br>Dessert&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1,50 €</font><br><br><br><br></div><font size="4"><br></font> 
<div align="center"><font size="4"><font size="4"></font></font></div><font size="4">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <br><br></font>
<div align="center"></div>
<div align="center"><font size="3"></font></div>
<div align="center"></div>                        </td>
                        <td width="30"></td>
                      </tr>
                    </table>

Ich bin für jeden Hinweis dankbar,
Gruß
Arne
 
...
- Das Template soll nur die Tabelle bearbeiten in dem der Text "Mittagstischkarte" vorkommt
- Die Zeilenumbrüche im Textfluss sollen erkannt werden
- Die Gerichte werden eindeutig nur durch die Preise getrennt, d.h. einen Nummer in der Form X.XX soll erkannt werden
- Zeilen die keinen echten Text (A-Z a-z 0-9) enthalten sollen ignoriert werden.
...
Hallo,

wenn du mit PHP arbeitest und die Seite als HTML-DOM-Objekt erfasst, dann würde ich zunächst in einem Template alle Tabellen selektieren und ...

zu 1.
... dort im Template eine Variable zuweisen, die den Text des Tabellenelements mitsamt aller Nachfahrenelemente enthält - so wie hier in den XSLT-FAQ vorgeschlagen wurde. Nun brauchst du nur noch mit der contains()-Funktion nachzuprüfen, ob sich das Wort "Mittagstisch" im Variableninhalt befindet.
Wenn das der Fall ist, dann kann man mit der gleichen Vorgehensweise wie bei der Textvariablen jeden Textknoten in Tags einhüllen (beispielsweise mit dem Namen "text") und in den Ausgabebaum schreiben:
XML:
<xsl:template match="//table">
	<xsl:variable name="string">
		<xsl:apply-templates mode="string" />
	</xsl:variable>
	<xsl:if test="contains($string,'Mittagstisch')">
		<xsl:copy>
			<xsl:apply-templates mode="text" />			
		</xsl:copy>
	</xsl:if>
</xsl:template>

<xsl:template match="text()" mode="string">
  <xsl:value-of select="normalize-space()" />
</xsl:template>

<xsl:template match="text()" mode="text">
	<text>
		<xsl:value-of select="normalize-space()" />
	</text>
</xsl:template>
_
zu 2.

In deiner Beispielspeisekarte gibt es keine "Zeilenumbrüche im Textfluss". Vermutlich meinst du BR-Elemente, die aber Textknoten voneinander trennen. Deshalb werden sie auch im oben gezeigten Template verarbeitet.
_
zu 3. und 4.

Auf der gleichen Seite (wie o.g. Link) befindet sich hier ein Vorschlag, wie man mit Hilfe der translate()-Funktion nichtnumerische Zeichen löschen kann.
Wenn man festlegt, was "kein echter Text" ist, kann man das auch dafür nutzen.
Gruß retour
 
Zuletzt bearbeitet von einem Moderator:
Hallo,

super Hilfe, danke,
damit bin ich deutlich weiter.

Mit folgender Transformation habe ich schon etwas was dem was ich haben möchte deutlich ähnlicher sieht.
Das mit dem erkennen der <br> hätte ich ich vermutlich nicht herausbekommen, dazu hatte ich einfach die falschen Ansätze (+ Null Erfahrung).

XML:
<xsl:template match="table">
    <xsl:variable name="string">
        <xsl:apply-templates mode="string" />
    </xsl:variable>
    <xsl:if test="contains($string,'Mittagstischkarte')">
        
        <xsl:copy select="div">
            <xsl:apply-templates mode="noemptydiv" />
        </xsl:copy>
        
    </xsl:if>
</xsl:template>
 
<xsl:template match="text()" mode="string">
  <xsl:value-of select="normalize-space()" />
</xsl:template>

<xsl:template match="div" mode = "noemptydiv">
    <xsl:if test=". != ''">
      <xsl:apply-templates mode="text" />
  </xsl:if>
</xsl:template>

<xsl:template match="text()" mode="text">
      <xsl:value-of select="normalize-space()" /><br/>
</xsl:template>

<xsl:template match="text()"/>

EIn Problem ist mir jetzt erst bewusst geworden und zwar hat
das orig. Dokument hat tausende von verschachtelten Tabellen, in etwa so:
HTML:
<table> *1
  <tr>
    <td>
      <table>
        Seiten Navigation
      </table>
      
      <table>  
        <tr><td>Header</td> </tr>
        <tr>
          <td>
            <table>
              <tr><td> xyz </td></tr>
            </table>
          </td>
        </tr>
        <tr>
          <td>
            <table> *2
              <tr><td>Menueauschnitt (siehe erstes post)</td></tr>
            </table>
          </td>
        </tr>
        <tr>
          <td>
            <table>
              <tr><td>Footer</td></tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>
<table>

  • Von meiner obigen Transformation wird jetzt Table *1 benutzt, ich würde mich aber gerne auf Table 2 beschränken (das wäre die, die das Schlüsselwort "Mittagstischkarte" enthält"). Habe ich das jetzt falsch umgesetzt ?

  • Bezüglich des Heruausfilterns von Zeichens würde ich das jetzt so lösen wollen das ich Zeilen, die ausschließlich gleiche Zeichen oder nur aus zwei unterschiedlichen Zeichen aber ein länge grösser 4, (alles ohne whitespace) nicht mit übernehmen möchte. Dazu habe ich aber auch noch keinen Ansatz mit xlst gefunden, translate() scheint mir noch nicht der richtige weg zu sein oder?

  • In meiner Transformation ist mir nicht klar warum meine "noemptydiv" if abfrage nicht direkt lösen kann, in etwa so:
XML:
<xsl:template match="table">
    <xsl:variable name="string">
        <xsl:apply-templates mode="string" />
    </xsl:variable>
    <xsl:if test="contains($string,'Mittagstischkarte')">
        <xsl:copy select="div">
          <xsl:if test=". != ''">
            <xsl:apply-templates mode="text" />
          </xsl:if>
        </xsl:copy>
    </xsl:if>
</xsl:template>

Gruß
Arne
 
Zuletzt bearbeitet von einem Moderator:
Hallo Arne,

ich habe die infernalische HTML-Speisekarte mal versuchsweise nachgebaut und mit folgendem Stylesheet transformiert:
XML:
  <xsl:param name="unechterText" select="'_-*'" />

  <!-- Nur TABLE-Elemente verarbeiten, die keine TABLE-Elemente
       als Nachfahren haben -->
  <xsl:template match="//table[not(descendant::table)]">
      <xsl:variable name="string">
        <xsl:apply-templates mode="string" />
      </xsl:variable>
      <xsl:if test="contains($string,'Mittagstisch')">
        <output1>
          <xsl:apply-templates mode="text" />
        </output1>
      </xsl:if>
  </xsl:template>

  <xsl:template match="text()" mode="string">
    <xsl:value-of select="normalize-space()" />
  </xsl:template>

  <xsl:template match="text()" mode="text">
    <text>
      <!-- "Unechte" Zeichen aus dem TEXT-Element löschen,
            u.U. erhält man dadurch ein leeres Element -->
      <xsl:value-of select="normalize-space(translate(.,$unechterText,''))" />
    </text>
  </xsl:template>

  <!-- Default Output unterbinden -->
  <xsl:template match="text()|@*" />
Die TABLE-Elemente werden mit einem Prädikat gefiltert, das nur Tabellen der untersten Ebene (die also keine TABLE-Elemente als Nachfahren haben) zulässt. Damit sollte das Problem mit den verschachtelten Tabellen gelöst sein.
Weiterhin habe ich ein Parameter definiert, der die "unechten" Textzeichen enthält. Das sind jetzt der Unterstrich [_], der Bindestrich [-] und das harte bzw. erzwungene Leerzeichen [& #160;]. Oben im Quelltext steht im selekt-Attribut statt des erzwungenen Leerzeichens ein Stern ! Diese Zeichen werden bei der Ausgabe aus dem Text genommen, so dass u.U. ein leeres Element entsteht.
Ich würde die leeren Elemente erst mal nicht löschen, vielleicht kann man sie noch gebrauchen.
Damit ergibt sich bei mir folgendes XML:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="output1.xsl"?>
<output1>
  <text>Mittagstischkarte</text>
  <text>Unser wöchentlich wechselnder Mittagstisch</text>
  <text>von 12.00 bis 14.00 Uhr</text>
  <text/>
  <text>"Eintopf der Woche"</text>
  <text>Linseneintopf mit Bockwurst</text>
  <text>€ 5,50</text>
  <text/>
  <text>Tagessuppe 1,50 €</text>
  <text/>
  <text>Kasseler mit Sauerkraut und Kartoffelpüree</text>
  <text>5,50 €</text>
  <text/>
  <text>kl. Schnitzel mit Sauce nach Wahl,</text>
  <text>Bratkartoffeln und Gemüse</text>
  <text>5,50 €</text>
  <text/>
  <text>frische Bratwurst</text>
  <text>mit Bratkartoffeln und Gemüse</text>
  <text>5,50 €</text>
  <text/>
  <text>fruchtiges Hähnchengeschnetzeltes</text>
  <text>im Reisrand mit Salat</text>
  <text>5,50 €</text>
  <text/>
  <text>2 Spiegeleier</text>
  <text>mit Salzkartoffeln und Blattspinat</text>
  <text>5,50 €</text>
  <text/>
  <text>Dessert 1,50 €</text>
  <text/>
</output1>
Im nächsten Schritt habe ich versucht die alle Preise und die Namen zu separieren:
XML:
  <xsl:template match="/output1">
    <output2>
      <xsl:apply-templates select="text" />
    </output2>
  </xsl:template>

  <xsl:template match="text">
    <xsl:variable name="name" select="normalize-space(translate(.,'0123456789,€',''))" />
    <xsl:variable name="preis" select="normalize-space(translate(.,translate($name,' ',''),''))" />

    <xsl:if test="string-length($name)>0">
      <name><xsl:value-of select="$name" /></name>
    </xsl:if>

    <xsl:if test="string-length($preis)>0 and contains($preis,'€')">
      <preis><xsl:value-of select="$preis" /></preis>
    </xsl:if>

    <xsl:if test="string-length($name)=0"><empty /></xsl:if>

  </xsl:template>
In der Variablen "name" werden alle numerischen Zeichen sowie das Komma und das Euro-Zeichen aus dem Text gelöscht.
In der Variablen "preis" werden alle Zeichen gelöscht, die in der Variable "name" vorkommen mit Ausnahme des Leerzeichens. Damit müssten dann in der Preisvariablen nur noch numerische Zeichen, das Komma, Euro- und Leerzeichen stehen. Der Variableninhalt wird dann mit unterschiedlichen Elementnamen in den Ausgabebaum geschrieben:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="output2.xsl"?>
<output2>
  <name>Mittagstischkarte</name>
  <name>Unser wöchentlich wechselnder Mittagstisch</name>
  <name>von . bis . Uhr</name>
  <empty/>
  <name>"Eintopf der Woche"</name>
  <name>Linseneintopf mit Bockwurst</name>
  <preis>€ 5,50</preis>
  <empty/>
  <empty/>
  <name>Tagessuppe</name>
  <preis>1,50 €</preis>
  <empty/>
  <name>Kasseler mit Sauerkraut und Kartoffelpüree</name>
  <preis>5,50 €</preis>
  <empty/>
  <empty/>
  <name>kl. Schnitzel mit Sauce nach Wahl</name>
  <name>Bratkartoffeln und Gemüse</name>
  <preis>5,50 €</preis>
  <empty/>
  <empty/>
  <name>frische Bratwurst</name>
  <name>mit Bratkartoffeln und Gemüse</name>
  <preis>5,50 €</preis>
  <empty/>
  <empty/>
  <name>fruchtiges Hähnchengeschnetzeltes</name>
  <name>im Reisrand mit Salat</name>
  <preis>5,50 €</preis>
  <empty/>
  <empty/>
  <name>Spiegeleier</name>
  <name>mit Salzkartoffeln und Blattspinat</name>
  <preis>5,50 €</preis>
  <empty/>
  <empty/>
  <name>Dessert</name>
  <preis>1,50 €</preis>
  <empty/>
</output2>

Im letzten Schritt müsste man nun noch die flache Struktur vertiefen und das könnte man das mit Hilfe der EMPTY-Elemente machen:

XML:
  <!-- STRUCT-Key matcht alle NAME- und PREIS-Elemente, die das übergebene USE-Element
       als nächstes vorhergehendes EMPTY-Element haben -->
  <xsl:key name="struct" match="name|preis" use="generate-id(preceding-sibling::empty[1])" />

  <xsl:template match="/output2">
    <output3>
      <xsl:apply-templates />
    </output3>
  </xsl:template>

  <xsl:template match="empty">
    <!-- Nur EMPTY-Elemente bearbeiten, denen ein NAME-Element folgt -->
    <xsl:if test="name(following-sibling::*[1])='name'">
      <gericht>
        <!-- Alle NAME- und PREIS-Elemente in den Ausgabebaum kopieren,
             die dem selektierten EMPTY-Element unmittelbar folgen -->
        <xsl:copy-of select="key('struct',generate-id(.))" />
      </gericht>
    </xsl:if>
  </xsl:template>

  <!-- Default Output unterbinden -->
  <xsl:template match="text()" />
Das Stylesheet ist kommentiert, so dass man sich evtl. darin zurecht finden könnte. Damit ergibt sich dann folgende Ausgabe:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<output3>
  <gericht>
    <name>"Eintopf der Woche"</name>
    <name>Linseneintopf mit Bockwurst</name>
    <preis>€ 5,50</preis>
  </gericht>
  <gericht>
    <name>Tagessuppe</name>
    <preis>1,50 €</preis>
  </gericht>
  <gericht>
    <name>Kasseler mit Sauerkraut und Kartoffelpüree</name>
    <preis>5,50 €</preis>
  </gericht>
  <gericht>
    <name>kl. Schnitzel mit Sauce nach Wahl</name>
    <name>Bratkartoffeln und Gemüse</name>
    <preis>5,50 €</preis>
  </gericht>
  <gericht>
    <name>frische Bratwurst</name>
    <name>mit Bratkartoffeln und Gemüse</name>
    <preis>5,50 €</preis>
  </gericht>
  <gericht>
    <name>fruchtiges Hähnchengeschnetzeltes</name>
    <name>im Reisrand mit Salat</name>
    <preis>5,50 €</preis>
  </gericht>
  <gericht>
    <name>Spiegeleier</name>
    <name>mit Salzkartoffeln und Blattspinat</name>
    <preis>5,50 €</preis>
  </gericht>
  <gericht>
    <name>Dessert</name>
    <preis>1,50 €</preis>
  </gericht>
</output3>
Man könnte das alles sicherlich auch ganz anders machen, aber vielleicht hilft dir das weiter. :)
 
Zuletzt bearbeitet von einem Moderator:
Hey super,

danke für die ausführliche Antwort, bzw. die Lösung meines Problems :)

Mir war die translate Funktion aber immer noch nicht sympatisch, es kann ja durchaus vorkommen, das ein oder zwei -- bzw. __ im Text vorkommen, daher habe ich das jetzt so gelöst:

XML:
  <xsl:template match="text()" mode="text">
    <xsl:if test="not(contains(.,'---'))  and not(contains(.,'___'))  and string-length(.)>2">
  <xsl:value-of select="normalize-space()" />

gruß
Arne
 
Zuletzt bearbeitet von einem Moderator:
Zurück