Regulære udtryk er et kraftfuldt sprog til at matche tekstmønstre. Denne side giver en grundlæggende introduktion til regulære udtryk i sig selv, der er tilstrækkelig til vores Python-øvelser, og viser, hvordan regulære udtryk fungerer i Python. Python-modulet “re” giver understøttelse af regulære udtryk.
I Python skrives en søgning med regulære udtryk typisk som:
match = re.search(pat, str)
Metoden re.search() tager et regulært udtryksmønster og en streng og søger efter dette mønster i strengen. Hvis søgningen er vellykket, returnerer search() et matchobjekt eller ellers None. Derfor efterfølges søgningen normalt umiddelbart af en if-statement for at teste, om søgningen lykkedes, som vist i følgende eksempel, der søger efter mønsteret “word:” efterfulgt af et ord på 3 bogstaver (detaljer nedenfor):
str = 'an example word:cat!!'match = re.search(r'word:\w\w\w', str)# If-statement after search() tests if it succeededif match: print 'found', match.group() ## 'found word:cat'else: print 'did not find'
Koden match = re.search(pat, str)
gemmer søgeresultatet i en variabel ved navn “match”. Derefter tester if-statementet match – hvis det er sandt, er søgningen lykkedes, og match.group() er den matchende tekst (f.eks. ‘word:cat’). Ellers hvis match er false (None for at være mere specifik), så lykkedes søgningen ikke, og der er ingen matchende tekst.
Den “r” i starten af mønsterstrengen betegner en python “rå” streng, der passerer gennem backslashes uden ændringer, hvilket er meget praktisk for regulære udtryk (Java har hårdt brug for denne funktion!). Jeg anbefaler, at du altid skriver mønsterstrenge med ‘r’ bare som en vane.
Grundlæggende mønstre
Kraften ved regulære udtryk er, at de kan angive mønstre og ikke kun faste tegn. Her er de mest grundlæggende mønstre, der passer til enkelte tegn:
- a, X, 9, < — almindelige tegn passer bare nøjagtigt til sig selv. Meta-tegn, som ikke passer til sig selv, fordi de har særlige betydninger, er: . ^ $ * + ? { \ | ( ) (nærmere oplysninger nedenfor)
- . (et punktum) — passer til ethvert enkelt tegn undtagen newline ‘\n’
- \w — (lille w) passer til et “ord”-tegn: et bogstav, et ciffer eller en understregning . Bemærk, at selv om “word” er et mnemonisk udtryk for dette, passer det kun til et enkelt ordtegn, ikke til et helt ord. \WW (stort W) passer til ethvert tegn, der ikke er et ord.
- \b — grænse mellem ord og ikke-ord
- \s — (lille s) passer til et enkelt whitespace-tegn — mellemrum, newline, retur, tabulator, form . \S (stort S) passer til ethvert tegn, der ikke er et mellemrum.
- \t, \n, \r — tabulator, newline, retur
- \d — decimalciffer (nogle ældre regex-værktøjer understøtter ikke andet end \d, men de understøtter alle \w og \s)
- ^ = start, $ = slut — matcher starten eller slutningen af strengen
- \ — hæmmer “specialiteten” af et tegn. Så brug f.eks. \. til at matche et punktum eller \\\ til at matche en skråstreg. Hvis du er usikker på, om et tegn har en særlig betydning, f.eks. “@”, kan du sætte en skråstreg foran det, \@@, for at sikre dig, at det behandles som et tegn.
Basiseksempler
Joke: Hvad kalder man en gris med tre øjne? piiig!
De grundlæggende regler for søgning med regulære udtryk efter et mønster i en streng er:
- Søgningen går gennem strengen fra start til slut og stopper ved den første match, der findes
- Hele mønsteret skal matches, men ikke hele strengen
- Hvis
match = re.search(pat, str)
lykkes, er match ikke None og i særdeleshed match.group() er den matchende tekst
## Search for pattern 'iii' in string 'piiig'. ## All of the pattern must match, but it may appear anywhere. ## On success, match.group() is matched text. match = re.search(r'iii', 'piiig') # found, match.group() == "iii" match = re.search(r'igs', 'piiig') # not found, match == None ## . = any char but \n match = re.search(r'..g', 'piiig') # found, match.group() == "iig" ## \d = digit char, \w = word char match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123" match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"
Gentagelse
Det bliver mere interessant, når du bruger + og * til at angive gentagelse i mønsteret
- + — 1 eller flere forekomster af mønsteret til venstre for det, f.eks. ‘i+’ = et eller flere i’er
- * — 0 eller flere forekomster af mønsteret til venstre for det
- ? — matcher 0 eller 1 forekomst af mønsteret til venstre
Leftmost & Largest
Først finder søgningen det mest venstre match for mønsteret, og dernæst forsøger den at bruge så meget af strengen som muligt — dvs. + og * går så langt som muligt (+ og * siges at være “grådige”).
Repetition Eksempler
## i+ = one or more i's, as many as possible. match = re.search(r'pi+', 'piiig') # found, match.group() == "piii" ## Finds the first/leftmost solution, and within it drives the + ## as far as possible (aka 'leftmost and largest'). ## In this example, note that it does not get to the second set of i's. match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii" ## \s* = zero or more whitespace chars ## Here look for 3 digits, possibly separated by whitespace. match = re.search(r'\d\s*\d\s*\d', 'xx1 2 3xx') # found, match.group() == "1 2 3" match = re.search(r'\d\s*\d\s*\d', 'xx12 3xx') # found, match.group() == "12 3" match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123" ## ^ = matches the start of string, so this fails: match = re.search(r'^b\w+', 'foobar') # not found, match == None ## but without the ^ it succeeds: match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"
E-mails Eksempel
Sæt, at du vil finde e-mail-adressen inde i strengen ‘xyz [email protected] lilla abe’. Vi bruger dette som et kørende eksempel til at demonstrere flere funktioner i regulære udtryk. Her er et forsøg med mønsteret r’\w+@\w+’:
str = 'purple [email protected] monkey dishwasher' match = re.search(r'\w+@\w+', str) if match: print match.group() ## 'b@google'
Søgningen får ikke hele e-mail-adressen i dette tilfælde, fordi \w ikke passer til “-” eller ‘.” i adressen. Vi retter dette ved hjælp af de regulære udtryksfunktioner nedenfor.
Firkantede parenteser
Firkantede parenteser kan bruges til at angive et sæt tegn, så de matcher ‘a’ eller ‘b’ eller ‘c’. Koderne \w, \s osv. fungerer også inden for firkantede parenteser med den ene undtagelse, at punktum (.) blot betyder et bogstaveligt punktum. I forbindelse med problemet med e-mails er de firkantede parenteser en nem måde at tilføje ‘.’ og ‘-‘ til de tegn, der kan forekomme omkring @ med mønsteret r’+@+’ for at få hele e-mail-adressen:
match = re.search(r'+@+', str) if match: print match.group() ## '[email protected]'
(Flere funktioner i firkantede parenteser) Du kan også bruge en streg til at angive et interval, så det passer til alle små bogstaver. Hvis du vil bruge en bindestreg uden at angive et interval, skal du sætte bindestregen til sidst, f.eks. En opadgående hat (^) i starten af et sæt med firkantede parenteser inverterer det, så det betyder alle tegn undtagen “a” eller “b”.
Gruppeudtrækning
Med “gruppe”-funktionen i et regulært udtryk kan du udvælge dele af den matchende tekst. Lad os antage, at vi i forbindelse med problemet med e-mails ønsker at udtrække brugernavn og vært separat. For at gøre dette skal du tilføje parenteser ( ) omkring brugernavn og vært i mønsteret, som her: r'(+)@(+)’. I dette tilfælde ændrer parenteserne ikke det, som mønstret vil matche, men etablerer i stedet logiske “grupper” inden for den tekst, der matcher. Ved en vellykket søgning er match.group(1) den matchtekst, der svarer til den første venstre parentes, og match.group(2) er den tekst, der svarer til den anden venstre parentes. Den almindelige match.group() er stadig hele matchteksten som sædvanlig.
str = 'purple [email protected] monkey dishwasher' match = re.search(r'(+)@(+)', str) if match: print match.group() ## '[email protected]' (the whole match) print match.group(1) ## 'alice-b' (the username, group 1) print match.group(2) ## 'google.com' (the host, group 2)
En almindelig arbejdsgang med regulære udtryk er, at du skriver et mønster for det, du leder efter, og tilføjer parentesgrupper for at udtrække de dele, du ønsker.
findall
findall() er nok den mest kraftfulde enkeltstående funktion i re-modulet. Ovenfor brugte vi re.search() til at finde det første match for et mønster. findall() finder *alle* matches og returnerer dem som en liste af strenge, hvor hver streng repræsenterer et match.
## Suppose we have a text with many email addresses str = 'purple [email protected], blah monkey [email protected] blah dishwasher' ## Here re.findall() returns a list of all the found email strings emails = re.findall(r'+@+', str) ## for email in emails: # do something with each found email string print email
findall With Files
For filer har du måske for vane at skrive en løkke til at iterere over filens linjer, og du kan derefter kalde findall() på hver linje. Lad i stedet findall() gøre iterationen for dig — meget bedre! Du skal blot føre hele filteksten ind i findall() og lade den returnere en liste over alle overensstemmelser i et enkelt trin (husk, at f.read() returnerer hele teksten i en fil i en enkelt streng):
# Open file f = open('test.txt', 'r') # Feed the file text into findall(); it returns a list of all the found strings strings = re.findall(r'some pattern', f.read())
findall og grupper
Parentesen ( ) gruppemekanismen kan kombineres med findall(). Hvis mønsteret indeholder 2 eller flere parentesgrupper, returnerer findall() i stedet for at returnere en liste af strenge en liste af *tupler* i stedet for at returnere en liste af *tupler*. Hver tuple repræsenterer et match af mønsteret, og inden for tuplen er group(1), group(2) … data. Så hvis der tilføjes 2 parentesgrupper til e-mail-mønsteret, returnerer findall() en liste af tupler, hver med længde 2, der indeholder brugernavn og vært, f.eks. (‘alice’, ‘google.com’).
str = 'purple [email protected], blah monkey [email protected] blah dishwasher' tuples = re.findall(r'(+)@(+)', str) print tuples ## for tuple in tuples: print tuple ## username print tuple ## host
Når du har listen af tupler, kan du lave en løkke over den for at foretage en beregning for hver tupel. Hvis mønsteret ikke indeholder nogen parenteser, returnerer findall() en liste over fundne strings som i tidligere eksempler. Hvis mønsteret indeholder et enkelt sæt parenteser, returnerer findall() en liste over strenge, der svarer til denne enkelt gruppe. (Obskure valgfri funktion: Sommetider har man paren ( ) grupperinger i mønsteret, men som man ikke ønsker at udtrække. I så fald skal du skrive parenteserne med en ?: i starten, f.eks. (?: ), og den venstre parentes vil ikke tælle som et grupperesultat.)
RE Workflow og fejlfinding
Regulære udtryksmønstre pakker en masse betydning ind i kun få tegn , men de er så tætte, at du kan bruge meget tid på at fejlfinde dine mønstre. Konfigurer din runtime, så du nemt kan køre et mønster og udskrive, hvad det matcher, f.eks. ved at køre det på en lille testtekst og udskrive resultatet af findall(). Hvis mønsteret ikke matcher noget, så prøv at svække mønsteret, ved at fjerne dele af det, så du får for mange matches. Når den ikke matcher noget, kan du ikke gøre fremskridt, da der ikke er noget konkret at kigge på. Når det matcher for meget, kan du arbejde på at stramme det gradvist op, så du rammer lige præcis det, du ønsker.
Optioner
Re-funktionerne tager imod indstillinger for at ændre mønstermatchets opførsel. Optionsflaget tilføjes som et ekstra argument til search() eller findall() osv., f.eks. re.search(pat, str, re.IGNORECASE).
- IGNORECASE — ignorerer forskelle mellem store og små bogstaver ved matchning, så ‘a’ matcher både ‘a’ og ‘A’.
- DOTALL — tillader punkt (.) at matche newline — normalt matcher det alt andet end newline. Dette kan give dig problemer — du tror, at .* passer til alt, men som standard går den ikke ud over enden af en linje. Bemærk, at \s (whitespace) omfatter newlines, så hvis du ønsker at matche en række whitespace, der kan omfatte en newline, kan du bare bruge \s*
- MULTILINE — I en streng bestående af mange linjer, tillad ^ og $ at matche starten og slutningen af hver linje. Normalt ville ^/$ bare matche starten og slutningen af hele strengen.
Greedy vs. Non-Greedy (valgfrit)
Dette er et valgfrit afsnit, som viser en mere avanceret teknik til regulære udtryk, der ikke er nødvendig for øvelserne.
Sæt, at du har tekst med tags i den: <b>foo</b> og <i>og så videre</i>
Sæt, at du forsøger at matche hvert enkelt tag med mønsteret ‘(<.*>)’ – hvad matcher det først?
Resultatet er lidt overraskende, men det grådige aspekt af .* får den til at matche hele ‘<b>foo</b> og <i>så videre</i>’ som ét stort match. Problemet er, at .* går så langt som muligt, i stedet for at stoppe ved den første > (dvs. den er “grådig”).
Der findes en udvidelse til regulære udtryk, hvor man tilføjer et ? i slutningen, f.eks. .*? eller .+?, hvilket ændrer dem til at være ikke-grådige. Nu stopper de så hurtigt, som de kan. Så mønsteret ‘(<.*?>)’ får bare ‘<b>’ som det første match, og ‘</b>’ som det andet match, og så videre med at få hvert <..>-par på skift. Stilen er typisk, at man bruger en .*?, og så straks til højre for den leder man efter en konkret markør (> i dette tilfælde), der tvinger enden af .*?-kørslen frem.
*?-udvidelsen stammer fra Perl, og regulære udtryk, der indeholder Perls udvidelser, kaldes Perl Compatible Regular Expressions — pcre. Python indeholder pcre-understøttelse. Mange kommandolinjeværktøjer osv. har et flag, hvor de accepterer pcre-mønstre.
En ældre, men meget anvendt teknik til at kode denne idé om “alle disse tegn undtagen stop ved X” bruger stilen med firkantede parenteser. For ovenstående kunne du skrive mønsteret, men i stedet for .* for at få alle tegn, kan du bruge *, som springer over alle tegn, der ikke er > (den forreste ^ “inverterer” sætningen af firkantede parenteser, så den matcher ethvert tegn, der ikke er i parenteserne).
Substitution (valgfrit)
Funktionen re.sub(pat, replacement, str) søger efter alle forekomster af mønster i den givne streng og erstatter dem. Erstatningsstrengen kan indeholde “\1”, “\2”, som henviser til teksten fra gruppe(1), gruppe(2) osv. fra den oprindelige matchende tekst.
Her er et eksempel, der søger efter alle e-mail-adresser og ændrer dem, så de beholder brugeren (\1), men har yo-yo-dyne.com som vært.
str = 'purple [email protected], blah monkey [email protected] blah dishwasher' ## re.sub(pat, replacement, str) -- returns new string with all replacements, ## is group(1), group(2) in the replacement print re.sub(r'(+)@(+)', r'@yo-yo-dyne.com', str) ## purple [email protected], blah monkey [email protected] blah dishwasher
Øvelse
For at øve regulære udtryk kan du se øvelsen Babynavne.