PHP Maniac

Das PHP Blog für PHP-Profis und alle die es werden wollen

HTML Seiten Crawlen (Links extrahieren)

30 April, 2009 (08:52) | Crawler, Grundlagen, PHP, Regular Expressions, Server | Von: PHP Maniac

Vor einigen Tagen habe ich ja einen Artikel zum HTTP GET verfasst. Die Resonanz, besonders vor dem Hintergrund der Crawler-Programmierung, hat mich doch positiv überrascht und daher werde ich nun noch einige Artikel zu dem Thema Crawler schreiben. Mit dem HTTP GET Request wissen wir bereits, wie wir Webseiten von einem Server mit PHP abrufen können. Zum fertigen mini Crawler fehlt eigentlich nur noch eine Analyse der gewonnen HTML Seiten um weitere Links zu finden und dadurch mehr Seiten Crawlen zu können. Da jedoch jede Webseite anders aussieht, ist dieses Unterfangen nicht ganz leicht zu lösen. Das es aber definitiv nicht aussichtslos ist, zeigt ein näherer Blick auf die Regular Expressions.

Zunächst einmal einige Worte zu den magischen Regular Expressions: Regular Expressions kommen überall dort ins Spiel, wo veränderliche Daten nach bestimten Mustern durchsucht werden sollen. Hierbei lässt sich sowohl auf das Vorhandensein von Mustern prüfen, als auch Teile der Daten nach Suchmustern extrahieren. Glücklicherweise werden die Regular Expressions von PHP direkt unterstützt und es bietet sich somit ein relativ leichter Einstieg. Das Formulieren der Suchmuster ist jedoch etwas gewöhnungsbedürftig, aber man sollte sich nicht abschrecken lassen: Die Übung macht den Meister!

Mögliche Linkformen

Bevor wir also ein Suchmuster erstellen können, sollten wir uns Fragen wie Links im HTML Quelltext überhaupt aussehen können. Viele werden jetzt sagen, dass das ja einfach sei und wir einfach nur nach dem passenden HTML Tag suchen müssen. Das ist im Prinzip auch richtig, jedoch gibt es eine Vielzahl von Möglichkeiten, einen (mehr oder weniger) konformen Link in eine Webseite einzubinden. Hier mal einige Beispiele:

<a href="http://www.domain.de">Linktext</a>
<a href="http://www.domain.de">Linktext</a>
<a href="/index.php">Linktext</a>
<a href="index.php?seite=test">Linktext</a>
<a href="http://www.domain.de">Linktext</a>

Dieses Spiel lässt sich quasi beliebig fortsetzen, da die Browser den HTML Code auch sehr gutmütig interpretieren funktionieren viele verschiedene Formate. Wir wollen uns fürs Erste auf einen einfachen, standardkonformen Link, beschränken:

<a href="http://www.domain.de">Linktext</a>

Linkerkennung mit Regular Expressions

Um diese Art von Links zu erkennen, müssen wir jetzt ein Suchmuster schreiben, was diese in möglichst vielen Versionen erkennen kann. Da Regular Expressions ein sehr komplexes Thema sind, möchte ich an dieser Stelle nicht zu tief einsteigen. Bei Gelegenheit werde ich hierzu weitere Artikel veröffentlichen. Daher werde ich zunächst nur die wichtigsten Grundlagen, die wir für das Finden von Links benötigen.

Die Allgemeine Form eines Suchmusters in Regex sieht wie folgt aus:

/suchmuster/optionen

Ein mögliches Regex um Links zu erkennen wäre z.B.

/<a.*?href="(.*?)".*?>(.*?)</is

Die Optionen sind in diesem Falle sehr wichtig, das i steht für „Case Insensitive“, also Groß- und Kleinschreibung wird ignoriert. Weiterhin wird durch die s Option die Regex Engine angewiesen die Daten als einen einzigen String zu interpretieren. Andernfalls würde nur die erste Zeile des HTML Dokuments geparsed. Nun zu dem Suchmuster. Schaut man genau hin, so erkennt man bereits die Linkstruktur:

<a href=""><

Da jedoch Linktext und URL variieren, können wir hier natürlich keine festen Werte vorgeben. Für dieses Problem gibt bei den Regular Expressions spezielle Platzhalter, die für ein oder mehrere beliebige Zeichen stehen können.

(.*?)a

Dieser Ausdruck akzeptiert eine beliebige Anzahl von beliebigen Zeichen, aber nur bis er auf ein „a“ trifft. Hierbei steht der Punkt für jedes beliebige Zeichen und der Stern erlaubt keine bis unendlich Wiederholungen jedes beliebigen Zeichens. Erst durch das Fragezeichen wird die Regex Engine angewiesen, dass nur so lange weitergesucht werden darf, bis ein bestimmtes Zeichen (in diesem Falle das „a“) gefunden wurde. Setzt man Teile des Suchmusters in Klammern, so wird dieser Teil extrahiert und steht zur Verwendung im weiteren Skript zur Verfügung.

Schauen wir uns noch einmal das Suchmuster für Links an:

/<a.*?href="(.*?)".*?>(.*?)</is

Dieses sucht also nach einem a-Tag, der an einer beliebigen Stelle ein „href“-Element gefolgt von einem Gleich und Anführungsstrichen enthält. Auf diese folgt ein Beliebiger Text, der eigentliche Link, bis dieser durch Anführungsstriche beendet wird. Eventuelle weitere Parameter des Tags werden übergangen, bis eine „spitze Klammer zu“ den Linktext-Anfang ankündigt. Dieser wird druch eine „spizte Klammer auf“ beendet. die URL und der Linktext werden hierbei extrahiert (siehe die Klammern).

Soweit für den groben Überblick. Eine genauere Erklärung der Regular Expressions werde ich in der nächsten Zeit veröffentlichen. Schauen wir einmal wie wir das entwickelte Suchmuster nun in PHP einsetzen können.

Regular Expressions in PHP

Für die Anwendung von Regex-Suchmustern in PHP stehen einige Funktionen bereit. Uns interessieren momentag lediglich die, die mit Hilfe von Suchmustern Text finden und extrahieren können: preg_match() und preg_match_all(). Da wir davon ausgehen können, dass merere Links in einem HTML Dokument vorkommen, wählen wir in diesem Fall preg_match_all(), die alle Treffer in einem Dokument finden kann.

preg_match_all($muster, $daten, $ergebnis);

Die preg_match_all() Funtion liefert einen mehrdimensionales Array zurück, in dem sich die gefundenen Textstellen im Original, sowie alle extrahierten Bereiche befinden. In unserem Fall sieht das Ganze wie folgt aus.

//$ergebnis[0] (Array der original Fundstellen)
//$ergebnis[1] (Array mit Links)
//$ergebnis[2] (Array mit Linktexten)

In der Regel ist man an weniger an den original Fundstellen interessiert, als an den extrahierten Textstellen, aber sie sind nunmal da und für spezielle Aufgaben manchmal auch durchaus nützlich.

Beispielcode

Hier nun eine kleine Beispielfunktione, wie sich alle Links einer Webseite finden lassen:

$data = file_get_contents("http://www.heise.de");
preg_match_all('/<a.*?href="(.*?)".*?>(.*?)</is', $data, $result);
print_r($result);

Zu beachten ist jedoch, dass das Suchmuster lediglich korrekte Links findet, bei welchen der Link von Anführungszeichen umschlossen ist. Leider halten sich längst nicht alle Webmaster an die korrekte Formatierung. Wer möchte kann das Suchmuster ja entsprechend ändern um auch Links ohne Anführungszeichen zu parsen. Weiterhin sollten relative Links, also z.B. nach „/upload/bilder/susi.jpg“ noch erkannt und mit dem aktuellen Pfad zu absoluten Links erweitert werden. Eine Liste mit absoluten Links erleichtert das Crawlen später enorm, da nicht immer entschieden werden muss, zu welcher Domain die Links jetzt genau gehören.

Soviel zum Crawlen von Webseiten bzw. dem Extrahieren von Links. Dank des großen Interesses werde ich diese kleine Crawler-Serie weiterhin fortführen und auch in den nächsten Tagen und Wochen weitere Artikel hierzu veröffentlichen, die dann auch weniger mit reinen Grundlagen zu tun haben und mehr ans Eingemacht gehen. Also schaut mal wieder rein!

Be Sociable, Share!

Kommentare

Kommentar von masine
Datum 14. Februar 2012 um 19:20 Uhr

Gut erklärt.
Wie bekomme ich es jetzt noch hin, das nur ausgehende Links ausgegeben werden?

Kommentar von PHP Maniac
Datum 14. Februar 2012 um 19:28 Uhr

Das kommt darauf an wie die Links im HTML realisiert sind. Bei relativen (also ohne http:// vorne) ist das recht einfach:

preg_match_all('/<a.*?href="(http.*?)".*?>(.*?)</is', $data, $result);

Findet alle Links die mit http anfangen. Interne Links, die direkt auf HTML-Dateien zeigen (z.B. <a href=“/kontakt.htm“> werden somit nicht gefunden). Wenn die Links allerdings komplett mit http sind, wird der Regex schon etwas komplizierter…

Kommentar von masine
Datum 14. Februar 2012 um 19:34 Uhr

Puuh, das ging aber schnell. Respekt.
Ja wenn schon denn schon. Die Links sind natürlich komplett mit http…

Kommentar von PHP Maniac
Datum 14. Februar 2012 um 19:50 Uhr

Das könnte man dann wie folgt probieren:

preg_match_all('/<a.*?href="(http:\/\/www\.(?!DOMAIN\.de).*?)".*?>(.*?)</is', $data, $result);

So wird nach http gesucht, auf das aber NICHT DOMAIN.de folgen darf. Natürlich musst du DOMAIN.de noch durch deine Domain ersetzen 🙂

Kommentar von masine
Datum 14. Februar 2012 um 19:55 Uhr

Ich könnte mir vorstellen die $url des Suchbegriffs als Filter zu nehmen.
Sie darf ja in der Ausgabe nicht enthalten sein.
Die Schwierigkeit dabei ist allerdings das die url verschieden einggeben werden kann.
http://www.domain.de, http://www.domain.de oder domain.de.
Man müsste also die letzten beiden Werte der Eingabe (domain. und de) erfassen und diese als Filter für die Ausgabe benutzen.
So stell ich mir das vor, bekomme es aber grade nicht gebacken. 🙂

Kommentar von PHP Maniac
Datum 14. Februar 2012 um 20:00 Uhr

Quick and Dirty kannst du sonst auch einfach alle Links extrahieren und danach in PHP aussortieren. Ungefähr so (Achtung, Pseudocode):


foreach($result as $res)
{
if(!strstr($res, "domain.de"))
{
$links[] = $res;
}
}

Denke das ist einfacher als den Regex zu einer Wissenschaft zu machen 😉

Kommentar von masine
Datum 14. Februar 2012 um 20:33 Uhr

Hmm, ist bei beiden Lösungen das selbe Problem.
Wie komm ich denn zu Form „domain.de“ im script wenn ich aber alle ausgehenden links aus „domain.de/links.html“ wollte. Eingabe erfolg ja übers Formular $domain = $_POST[„url“]?

Kommentar von PHP Maniac
Datum 14. Februar 2012 um 20:54 Uhr

Verstehe dein Problem da jetzt nicht ganz, guck dir mal die Doku zu strstr auf PHP.net an. Die Funktion sucht eine Zeichenkette in einer anderen. Im Endeffekt tut die Schleife oben nichts anderes als alle gefundenen Links nach „domain.de“ zu durchsuchen und übernimmt nur die OHNE „domain.de“ in den $links-Array. Am Ende hast du dann alle externen Links im $links array und das Ganze lässt sich leicht erweitern um die Links auch anderweitig noch zu sortieren.

Kommentar von masine
Datum 14. Februar 2012 um 21:27 Uhr

ok, ich werd es mir mal anschauen.
Das die Schleife oben alle gefundenen Links nach “domain.de” durchsucht ist ja ganz schön. Die Frage aber ist doch, wer gibt Ihr den Befehl dazu? Ich kann doch nicht jedesmal neu ein andere Domain in das Script schreiben. Und aus meiner Eingabe ist es scheinbar recht schwer eine domain.de zu filtern und diese dann zu verwenden. Das ist mein Problem. Ich gebe domain.de/test/index.html als meine zu durchsuchende Datei ein und muss aber daraus „domain.de“ extrahieren um sie eventuell als filter einzusetzen.
Das wird sicherlich so irgendwie gehen kann aber nicht der Weisheit letzter Schluss sein. ich dachte, das kann man bestimmt noch anders lösen.

Kommentar von Fred
Datum 14. März 2012 um 21:26 Uhr

Der Beitrag hat mich gerade zu etwas inspiriert…
daher mal eine ganz allgemeine Frage:
Kann man so ein Script auch dafür verwenden nach anderen Inhalten als Links zu suchen?

Kommentar von PHP Maniac
Datum 14. März 2012 um 21:28 Uhr

Im Prinzip kann man mit RegEx alles und jedes Muster suchen das einem einfällt. Man muss halt das Suchmuster entsprechend anpassen. Was schwebt dir denn vor?

Kommentar von PHP Maniac
Datum 14. März 2012 um 21:30 Uhr

Naja, wenn du die URL „domain.de/irgendwas/bla.htm“ hast kannst du den Domainnamen natürlich auch aus diesem String mit einem Regex rausfischen. Funktioniert wie bei den Links auch, nur halt mit anderem Suchmuster…

Kommentar von Fred
Datum 16. März 2012 um 17:30 Uhr

Ich würde gerne nach einem span mit bestimmter id suchen und den inhalt des span wiedergeben…
nur leider hat das bei mit nicht wirklich geklappt.

Schreibe einen Kommentar