Pagina 1 van 1

[microcursus] reguliere expressies in PHP

Geplaatst: ma 21 dec 2009, 11:11
door Jan van de Velde
Er is ook een volledig overzicht van alle cursussen, FAQ's en handleidingen .


Als je van deze cursus gebruik maakt, willen we je vriendelijk vragen te laten weten wat je er van vond:
  • Geef eventuele foutjes aan;
  • Zijn de onderdelen soms onduidelijk, of net erg helder?
  • Ontbreken er volgens jou stukken, of heb je suggesties?
  • ...
Reageren kan in vragen en opmerkingen over de cursus. We wensen je veel plezier en succes met cursus.


---------------------------------------------------------------------------------------


[microcursus] Reguliere expressies in PHP


Auteur: Ger


Deze microcursus is bedoeld voor mensen die reeds enige ervaring hebben met programmeren, al dan niet specifiek in PHP.

Er wordt uitgegaan van basiskennis betreffende inputs, outputs, variabelen, etc.


1. Inleiding


Met reguliere expressies kun je onderzoeken of teksten (in programmeerwereld doorgaans "strings" genoemd) aan bepaalde patronen voldoen, en er eventueel handige "zoek en vervang"-acties op uitvoeren. Zo'n patroon kan een verzameling letterlijk opgegeven tekens zijn, maar kan ook jokertekens bevatten.


Jokertekens: Neem bijvoorbeeld de woorden
handvat, verhandelen, backhand, hondenhok, wolfshond
Zoek je naar het patroon hand, dan zul je alleen de eerste drie woorden vinden. Zoek je echter naar h(iets)nd dan zie je dat het patroon in elk woord voorkomt. Het stukje (iets) is in dit geval de joker.


Met veel programmeertalen kun je eenvoudig zoeken naar dit soort patronen. Zo'n patroon noemen we een reguliere expressie (vaak afgekort als regexp of regex).

De kracht van reguliere expressies is vooral dat je bepaalde eisen kunt stellen aan de jokers. Zo kun je bijvoorbeeld het aantal tekens binnen de joker beperken, aangeven dat de joker uit cijfers of juist letters moet bestaan, of dat er in de joker bepaalde zaken juist niet mogen zitten. De mogelijkheden om eisen te stellen zijn, mede door slim combineren ervan, bijna eindeloos.


In deze cursus gebruiken we als voorbeeld steeds de programmeertaal PHP. Reguliere expressies zijn echter in vrijwel iedere programmeertaal te gebruiken. Over het algemeen gaat het zoeken naar patronen ongeveer hetzelfde voor elke taal.


Grofweg zijn er 2 hoofdsoorten reguliere expressies te onderscheiden in PHP: de "ouderwetse" (POSIX) en de Perl-compatible (PCRE). De ouderwetse hebben beperkte mogelijkheden, zijn relatief traag en staan op de nominatie om te komen vervallen. Daarom behandelen we in deze cursus alleen de Perl-compatible reguliere expressies.


2. Letterlijke patronen


2.1 preg_match


De simpelste functie voor een reguliere expressie is preg_match. Deze functie zoekt binnen een string naar een patroon dat overeenkomt (matcht) met het door jou opgegeven patroon. Zie onderstaand voorbeeld:

Code: Selecteer alles

<?php

$string = "Jan, Piet en Klaas deden mee met een potje voetbal op het plein.";

if ( preg_match( '#Piet#', $string ) ) {

	 echo "Piet deed mee";

}

else {

	 echo "Piet deed niet mee";

}

?>
Belangrijkste in dit stuk is natuurlijk preg_match( '#Piet#', $string ), wat zegt dat naar Piet gezocht moet worden binnen $string. De output hiervan is 0 (geen match) of 1 (wel een match). Meer kan niet, aangezien preg_match stopt met zoeken na de eerste match. In bovenstaande code wordt dat gebruikt als boolean variabele, dus false respectievelijk true. Omdat Piet in $string genoemd staat (dus 1), zal deze code vermelden dat Piet mee heeft gedaan met het potje voetbal.


Laten we het patroon eens even uitsplitsen: '#Piet#'
  1. '...' : aanhalingstekens begrenzen de inputstring, zoals dat altijd gebeurt bij strings in PHP. Niets bijzonders dus.
  2. #...# : hekjes begrenzen het basispatroon.

    Hiervoor kun je ook een ander teken gebruiken, vrijwel elk speciaal karakter is toegestaan. De slash (/) wordt ook veel gebruikt. Aanhalingstekens mogen niet.
  3. Piet : geeft aan dat we naar de expressie "Piet" zoeken.
2.1 preg_replace


Een tweede functie is preg_replace. Hiermee kun je een eerder gevonden patroon vervangen door iets anders. Bijvoorbeeld:

Code: Selecteer alles

<?php

$string = "Jan, Piet en Klaas deden mee met een potje voetbal op het plein.";

$output = preg_replace('#Piet#', 'Kees', $string);

echo $output;

?>
We gaan de opdracht op de derde regel weer uitsplitsen:

'#Piet#', 'Kees', $string
  1. '...' : aanhalingstekens begrenzen de inputstring.
  2. #...# : hekjes begrenzen het basispatroon.
  3. Piet : geeft aan dat we naar de expressie "Piet" zoeken.
  4. 'Kees' : die gezochte expressie moet worden vervangen door "Kees".
  5. $string : dit moet gebeuren in de eerder gedefinieerde string "$string".
Dit zal resulteren in:

Code: Selecteer alles

Jan, Kees en Klaas deden mee met een potje voetbal op het plein.
Zoals je ziet is dit een letterlijke vervanging zonder poespas. Die poespas is wel mogelijk, maar daar komen we later op terug.


terzijde: als je het exacte patroon al weet en je hoeft maar 1 patroon binnen een string te vervangen, dan is str_replace een snellere methode. Dit voorbeeld wordt slechts gebruikt ten behoeve van het basisbegrip voor reguliere expressies.


3. Variabele patronen


Zoeken naar een letterlijk patroon is natuurlijk vrij simpel. Veel interessanter is echter het zoeken naar een variabel patroon. Patronen kun je variabel maken door jokertekens toe te passen, waarmee je niet letterlijk aangeeft wat je zoekt, maar wat voor iets (letters, cijfers, of misschien simpelweg ongespecificeerde tekens) je zoekt. Stel dat je bijvoorbeeld niet weet wie er meededen met het potje voetbal, dan kun je natuurlijk apart gaan controleren voor alle namen, maar je kunt het ook middels een reguliere expressie uitzoeken:

Code: Selecteer alles

<?php

$string = "Jan, Piet, Kees en Klaas deden mee met een potje voetbal op het plein.";

preg_match('#(.+) deden .+ potje (.+)\.#', $string, $matches);

print_r($matches);

?>
Opvallend:
  • Een extra variabele in de preg_match, namelijk $matches. Deze variabele kun je gebruiken om het resultaat op te slaan, wat gebeurt in een array zoals je kunt zien in de output.
  • print_r in plaats van echo. Met "echo" zou PHP alleen weergeven dat er een array is, maar niet de inhoud ervan.
Bovenstaande zal resulteren in:

Code: Selecteer alles

Array

(

	[0] => Jan, Piet, Kees en Klaas deden mee met een potje voetbal

	[1] => Jan, Piet, Kees en Klaas

	[2] => voetbal

)
De [0]-key van de array bevat het complete patroon, de volgende keys bevatten de matches die in de substring (.*) zijn gevonden, in de volgorde dat PHP ze tegenkwam.


Kijken we weer naar de derde opdrachtregel: eerst de te zoeken expressie: '#(.+) deden .+ potje (.+)\.#'
  1. '...' : aanhalingstekens begrenzen de string als geheel.
  2. #...# : hekjes begrenzen het basispatroon .
  3. Verder staan er nog letterlijke woorden uit de zin die we zoeken (deden, potje).
  4. (.+) :
    • Haakjes : geven aan dat we dit resultaat verderop nog willen gebruiken. We noemen dit een substring. De substrings komen uit in de array $matches, waarbij de [0]-key de gehele string bevat, de [1]-key de eerste substring, de [2]-key de tweede substring, en zo verder als je nog meer substrings zou hebben.
    • Punt : heet range, en geeft aan dat je naar een variabel patroon (met jokers) op zoek bent.

      Een punt als range betekent dat we een willekeurig teken behalve een newline (ook wel: linebreak; de begrenzing van een coderegel) willen zien. Je kunt namelijk ook aangeven dat je op zoek bent naar alleen letters (aangegeven met [A-Za-z]), alleen cijfers (aangegeven met [0-9] of de "d" van digits), of misschien alleen één of een aantal specifieke letters, cijfers of tekens. Het is daarom goed om na te denken wat je wel en niet in je range stopt. In dit voorbeeld gebruiken we de meest vrije mogelijkheid, maar soms wil je dat juist niet. Het zal bij reguliere expressies namelijk vaak voorkomen dat je wil controleren of een bepaalde invoer aan je voorwaarden voldoet. Dan wordt het dus heel belangrijk dat je goed nadenkt over de voorwaarden!
    • Plus : heet quantifier De quantifier geeft aan naar hoeveel tekens binnen je range je op zoek bent. Er zijn meer mogelijkheden voor quantifiers:
      • * : geeft aan dat we naar 0 of meerdere tekens zoeken,
      • + : geeft aan dat we naar 1 of meerdere tekens zoeken,
      • {4} : geeft aan dat we exact 4 tekens zoeken
      • {10-13} : bijvoorbeeld als je de invoer voor een telefoonnummer wilt controleren, en je niet weet of men nationale of internationale nummers gebruikt. Hiermee wordt alles van 10 tot en met 13 tekens herkend.
      • {10|13} : Zelfde als bovenstaande, maar nu worden alleen strings van 10 of 13 herkend, en dus niet de tussenliggende waarden.


      Voor een lijst met alle mogelijkheden voor ranges en quantifiers verwijzen we naar de bijlage onderaan deze cursus.
    We gebruiken (.+) twee keer, omdat we weten dat er een spel gedaan is, maar we weten niet welk spel of door wie.
  5. .+ : dit geeft ook aan dat we een willekeurig teken (aangegeven door de punt) 1 of meerdere keren willen zien (aangegeven door het plusje). Het verschil met (.+), zijn de haakjes die er hier niet omheen staan.

    Die haakjes gaven aan dat we het resultaat verderop nog willen gebruiken. Omdat voor ons de tekst "mee aan een" niet van belang is , zetten we dat niet in een substring zodat we er in de array ook geen last van hebben.
  6. \. : de punt sluit in het patroon de zin af. De backslash voorkomt dat de punt wordt gezien als een regexp-functie.

    De punt is hier bedoeld als een letterlijke punt, niet als functie in je reguliere expressie. Met de backslash ervoor geef je dat aan, "escape" (omzeil) je de speciale functie. Ook de tekens $, ^, (, ), <, >, |, \, {, [, ., *, +, of ? hebben een betekenis in reguliere expressies. Als die in je string voorkomen, moet je ze dus ook "escapen" (omzeilen) door er een backslash voor te zetten .

Onthoud:

Als je de inhoud van een array wil weergeven, gebruik dan print_r in plaats van echo


De tekens $, ^, (, ), <, >, |, \, {, [, ., *, +, of ? hebben een betekenis in reguliere expressies. Als die in je string voorkomen, moet je ze "escapen" (omzeilen) door er een backslash voor te zetten

NB: Je ziet in dit rijtje ook de backslash zelf staan. Ook die moet je dus escapen door er een backslash voor te zetten. Als je patroon dus een letterlijke backslash bevat, zal dat in je reguliere expressie " \\" opleveren; 2 backslashes dus.



4. Meer dan 1 match


Het kan natuurlijk voorkomen dat je in een tekst meerdere matches hebt. Stel je voor dat je een tekst hebt als:
Moeder riep: "Jan, hierkomen alsjeblieft!". Jan reageerde niet. Moeder riep: "Ja-an, hierkomen!". Nog steeds gaf Jan geen reactie. Moeder riep: "JAN! Hierkomen! Nu!".
Wil je daar uit filteren hoe het taalgebruik van moeder verandert bij het verliezen van haar geduld, dan kun je dat niet gemakkelijk filteren met preg_match. Die geeft namelijk alleen de eerste match weer, en stopt vervolgens. Je kunt dan gebruik maken van preg_match_all, dat, zoals de naam al zegt, door blijft zoeken totdat het einde van de inputstring bereikt is:

Code: Selecteer alles

<?php

$string = 'Moeder riep: "Jan, hier komen alsjeblieft!". Jan reageerde niet. 

Moeder riep: "Ja-an, hier komen!". Nog steeds gaf Jan geen reactie. 

Moeder riep: "JAN! Hier komen! Nu!".';

preg_match_all('#"(.*)"#', $string, $matches);

print_r($matches);

?>
Bovenstaande zal resulteren in:

Code: Selecteer alles

Array

(

	[0] => Array


(


[0] => "Jan, hier komen alsjeblieft!"


[1] => "Ja-an, hier komen!"


[2] => "JAN! Hier komen! Nu!"


)


[1] => Array


(


[0] => Jan, hierkomen alsjeblieft!


[1] => Ja-an, hierkomen!


[2] => JAN! Hierkomen! Nu!


)


)
Dit is een zogenaamde 'multidimensionale array'. De eerste subarray geeft de gevonden patronen compleet weer, de tweede de gevonden substrings. Het verschil in dit geval zijn de aanhalingstekens, die wel onderdeel uitmaken van het patroon, maar niet van de substring.


5. Matches gebruiken


Gevonden substrings (delen in je patroon tussen haakjes) worden opgeslagen in een array. Die kun je natuurlijk gebruiken om met een loop bepaalde zaken op toe te passen. Je kunt ze echter ook direct weer gebruiken, bijvoorbeeld bij een zoek-en-vervang opdracht. Zo kun je bijvoorbeeld heel handig een eigen BBcode aanmaken. Hieronder een voorbeeld voor een BBcode [strike]...[/strike] om stukken tekst door te strepen:

Code: Selecteer alles

<?php

$bericht = 'Falderie [strike]faldera[/strike] falderee [strike]falderoo[/strike].';

$bericht = preg_replace('#\[strike](.+)\[/strike]#Uis', '<span style="text-decoration: line-through;">$1</span>', $bericht);

echo $bericht; 

?>
Dit geeft als output:
Falderie faldera falderee falderoo

Laten we de reguliere expressie hier weer onder de loep nemen.

'#\[strike\](.+)\[\/strike\]#Uis', '<span style="text-decoration: line-through;">$1</span>'
  1. '...' : aanhalingstekens begrenzen de string als geheel.
  2. #...# : hekjes begrenzen het basispatroon.
  3. \[strike\] en \[\/strike\] : geeft aan dat we iets zoeken tussen [strike] en [/strike] tags.

    De speciale tekens ( de vierkante haken) hier omzeild (escaped) met een backslash. In dit voorbeeld escapen we meer dan strikt noodzakelijk is (de vierkante sluithaken escapen is hier niet nodig), maar dit kan afhankelijk van de gebruikte taal nodig zijn. Het kan in principe nooit kwaad om meer dan strikt noodzakelijk te escapen
  4. (.+) : zoek naar een of meerdere willekeurige tekens.
  5. Uis : pattern modifiers (patroon aanpassers), waarmee je bepaalde eisen of aanpassingen aan het patroon als geheel kunt geven. Voor alle mogelijkheden wordt verwezen naar de "cheatsheet" (spiekbriefje) die als bijlage onderaan deze cursus is opgenomen:
    • De U (let op de hoofdletter!) maakt het patroon "Ungreedy".

      Greedy, de standaard bij preg_match, betekent dat gezocht wordt naar een zo groot mogelijke match. Dat zou betekenen dat het patroon vanaf de eerste [strike] tot de laatste [/strike] als 1 match wordt gezien. Door het ungreedy te maken, wordt gezocht naar een zo klein mogelijke match, en wordt elke match apart gezien. Bij preg_match_all werkt het precies andersom: preg_match_all is standaard ungreedy en wordt met de U-modifier juist greedy gemaakt.

      Dit principe is ook toe te passen door in een substring een vraagteken te zetten. In dit geval hadden we van de substring (.+?) kunnen maken om hetzelfde te bereiken. Door dat op het niveau van de substring te doen kun je specifieker werken om een patroon te filteren.
    • De i maakt het patroon ongevoelig voor hoofdlettergebruik. We weten immers niet of de gebruiker de -tags invoert met SHIFT of CAPS LOCK ingetoetst, en ons maakt dat niet uit. Zo maken we het leven weer wat gemakkelijker!
    • De s geeft aan dat het patroon als een enkele lijn gezien moet worden, ook al is het dit niet. Als de gebruiker bij de invoer tussen de tags een keer op ENTER drukt, zou het patroon anders niet herkend worden.
  6. <span style="text-decoration: line-through;"> </span> : niks meer of minder dan HTML waarmee we iets kunnen doorhalen.
  7. $1 : Hiermee halen we de eerdere substring uit het patroon terug in onze BBcode-vervanging.

    Feitelijk zeggen we hier:
    "Zoek naar een stuk tekst waar [strike]-tags omheen staan.

    Pak vervolgens die tekst en vervang de strike-tags door HTML-code."
    De tekst zelf, binnen de [strike]-tags willen we dus weer terugzien.

    Het cijfer achter de $ geeft aan de hoeveelste substring in het patroon wordt teruggehaald:

    de eerste met $1, de tweede met $2, etc.


6. Tot slot


Reguliere expressies zien er in het begin heel ingewikkeld uit, en dat zijn ze misschien ook wel. De kunst is om stap voor stap na te denken. Begin met bedenken wat je grote patroon is, vervolgens wat er variabel aan is en of je dat nodig hebt als substring of niet. Vervolgens ga je die variabelen invullen volgens de regels in onderstaande bijlage. Denk nog even aan het escapen van de juiste karakters en het toepassen van de juiste pattern-modifiers en je hebt je reguliere expressie! :eusa_whistle:


Verder nog een laatste tip:

Kom je er niet uit? Gebruik dan print_r op je variabelen om te zien wat er eigenlijk in staat. Dan zie je meestal snel genoeg waar het fout gaat.


Bijlage


Bijlage: Regular expressions cheatsheet van Added Bytes