tl;dr

Meine reguläre Expression schaut folgendermaßen aus:

pattern = (
    r"^" # start
    r"(https?://)?" # Optionales Protokoll
    r"(([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]{2,}|" # Domain?
    r"(\d{1,3}\.){3}(\d{1,3}))" # Oder IPv4?
    r"(:\d+)?" # Optionaler Port
    r"(/[a-z\d\-%_.~+]*)*" # Optionaler Pfad
    r"(\?[a-z\d%_.~+=&]*)?" # Query Parameter
    r"(\#[a-z\d_\-]*)?" # Fragmente
    r"$" # End
)

Erklärung

Was in jeder Zeile passiert:

  • ^ - Das definiert den Anfang des regulären Ausdrucks. Davor sollte nichts stehen.
  • (https?://)? - Das Protokoll. Das Fragezeichen am Ende definiert dass das Protokoll garnicht gegeben sein könnte; auch das s ist optional (dadurch matche ich http:// und auch https://). Mir ist klar dass es mehr Protokolle geben kann, aber das genügt aktuell für mich.
  • Die nächsten zwei Zeilen gehören zusammen - insbesondere sind sie per | (am Ende der ersten Zeile) durch ein “ODER” verbunden:
    • ([a-z\d][a-z\d-]*[a-z\d]\.)+[a-z]{2,} definiert die Nutzung eines Domain Namens.
      Von hinten nach vorne betrachtet: Wir haben eine Top Level Domain, die aus Buchstaben besteht und mindestens zwei Zeichen lang ist. Das ist der [a-z]{2,} Teil.
      Die Gruppe davor definiert jedes Level der Domain; per + haben wir mindestens ein Subdomain-Level. Jede Subdomain muss mit Zahl oder Buchstabe enden, in der Mitte dürfen beliebig viele Zahlen, buchstaben und auch --Zeichen sein.
    • Als zweite Option könnte eine IP Adrese vorkommen. Hier beachte ich nur IPv4 Adressen; v6 kommt zu einem späteren Zeitpunkt. Wir haben zuerts drei Blocks mit jeweils zwischen einer und drei Zahlen und einem Punkt; der letzte Block hat wieder ein bis drei Zahlen, diesmal aber keinen Punkt.
  • Der Port ist wieder optional und ansonsten recht simpel: Zuerst ein Doppelpunkt, dann einige Zahlen. Wir könnten uns hier technisch gesehen auf Zahlen unter $65535$ beschränken, aber das ist den Aufwand nicht wert.
  • Der Pfad ist wiederum simpel: Starte mit einem Schrägstrich, danach kommt eine beliebige Anzahl an Zeichen, die hier definiert sind. Die Anzahl solcher Pfade ist beliebig.
  • Query und Fragmente sind etwas schwieriger zu definieren, also habe ich es mir leicht gemacht: Wir starten mit ? bzw. #, und dann haben wir eine Anzahl an erlaubten Zeichen. Das ist formal nicht ganz korrekt, aber gut genug. Beide Teile sind optional.
  • Am Ende wird die reguläre Expression beendet - auch dahinter darf nichts mehr kommen.