PHP/Tutorials/Link-Checker

Aus SELFHTML-Wiki
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.[1]

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. Früher wurde das Dokument mit String-Funktionen untersucht – heute kann man mit der DOMDocument-Klasse Methoden wie in JavaScript verwenden:

Überprüfung auf vorhandene Verweise
<?php
function extractLinks(string $html): array
{
    $links = [];

    if (strlen($html) < 10) {
        return $links;
    }

    libxml_use_internal_errors(true);

    $doc = new DOMDocument();
    $doc->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);

    /** @var DOMNodeList $nodes */
    $nodes = $doc->getElementsByTagName('a');

    foreach ($nodes as $node) {
        /** @var DOMElement $node */
        $href = trim($node->getAttribute('href'));
        $text = trim(preg_replace('/\s+/', ' ', $node->textContent));

        if ($href !== '') {
            $links[] = [
                'href' => $href,
                'text' => $text
            ];
        }
    }
?>

Die Funktion extractLinks(string $html) findet nun alle links und überprüft sie.

$doc = new DOMDocument(); erzeugt einen DOM-Parser, der dann ($doc->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);) den HTML-String lädt und in eine Baumstruktur umwandelt.

Mit $nodes = $doc->getElementsByTagName('a'); werden alle Links gesucht und mit $href = trim($node->getAttribute('href')); die href-Attribute ausgelesen, wobei mit trim() Leerzeichen entfernt werden.

$links[] = [
    'href' => $href,
    'text' => $text
];

Sowohl die href-Attribute als auch die Linktexte innerhalb der Verweise werden 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 dieser Variante 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.

cURL

Heute ist cURL und eine direkte Überprüfung der HTTP-Statuscodes der richtige Weg.

Überprüfen der Links mit cURL
function isBrokenLink(string $url): bool
{
    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_NOBODY => true,        // HEAD request
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_USERAGENT => 'LinkChecker/1.0'
    ]);

    curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ($status >= 400 || $status === 0);
}

Es wird eine HEAD-Anfrage verwendet, sodass nur die HTTP-Header angefordert werden. Dadurch wird die Überprüfung beschleunigt, da keine Seiteninhalte heruntergeladen werden.

Statuscodes von 400 oder höher weisen auf einen defekten Link hin, z. B. 404 (Nicht gefunden) oder 500 (Serverfehler).

Ein Statuscode von 0 bedeutet, dass die Anfrage vollständig fehlgeschlagen ist, in der Regel aufgrund einer Zeitüberschreitung, eines DNS-Fehlers oder eines SSL-Problems.

Auflösung relativer Links
function resolveUrl(string $base, string $link): string
{
    if (parse_url($link, PHP_URL_SCHEME)) {
        return $link;
    }

    return rtrim($base, '/') . '/' . ltrim($link, '/');
}

Script

Das fertige Script
<?php
function extractLinks(string $html): array
{
    $links = [];

    if (strlen($html) < 10) {
        return $links;
    }

    libxml_use_internal_errors(true);

    $doc = new DOMDocument();
    $doc->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);

    $nodes = $doc->getElementsByTagName('a');

    foreach ($nodes as $node) {
        $href = trim($node->getAttribute('href'));
        $text = trim(preg_replace('/\s+/', ' ', $node->textContent));

        if ($href !== '') {
            $links[] = [
                'href' => $href,
                'text' => $text
            ];
        }
    }

    return $links;
}

function resolveUrl(string $base, string $link): string
{
    // already absolute
    if (parse_url($link, PHP_URL_SCHEME)) {
        return $link;
    }

    // ignore anchors and mailto/tel
if ($link[0] === '#' ||
    strpos($link, 'mailto:') === 0 ||
    strpos($link, 'tel:') === 0) {
    return '';
}

    return rtrim($base, '/') . '/' . ltrim($link, '/');
}

function isBrokenLink(string $url): bool
{
    if ($url === '') {
        return false;
    }

    $ch = curl_init($url);

    curl_setopt_array($ch, [
        CURLOPT_NOBODY => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_USERAGENT => 'BrokenLinkChecker/1.0'
    ]);

    curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ($status >= 400 || $status === 0);
}

// ---------- MAIN LOGIC ----------

$broken = [];
$url = '';

if (!empty($_POST['url'])) {
    $url = trim($_POST['url']);

    $html = @file_get_contents($url);

    if ($html !== false) {
        $links = extractLinks($html);

        foreach ($links as $link) {
            $absolute = resolveUrl($url, $link['href']);

            if ($absolute && isBrokenLink($absolute)) {
                $broken[] = [
                    'url'  => $absolute,
                    'text' => $link['text']
                ];
            }
        }
    }
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Link-Checker</title>
<link rel="stylesheet" href="https://wiki.selfhtml.org/extensions/Selfhtml/example.php/Beispiel:Grundlayout.css">
<style>
body {
    font-family: system-ui, sans-serif;
    max-width: 50em;
    margin: 2rem auto;
}
input {
    width: 100%;
    padding: .5rem;
}
button {
    margin-top: 1rem;
}
li {
    margin-bottom: .75rem;
}
small {
    color: #666;
}
</style>
</head>
<body>

<h1>Link-Checker</h1>

<form method="post">
    <label for="url">Bitte gib eine URL ein:</label>
    <input
        type="url"
        name="url" id="url"
        placeholder="https://example.com"
        required
        value="<?= htmlspecialchars($url) ?>"
    >
    <button type="submit">Überprüfen</button>
</form>

<?php if ($broken): ?>
    <h2>❌ Tote Links</h2>
    <ul>
        <?php foreach ($broken as $b): ?>
            <li>
                <strong><?= htmlspecialchars($b['text'] ?: '[kein Linktext]') ?></strong><br>
                <small><?= htmlspecialchars($b['url']) ?></small>
            </li>
        <?php endforeach; ?>
    </ul>
<?php elseif ($_POST): ?>
    <p>✅ Keine toten Links gefunden.</p>
<?php endif; ?>

</body>
</html>

Beispiel

Selfhtml-beispiel 150.svg

So sieht's aus:

Siehe auch

  • 404-Fehlerseite
    (PHP-Tutorial)
  • htaccess/Fehlermeldungen

Weblinks

  1. Dies ist eine Überarbeitung des Selfhtml-aktuell-Artikels Existenzprüfung externer HTTP-Resourcen von Alexander Brock aus dem Jahre 2005.