PHP/Tutorials/Link-Checker

Aus SELFHTML-Wiki
< PHP‎ | Tutorials
Wechseln zu: Navigation, Suche

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.


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.

Öffnen und Auslesen von HTML-Dokumnenten
 <?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



--Matthias Scharwies (Diskussion) 14:51, 19. Nov. 2018 (CET)

https://www.the-art-of-web.com/php/html-xpath-query/

Überprüfung auf vorhandene Verweise
<?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
    ];
  }
?>
Überprüfung auf vorhandene Verweise
 <?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.

Überprüfung mit fopen()
<?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:


Überprüfung mit http_test_existence($url)
<?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.

Überprüfung mit fsockopen()
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 zu http://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'
);
Beachten Sie: Die Funktion kann mit Benutzernamen / Passwort-Angaben umgehen, wenn diese folgendermaßen angegeben werden: http://benutzer:passwort@example.org/. Diese werden in einen standardkonformen HTTP-Header für HTTP-Basic-Auth umgewandelt.


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

Selfhtml-beispiel 150.svg

So sieht's aus:

  • Link-Checker


Weblinks

Dieser Artikel ist die überarbeitete Version eines Selfhtml-aktuell-Artikels aus dem Jahre 2007: