Quantcast
Channel: Perfect Knowhow
Viewing all articles
Browse latest Browse all 24

Regex by Example (HTTP_ACCEPT_LANGUAGE)

$
0
0

Einleitung

tipp-regexRegex steht in der Programmierung für die Mustererkennung innerhalb eines Strings. Die Syntax für ein solches Suchpattern sieht dann etwa so aus:

      /([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

Regex ist hierbei insbesondere bei der Validierung von Feldern und Auswertung von Zeichenketteninhalten hilfreich.

Bei folgenden Frage-/Aufgabenstellungen (unter unzähligen, vielen anderen) könnte man regex einsetzen:

  • Ist der übergebene Zeichenstring eine gültige E-Mail Adresse?
  • Erfüllt eine Zeichenkette meine Anforderungen wie mind. 3 Zeichen, nur Buchstaben, keine Leerzeichen als erstes Zeichen, keine dreifachen Zeichenwiederholungen, …
  • Welche Sprachangaben werden im HTTP Request bei einer Browseranfrage geliefert?
  • u.s.w.

Sprachunabhängigkeit

Die Grundsyntax der Patterns (Suchmuster) von Regex ist bis auf wenige Ausnahmen programmiersprachenunabhängig, so dass einmal gelernt, man die Suchpattern sowohl mit Java als auch z.B. mit PHP (preg_match_all) verwenden kann.

 

Beispiel:  Pattern von HTTP_ACCEPT_LANGUAGE

Ich möchte in diesem Blogartikel ausgehend von dem Suchpattern

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

  • die einzelnen, in diesem Suchpattern verwendeten Bestandteile erklären und damit aufzeigen, wieso dieses Suchpattern geeignet ist, die von HTTP_ACCEPT_LANGUAGE gelieferte Zeichenkette auszuwerten.
  • aufzeigen, was man als Ergebnis der Suchabfrage erhält. Denn das Ergebnis ist abhängig von der Formulierung des Suchpatterns und damit ist der Inhalt des Ergebnisarrays abhängig vom Pattern.

Der Lernansatz bei meinem „Regex by Example“  Beispiel ist vielleicht etwas ungewöhnlich, weil er eher in Richtung „reverse engineering“ geht. Wir analysieren das finale Pattern und zeigen auf, warum dieses die Anforderungen unserer Aufgabenstellung erfüllt.

 

Aufgabenstellung und Hintergrundwissen

HTTP_ACCEPT_LANGUAGE

Szenario: Nehmen wir an, dass wir eine Website haben und irgendwo auf der Welt jemand unsere Produkt- oder Dienstleistungsseite aufrufen möchte. Da wir daran interessiert sind, international unsere Produkte zu verkaufen, haben wir unsere Produktseite viersprachig ausgelegt (de, en, fr, sp) und möchten , dass dem Besucher sofort die Produktseite in seiner Sprache angezeigt wird und auch die Währungs-Grundeinstellung benuterspezifisch erfolgt.
(Natürlich bieten wir auch eine Sprachumstellungsmöglichkeit an, aber wir wollen die Benutzererfahrung optimieren und sofort die „sprachlich richtige“ Seite anbieten.)

Browser: Wenn ein Websitebesucher unsere Internetseite aufruft, dann liefert der Browser im Header des Seitenaufrufs, die vom Besucher bevorzugte Sprache(n) und ggf. einen Ländercode mit. Diese Informationen stehen im Header Metafeld HTTP_ACCEPT_LANGUAGE und können von Java, PHP, … ausgelesen werden.

Normierung: Der Inhalt und die Formatierung des HTTP_ACCEPT_LANGUAGE Feldes ist festgelegt [6] und die Browser (Firefox, Chrome, Internet Explorer, …) halten sich im Wesentlichen an diese Normierung.  Ich schreibe im Wesentlichen, weil manche Browser sich eben gewisse Freiheiten innerhalb der Vorgaben nehmen. 

Regex als Retter in der Not: Wenn es z.B. in der Regel heißt, dass der Sprachcode [5] und ein Ländercode mit Bindestrich getrennt anzugeben sind (beispielsweise „en-gb„), dann erfüllt ein Browser auch die Vorgaben, wenn er Großschreibung für den Ländercode benutzt („en-GB„).

Und in der Vorgabe steht auch nur, dass die beiden Codes mit Bindestrich zu trennen sind. Da wird nicht vorgegeben, dass der Bindestrich unmittelbar folgen muss. Da nimmt sich dann eben der eine oder andere Browser die Freiheit, noch ein Leerzeichen vor oder nach dem Bindestrich zu setzen. Dies ist zwar regelkonform, erschwert aber natürlich die Auswertung. 

Gerade hier erweist sich regex mit seinen Suchpatterndefinitionsmöglichkeiten als Hilfe und Retter, um die Formatierung laut Grundregel zu verifizieren, dabei jedoch solche zulässigen und bekannten „Variationen“ zu berücksichtigen und zu tolerieren.

 

Ausgangsinformationen für das Beispiel

Syntaxvorgabe für HTTP_ACCEPT_LANGUAGE

In der Spezifikation [6] für HTTP steht folgende Syntaxbeschreibung für ACCEPT_LANGUAGE:

The Accept-Language request-header field is similar to Accept, but
   restricts the set of natural languages that are preferred as a
   response to the request. Language tags are defined in section 3.10.

       Accept-Language = "Accept-Language" ":"
                         1#( language-range [ ";" "q" "=" qvalue ] )
       language-range  = ( ( 1*8ALPHA *( "-" 1*8ALPHA ) ) | "*" )

Ich will versuchen, dies einmal etwas verständlicher, in meinen Worten auszudrücken:

  • Die Angabe von ACCEPT_LANGUAGE kann, muss aber nicht im Header der Browseranfrage auftauchen. Wenn sie im Header auftaucht, dann wird sie mit „Accept-Language:“ eingeleitet.
  • Die Sprachangabe setzt sich aus 1 bis 8 Buchstaben und optional einem Bindestrich gefolgt von 1 bis 8 Buchstaben zusammen.
  • Hinter der Sprachangabe kann (muss nicht) ein Semikolon stehen, gefolgt von einem „q“, einem „=“ und einem Wert (qvalue).
  • Das Ganze kann sich beliebig oft wiederholen.

Wenn man sich den folgenden Teststring ansieht, wird die abstrakte Syntaxbeschreibung etwas anschaulicher.

 

Teststring:  Um unser Regex-Pattern zu testen, wurde folgender Teststring, der auch in der ACCEPT-LANGUAGE Spezifikation [6] verwendet wird, verwendet:

$test_lang = „da, en-gb;q=0.8, en;q=0.7″;

Der Teststring enthält drei Sprachdefinitionen (durch Komma getrennt), die ein Browser bei seiner Seitenanfrage mitgesendet hat.

  • Der Besucher bevorzugt Seiten auf Dänisch (da)[5]. Es ist kein Land bei der Sprache Dänisch angegeben und auch kein Qualifizierungsfaktor (q=). Bei Sprachangaben ohne Qualifizierungsfaktor wird dieser implizit mit q=1 gleichgesetzt, d.h. diese Sprache hat die höchste Priorität.
  • Als zweite (q=0.8) Alternative (en-gb;q=0.8) wäre für den Besucher Englisch wie es in Großbritannien gesprochen wird akzeptabel.
  • Als Fallback mit dem niedrigsten Wert (q=0.7) hat sich der Besucher für Englisch allgemein (en;q=0.7    keine Länderangabe) entschieden.

 

Suchpattern:  Als Arbeitsgrundlage für das Beispiel verwenden wir das Suchpattern

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

Das Suchpattern erfüllt die Anforderungen an die Syntax von HTTP_ACCEPT_LANGUAGE. Kann jedoch noch in einem Punkt bzgl. der Ergebnisausgabe optimiert werden (s.a. Quizfrage/Verständnisfrage).

 

Suchpattern analysieren

Modifier

Modifier sind Anweisungen, die auf den ganzen Patternausdruck wirken. In unserem Beispiel wird der Modifier „i“ verwendet, der die Pattern „case insensitiv“ behandelt, d.h. Groß- und Kleinschreibung wird ignoriert.  Die Syntax des Modifier-Konstrukts ist recht einfach:

        Modifier-Syntax:       Begrenzer Pattern Begrenzer Modifier
                                                  wobei in unserem Beispiel der Slash (/) als Begrenzer dient

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

Anmerk.: Der case-insensitive Modifier „i“ bewirkt nur, dass bei der Mustererkennung sowohl Groß- und Kleinschreibung erkannt werden. Er verändert nicht (wie z.B. ein strtolower() ) den Verarbeitungsstring oder die Werte im Ergebnisarray.  Der i-Modifier ist mehr eine Vereinfachung. Anstatt z.B. explizit [a-zA-Z] schreiben zu müssen, genügt ein [a-z].

Weitere Modifier wären z.B. gmixXsuUAJ (s.a. Regex Online Tester [8])

Eckige Klammern

In eckigen Klammern steht ein erlaubtes Zeichen z.B. [d] oder ein Bereich von erlaubten Zeichen.

So entspricht [a-z] einem Zeichen innerhalb des Alphabets, angefangen vom kleinen „a“ bis zum „z“. Die großen Buchstaben würden allerdings von diesem Pattern nicht gefunden werden. Dazu wäre es erforderlich diese zusätzlich anzugeben (s.a. Modifier).

[a-zA-Z]

Minimale/Maximale Zeichenanzahl

In der Vorgabe für unser HTTP_ACCEPT_LANGUAGE Beispiel steht, dass als erstes das Sprachkennzeichen steht, welches aus bis zu 8 Alphabetszeichen bestehen darf.

In Regex drückt man eine solche Minimal-/Maximalanweisung durch geschweifte Klammern aus.

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

 

Optionale Zeichenkette

In der Vorgabe für unser HTTP_ACCEPT_LANGUAGE Beispiel steht, dass hinter dem Sprachkz. noch ein Bindestrich gefolgt von einem achtstelligen Ländercode stehen kann.

Diese optionale Angabe (kann stehen, muss aber nicht) wird kann man in regex durch ein Fragezeichen, welches auf eine durch Klammern eingeschlossene Gruppe wirkt, abbilden.

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

 

Generic Whitespaces

Wir hatten ja in unserer Vorgabe für den Aufbau des HTTP_ACCEPT_LANGUAGE Strings schon erfahren, dass vor und nach dem „Gleichheitszeichen“ oder auch vor und nach dem „Semikolon“ von den Browsern teilweise noch Leerzeichen eingefügt werden.

Um dies in unserem Suchpattern abzubilden, verwenden wir „\s“ als Platzhalter für das Leerzeichen.

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

wobei anzumerken ist, dass ein „\s“ nicht nur der Platzhalter für das Leerzeichen, sondern für eine ganze Gruppe nicht druckbarer Zeichen ist.

Das angehängte „*“ ist ein Multiplikator und bedeutet, dass das vorangegangene Zeichen Null-mal bis beliebig oft vorkommen darf. 

\s       „generic whitespace“ genannt, steht u.a. für space, tab, newline und carriage return

\s*      siehe \s, beliebig oft (auch null mal)

 

Oder Ausdrücke

In der Vorgabe für den Aufbau des HTTP_ACCEPT_LANGUAGE Strings steht, dass ein Qualifizierung- bzw. Prioritätsfaktor für die Sprachangabe angegeben werden kann. Wenn dieser angegeben wird, kann er z.B. so (q=1) oder also auch so (q=0.8) aussehen.  Diese „Oder“-Bedingung finden wir beim Regex-Pattern durch das „|“ Zeichen ausgedrückt. Das „|“ ist sicherlich von anderen Programmiersprachen als Oder-Zeichen bekannt.

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

 

Beliebiges Zeichen oder Punkt

Wenn wir ein Suchpattern aufstellen, so gilt im Allgemeinen, dass ein Zeichen für sich selbst steht, also ein „a“ steht für ein „a“, das große „K“ für ein großes „K“ und ein Zeichen wie „;“ für ein Semikolon.

Aber es gibt in regex wie in jeder anderen Sprache auch Steuerungszeichen wie den Bindestrich in [az] oder das „?“ oder das „+“ oder …

Wenn wir diese Zeichen nicht als regex-Platzhalter/Steuerzeichen, sondern als explizites Zeichen von regex interpretiert wissen wollen, dann müssen wir dies regex mitteilen, in dem wir das Zeichen mit einem Backslash „\“ maskieren, einen Backslash voranstellen.

In unserem Beispiel möchten wir auf eine Dezimalzahl mit Punkt (q=0.8) suchen.  Der Punkt steht allerdings in regex für „ein beliebiges Zeichen“.  Hat also eine Doppelbedeutung, wenn man so will. Um explizit einen Dezimalpunkt zu fordern, ist die Maskierung mit einem Backslash erforderlich.

/([a-z]{1,8}([a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

Anmerk.:  Eigentlich gilt für den Bindestrich das Gleiche. Auch dieser hat eine Doppelbedeutung. Er kann für sich selbst stehen, oder einen Bereich aufspannen ([az]).
Und jetzt wird es etwas tricky (Feinheit von regex).
Schreibe ich den Bindestrich außerhalb der eckigen Klammer,

/([a-z]{1,8}([a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

dann steht er automatisch für sich selbst, einen Bindestrich.

Will man den Bindestrich innerhalb der eckigen Klammer als zulässiges Zeichen neben „a-z“ erlauben, dann müsste man den Bindestrich maskieren.

[a-z\-]

 

Klammern und Gruppenbildung

In Regex kommen alle drei gängigen (eckige, geschweifte und runde) Klammern (eckige, geschweifte und runde) zum Einsatz.

Eckige Klammern: Diese haben wir schon kennengelernt und verwenden eckige Klammern, um die Menge der zulässigen Zeichen z.B. [a-z\.0-9] festzulegen.

Geschweifte Klammern: Auch diese haben wir schon verwendet, um die Min-/Max Anzahl der vorangestellten Zeichen festzulegen z.B. {1,8}.

Runde Klammern: Mit den runden Klammern kann man in Regex-Patterns Zeichengruppen bilden. Die Bildung von Gruppen ist in zweierlei Hinsicht wichtig. Zum einen kann man auf diese Gruppen  dann Wiederholungsfaktoren wie „*“, „?“ oder „+“ anwenden und zum anderen kann man damit die Ausgabe von Regex steuern (capturing groups). 

Bei PHP wird vom reg_match_all Befehl ein Array als Result zurückgegeben. Dieses zweidimensionale Array enthält die Gruppen in der ersten Dimensionsstufe.

Um dies anschaulich zu machen, folgt die nun die Zuordnung der Arrayelemente erster Stufe für unser HTTP_ACCEPT_LANGUAGE Suchpattern, gesteuert durch die Platzierung der runden Klammern

$result[0][…]

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

$result[1][…]

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

$result[2][…]

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

$result[3][…]

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

$result[4][…]

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

Die Zuordnung von Arrayelement zur Gruppe erfolgt vom Groben (äußerste Klammer) zum Feinen und dann von links (linke runde Klammerung) nach rechts. 

 

Quizfrage:

Soweit alles verstanden? Hier schnell zu beantwortende Verständnisfragen. Die Lösungen finden sich am Ende des Blogartikels.

1a) Wie müsste der bisher verwendete, obige Pattern-String

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

verändert werden, damit die gelb markierte Suchpattern-Stelle als $result[x][…] Wert ausgegeben werden würde?

1b) Welchen Wert hätte das x bei $result[x][…] für diese Pattern-Gruppe?

1c) Welche Auswirkung hat die Änderung auf die Anzahl der Array-Werte von $result[][]

 

Rückgabewerte der Regex Funktion

In einem vorangegangenen Abschnitt haben wir gelernt, dass die Gruppen im Rückgabearray der Regex Funktion der ersten Dimensionsstufe zugeordnet sind ($result[0], $result[1], $result[2], …).

Hier nun soll an unserem Beispiel gezeigt werden, was wir als Ergebnis in der zweiten Array-Dimensionsstufe erwarten dürfen.


 $result[0]…  enthält die Gesamtstrings der Patternübereinstimmungen. In unserem Beispiel gibt es drei Sprachübergaben im HTTP_ACCEPT_LANGUAGE String

 $test_lang = „da, en-gb;q=0.8, en;q=0.7„;
 /([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

    $result[0][0] = „da“
    $result[0][1] = „en-gb;q=0.8“
    $result[0][2] = „en;q=0.7“


$result[1]…  enthält die erste Pattern-Gesamtgruppe

 $test_lang = „da, en-gb;q=0.8, en;q=0.7″;
 /([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

    $result[1][0] = „da“
    $result[1][1] = „en-gb“
    $result[1][2] = „en“


$result[2]…  enthält die optionale (siehe das ‚?‘) Ländergebietsgruppe. Wenn diese nicht vorhanden ist, dann wird ein leerer String zurückgegeben.

$test_lang = „da, en-gb;q=0.8, en;q=0.7″;
 /([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

    $result[2][0] = „“
    $result[2][1] = „-gb“
    $result[2][2] = „“


$result[3]…  enthält die optionale (siehe das ‚?‘) Prioritätsfaktor Zeichengruppe. Wenn das Suchpattern dieser Gruppe nicht greift, dann wird ein leerer String zurückgegeben.

$test_lang = „da, en-gb;q=0.8, en;q=0.7„;
/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

    $result[3][0] = „“
    $result[3][1] = „;q=0.8“
    $result[3][2] = „;q=0.7“


$result[4]…  enthält den Priorisierungswert der Sprache.

$test_lang = „da, en-gb;q=0.8, en;q=0.7„;
/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

    $result[4][0] = „“
    $result[4][1] = „0.8“
    $result[4][2] = „0.7“


Abschlusswort

Das war meine kleine Einführung in Regex anhand eines Beispiels (HTTP_ACCEPT_LANGUAGE).

Ich denke, dass klar geworden ist,

  • dass Regex ein mächtiges Werkzeug ist
  • dass die Patterns von Regex am Anfang etwas furchteinflößend sind, aber wenn man sich etwas damit befasst, man schnell in der Lage ist, fremde Patterns zu lesen und eigene Patterns aufzustellen.

Viel Spaß beim regex’en

Blogmaster
Manfred Roos

 


Lösung der Quizfrage:

1a) Wie müsste der obige Pattern-String

/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

verändert werden, damit die gelb markierte Suchpattern-Stelle als $result[x][…] Wert ausgegeben werden würde?

Antwort: Um den gelb-markierten Bereich sind runde Klammern zu setzen, damit dieser Teil als Gruppe gekennzeichnet ist und damit im Ergebnisarray zurückgeliefert wird.  

      /(([a-z]{1,8})(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i

1b) Welchen Wert hätte das x bei $result[x][…] für diese Pattern-Gruppe?

       Antwort:  x = 2

1c) Welche Auswirkung hat die Änderung auf die Anzahl der Array-Werte von $result[][]

      Antwort:

         – Zum einen erhöht sich die Anzahl der Arrays von
           bisher 5 auf 6.
           $result läuft nach der Änderung des Patternstrings
           jetzt bis $result[5].

        – Durch das Einschieben der neuen Gruppe an die
          Stelle $result[2] verschieben sich natürlich auch
          die bisherigen, nachfolgenden Gruppen zu
          $result-Zuordnungen, d.h. die Codierung ist
          anzupassen.

 

 

Links zum Thema:

[1] ComputerHope: Regex

[2] php net: preg_match_all

[3] php.net: $_SERVER Variablen

[4] Buch von Oreilly: Mastering Regular Expressions, 3rd Edition

[5] selfHtml: Sprachkürzel

[6] RFC 2616-Hypertext Transfer Protocol: Accept-Language

[7] w3programmers: Working with PHP Regular Expressions

[8] regex101.com: Regex Online Tester

[9] Daniel Fett: Tutorial reguläre Ausdrücke

[10] Wichtiger Sicherheitshinweis: Can’t Trust the $_Server


Viewing all articles
Browse latest Browse all 24