preg_match
og det første regulære udtryk^
og $
preg_match
's tredie argumentI denne note gives en introduktion til brugen af regulære udtryk, også kaldet mønstre, i PHP. Noten beskriver hvordan regulære udtryk bruges til at klassificere
strenge og derved kan bruges til at sikre at brugerinput følger en specificeret form
og til at identificere, og derved gøre brug af, en specificeret del af en streng.
Konkret introduceres læseren til PCRE (Perl Compatible Regular Expressions) og PHP-funktionen preg_match
.
Regulære udtryk bruges til at klassificere tekststrenge. Uden brug af regulære udtryk kan
vi skrive PHP-kode som undersøger om to strenge er identiske (består af de samme tegn).
Følgende PHP-kode undersøger for eksempel om en variabel $email
indeholder strengen
holbech@itu.dk, strengen someone@itu.dk eller en helt tredie streng:
<?php
if ( $email == 'holbech@itu.dk' ) {
// gør noget
} elseif ( $email == 'someone@itu.dk' ) {
// gør noget andet
} else {
// gør noget tredie
}
?>
Denne løsning virker fint så længe vi arbejder med en begrænset, foruddefineret, mængde data. Hvis vi derimod arbejder med input fra brugere, må vi ofte understøtte en større mængde af inputmuligheder, såsom alle strenge som tager form af et cpr-nummer, alle strenge som tager form af en emailadresse, og så videre. Som vi skal se i det følgende giver regulære udtryk, også kaldet mønstre, mulighed for, meget præcist at specificere hvilken form for data der skal accepteres af programmet.
Når man søger på regulære udtryk vil en stor del af resultaterne omhandle en anden slags regulære udtryk kaldet POSIX. Forskellen er ikke nødvendigvis så stor, men vær opmærksom på at ikke alt der gælder for POSIX gælder for PCRE og omvendt.
Regulære udtryk er nyttige til en lang række anvendelser i forbindelse med udvikling af webapplikationer:
Validering af brugerinput
Det er som hovedregel nødvendigt at validere/tjekke input fra brugere for at sikre systemet. Herudover giver
validering af brugerinput mulighed for at hjælpe brugere med præcise fejlmeddelelser. Hvis for eksempel vores system beder en bruger
om en emailadresse til at sende en adgangskode til, kan systemet,
ved brug af regulære udtryk, undersøge om brugerens indtastning ligner
en emailadresse. På samme måde kan systemet sikre at
postnumre ligner postnumre, at cpr-numre ligner cpr-numre, og så videre.
Bemærk dog at regulære udtryk ofte kun kan benyttes til at undersøge
om data er velformet og ikke om data konkret giver mening. Eksempelvis
giver regulære udtryk ikke mulighed for at undersøge om en
email-adresse konkret eksisterer, at en person med et givet cpr-nummer
eksisterer eller om et postnummer eksisterer. På samme vis er det
vanskeligt med regulære udtryk præcist at undersøge om en angivet dato
eksisterer (f.eks. er det vanskeligt at specificere at datoen
2007-02-29 ikke er en eksisterende dato, men at datoen 2004-02-29 er
eksisterende). Det er dog muligt at bruge regulære udtryk til at
undersøge om data følger ISO-formatet YYYY-MM-DD
.
Anvendelse af data fra fremmede websider
Anvendelse af data fra fremmede websider (også kaldet webscraping) er ofte let at implementere ved brug af regulære udtryk
til præcist at specificere hvilke dele af en webside systemet vil gøre brug af.
Det er således muligt med regulære udtryk at finde, og gøre brug af, data der allerede er publiceret på
Internettet. Eksempler på sådan data inkluderer valutakurser,
vejrprognoser, nyheder og aktiekurser.
Søgning efter mønstre i datamængder
Regulære udtryk kan med fordel benyttes til at søge efter data i begrænsede
datamængder, såsom strenge af begrænset størrelse. Regulære udtryk kan
også bruges til at søge efter relevante rækker i en databasetabel;
eksempelvis understøtter MySQL brug af regulære udtryk i
WHERE
-betingelser.
Tekstsubstitution i strenge
Regulære udtryk kan også
bruges til udskiftning af delstrenge i en streng (søg-og-erstat). En sådan anvendelse af regulære udtryk kan for
eksempel bruges til at generere specifikke email-beskeder ud fra en
template, hvori modtagerens navn er refereret til som NAVN
.
preg_match
og det første regulære udtrykI det følgende introduceres begrebet "mønstre" (engelsk: patterns), der egentligt er et synonym for "regulære udtryk".
Et mønster er en stump tekst der definerer hvilken form, det vi leder efter har (f.eks. indeholder stregen kun bogstaver, kun tal etc.).
Sammen med mønsteret har vi en streng (ofte brugerinput) der angiver hvor vi leder henne.
Ud fra vores mønster kan vi så spørge "matcher mønsteret X strengen Y"
Eller: findes mønsteret X i strengen Y
PHP funktionen vi skal benytte hedder preg_match
. Den tager to til tre argumenter. Først det mønster vi ledder efter, dernæst den streng vi leder i og til sidst kan vi angive navnet på et array
vi vil fylde noget i. Det tredie argument kigger vi på senere.
Lad os kigge på et eksempel:
<?php
if(preg_match('/a/', 'kat')){
echo "Der er et 'a' i 'kat'";
} else {
echo "Der er ikke noget 'a' i 'kat'";
}
?>
/a/
og vi leder i strengen "kat"1
eller 0
a
/
fortæller hvornår vores regulære udtryk starter og hvornår det stopperFor at teste dine regulære udtryk kan du med fordel snuppe nedenstående script og gemme det på din server som pcre_test.php
, eller du kan finde det her: pcre_test.php. Mens du læser, så prøv lidt udtryk af, det er meget lettere når man gør det
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
<link rel="stylesheet" href="style.css" type="text/css" media="screen" />
<title>Perl Compatible Regular Expressions (PCRE)</title>
</head>
<body>
<h1>Perl Compatible Regular Expressions (PCRE)</h1>
<?php
if(isset($_REQUEST['expr'])){
$matches=array();
$expr=$_REQUEST['expr'];
$str=$_REQUEST['str'];
echo "<div>";
if(preg_match($expr, $str, $matches)){
echo "<p class='success'>Dit udtryk <code>".htmlspecialchars($expr)."</code> matcher strengen</p>";
echo "<div>Arrayet der indeholder \"matches\" ser således ud:</div>";
echo "<pre class='shell'>";
$s=print_r($matches, 1);
echo htmlspecialchars($s);
echo "</pre>";
} else {
echo "<p class='error'>Dit udtryk <code>".htmlspecialchars($expr)."</code> matcher IKKE strengen</p>";
}
echo "</div>";
}
?>
<form action='pcre_test.php' method='post'>
<div>
<label>Regulært udtryk (husk delimiters (/ /)<br />
<input type='text' class='formfield largefriendlyletters' name='expr' size='50' value="<?php echo htmlentities($_REQUEST['expr']);?>" /></label>
</div>
<div>
<label>Streng der skal tjekkes<br />
<textarea name='str' class='formfield' rows='5' cols='30'><?php echo $_REQUEST['str'];?></textarea></label>
</div>
<div>
<input type='submit' value='Tjek' />
</div>
</form>
</body>
</html>
Det kan være unødvendigt bøvlet at lave regulære udtryk, et par hints.
Tidligere så vi det regulære udtryk /a/
, oversat betyder det at:
/
: her starter vores regulære udtryka
: her er selve mønsteret vi leder efter/
: her slutter vores regulære udtrykDe to /
tegn kaldes "delimiters". Vi kan bruge forskellige tegn som delimiters (oftest /
, !
eller |
). Det tegn vi bruger som delimiters, kan vi ikke bruge som en del af det mønster vi ønsker at finde (UDEN at escape tegnet - mere om det senere). Delimiter-tegnet må ikke være bogstaver eller tal
Det tegn vi starter med skal også være det vi slutter med.
De simpleste mønstre vi kan lave er mønstre der matcher bogstav for bogstav
Mønsteret /as/ matcher eksempelvis strengene: Jonas og bas Mønsteret /dag/ matcher eksempelvis strengene: Mandag og tirsdag
Møsntre er som udgangspunkt "case sensitive", dvs der er forskel på store og små bogstaver
Mønsteret /as/ matcher IKKE strengen: Asger Mønsteret /As/ matcher blandt andet strengen: Asger
^
og $
De to tegn ^
og $
har en speciel betydning i regulære udtryk
Indtil videre har vores regulære udtryk ledt efter matches inde i strengen, når vi har ledt efter et bogstav har vi været tilfredse hvis bogstavet bare var et eller andet sted.
Når vi tjekker brugerinput er det ikke godt nok.
^
angiver at vores regulære udtryk skal matche fra starten af strengen
$
angiver at vores regulære udtryk skal matche til slutningen af strengen
Udtrykket /Hans/ matcher strengen: "Anders Hansen" fordi Hans findes inde i strengen Udtrykket /^Hans/ matcher ikke strengen fordi strengen ikke starter med Hans Udtrykket matcher tilgengæld strengen: "Hansen, Anders" Udtrykket /Hans$/ matcher ikke strengen fordi den ikke slutter på: "Hans" Men udtrykket matcher stadig mere end Hans, eksempelvis: "Der er Hans" Rigtigt ofte benytter vi ^ og $ sammen til at tjekke på hele strengen: /^Hans$/ matcher KUN strengen "Hans", intet andet
"Stregen" |
betyder i regulære udtryk: eller, som i enten eller.
I det efterfølgende bruger vi det engelske ord "pipe".
Vi kan benytte pipe'n sammen med paranteser til at lave nogle ret avancerede ting
Mønsteret /ab|cd/ mather de to strenge: ab og cd
Dvs. | siger enten skal det være alt det på min venstre side, ellers skal det være det fra min højre
Så kan vi bruge paranteser til at indsnævre hvor langt til siden den må kigge
Mønsteret /(Man|Tirs|Ons|Tor|Fre)dag/ mather strengene: Mandag, Tirsdag, Onsdag, Torsdag og Fredag
Foreløbigt er det her ikke særligt smart,vi skal stadig skrive ret præcists hvad det er vi vil have.
Character classes to the rescue
Vi kan angive tegn inde i hårde klammer [
og ]
og så skal strengen bare matche et af tegnene
Samtidigt kan vi angive intervaller, a la a-z
, A-Z
, 0-9
og A-z
Og så kan vi kombinere de to ting: Alle danske bogstaver: [a-zæøå]
Et mønster der matcher datoer (danske): /[0-3][0-9]-[0-1][0-9]-[0-9][0-9][0-9][0-9]/ Bindestregerne imellem klasserne er bare bindestreger, så vores datoer ser således ud: 31-12-2010 Et mønster der matcher danske navne der starter med stort og har fire bogstaver: /[A-ZÆØÅ][a-zæøå][a-zæøå][a-zæøå]/ Alle almindelige tegn, inklusiv mellemrum /[A-zÆØÅæøå0-9 -]/
Stadig ikke smart nok? No worries
Med "quantifiers" kan man sige, forgående del-mønster vil vi gerne have X gange
Vi skal kende følgende quantifiers
Quantifier | Betydning | Eksempel på brug | Note |
---|---|---|---|
? | 0 eller 1 gang | /(+45)?/ | Nul eller en gang dansk landekode (hele blokken +45 nul eller en gang) |
* | 0 eller flere gange | /[0-9]*/ | Nul ellere flere tal |
+ | 1 eller flere gange | /[A-Z][a-z]+/ | Et stort bogstav, efterfulgt af en eller flere små bogstaver |
{antal} | Præcis antal matches |
/[0-9]{4}/ | Præcis fire tal (et postnummer?) |
{start, slut} | Mellem start og slut matches, begge inklusive |
/[0-9]{2,4}/ | Fra to til fire tal (evt årstal med uden 20..) |
{start, } | start eller flere matches |
/[0-9]{1,}/ | Et eller flere tal (svarer til + ) |
Quantifiers knytter sig til det der står lige til venstre for dem, og ikke ligesom |
der er "grådig"
For at få quantifiers til at knytte sig til mere, benytter vi paranteser eller klasser
Det regulære udtryk /ab*/ matcher a, ab, abb, abbb, abbbbbbb ... Fordi vi ikke bruger parantes står der: "et a, efterfulgt af nul eller flere b'er Det regulære udtryk /(ab)*/ matcher (ud over en tome streng!) ab, abab, ababab ... Nul eller flere forekomster af hele parantesen
Regulære udtryk er som udgangspunkt grådige
De forsøger at matche så meget de overhovedet kan
Hvis vi har strengen: ababababab Så matcher det regulære udtryk: /[ab]*/ hele strengen
Der er, som vi faktisk allerede har set, en række tegn der har speciel betydning i regulære udtryk
Tegn | Betydning |
---|---|
\ | Escape tegn |
^ | Match fra starten af strengen |
$ | Match til slutningen af strengen |
. | Wildcard, alle tegn undtagen linieskift (\n) |
| | Eller (som i enten eller) |
[ ] | Start og slut klasser |
( ) | Sub patterns (grupperinger) |
{ } | Start/slut quantifiers |
Hvis vi vil matche disse tegn skal vi escape dem. Escaping foregår ved at vi sætter escpaetegnet (\
) foran.
Det ene tegn der endnu ikke er gennmemgået er punktummet.
Som der står er det et wildcard, dvs det gælder for alt (bortset fra linieskift: \n)
Det regulære udtryk /^.{2}$/ mather en hver streng der består af to tegn (igen, uden linieskift) Eks: .a, ba, '", 9t Det regulære udtryk /^\.\.$/ mather strengen: .. Et simpelt mønster til at matche email adresser: /^.+@.+\.[a-z]+$/
Det sidste udtryk forklaret:
^
og $
til at angive det er hele strengen der skal matche, email adressen skal ikke bare være et sted i strengen.+
: først en række vilkårlige tegn@
: så skal der komme et snabel-a.+
: efterfulgt af en række vilkårlige tegn\.
: et punktum (bemærk vi escaper punktummet så det ikke længere er et wildcard)[a-z]+
: et eller flere bogstaver (domæneendelse, a la, "dk")Når vi benytter de her meta karakterer inde i klasser ([ ]
), mister de fleste deres specielle betydning og behøver ikke escapes
Der er dog fire af tegnene vi skal være opmærksomme på når de benyttes i klasser:
^
: Når ^
er første tegn i klassen betyder den "må ikke forekomme"
Så udtrykket /[^a-z]/ matcher alle strenge der ikke indeholder et af bogstaverne fra a-z
\
: Escape tegnet er stadig escape tegnet-
: Bindestregen bruges jo til at angive intervaller [a-z]
, hvis vi vil have at klassen skal matche bindestreger skal det være det sidste tegn
Udtrykket /[a-z-]+/ matcher eksempelvis: sammen-slutning
]
: er det tegn der lukker klassen, og derfor kan vi ikke bruge det (uden at escape)Der findes en række modifiers til regulære udtryk. Modifiers er (surprise!) ting der modificerer et udtryk.
Vi angiver vores modifiers efter den sidste delimiter
Vi bekymrer os kun om to her
i
: angiver at det regulære udtryk skal opfattes "case-insensitive"U
: angiver at udtrykket skal være "non-greedy"Udtrykket /^anders$/ mathcer IKKE strengen: Anders Mens udtrykket /^anders$/i gør U: Hvis vi har strengen: <a href='somewhere.html'>Link</a> Så matcher det regulære udtryk: /<.+>/ hele strengen, mens /<.+>/U matcher til første '>' (<a href='somewhere.html'>)
preg_match
's tredie argumentIndtil videre har vi kun testet på OM noget har matchet, men ikke HVAD der har matchet
Funktionen preg_match
kan tage et tredie argument som vil blive fyldt med alle matches (i et array)
Logikken er at alt der matches ender som index 0, første parantes som index 1, anden parantes som index 2 ...
<?php
$string="<h1>All your base are belong to us</h1>"
preg_match('!<h1>([A-Za-z ]+)</h1>!', $string, $matches);
echo $matches[0]; // udskriver <h1>All your base are belong to us</h1>
echo "<br />";
echo $matches[1]; // udskriver indholdet af første parantes: All your base are belong to us
?>
Læg mærke til at vi benytter !
som delimiters, da strengen vi kigger i indeholder /
Med funktionen preg_replace($pattern, $replacement, $subject)
kan vi så lave søg og erstat
Funktionen tager tre argumenter, $pattern
der angiver hvad vi søger efter, $replacement
der angiver hvad vi vil udskifte med og $subject
der angiver hvilken streng vi leder i.
Funktionen returnerer så en streng hvor alle forekomster af $pattern
i strengen $subject
er udskiftet med $replacement
<?php
$string="Kære NAME, velkommen ombord";
$newname="Anders";
$newstring=preg_replace('/NAME/', $newname, $string);
echo $newstring; // Udskriver: Kære Anders, velkommen ombord
?>
Dette afsnit er ikke en del af pensum!
PCRE er ganske omfattende og kan rigtigt mange ting der ikke er blevet gennemgået her. Dette er kun en introduktion til det mest basale. Nedenfor er et par links hvis dui vil vide mere
Husk, der er altid mange måder at lave sine regulære udtryk på
Ligner det en gyldig email: /^[A-Za-z_.-]+@[A-Za-z_.-]+\.[a-z]+$/ Eller ved at angive hvad der ikke må være: /^[^@]+@[^@]+\.[^@]+$/ Et gyldigt dansk postnummer (før Danmarks Radio fik et postnummer der startede med nul): /^[1-9][0-9]{3}$/ eller /^[1-9][0-9][0-9][0-9]$/ eller (med nul) /^[0-9]{4}$/ Navne: Fornavn, Evt mellemnavne og Efternavn /^[A-ZÆØÅ][a-zæøå-]+( [A-ZÆØÅ][a-zæøå-]+)+$/ Læg mærke til at den sidste del kan gentages en eller flere gange: ( [A-ZÆØÅ][a-zæøå-]+)+
Dette afsnit er ikke en del af pensum!
Fordi vi så ofte benytter de samme klasser, findes der et par shortcuts i sproget
Klasse | Shortcut |
---|---|
[0-9] |
\d |
[\f\r\t\n\v] |
\s (betyder hvilken som helst slags "white-space") |
[A-Za-z0-9_] |
\w |
[^0-9] |
\D |
[^\f\r\t\n\v] |
\S |
[^A-Za-z0-9_] |
\W |
Jonas Holbech, 2010, IT Universitetet i København, 23. februar 2010, Rued Langgaards Vej 7, 2300 København S, Danmark.