Regulære udtryk fra den virkelige verden

Jeg skriver rigtig mange regulære udtryk og kender alle de advancerede ting som look-around assertions og uafhængige deludtryk. Teorien bag regulære udtrykt har jeg nogenlunde styr på, så jeg har en ide om hvor hurtige de er. Kort sagt: Jeg kan mine regulære udtryk.

Når nybegyndere spørger hvordan de kan bruge regulære udtryk til at validere email-adresser eller til at parse HTML-fragmenter slår jeg korsets tegn for mig. Når de forklarer at det bare er for at lære regulære udtryk slår jeg lige korsets tegn igen.

Men hvordan ser mine egne udtryk egentlig ud? Inspireret af Anne-Sofies indlæg lavede jeg et review af brugen af regulære udtryk i mit teams kodebase.

I et repræsentativt uddrag (cirka 14000 statements) fandt jeg 153 regulære udtryk. Det længste udtryk er 82 tegn langt, men er lidt specielt fordi det som det eneste udtryk bruger "named captures" istedet for nummererede captures. Uden named captures er det stadigvæk det længste udtryk med 54 tegn:

^https?://shares.[^/]+/s#([^/]+)/([^/]+)/([^/]+)(.*)$

De mest brugte udtryk er '^\d+$' (14 gange) og '/$' (7 gange). Over en fjerdedel af alle vores regulære udtryk (39 gange) leder vi efter en simpel delstreng ala '.sqlite$'. Herudover er der 19 gange hvor vi leder efter indholdet af en variabel. Det kræver at man beskytter sig mod regex-injections og escaper indholdet ala '^\Q$prefix'.

Ofte bruger vi bare regulære udtryk som en kompakt mulighed om en streng har en af flere muligheder ala '^(GET|HEAD|POST)$' (7 gange). Hvor godt det kan betale sig afhænger af sproget.

Langt de fleste af de resterende er ikke meget mere advancerede end bare at de bruger konstruktioner som '.*', '\d\d\d\d-\d\d-\d\d' eller '[^/]+'. Men vi har to komplicerede udtryk:

(?:<([^>]+)>)?\s<em>((([^)]+)\s</em>)+)\s*
(^|/)(?!../)[^/]+/..(/|$)

Det første kræver bare lige at man har tungen lige i munden: Måske noget i større-/mindre-end tegn efterfulgt af en række ikke-tomme parenteser. Det er vores eneste tilfælde af indlejrede parenteser og selvfølgelig lige i et tilfælde hvor vi har brug for at matche på netop parenteser.

Den anden bruger "negative look ahead". Det er en del af en forholdsvis veldokumenteret normalisering af filnavne og tager sig af at håndtere stier der indeholder '..'-kataloger.

Ud af 153 regulære udtryk har vi altså to som ikke umidelbart bare lader sig læse fra den ene ende til den anden. Det er den slags udtryk I skal lære!

Kommentarer (36)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
#4 Pelle Söderling

Jeg elsker regular expressions og bruger dem tæt på dagligt. Må dog indrømme at de normalt mere ligner dine "komplekse" udtryk end de simple. Simple checks såsom om en streng repræsentere et tal (decimal værdi reelt set i dit tilfælde) eller om en streng slutter på "/" eller "sqlite" virker dog umiddelbart så simple at jeg undrer mig lidt over brugen af regex i disse tilfælde.

Noget andet der undrer mig er at du siger i kun i et enkelt tilfælde bruger named captures - jeg skriver næsten aldrig et statement uden, jeg finder at det både gør statementet mere læseligt og mere selv-dokumenterende. Jeg kan sagtens følge at det er ligegyldigt i simple cases, men er der nogen speciel grund til at I ellers undgår named captures?

  • 1
  • 0
#5 Morten Jensen

@Pelle Söderling

Jeg er også stor fan af named captures, især i Python, hvor re-biblioteket kan aflevere et match som en dictionary - skønt! Det føles "Pythonic" og idiomatisk i Python, men måske knap så meget i sprog hvor dictionaries ikke er en central del af sproget eller et førsteordens-objekt. Jeg synes også det virker godt i JS, hvor man har dictionaries lige ved hånden.

  • 0
  • 0
#8 Troels Henriksen

Hvordan mon dit regulære udtryk, der finder regulære udtryk i Perl kode ser ud :)

Strengt taget kan et sådan udtryk ikke eksistere, da grammatikken for regulære udtryk ikke er regulær.

(At grammatikken for Perl heller ikke er det, det er så en anden sag.)

For at holde sig til emnet, så bruger jeg selv kun regulære udtryk til hurtige hacks i shell scripts og deslige. Til større programmer foretrækker jeg at bruge rigtige parsere til komplekse formål, og almindelige funktionskald til de enkle. Jeg synes at 'dropWhile isSpace' er rarere at læse end 's/^ +//', og 'all isDigit' er rarere end '^[0-9]*', for selvom det fylder flere tegn, så er det hurtigere at både skrive og læse.

Hvis jeg endelig skal bruge regulære udtryk, så ville jeg foretrække en implementering, der garanterede at mine regulære udtryk var veldannede. Jeg har altid haft en aversion mod at inkludere kode i tegnfølger i mit program, og først vente med noget så basalt som syntakstjek til køretiden.

  • 3
  • 0
#10 Carsten Hansen

Jeg fandt http://www.myregextester.com/index.php

hvor det er muligt at sætte hak i explain, så vi andre også kan forstå hvad de regulære udtryk handler om.

Enig med Troels, at det skal overvejes en ekstra gang før de bruges i produktionskode. Der stilles høje krav til programmørernes abstraktionsevne og der er masser af fejlmuligheder.

Jeg bruger dog selv et enkelt til at teste for en valid email-adresse, men det er jo også meget standardiseret. Dejligt med en one-liner.

  • 0
  • 0
#11 Peter Mogensen

Jeg bruger dog selv et enkelt til at teste for en valid email-adresse, men det er jo også meget standardiseret.

Ja. Det er ret standardiseret, men inden du forsøger at parse det med et regexp, så skal du nok læse sektion 3.4.1 af RFC 5322 en gang til (inkl. alle produktioner. Også "obsolete). Og f.eks. appendix A.5. Afhængig af formålet vedtager man ofte et mere håndterligt subset.

  • 1
  • 0
#12 Troels Henriksen

Jeg bruger dog selv et enkelt til at teste for en valid email-adresse, men det er jo også meget standardiseret. Dejligt med en one-liner.

Med mindre din validering er meget enkel, så er den sandsynligvis forkert. Validering af email-adresser med regulære udtryk er for mig en af de mest almindelige fejlanvendelser. En bedre strategi er at bede om indtastning af adressen to gange, og derefter sende et brev til den givne adresse. Hvis det når frem (kontrolleret via et valideringslink brugeren skal trykke på), så er adressen gyldig.

  • 2
  • 0
#13 Leif Neland

En bedre strategi er at bede om indtastning af adressen to gange, og derefter sende et brev til den givne adresse.

Første trin er at verificere at der er en mx-record for domænet.

Ganske vist er det lovligt at sende til en a-record, men det er vist så sjældent, at de, der kunne tænkes at have en sådan adresse, godt kan finde ud af en fejlmeddelelse, der siger "@maskine.dom.tld ser ikke ud til at modtage post (Ingen MX-record)", og reagere passende på det.

  • 0
  • 0
#15 Pelle Söderling

Det kan være ok at lave noget simpel validering bare for at informere brugeren om at vedkommende har indtastet noget der vist ikke var en email-adresse i feltet - i den simpleste form kunne en regex hertil se således her ud: .+@.+\..+

Problemet som Troels er inde på er når man forsøger at lave 100% (eller tæt på) validering af en email-adresse, for her er der mange fald-gruber og mange af de regex udtryk man kan hente rundt om på nettet til formålet indeholder fejl - det er især farligt at bruge dem hvis man ikke forstår dem.

  • 0
  • 0
#20 Peter Makholm Blogger

Validering af email-adresser er en skummel opgave der ser meget let ud på overfladen. Derfor begynder folk let at insistere på at bruge regulære udtryk og ofte endda på at ville gøre det med ét udtryk.

Nogle af folks fejl er selvfølgelig ret trivielle. Det er ikke lang tid siden jeg fangede folk i at antage at det sidste led i et domænenavn altid var på to eller tre tegn. Det er selvfølgelig også kun 11 år siden .info blev introduceret.

Men selvfølgelig kan det gøres. Hvis man lige begynder med at fjerne kommentar-felterne kræver det bare lige at man har tungen lige i munden: http://www.ex-parrot.com/~pdw/Mail-RFC822-Address.html

Hvis man vil være sikker på at en email-adresser er valid, så kommer man ikke uden om at prøve den. Hvis det er uhensigtsmæssigt må mindre gøre det.

På et tidspunkt skrev jeg følgende kode til at validere email-adresser:

sub validate_email {  
    my $email = shift;  
    # A full validation of email addresses is next to impossible.  
    # Just catch some simple mistakes.  
   
    return 0 if $email !~ /\@/             # - no at sign (Oh no! what about bang paths? ;-)  
             || $email =~ /\@.*\@/         # - two at signs  
             || $email =~ /[.\@]{2}/       # - consecutive dots and at signs  
             || $email =~ /^[.\@]|[.\@]$/; # - initial or terminal dot or at sign  
   
    try {  
        normalize_email($email);  
    } catch {  
        return 0;  
    }  
   
    # Possible wrong negatives:  
    #   - I'm not entirely sure that the dot-rules MUST hold for the  
    #     local-part  
    #  
    # Possible extra tests:  
    #   - Either enforce stricter validation of IP addresses or disallow  
    #     them entirely  
    #  
    # Tests that would be very wrong to introduce  
    #   - Test if that a TLD is legal  
    #   - Disallow email addresses with only a TLD. Improbable right now,  
    #     but who knows with the new sponsored TLD's  
   
    return 1;  
}

Funktionen normalize_email tager sig af nogle smådetaljer vedrørende email-adresser der ikke er rent ascii. Den kører nameprep algoritmen på hele adressen og konvertere host-part til IDN. At lave nameprep på local-part er hvis formelt set ikke standardiseret, men det er vores brugere heller ikke.

Ovenstående er muligvis at overdrive en smule, men jeg blev lidt stædig da jeg skulle erstatte en af de utallige forkerte måder at validere email-adresser på.

(Jeg er ganske sikker på at DK-Hostmaster på et tidspunkt havde en MX record på dk.)

  • 1
  • 0
#22 Carsten Hansen

Jeg må krybe til korset. Jeg har brugt: var okEpost = field.value.match(/\b(^(\S+@).+((.com)|(.net)|(.edu)|(.mil)|(.gov)|(.biz)|(.org)|(.info)|(..{2,2}))$)\b/gi);

Siden 2007 og har ikke modtaget klager endnu trods knap 100.000 uploads, men den virker i hvert fald ikke efter at slutdomænenavne gives fri. Derudover, så er der vel noget med tegnsæt.

Den fanger dem, der glemmer @ og tror, at de skal angive et almindeligt brugernavn.

  • 0
  • 0
#23 Peter Makholm Blogger

var okEpost = field.value.match(/\b(^(\S+@).+((.com)|(.net)|(.edu)|(.mil)|(.gov)|(.biz)|(.org)|(.info)|(..{2,2}))$)\b/gi);

En af de ting som jeg iøvrigt rigtig gerne ville fange var folk der ramte komma-tasten istedet for punktum-tasten. Det opnår jeg selvfølgelig heller ikke ved mit eksempel ovenover, men her ville jeg da absolut få det ordnet ved at escape de rigtige af dine punktummer.

Iøvrigt .int er fra 1988, .name er fra 2001 og .cat er fra 2005.

Det er ikke bare engang i fremtiden at det er svært at opliste alle TLD'er på mere end to tegn. Det løb er kørt for lang tid siden.

  • 1
  • 0
#24 Peter Mogensen

Apropos validering af email-adresser og brugernavne. Jeg faldt for nylig over dette skræk-eksempel, der ledsagedes af dokumentationen:

"Allows to define a regular expression rule for validation of the user name. The default setting matches the usual cases inclusive E-Mail adresses and "domain/user" names"

Værdien var: ^[[:alnum:]]+([%20@/._-][[:alnum:]]+)*$

Hvad der lige er sket i hovedet på den udvikler må man gisne om. Hvorfor må man f.eks. ikke have email-adressen: mig@mit--domaene.dk ?

  • 0
  • 0
#25 Peter Makholm Blogger

Hvorfor må man f.eks. ikke have email-adressen: mig@mit--domaene.dk?

Det må du ikke fordi RFC952 siger det:

      <domainname> ::= <hname>  
      <hname> ::= <name>*["."<name>]  
      <name>  ::= <let>[*[<let-or-digit-or-hyphen>]<let-or-digit>]

Det vil sige at to bindestreger lige efter hinanden er et no-go. RFC1123 opdatere reglen så de enkelte led gerne må begynde med et ciffer. At to bindestreger efter hinanden var forbudt gjorde det muligt at bruge netop denne konstruktion til at angive IDN-domæner der starter med 'xn--'.

Dem må du tilgengæld så ikke bruge...

  • 0
  • 0
#27 Peter Mogensen

Jeg er/var sikker på at 'xn--' som prefix var valgt netop fordi '--' ikke var lovligt i domænenavne.

Det ville have været en plausibel forklaring, men jeg kan faktisk ikke finde noget på tryk om historien bag "--". Der er derimod rigeligt om historien bag "xn". Men ikke desto mindre, så blev jeg opmærksom på at det regexp var begravet i koden netop fordi det viste sig at disse domænenavne eksisterer "in the wild".

  • 0
  • 0
#28 Peter Makholm Blogger

Unit tester i jeres regulære udtryk?

Nej, ligesom jeg heller ikke isoleret set unit-tester if-sætninger eller while-løkker.

Selvfølgelig vil en god unit-test ofte berøre alle dele af det regulære udtryk, lige som en god unit-test ofte vil prøve alle veje gennem en if-sætning. Og i de mange tilfælde hvor jeg bruger regulære udtryk til validering af inddata til en metode er det selvfølgelig indlysende at dette er en del af metodens interface som skal testes ordentligt.

Den største forskel er som sådan at jeg ikke har så gode værktøjer til at beregne coverage metrics for mine regulære udtryk.

  • 0
  • 0
#29 Povl Kvols

Nu er rigtig mange regulære udtryk ganske meget mere komplekse end mange while- og if-sætninger, så der kunne måske være god grund til at lave isolerede unit-tests af dem. Og af flere årsager. Unit-tests er ikke kun og udelukkende test af om de faktisk fungerer, men også en slags dokumentation for, hvad der konkret er taget forbehold for, hvis man senere skal lave rettelser.

  • 0
  • 0
#30 Peter Makholm Blogger

I blogindlæget nævner jeg ikke hvilket sprog jeg skriver i, selvom det ikke er den store hemmelighed. Men sproget betyder selvfølgelig en hel del for hvordan man bruger regulære udtryk.

Hvis man først skal oprette en Pattern-objekt, der først parser udtrykket på køretidspunktet, og dernæst skal oprette et Match-objekt og endelig skal iterere i hånden over matches eller captures, har man helt klart et andet forhold til regulære udtryk end hvis de er en integreret del af syntaksen der er lige så lettilgængelig som at teste for lighed og hvor udtrykket bliver oversat sammen med resten af programmet.

Ligeledes hvis regulære udtryk ikek er lettilgængelige, så er der større sandsynlighed for at der opstår standard library funktioner for rtrim(), trim(), ltrim() og isDigit(). I Perl findes disse funktioner ikke, tilgengæld skal jeg ikke lige række ud efter de fjerneste steder i hukommelsen når det jeg skal trimme ikke lige er mellemrumstegn.

Hvis ens sprog kun har boxed værdier, så kan det også være dyrt at iterere over tegnene i en streng. Derfor vil jeg i Perl nok ofte bruge regulære udtryk hvor en C-programmør ofte bare ville skanne strengen igennem og sætte en NULL-byte et passende sted (og iøvrigt så ignorere hvem der skal deallokere de sidste bytes).

På grund af mit spriogvalg bruger jeg derfor nok forholdsvis simple regulære udtryk og i en række situationer hvor andre sprog bruger andre idiomer.

  • 1
  • 0
#31 Peter Makholm Blogger

Nu er rigtig mange regulære udtryk ganske meget mere komplekse end mange while- og if-sætninger, så der kunne måske være god grund til at lave isolerede unit-tests af dem.

Gennemgangen af min konkrete kode-base fortæller mig noget andet. Derfor er jeg helt tryg ved at jeg, som team-leder, har foretaget det korrekte valg ved ikke at betragte regulære udtryk som noget der skal testes som selvstændige enheder.

Ud af 153 regulære udtryk er der to som måske isoleret set kunne fordre en selvstændig test. Betragtet i deres kontekst mener jeg stadigvæk at de er bedre testet i deres sammenhæng. Det er efter min mening langt lettere at formulere en overbevisende test-suite for 'normalisering af filnavne' end for den specifikke regl der sikrer at '../' bliver håndteret i den rigtige rækkefølge.

Men det afhænger selvfølgelig i høj grad af hvordan man bruger regulære udtryk.

  • 0
  • 0
#32 Thomas Balstrup

Nu arbejder jeg primært i .Net, og der er det ret ligetil med at validere e-mails uden brug af regex. En kodesnip á la try { System.Net.Mail.MailAddress email = new System.Net.Mail.MailAddress(emailTxt); } Hvis den fejler kan .Net frameworket ikke lave det indtastede om til en e-mail og dermed, umiddelbart, ikke sende mails til denne adresse... og så kan det jo være det samme.

  • 0
  • 0
#33 Peter Mogensen

System.Net.Mail.MailAddress

Forudsat passende dokumentation er den slags indbyggede validerings funktioner i libraries da også at foretrække. http://msdn.microsoft.com/en-us/library/system.net.mail.mailaddress.aspx

... fremfor dette: http://ex-parrot.com/~pdw/Mail-RFC822-Address.html

Særligt, hvis man kan få kildeteksten til librariet og finde ud af, hvad der evt. skulle gå galt i forhold til dokumentationen og/eller standarden.

  • 0
  • 0
#35 Lars Lundin

Ifbm. noget web-scraping brugte jeg for en 12 års tid siden

/($vejnavne)\s+($husformat)/io

-- variablen $vejnavne indeholdt alle Danmarks autoriserede vejnavne, mens $husformat indeholdt formatet for husnummer, etager mm. i henhold til en specifikation fra Post Danmark.

Jeg mener at længden af udtrykket var omkring 1MB, derfor var o-optionen vigtig.

Der var ikke et krav om at det skulle køre specielt hurtigt og det virkede fint.

  • 0
  • 0
#36 Lars Lundin

Bagtanken var også mest af Perl syntax ikke alene ikke er regulær, men på ingen måde pæn. Jeg fandt dette citat som nok er meget sigende: "Perl's grammar can not be reduced to BNF. The work of parsing perl is distributed between yacc, the lexer, smoke and mirrors."

Jo, det er nemt at være vittig på bekostning af perl, men for 20 år siden var perl omtrent det mest portable scripting sprog.

Denne 16 år gamle liste over køretider for en bestemt linær ligningsløser ville ikke være blevet nær så lang uden perl:

http://eso.org/~llundin/phd/RelSol.pdf

(PCen kører naturligvis Linux).

  • 0
  • 0
Log ind eller Opret konto for at kommentere