12. Februar 2009 URLs richtig normalisieren
in Kategorie PHP
Tags: Normalisierung, PHP, URL
Hin und wieder kommt es vor, dass man mit vielen URLs arbeitet, diese in einer Datenbank halten muss, und deshalb gerne wüsste, ob die einzutragende URL bereits in der Datenbank enthalten ist. Mit einem einfachen String-Vergleich ist es hier aber nicht getan.
Ich habe hierzu ein PHP-Script geschrieben, welches mir diese Aufgabe stark vereinfacht und werde dieses in den folgenden Abschnitten näher erläutern.
URLs sind nun mal nicht eindeutig
URLs sind schön, URLs sind elegant. Ok, darüber kann man sich streiten, aber URLs sind definitiv nicht eindeutig. http://www.example.com und http://www.example.com/ zeigen auf die selbe Ressource, ein einfacher Vergleich würde hier aber scheitern. Und das ist lediglich ein einfaches Beispiel.
Normalisierung?
Wenn ich URLs vergleichen möchte, so muss ich diese zuerst so umformen, dass semantisch äquivalente URLs auch wirklich gleich dargestellt werden. Diesen Prozess bezeichnet man als Normalisierung.
Eine URL Normalisierung besteht aus vielen kleinen Schritten, die zum Teil leider auch auf Annahmen beruhen, die nicht unbedingt auf jedem Server zutreffen müssen. Der englische Wikipedia-Eintrag hierzu bietet eine gute Übersicht. Bei meiner Implementierung habe ich deshalb nur auf sehr sichere Annahmen zurückgegriffen.
Auseinandernehmen…
Die Grundlage meines Scripts bildet die PHP-Funktion parse_url(), welche mir aus einer gegebenen URL die entsprechenden Komponenten (zum Beispiel Schema, Host, Port, etc.) extrahiert. Danach verarbeite ich die Komponenten separat, um sie am Ende zu einem normalisierten URL zusammenzufügen.
$comp = parse_url($url);
Das URL-Schema (http oder https) und der Host (www.example.com) werden lediglich in Kleinbuchstaben umgewandelt:
$comp['scheme'] = strtolower($comp['scheme']); $comp['host'] = strtolower($comp['host']);
Die Standard-Subdomain www belasse ich hingegen nach dem original URL, da diese nicht zwangsläufig optional ist. Zusätzlich könnte man vor dem Normalisieren den URL auf Weiterleitungen überprüfen, um evtl. diese Subdomain zu eliminieren, da häufig www.example.com auf example.com weitergeleitet wird, oder umgekehrt.
Der Pfad des URLs erfordert hingegen etwas mehr Aufmerksamkeit. Eine Umwandlung in Kleinbuchstaben ist hier nicht möglich, da manche Server durchaus case-sensitive sind.
Nehmen wir nun an, ich habe den Pfad /a/./../b/./ gegeben. Dann sollte man die Punkte mit dem im URI RFC 3986 unter Abschnitt 5.2.4 beschriebenen Algorithmus auflösen. Der Algorithmus ist nicht sehr kompliziert und kann nach dem gegebenen Pseudo-Code einfach implementiert werden. Der aufgelöste Pfad würde dann so aussehen: /b/
Nun möchten wir am Ende unseres Pfades gerne ein abschließendes Slash haben, wenn das letzte Segment des Pfades ein Ordner ist, im Falle einer Datei nicht. Hierzu machen wir uns die Dateiendung zu nutze und suchen im letzten Segment nach einem Punkt:
$seg = explode('/', $path); $last_seg = $seg[sizeof($seg)-1]; if(!empty($last_seg) && strpos($last_seg, '.') === false) { $path .= '/'; }
Häufig kommen in den Pfaden auch sog. Percent-Escape-Sequences (Bsp.: %A3) vor, diese können wir im gegensatz zum Rest des Pfades in Großbuchstaben umwandeln:
for($i = 0; $i < sizeof($path); $i++) { if($path[$i] == '%') { $path[$i+1] = strtoupper($path[$i+1]); $path[$i+2] = strtoupper($path[$i+2]); $i += 3; } }
Eine Annahme habe ich dann doch getroffen. Eine Pfadendung der Form index.XYZ lösche ich aus dem Pfad, da diese Dateien in aller Regel die Standard-Dateien sind, die im Falle des Aufrufs eines Verzeichnisses geladen werden.
$seg = explode('/', $path); $last_seg = $seg[sizeof($seg)-1]; $pos = strrpos($last_seg, '.'); if($pos !== false && substr($last_seg, 0, $pos) == 'index') { $pos = strrpos($path, '/'); if($pos === false) { $path = '/'; } else { $path = substr($path, 0, $pos+1); } }
Auch der eventuell vorhandene Query-Teil des URLs (Bsp.: foo=bar&a[]=1&a[]=2) lässt sich etwas bearbeiten. Zuerst sollte man die bekannten PHPSESSID-Varialben komplett entfernen, da diese eine nutzerspezifische Session-ID mitführen und sowieso nicht in einem öffentlichen URL enthalten sein sollten. Zusätzlich sortiere ich alle Variablen alphabetisch, um eine fixe Reihenfolge zu erhalten.
Den Fragment-Teil (Bsp.: #top) entferne ich vollständig. Bei manchen Anwendungen kann es aber durchaus sinnvoll sein, diesen beizubehalten.
… und wieder zusammensetzen
Beim Zusammenfügen der einzelnen Teile wird der Port ignoriert, falls es sich um den Standardport des jeweiligen Schemas handelt:
if(isset($comp['port']) && (($comp['scheme'] == 'http' && $comp['port'] != 80) || ($comp['scheme'] == 'https' && $comp['port'] != 443))) { $url .= ':' . $comp['port']; }
Natürlich wird auch der Query-Teil ignoriert, falls nur ein Fragezeichen aber keine Variablen vorhanden sein sollten.
Fazit
URLs zu normalisieren ist nicht ganz unproblematisch, da sich zum einen viele Websites nicht an den Standard halten, zum anderen der Standard selbst natürlich schon sehr viele Freiheiten lässt. Bis zu einem gewissen Grad lassen sich aber mit einer nicht zu aggressiven Normalisierung bereits die meisten doppelten URLs aufspüren. Dennoch muss jede Anwendung immer abwägen, wie viel Normalisierung wirklich notwendig ist.
Das hier besprochene PHP-Script gibt es auch in Gänze zum freien Download unter der LGPL 3.0:
Ähnliche Artikel
Der Beitrag wurde am Donnerstag, den 12. Februar 2009 um 18:08 Uhr veröffentlicht und wurde unter PHP abgelegt. du kannst die Kommentare zu diesen Eintrag durch den RSS 2.0 Feed verfolgen. du kannst einen Kommentar schreiben, oder einen Trackback auf deiner Seite einrichten.

Kommentare
Keine Reaktion zu “URLs richtig normalisieren”.