PHP/Tutorials/Link-Checker
Bei dynamischen Webseiten taucht häufiger das Problem auf, dass externe URLs nicht nur auf ihre formale Gültigkeit, sondern auch auf die Existenz einer Resource dahinter überprüft werden müssen. Dabei soll bei dem hier gezeigten Link-Checker (englisch: für Verweis-Überprüfer) zwischen toten Links und nur vorübergehend nicht erreichbaren Seiten unterschieden werden.
Information
Inhaltsverzeichnis
Anwendungsbeispiel
Unser Link-Checker soll eine eingegebene URl auf Links untersuchen und diese dann auf ihre Verfügbarkeit überprüfen. Das dazu gehörende Template wird im letzten Unterkapitel besprochen.
Öffnen und Lesen von HTML-Dokumenten
In PHP kann eine URL mit fopen() geöffnet und mit fread() gelesen werden.
<?php
function getPage($link){
if ($fp = fopen($link, 'r')) {
$content = '';
while ($line = fread($fp, 1024)) {
$content .= $line;
}
}
return $content;
}
?>
Die Funktion getPage()
öffnet den als Parameter #link
übermittelte URL und liest den Dokumentinhalt in 1kB großen Blöcken ein und weist sie der Variablen $content
zu. Diese enthält das gesamte HTML-Markup als Zeichenkette.
Links identifizieren
Innerhalb dieser Zeichenkette müssen nun alle Verweise (erkennbar am <a>-Tag) und die dort referenzierten URLs identifiziert werden.
ToDo (weitere ToDos)
Neu Ansatz mit DOMDocument::getElementsByTagName
https://www.the-art-of-web.com/php/html-xpath-query/
<?php
$doc = new \DOMDocument();
$doc->loadHTML($htmlinput);
// all links in document
$links = [];
$arr = $doc->getElementsByTagName("a"); // DOMNodeList Object
foreach($arr as $item) { // DOMElement Object
$href = $item->getAttribute("href");
$text = trim(preg_replace("/[\r\n]+/", " ", $item->nodeValue));
$links[] = [
'href' => $href,
'text' => $text
];
}
?>
<?php
function checkPage($content){
$links = array();
$textLen = strlen($content);
if ( $textLen > 10){
$startPos = 0;
$valid = true;
while ($valid){
$spos = strpos($content,'<a ',$startPos);
if ($spos < $startPos) $valid = false;
$spos = strpos($content,'href',$spos);
$spos = strpos($content,'"',$spos)+1;
$epos = strpos($content,'"',$spos);
$startPos = $epos;
$link = substr($content,$spos,$epos-$spos);
if (strpos($link,'http://') !== false) $links[] = $link;
}
}
return $links;
}
?>
Die Funktion checkPage()
überprüft nun die übergebene Zeichenkette. Mit strpos() wird die Position des ersten Vorkommens des Suchstrings in einem String gesucht. function. Dies wird in einer while-Schleife solange wiederholt, bis alle Vorkommen gefunden sind.
In ewiner weiteren Suche werden nun die href-Attribute innerhalb der Verweise gesucht und in den Array $links
gespeichert, der dann als Rückgabewert zurückgegeben wird.
Links überprüfen
Die so erhaltenen Verweise müssen nun geöffnet und ihr HTTP-Status überprüft werden.
fopen ()
In PHP gibt es zwei eingebaute Funktionen, mit denen dies möglich ist: fopen() und fsockopen(). Im Folgenden sollen die Vor- und Nachteile beider Varianten erläutert werden.
<?php
function http_test_existence($url) {
return (($fp = @fopen($url, 'r')) === false) ? false : @fclose($fp);
}
?>
Bei dieser Methode wird versucht, den URL direkt an die Funktion fopen()
zu übergeben. Falls die Funktion nicht fehlschlägt, wird der Zeiger wieder geschlossen. Im Erfolgsfall wird true
zurückgegeben, im Fehlerfall false
.
Diese Methode ist zwar relativ einfach, hat aber ein paar gravierende Nachteile:
- Man verlässt sich auf die
url_fopen_wrapper
, die deaktiviert sein können. - Man übergibt den URL ohne vorige Überprüfung an eine Funktion, die zum Öffnen von Dateien gedacht ist.
- Falls jemand einen manipulierten URL angibt, wäre es denkbar, Schaden auf dem Server anzurichten (wenn auch begrenzten, da das Handle sofort wieder geschlossen wird).
- Sie unterscheidet nicht zwischen den HTTP-Response-Headern. Bei einem Code wie 301 würde sich hinter der Resource beispielsweise eine Weiterleitung verbergen. PHP folgt einfach dieser Weiterleitung und die Funktion würde
true
zurückgeben – ohne, dass man Einfluss darauf hätte. Ferner wird bei Fehlern nicht weiter unterschieden, man hat also keine Möglichkeit, bei einen temporären Serverausfall anders zu reagieren, als bei einem 404 Not Found. - Man erhält überhaupt keine weiteren Informationen über die Resource.
- Man kann keine Zeitbeschränkung einstellen, wenn man weder die PHP-Konfiguration verändern noch
ini_set
verwenden darf.
Wenn Ihnen diese Methode dennoch reicht, können Sie sie wie folgt verwenden:
<?php
if (http_test_existence('http://example.org/')) {
echo 'Test erfolgreich.';
}
else {
echo 'Test fehlgeschlagen.';
}
?>
fsockopen ()
Bei diesem Ansatz wird ein HTTP-Request manuell abgesetzt, anstelle dies von PHP durchführen zu lassen. Somit kann man Fehlersituationen detaillierter erkennen und hat mehr Kontrolle über die gesendeten Daten. Dazu wird die Funktion fsockopen()
verwendet. Diese baut hier eine TCP-Verbindung zum Webserver auf. Um den Hostnamen des fremden Servers in Erfahrung zu bringen, wird der URL mit der Funktion parse_url
zerlegt.
function http_test_existence(
$url,
$timeout = 10
) {
$timeout = (int)round($timeout/2+0.00000000001);
$return = array();
### 1 ###
$inf = parse_url($url);
if (!isset($inf['scheme']) or $inf['scheme'] !== 'http') return array('status' => -1);
if (!isset($inf['host'])) return array('status' => -2);
$host = $inf['host'];
if (!isset($inf['path'])) return array('status' => -3);
$path = $inf['path'];
if (isset($inf['query'])) $path .= '?'.$inf['query'];
if (isset($inf['port'])) $port = $inf['port'];
else $port = 80;
### 2 ###
$pointer = fsockopen($host, $port, $errno, $errstr, $timeout);
if (!$pointer) return array('status' => -4, 'errstr' => $errstr, 'errno' => $errno);
socket_set_timeout($pointer, $timeout);
### 3 ###
$head =
'HEAD '.$path.' HTTP/1.1'."\r\n".
'Host: '.$host."\r\n";
if (isset($inf['user']))
$head .= 'Authorization: Basic '.
base64_encode($inf['user'].':'.(isset($inf['pass']) ? $inf['pass'] : ''))."\r\n";
if (func_num_args() > 2) {
for ($i = 2; $i < func_num_args(); $i++) {
$arg = func_get_arg($i);
if (
strpos($arg, ':') !== false and
strpos($arg, "\r") === false and
strpos($arg, "\n") === false
) {
$head .= $arg."\r\n";
}
}
}
else $head .=
'User-Agent: Selflinkchecker 1.0 (http://aktuell.selfhtml.org/artikel/php/existenz/)'."\r\n";
$head .=
'Connection: close'."\r\n"."\r\n";
### 4 ###
fputs($pointer, $head);
$response = '';
$status = socket_get_status($pointer);
while (!$status['timed_out'] && !$status['eof']) {
$response .= fgets($pointer);
$status = socket_get_status($pointer);
}
fclose($pointer);
if ($status['timed_out']) {
return array('status' => -5, '_request' => $head);
}
### 5 ###
$res = str_replace("\r\n", "\n", $response);
$res = str_replace("\r", "\n", $res);
$res = str_replace("\t", ' ', $res);
$ares = explode("\n", $res);
$first_line = explode(' ', array_shift($ares), 3);
$return['status'] = trim($first_line[1]);
$return['reason'] = trim($first_line[2]);
foreach ($ares as $line) {
$temp = explode(':', $line, 2);
if (isset($temp[0]) and isset($temp[1])) {
$return[strtolower(trim($temp[0]))] = trim($temp[1]);
}
}
$return['_response'] = $response;
$return['_request'] = $head;
return $return;
}
?>
Die Funktion prüft dann ansatzweise die Korrektheit der URL und gibt im Fehlerfall ein assoziatives Array zurück:
- Wenn das Schema fehlt oder nicht http ist:
array('status' => -1)
. - Wenn der Hostname fehlt:
array('status' => -2)
. - Wenn der Pfad fehlt:
array('status' => -3)
. Der Pfad muss mit einem Slash beginnen,http://example.com
ist also im Gegensatz zuhttp://example.com/
falsch.
In Abschnitt 2 wird die Verbindung zum Server hergestellt und mit socket_set_timeout
die Zeitbeschränkung für das Lesen der Server-Antwort eingestellt. Wenn ersteres fehlschlägt, wird wieder ein Array zurückgegeben:
array( 'status' => -4 'errno' => # (String) Nummer des Fehlers, die fsockopen in den # dritten Parameter geschrieben hat. 'errstr' => # (Integer) Fehlermeldung, die fsockopen in den # vierten Parameter geschrieben hat. )
In Abschnitt 3 wird der eigentliche HTTP-Request zusammengebaut. Er besteht mindestens aus vier Zeilen:
HEAD /url HTTP/1.1 Host: example.org Connection: close Leerzeile
Die Parameter der Funktion spielen hier eine besondere Rolle:
- Wird kein dritter Parameter angegeben, so wird User-Agent: Selflinkchecker 1.0 ([ http://aktuell.de.selfhtml.org/artikel/php/existenz/ Link einfügen]) gesendet.
- Wird null (oder ein Leerstring) als dritter Parameter angegeben, so wird kein User-Agent gesendet.
- Werden mehr als zwei Parameter angegeben, so wird jeder weitere einschließlich des dritten als Request-Header verwendet.
In Abschnitt 4 wird der erzeugte HTTP-Request an den Server geschickt. Dann wird die Server-Antwort zeilenweise eingelesen, wobei nach jeder Zeile überprüft wird, ob eine Zeitüberschreitung aufgetreten ist. In diesem Fall bricht die Funktion ab und gibt wieder mal ein Array zurück:
array( 'status' => -5, '_request' => # (String) der gesendete HTTP-Request )
Wenn bis dahin alles erfolgreich war wird im fünften Abschnitt die Antwort des Servers in ein handliches Array zerlegt. Dazu werden alle mäglichen Zeilenumbrüche in den Unix-Zeilenumbruch umgewandelt und die Antwort an letzterem in ein Array zerlegt. Das erste Element enthält dann die höchste HTTP-Version, die der Server unterstützt (HTTP/1.0 oder HTTP/1.1), den Status-Code (z. B. 200) und die Reason-Phrase (z. B. OK, Not Found, Moved Permanently etc.). Die beiden letzten Angaben werden folgendermaßen in das Array, das am Ende zurückgegeben wird geschrieben:
$return['status'] = # (String) Status-Code (z. B. 200) $return['reason'] = # (String) Reason-Phrase (z. B. OK, Not Found, Moved Permanently etc.)
Alle weiteren Zeilen werden am Doppelpunkt geteilt und jeweils der Teil vor dem Doppelpunkt in Kleinschrift als Schlüssel für den Teil nach dem Doppelpunkt in das Rückgabe-Array geschrieben. Beide Zeichenketten werden mittels trim() von eventuell vorhandenen Leerzeichen / Tabulatoren an den Rändern befreit.
Zuletzt werden noch der kompletter gesendete HTTP-Request mit dem Schlüssel _request und die komplette unveränderte Serverantwort mit dem Schlüssel _response in das Rückgabe-Array geschrieben.
Beispiel 1
Zunächst ein ganz einfaches Anwendungsbeispiel:
$result = http_test_existence('http://selfhtml.org/'); print_r($result);
Die Ausgabe des Scripts sollte in etwa so aussehen:
Array ( [status] => 200 [reason] => OK [date] => Fri, 11 Nov 2005 21:27:42 GMT [server] => Apache/1.3.33 (Unix) (Gentoo/Linux) PHP/4.4.0-gentoo-r1 mod_auth_ldap/2.4.2 mod_ssl/2.8.24 OpenSSL/0.9.7e mod_gzip/1.3.26.1a [last-modified] => Fri, 25 Mar 2005 07:37:20 GMT [etag] => "24f636-4800-4243bfb0" [accept-ranges] => bytes [content-length] => 18432 [connection] => close [content-type] => text/html [_response] => HTTP/1.1 200 OK Date: Fri, 11 Nov 2005 21:27:42 GMT Server: Apache/1.3.33 (Unix) (Gentoo/Linux) PHP/4.4.0-gentoo-r1 mod_auth_ldap/2.4.2 mod_ssl/2.8.24 OpenSSL/0.9.7e mod_gzip/1.3.26.1a Last-Modified: Fri, 25 Mar 2005 07:37:20 GMT ETag: "24f636-4800-4243bfb0" Accept-Ranges: bytes Content-Length: 18432 Connection: close Content-Type: text/html [_request] => HEAD / HTTP/1.1 Host: selfhtml.org User-Agent: Selflinkchecker 1.0 (http://aktuell.selfhtml.org/artikel/php/existenz/) Connection: close )
Wenn Sie nur testen wollen, ob die Resource existiert können Sie alles außer dem Schlüssel status ignorieren. Der Status 200 steht für OK, der Server kann die angeforderten Daten wie gewünscht versenden. Dies ist der Normalfall, wenn keine Probleme auftauchen.
Manchmal kann es sinnvoll sein, einen bestimmten Referer zu senden, wenn man die Resource abfragt (wenn der Server auf unterschiedliche Referer unterschiedlich reagiert):
$url = 'http://example.org/komische_unterseite/'; $result = http_test_existence($url, 10, 'Referer: '.$url);
Beispiel 2
Des Weiteren wäre es denkbar, einen Browser-Request so genau, wie möglich zu simulieren. Folgendes Beispiel führt einen Request durch und lässt den Server glauben, es handele sich um einen Mozilla Firefox in der Version 1.5 unter Windows 2000:
$result = http_test_existence('http://selfhtml.org/', 10, 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.8) Gecko/20051107 Firefox/1.5', 'Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5', 'Accept-Language: de,en;q=0.7,en-us;q=0.3', 'Accept-Encoding: gzip,deflate', 'Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7' );
Rückmeldung
Die so erhaltene Rückmeldung soll nun ausgegeben werden. Dafür fügen Sie eine entsprechende Liste der Links mit ihrem Status in das HTML-Dokument ein:
Beispiel
So sieht's aus:
- Link-Checker
Weblinks
Dieser Artikel ist die überarbeitete Version eines Selfhtml-aktuell-Artikels aus dem Jahre 2007:
- Existenzprüfung externer HTTP-Resourcen – Funktion, mit der die Existenz externer HTTP-Resourcen überprüft werden kann
- How to create a link checker (phptoys.com)