Een e-mailadres validator schrijven met Regular Expressions
Veel mensen hebben moeite met regular expressions. Het wordt vaak beschreven als een soort voodoo waar je een licht-schuwe nerd voor moet zijn om ooit te begrijpen. Dat valt allemaal wel mee (of ik ben een licht-schuwe nerd, mag ook ;-) ) en als je er een klein beetje tijd in steekt krijg je er met regular expressions een enorme krachtige tool bij. Dit artikeltje begint bij het begin en legt uit hoe je een regular expression kunt maken voor een emailadres.
Let op: Nu mag een emailadres officieel veel meer bevatten dan je zou denken. Zo mag een emailadres bijvoorbeeld over meerdere regels geschreven worden. Aangezien hopelijk niemand dit gebruikt en het doel van dit artikeltje niet is om een compleet-sluitende emailadres parser te schrijven zullen we alleen het meest gangbare implementeren. Alles wat je ooit wilde weten over een emailadres (en meer) kun je vinden in hoofdstuk 6 van RFC 822.
Het begin
Een "regular expression" is in feite niet meer dan een soort programmeer statement geschreven in een speciale taal om patronen te herkennen in strings. Als je wilt leren een regular expression te schrijven is het dus slim om op dezelfde manier naar je te matchen string te kijken. Door te zoeken naar kleine patronen binnen de string die je wilt matchen kun je een goed idee krijgen van wat je te doen staat.
Dus dat gaan we doen. Zoals gezegd gaan we in dit artikeltje kijken hoe we een regular expression kunnen schrijven die verschillende emailadressen kan matchen. Een typisch emailadres is:
-
info@example.org
Zoals je weet en ook in het bovenstaande emailadres ziet, bestaat een emailadres in de eerste plaats uit twee elementen gescheiden door het @-teken.
[gebruikersnaam] @ [server naam]
Vervolgens kan de server naam zelf ook weer uit enkele delen bestaan:
[domein naam] . [TLD]
TLD staat voor Top Level Domain en zijn de toevoegsels .nl of .com bij een domein naam.
Een eerste stap: de gebruikersnaam
De gebruikersnaam kan natuurlijk bestaan uit letters en cijfers. Maar een gebruikersnaam mag ook puntjes en streepjes bevatten. We zullen dus een eerste patroon moeten maken dat iets dergelijks kan begrijpen.
In een regular expression kun je dit opschrijven met een character class. Een character class wordt geschreven tussen 2 rechte haken en houdt standaard in dat de te matchen string, een van de waarden binnen deze definitie eenmaal moet voorkomen.
Een character class kan bestaan uit individuele waarden en ranges. Twee waarden gesplitst door een streepje, zoals A-Z, is een range. Dit houdt in dat alle waarden van A tot en met Z geaccepteerd worden. De gebruikersnaam mag letters, cijfers, streepjes, underscores en puntjes bevatten en dit schrijf je dan als volgt:
-
[a-z0-9_.\-]
Deze character class voor de username geeft dus aan dat de te matchen string 1 karakter uit de volgende combinaties moet bevatten:
- alle letters van a tot z
- alle nummers van 0 tot 9
- een underscore
- een punt
- een streepje
Het is je vast opgevallen dat er een backslash voor het streepje staat en dat deze niet voorkomt in de lijst van mogelijke waarden die ik zojuist beschreef. Dit komt omdat het streepje een karakter is dat ook gebruikt wordt voor het opgeven van ranges. Het streepje is een zogenaamd meta-character: het heeft zowel een functionele betekenis (wordt gebruikt om een range te definieren) als een letterlijke betekenis (het streepje zelf). Om de letterijke betekenis te krijgen escapen we het streepje door er een backslash voor te zetten.
In RFC 822 staat dat het gebruikersnaam onderdeel van een emailadres moet beginnen met wat zij definieren als een woord. Een woord begint nooit met een punt of een streepje, dus dit is iets dat we moeten aangeven in ons patroon. Dit kunnen we doen door nog een character class te maken, met alleen de onderdelen waarmee een emailadres gebruikersnaam mag beginnen:
-
[a-z0-9][a-z0-9_.\-]
Kortom, de gebruikersnaam moet nu beginnen met een letter of cijfer en kan dan gevolgd worden door een letter, cijfer, of speciaal teken.
Gaat heen en multiply uzelf!
Wat nu nog mist in ons patroon is de mogelijkheid om te zeggen dat er meerdere karakters uit deze character class (of reeks) mogen voorkomen in de gebruikersnaam. Eerder zei ik al dat uit een standaard character class definitie 1 waarde éénmaal moet voorkomen. Niet minder keer en niet meerdere keren. Je kunt dan zeggen dat een character class een standaard multiplier heeft van 1.
Deze multiplier is te wijzigen met de multiplier operator welke je achter een subpatroon, zoals bijvoorbeeld een enkel karakter, een character class of een groep, plaatst:
- + betekent 1 of meer keer herhalen
- ? betekent 0 of 1 keer herhalen
- * betekent 0 of meer keer herhalen
- {3} betekent exact 3 keer herhalen
- {2,5} betekent tussen de 2 en 5 keer herhalen
- {2,} betekent 2 of meer keer herhalen
We weten niet precies hoe lang een gebruikersnaam gaat zijn maar we weten wel dat deze langer mag zijn dan 1 karakter. Uit het lijstje hierboven houden we dan 2 multiplier operators over: + en *.
Het eerste deel van onze regular expression (de verplichte letter of cijfer) hoeft niet meerdere keren voor te komen. We willen juist dat deze exact eenmaal voor komt en dus hoeven we daar geen multiplier operator achter te zetten. Het tweede deel daarentegen mag wel meerdere keren voorkomen. In principe mag een emailadres als a@example.org en dus is de multiplier * de juiste keuze. Met de *-multiplier mag het patroon dus 0 of meerdere keren voorkomen waardoor dit deel van het patroon in feite optioneel is geworden:
-
[a-z0-9][a-z0-9_.\-]*
Stap twee: server naam
Nu we een aardig patroon hebben voor de gebruikersnaam laten we deze nu even voor wat hij is en kunnen we ons richten op de server naam. Dit is wat lastiger aangezien alle volgende voorbeelden geldige servernamen zijn:
- example.org
- voorbeeld.example.org
- subvoorbeeld.voorbeeld.example.org
Laten we beginnen met de eerste, example.org. Aan het begin zeiden we al dat deze uit 2 onderdelen bestond:
[domein naam] . [TLD]
Een domein naam kan ook uit letters, cijfers en streepjes bestaan. Net als bij een gebruikersnaam geldt wel dat een domein naam met een letter of cijfer moet beginnen. We kunnen dus een heel stuk van onze patroon voor een gebruikersnaam overnemen. We halen alleen de underscore en het puntje er uit:
-
[a-z0-9][a-z0-9\-]*
Kortom: een domein naam begint met een letter of cijfer en kan dan bestaan uit 0 of meer letters, cijfers en/of streepjes.
De server naam is niet compleet zonder TLD en dus deze moeten toevoegen aan het patroon. Er zijn verschillende lengtes TLDs:
- De standaard 2 letterige TLDs voor landen, bijvoorbeeld nl, be en de
- De minder standaard biz en bijv museum. Deze laatste museum is momenteel de langste TLD in gebruik.
Zoals je ziet heeft een TLD geen nummers en bestaat ze uit minstens 2 en maximaal 6 letters (museum) . Uit het rijtje van onze multiplier operators nemen we dan de een na laatste operator: {2,5}. Deze houdt in dat het patroon van 2 moet en tot en met 5x herhaald mag worden, dus als we die wijzigen naar wat we nodig hebben krijgen we als patroon:
-
[a-z]{2,6}
Een domein naam en een TLD worden gescheiden door een puntje. Dit is weer een meta character en is een soort alias voor een character class die alle mogelijke waarden accepteert, daarover in een volgend artikel meer. Aangezien we nu graag de letterlijke waarde willen hebben zullen we hem, net als het streepje dat we eerder zagen, moeten escapen met een backslash. Om een domein naam en TLD te matchen hebben we dan dit patroon:
-
[a-z0-9][a-z0-9\-]*\.[a-z]{2,6}
Als laatste zullen we nu nog een stuk moeten toevoegen om eventuele subdomeinen als voorbeeld.example.org toe te staan. Voor subdomein namen gelden dezelfde regels als voor domein namen: ze moeten beginnen met een letter of cijfer en mogen dan bestaan uit verschillende letters, cijfers en streepjes. Dus het basis patroon is wederom:
-
[a-z0-9][a-z0-9\-]*
Een subdomein wordt ook altijd gevolgd door een puntje. Een subdomein staat namelijk altijd voor een domein naam of een andere subdomein naam. Een puntje is een meta-character en moet dus geëscaped worden:
-
[a-z0-9][a-z0-9\-]*\.
Maar nu lopen we tegen een probleem aan. Niet iedere server naam heeft een subdomein naam en dus moet het patroon optioneel worden. Daarbij kan het zijn dat er meerdere subdomeinen zijn en deze moeten ook worden toegestaan. We zullen dus met een multiplier moeten werken. Plaatsen we deze multiplier echter achter het puntje dan zal deze alleen voor het puntje zelf werken aangezien deze als een subpatroon wordt beschouwd.
We kunnen dit oplossen door dit subpatroon een groep te maken. Een groep omsluit een of meer subpatronen en gedraagt zich vervolgens zelf ook als een subpatroon. Op een subpatroon kun je vervolgens een multiplier toepassen. Ook stelt een groep je in staat om in bijvoorbeeld een PHP script het resultaat van dat specifieke subpatroon apart op te halen.
Een subdomein mag dus 0 of meerdere keren voorkomen; we gebruiken dus de *-multiplier operator. Een groep maak je door een subpatroon tussen haakjes te zetten dus het juiste patroon wordt dan:
-
([a-z0-9][a-z0-9\-]*\.)*
Het gehele patroon voor de server naam wordt dan:
-
([a-z0-9]+\.)*[a-z0-9][a-z0-9\-]+\.([a-z]{2,6})
Het gehele patroon van het emailadres is dan uiteindelijk:
-
[a-z0-9][a-z0-9_.\-]*@([a-z0-9]+\.)*[a-z0-9][a-z0-9\-]+\.([a-z]{2,6})
Cap'tain, our anchors are missing!
We missen echter nog iets. PCRE zal namelijk proberen om een opgegeven patroon overal in een string toe te passen, tenzij hij anders wordt verteld. Als wij PCRE inderdaad niets vertellen en we zouden het bovenstaande patroon gebruiken op de volgende string:
-
!!GEEN EMAILADRES!!!!info@example.org
..dan zou PCRE deze alsnog goedkeuren. Het verwachtte patroon staat namelijk aan het einde van deze string. Om dit op te lossen kun je zogenaamde anchors gebruiken:
- ^ staat voor "begin van de string"
- $ staat voor "einde van de string"
Gebruik je alleen ^ dan zeg je in feite: het opgegeven patroon moet aan het begin van de te matchen string voorkomen; wat er achter staat maakt niet uit. $ werkt op dezelfde manier maar dan voor het einde van de string. Geef je ze allebei op, dan zeg je in feite: dit patroon is het enige wat in deze string mag voorkomen. Dat is precies wat we nodig hebben voor ons emailadres patroon!
-
^[a-z0-9][a-z0-9_.\-]*@([a-z0-9]+\.)*[a-z0-9][a-z0-9\-]+\.([a-z]{2,6})$
Over delimiters en modifiers
Voordat we deze regular expression daadwerkelijk kunnen gaan gebruiken in PHP moeten we nog 2 dingen doen. Een PCRE (perl compatible regular expression) is namelijk standaard case-sensitive. Dat is lastig, aangezien een emailadres als Henk.de.Tester@example.org een valide emailadres is. Wat we hadden kunnen doen is de reeks A-Z (in hoofdletters dus) toevoegen aan onze character classes. Dat had ons patroon echter zo'n 18 karakters langer gemaakt wat het toch al moeilijk leesbare patroon nog net even lastiger te lezen maakt.
Gelukkig hebben we nog een andere optie en dat is gebruik maken van een modifier. Een optie die in dit geval voor het gehele patroon van toepassing is. Een modifier geef je op in het patroon zelf, maar waar?
Iedere regular expression dat met de PCRE functies gebruikt wordt moet tussen 2 delimiters staan. Tussen de delimiters staat de regular expression en achter de laatste delimiter staan eventuele modifiers. De modifier om PCRE case-insensitive te maken is i. Het complete patroon dat we nu aan een PCRE functie kunnen gaan voeren wordt dan:
-
~^[a-z0-9][a-z0-9_.\-]*@([a-z0-9]+\.)*[a-z0-9][a-z0-9\-]+\.([a-z]{2,6})$~i
Ik gebruik hier tildes als delimiters maar je mag in principe elk karakter gebruiken. Let op: de delimiter die je gebruikt wordt zelf automatisch ook een meta-character. Wil je de letterijke waarde ervan gebruiken in je patroon, dan zul je deze dus ook moeten escapen.
Toepassen in PHP:
Om een string te valideren tegen een patroon kun je de functie preg_match() gebruiken. Je kunt deze functie ook gebruiken om gedefinieerde groepen op te halen, maar dat is voor het volgend artikel over regular expressions.
De functie preg_match() retourneert het aantal matches dat het patroon heeft opgeleverd. Krijg je dus 0 terug, dan komt de gegeven string dus niet overeen met de regular expression:
-
function checkEmail($email)
-
{
-
if ( preg_match('~^[a-z0-9][a-z0-9_.\-]*@([a-z0-9]+\.)*[a-z0-9][a-z0-9\-]+\.([a-z]{2,6})$~i', $email) )
-
{
-
echo "Juist emailadres
-
";
-
} else {
-
echo "Onjuist emailadres
-
";
-
}
-
}
-
-
checkEmail("info@example.org");
-
checkEmail("info@subdomein.example.org");
-
checkEmail("I.nfo@subsubdomein.subdomein.example.org");
-
checkEmail("-nfo@-example.org");
Conclusie
In dit artikel hebben we de basis syntax van regular expressions behandeld. Hoewel we ons gericht hebben op PCRE werkt het meeste van wat hier beschreven is ook met de POSIX functionaliteit (dat zijn de ereg() functies). Met uitzondering van het gebruik van delimiters en modifiers.
Op PHP.net kun je meer informatie lezen over de syntax van PCRE en ook over de beschikbare modifiers. In een volgend artikel volgen specifieke features van PCRE en een overzicht van de verschillende PCRE functies die PHP biedt.
Volg Scriptorama via RSS!
Reageer ook!
Juist, naar zo'n artikel was ik al een tijdje opzoek. Kort en duidelijk de wondere wereld van PCRE beschreven. THNX!
Door Jeroen Sen
op 05.08.06 @ 8:08 am | Permalink
Heel goed artikel. Erg duidelijk!
Door Shachar Nemirovsky
op 05.08.06 @ 12:57 pm | Permalink
Hier ook nog een mooie voor domeinnamen: http://www.shauninman.com/plete/2006/05/validating-domain-names :)
Door Krijn Hoetmer
op 05.08.06 @ 2:08 pm | Permalink
Mooi artikel.
Door Mark Pieper
op 08.28.06 @ 2:21 pm | Permalink
En uitgangen als co.uk dan?
Door Kees jansen
op 09.03.06 @ 12:40 pm | Permalink
Hoi Kees,
Zoals gezegd is dit artikel niet bedoeld als een complete validator. Met dat gezegd zal de regular expression "gewoon" werken voor co.uk domeinen, aangezien 'co' als een domein gezien kan worden.
Door Mathieu Kooiman
op 09.05.06 @ 8:01 pm | Permalink
Een leuke vingeroefening voor regexps maar mail-adressen moeten voldoen aan RFC2822 (tot voor kort aan RFC822).
Het zal niet vaak voorkomen maar ook ASCII karakters zoals + zijn theoretisch toegestaan, en omringende "" en []. Niet dat het met alle software zal werken, of vaak zal voorkomen, maar het lijkt mij een streven om alle volgens die standaard toegestande mailadressen op zijn minst toe te staan.
Om dat niet zelf te hoeven schrijven spiek je op http://code.iamcal.com/php/rfc822/ .
De broncode is te zien in rfc822.phps en de 2822 versie, de *.php files laten zien wat er wel en niet wordt doorgelaten.
Door Arakrys
op 11.17.06 @ 12:13 am | Permalink
Hey,
Dit is precies wat ik nodig had om verder te komen met mijn opdracht.
Thx, mijn opdracht is voltooid.
Door Dominique
op 02.05.08 @ 10:22 am | Permalink
[...] je na het lezen van Scriptorama's inleiding tot Regular Expressions een tooltje willen hebben om te controleren of een bepaalde regex die je hebt geschreven wel [...]
Door Scriptorama.nl » Online regular expressions testen op 05.17.08 @ 3:32 pm | Permalink
Line and paragraph breaks automatic, e-mail address never displayed, HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>