Enkelt sagt, regulære uttrykk (regexes eller regexps for short) er en måte å spesifisere streng mønstre på. Du er utvilsomt kjent med søke- og erstattingsfunksjonen i din favoritt tekstredigerer eller IDE. Du kan søke etter eksakte ord og uttrykk. Du kan også aktivere alternativer, for eksempel tilfeldighetsfølsomhet, slik at et søk etter ordet "color" også finner "Color", "COLOR" og "CoLoR". Men hva om du ville søke etter stavelsesvarianter av ordet "color" (amerikansk stavemåte: farge, britisk stavemåte: farge) uten å måtte utføre to separate søk?
Hvis det eksemplet virker for enkelt, hva med om du vil søke etter alle stavelsesvarianter av det engelske navnet "Katherine" (Katharine, Katharine, Kathryn, Kathryn osv. For å nevne noen). Mer generelt kan det være lurt å søke i et dokument for alle strenger som lignet heksadesimale tall, datoer, telefonnumre, e-postadresser, kredittkortnummer osv..
Vanlige uttrykk er en kraftig måte å (delvis eller fullt) takle disse (og mange andre) praktiske problemer som involverer tekst.
Strukturen til denne opplæringen er som følger. Jeg vil introdusere kjernekonseptene du trenger å forstå ved å tilpasse en tilnærming som brukes i teoretiske lærebøker (etter å ha fjernet unødvendig strenghet eller pedantri). Jeg foretrekker denne tilnærmingen fordi den lar deg forstå forståelsen av kanskje 70% av funksjonaliteten du trenger, i sammenheng med noen grunnleggende prinsipper. De resterende 30% er mer avanserte funksjoner som du kan lære senere eller hoppe over, med mindre du tar sikte på å bli en regex maestro.
Det er en mengde syntax assosiert med vanlige uttrykk, men det meste er bare der for å tillate at du bruker de kjente ideene så kortfattet som mulig. Jeg vil introdusere disse trinnvis, i stedet for å droppe et stort bord eller en liste for deg å huske.
I stedet for å hoppe rett inn i en Swift-implementering, vil vi utforske det grunnleggende ved hjelp av et utmerket onlineverktøy som vil hjelpe deg å designe og evaluere vanlige uttrykk med minimum friksjon og unødvendig bagasje. Når du blir komfortabel med hovedidéene, skriver du Swift-koden i utgangspunktet et problem med å kartlegge din forståelse for Swift API.
Gjennom hele tiden vil vi forsøke å holde en pragmatisk tankegang. Regexes er ikke det beste verktøyet for hver strengbehandlingssituasjon. I praksis må vi identifisere situasjoner hvor regexes fungerer veldig bra og situasjoner der de ikke gjør det. Det er også en midtbane hvor regexes kan brukes til å gjøre en del av jobben (vanligvis litt forbehandling og filtrering) og resten av jobben igjen til algoritmisk logikk.
Regelmessige uttrykk har sine teoretiske grunner i "teorien om beregning", et av emnene som studeres av datavitenskap, hvor de spiller rollen som inngangen som brukes til en bestemt klasse abstrakte databehandlingsmaskiner kalt endelige automater.
Slapp av, men du er ikke pålagt å studere den teoretiske bakgrunnen for å bruke vanlige uttrykk praktisk talt. Jeg nevner bare dem fordi tilnærmingen jeg vil bruke for å først motivere vanlige uttrykk fra grunnen opp spegler tilnærmingen som brukes i datavitenskap lærebøker for å definere "teoretiske" regulære uttrykk.
Forutsatt at du har noe kjent med rekursjon, vil jeg gjerne huske på hvordan rekursive funksjoner er definert. En funksjon er definert i form av enklere versjoner av seg selv, og hvis du sporer gjennom en rekursiv definisjon, må du ende opp med en basis sak som er uttrykkelig definert. Jeg tar opp dette fordi vår definisjon nedenfor kommer til å bli rekursiv også.
Legg merke til at når vi snakker om strenge generelt, har vi implisitt et tegn i tankene, for eksempel ASCII, Unicode osv. La oss late som øyeblikket vi lever i et univers hvor strenger består av de 26 bokstavene i små bokstaver alfabet (a, b, ... z) og ingenting annet.
Vi begynner med å hevde at hver karakter i dette settet kan betraktes som et vanlig uttrykk som matcher seg som en streng. Så en
som et vanlig uttrykk samsvarer med "a" (betraktet som en streng), b
er en regex som samsvarer med strengen "b" osv. La oss også si at det er et "tomt" vanlig uttrykk Ɛ
som matcher den tomme strengen "". Slike tilfeller samsvarer med trivielle "basissaker" av rekursjonen.
Nå vurderer vi følgende regler som hjelper oss med å lage nye vanlige uttrykk fra eksisterende:
La oss gjøre denne betongen med flere enkle eksempler med våre alfabetiske strenge.
Fra regel 1, en
og b
å være regulære uttrykk som samsvarer med "a" og "b", betyr ab
er et vanlig uttrykk som matcher strengen "ab". Siden ab
og c
er vanlige uttrykk, abc
er et vanlig uttrykk som samsvarer med strengen "abc", og så videre. Fortsett på denne måten kan vi lage vilkårlig, langt vanlig uttrykk som samsvarer med en streng med identiske tegn. Det har ikke skjedd noe interessant ennå.
Fra regel 2, o
og en
å være vanlig uttrykk, o | en
matcher "o" eller "a". Den vertikale linjen representerer veksling. c
og t
er regulære uttrykk, og i kombinasjon med regel 1 kan vi hevde det c (o | a) t
er et vanlig uttrykk. Parantesene brukes til gruppering.
Hva samsvarer det med? c
og t
bare matche seg, noe som betyr at regexen c (o | a) t
matcher "c" etterfulgt av enten en "a" eller en "o" etterfulgt av "t", for eksempel strengen "katt" eller "barneseng". Legg merke til at det gjør det ikke match "kappe" som o | en
matcher bare "a" eller "o", men ikke begge samtidig. Nå begynner ting å bli interessant.
Fra regel 3, en*
matcher null eller flere forekomster av "a". Den matcher den tomme strengen eller strengene "a", "aa", "aaa" og så videre. La oss utøve denne regelen sammen med de to andre reglene.
Hva gjør varmt
kamp? Den samsvarer med "ht" (med null forekomster av "o"), "hot", "hoot", "hooot" og så videre. Hva med b (o | a) *
? Det kan matche "b" etterfulgt av noen forekomster av "o" og "a" (inkludert ingen av dem). "b", "boa", "baa", "bao", "baooaoaoaoo" er bare noen av det uendelige antall strenger som dette vanlige uttrykket samsvarer med. Merk igjen at parentesene brukes til å gruppere sammen den delen av det regulære uttrykket som *
blir brukt.
La oss prøve å oppdage vanlige uttrykk som matcher strenger vi allerede har i tankene. Hvordan skal vi lage et vanlig uttrykk som gjenkjenner saueblodning, som jeg ser på som noen gjentakelser av grunnleggende lyden "baa" ("baa", "baabaa", "baabaabaa" osv.)
Hvis du sa, (Baa) *
, så er du nesten riktig. Men legg merke til at dette regulære uttrykket vil matche den tomme strengen også, som vi ikke vil ha. Med andre ord vil vi ignorere ikke-bløtende får. baa (bæ) *
er det vanlige uttrykket vi leter etter. På samme måte kan en ku mooing være moo (moo) *
. Hvordan kan vi gjenkjenne lyden av begge dyr? Enkel. Bruk veksling. baa (BAA) * | moo (moo) *
Hvis du har forstått de ovennevnte ideene, gratulerer, er du godt på vei.
Husk at vi satte en dum begrensning på våre strenger. De kan bare bestå av små bokstaver i alfabetet. Vi vil nå slippe denne begrensningen og vurdere alle strenger som består av ASCII-tegn.
Vi må innse at for at regelmessige uttrykk skal være et praktisk verktøy, må de selv være representert som strenge. Så, i motsetning til tidligere, kan vi ikke lenger bruke tegn som *
, |
, (
, )
, etc. uten å si om vi bruker dem som "spesielle" tegn som representerer veksling, gruppering etc. eller om vi behandler dem som vanlige tegn som må matches bokstavelig talt.
Løsningen er å behandle disse og andre "metakarakterer" som kan ha en spesiell betydning. For å bytte mellom en bruk og den andre, må vi kunne unnslippe dem. Dette ligner ideen om å bruke "\ n" (escaping n) for å indikere en ny linje i en streng. Det er litt mer komplisert ved at, avhengig av kontekstavennet som vanligvis er "meta", kan det representere sitt bokstavelige selvbilde uten rømming. Vi vil se eksempler på dette senere.
En annen ting vi verdsetter er konsistens. Mange regulære uttrykk som kan uttrykkes ved hjelp av den forrige delenes notasjon, vil være kjedelig ordentlig. For eksempel, anta at du bare vil finne alle to tegnstrenger som består av et lite bokstav etterfulgt av et tall (for eksempel strenger som "a0", "b9", "z3" osv.). Ved å bruke notasjonen vi diskuterte tidligere, ville dette resultere i følgende regulære uttrykk:
(A | b | c | d | e | f | g | h | i | j | k | l | m | n | O | P | Q | R | s | t | u | v | w | x | y | z) (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)
Bare skrive det monsteret tørket meg ut.
ikke [Abcdefghijklmnopqrstuvwxyz] [0123456789]
ser ut som en bedre representasjon? Merk metategnene [
og ]
som betyr et sett med tegn, hvorav en gir en positiv kamp. Faktisk, hvis vi anser at bokstavene a til z, og tallene 0 til 9 forekommer i rekkefølge i ASCII-settet, kan vi hakke regexet ned til en kul [A-z] [0-9]
.
Innenfor rammer av et tegnsett, bindestrek, -
, er en annen metakarakter som indikerer en rekkevidde. Legg merke til at du kan klemme flere intervaller inn i samme par firkantede parenteser. For eksempel, [0-9a-zA-Z]
kan matche alle alfanumeriske tegn. De 9 og en (og z og EN)Klemmet mot hverandre kan se morsomt ut, men husk at vanlige uttrykk handler om korthet og meningen er tydelig.
Når det gjelder korthet, er det enda mer konkrete måter å representere visse klasser av relaterte tegn som vi vil se om et minutt. Merk at vekslingslinjen, |
, er fortsatt gyldig og nyttig syntaks som vi vil se på et øyeblikk.
Før vi begynner å praktisere, la oss ta en titt på litt mer syntaks.
Perioden, .
, samsvarer med et enkelt tegn, med unntak av linjeskift. Dette betyr at c.t
kan matche "cat", "crt", "c9t", "c% t", "c.t", "c t" og så videre. Hvis vi ønsket å matche perioden som en vanlig karakter, for eksempel for å matche strengen "c.t", kunne vi enten unnslippe det (c \ .T
) eller sett den i en karakterklasse av seg selv (c [.] t
).
Generelt gjelder disse ideene for andre meta tegn, for eksempel [
, ]
, (
, )
, *
, og andre vi ikke har møtt enda.
Parenteser ((
og )
) brukes til gruppering som vi så før. Vi skal bruke ordet pollett å bety enten en enkelt karakter eller et parentesert uttrykk. Årsaken er at mange regex operatører kan brukes til enten.
Parenteser brukes også til å definere fange grupper, slik at du kan finne ut hvilken del av kampen din var fanget av en bestemt fangstgruppe i regexen. Jeg vil snakke mer om denne svært nyttige funksjonaliteten senere.
EN +
Følgende token er en eller flere forekomster av det token. I vårt saueblodseksempel, baa (bæ) *
kan bli representert mer kortfattet som (Baa)+
. Husk det *
betyr null eller flere forekomster. Noter det (Baa)+
er forskjellig fra baa+
, fordi i den tidligere +
brukes til baa
token mens i sistnevnte gjelder det bare for en
før det. I sistnevnte samsvarer det med strenger som "baa", "baaa" og "baaaa".
EN ?
Følgende token betyr null eller en forekomst av det token.
RegExr er et utmerket online verktøy for å eksperimentere med vanlige uttrykk. Når du er komfortabel å lese og skrive vanlige uttrykk, vil det være mye lettere å bruke regelverkets uttrykks API for rammeverket. Selv da blir det lettere å teste ditt vanlige uttrykk i sanntid på nettstedet først.
Besøk nettsiden og fokus på hoveddelen av siden. Dette er hva du vil se:
Du skriver inn et vanlig uttrykk i boksen øverst og skriver inn teksten du leter etter kamper.
"/ G" på slutten av uttrykksboksen er ikke en del av det vanlige uttrykket per se. Det er et flagg som påvirker den generelle matchende oppførselen til regex-motoren. Ved å legge til "/ g" til det vanlige uttrykket, søker motoren etter alle mulige treff i det vanlige uttrykket i teksten, som er oppførselen vi ønsker. Det blå høydepunktet indikerer en kamp. Hovering med musen over det vanlige uttrykket er en praktisk måte å minne om betydningen av dens utgjørende deler.
Vet at faste uttrykk kommer i forskjellige smaker, avhengig av hvilket språk eller bibliotek du bruker. Dette betyr ikke bare at syntaksen kan være litt annerledes blant de forskjellige smaker, men også evner og funksjoner. Swift bruker for eksempel mønster syntaks spesifisert av ICU. Jeg er ikke sikker på hvilken smak som brukes i RegExr (som kjører på JavaScript), men innenfor omfanget av denne opplæringen, er de ganske like, om ikke identiske.
Jeg oppfordrer deg også til å utforske ruten på venstre side, som har mye informasjon presentert på en kort måte.
For å unngå potensiell forvirring, bør jeg nevne at når vi snakker med ordinært uttrykksmatching, kan vi tenke på to ting:
Standard betydning med hvilken regex motorer opererer er (1). Det vi har snakket om så langt er (2). Heldigvis er det enkelt å implementere mening (2) ved hjelp av meta tegn som vil bli introdusert senere. Ikke bekymre deg for dette for nå.
La oss starte enkle ved å teste ut vårt sau bleating eksempel. Type (Baa)+
inn i uttrykkboksen og noen eksempler for å teste for kamper som vist nedenfor.
Jeg håper du forstår hvorfor kampene som lyktes faktisk lyktes og hvorfor de andre mislyktes. Selv i dette enkle eksempelet er det noen interessante ting å påpeke.
Har strengen "baabaa" to kamper eller en? Med andre ord, er hver enkelt "baa" en kamp eller er hele "baabaa" en enkelt kamp? Dette kommer ned til hvorvidt en "grådig kamp" blir søkt. En grådig kamp forsøker å matche så mye av en streng som mulig.
Regex-motoren matcher nå greedily, noe som betyr at "baabaa" er en enkelt kamp. Det finnes måter å lazy matche på, men det er et mer avansert emne, og siden vi allerede har platene våre, vil vi ikke dekke det i denne opplæringen.
RegExr-verktøyet etterlater et lite men merkbart gap i uthevet hvis to tilstøtende deler av en streng hver enkelt (men ikke kollektivt) samsvarer med det vanlige uttrykket. Vi vil se et eksempel på denne oppførelsen litt.
"Baabaa" mislykkes på grunn av store bokstaver "B". Si at du ville tillate bare den første "B" å være stor, hva ville det tilsvarende regulære uttrykket være? Prøv å finne ut det selv.
Ett svar er (B | b) aa (BAA) *
. Det hjelper hvis du leser det høyt. En stor eller liten bokstav "b", etterfulgt av "aa", etterfulgt av null eller flere forekomster av "baa". Dette er brukbart, men merk at dette raskt kunne bli ubeleilig, spesielt hvis vi ønsket å ignorere kapitalisering helt. For eksempel må vi spesifisere alternativer for hvert tilfelle, noe som vil resultere i noe uhåndterlig som ([Bb] [Aa] [Aa])+
.
Heldigvis har vanlige uttrykksmotorer vanligvis mulighet til å ignorere saken. I tilfelle av RegExr, klikk på knappen som viser "flagg" og merk av i boksen "ignore case". Legg merke til at bokstaven "i" er prepended til listen over alternativer på slutten av det vanlige uttrykket. Prøv noen eksempler med blandede bokstaver, for eksempel "bAABaa".
La oss prøve å lage et vanlig uttrykk som kan fange varianter av navnet "Katherine". Hvordan ville du nærme deg dette problemet? Jeg ville skrive ned så mange variasjoner, se på de vanlige delene, og prøv deretter å uttrykke variasjonene (med vekt på alternativer og valgfrie bokstaver) som en sekvens. Deretter vil jeg forsøke å formulere det vanlige uttrykket som assimilerer alle disse variasjonene.
La oss prøve det med denne listen over variasjoner: Katherine, Katharine, Katharene, Katrine, Katrine, Kathryn, Catrin, Catrin. Jeg vil la det opp til deg å skrive ned flere hvis du vil. Ser på disse variasjonene, kan jeg grovt si det:
Med denne ideen i tankene kan jeg komme med følgende regulære uttrykk:
[Kc] ath [AE]?? (R | l) (i | ee | y) ne?
Merk at første linje "KatherineKatharine" har to kamper uten noen skille mellom dem. Hvis du ser det nært i RegExrs tekstredigerer, kan du observere den lille pause i fremhevingen mellom de to kampene, som jeg snakket om tidligere.
Vær oppmerksom på at det ovennevnte regulære uttrykket også samsvarer med navn vi ikke trodde, og det kan ikke engang eksistere, for eksempel "Cathalin". I den nåværende sammenhengen påvirker dette oss ikke noe negativt. Men i enkelte programmer, for eksempel e-post validering, vil du være mer spesifikk om strenger du samsvarer med og de du avviser. Dette legger vanligvis til kompleksiteten til det vanlige uttrykket.
Før vi går videre til Swift, vil jeg gjerne diskutere noen flere aspekter av syntaksen av regulære uttrykk.
Flere klasser av beslektede tegn har en kortfattet representasjon:
\ w
alfanumerisk karakter, inkludert understrek, tilsvarende [A-zA-Z0-9_]
\ d
representerer et siffer tilsvarende [0-9]
\ s
representerer hvitt plass, det vil si mellomrom, tabulator eller linjeskiftDisse klassene har også tilsvarende negative klasser:
\ W
representerer en ikke-alfanumerisk, ikke-understrekende karakter\ D
et ikke-siffer\ S
et ikke-mellomrom karakterHusk de uncapitalized klassene og så husk at den tilsvarende kapitaliserte en matcher hva den uncapitalized klassen ikke samsvarer med. Merk at disse kan kombineres ved å inkludere innvendige firkantede parenteser om nødvendig. For eksempel, [\ S \ S]
representerer et hvilket som helst tegn, inkludert linjeskift. Husk at perioden .
matcher alle tegn unntatt linjeskift.
^
og $
er ankre som representerer starten og slutten av en streng henholdsvis. Husk at jeg skrev at du kanskje vil matche en hel streng, heller enn å se etter substring-kamper? Slik gjør du det. ^ C [OAU] t $
matcher "katt", "barneseng" eller "kutt", men ikke, si "fangst" eller "recut".
\ b
representerer en grense mellom ord, som for eksempel på grunn av mellomrom eller tegnsetting, og også starten eller slutten av strengen. Vær oppmerksom på at det er litt annerledes fordi det samsvarer med en posisjon i stedet for et eksplisitt tegn. Det kan bidra til å tenke på et ordgrense som en usynlig divider som skiller et ord fra forrige / neste. Som du forventer, \ B
representerer "ikke et ordgrense". \ Bcat \ b
finner kamper i "cat", "a cat", "Hei, katt", men ikke i "acat" eller "catch".
Ideen om negasjon kan gjøres mer spesifikk ved hjelp av ^
metakarakter inne i et tegnsett. Dette er en helt annen bruk av ^
fra "start av strenganker". Dette betyr at for negasjon, ^
må brukes i et tegnsett rett ved starten. [^ A]
matcher ethvert tegn i tillegg til bokstaven "a" og [^ A-z]
matcher ethvert tegn bortsett fra små bokstaver.
Kan du representere \ W
bruker negasjon og karakterområder? Svaret er [^ A-Za-z0-9_]
. Hva tror du [A ^]
fyrstikker? Svaret er enten et "a" eller et "^" tegn siden det ikke skjedde i begynnelsen av tegnsettet. Her "^" matcher seg bokstavelig talt.
Alternativt kan vi unnslippe det eksplisitt slik: [\ ^ A]
. Forhåpentligvis begynner du å utvikle noe intuisjon om hvordan flykting fungerer.
Vi så hvordan *
(og +
) kan brukes til å matche en token null eller mer (og en eller flere) ganger. Denne ideen om å matche et token flere ganger kan gjøres mer spesifikt ved hjelp av kvantifiserende stoffer i krøllete braces. For eksempel, 2, 4
betyr to til fire kamper av foregående token. 2,
betyr to eller flere kamper og 2
betyr nøyaktig to kamper.
Vi vil se på detaljerte eksempler som bruker de fleste av disse elementene i neste opplæring. Men for praktens skyld oppfordrer jeg deg til å lage dine egne eksempler og teste ut syntaksen vi nettopp så med RegExr-verktøyet.
I denne opplæringen har vi først og fremst fokusert på teorien og syntaksen av regulære uttrykk. I neste opplæring legger vi til Swift i blandingen. Før du går videre, må du forstå hva vi har dekket i denne opplæringen ved å leke med RegExr.