Enkelt uttryckt är reguljära uttryck (regexes eller regexps för korta) ett sätt att specificera strängmönster. Du är utan tvekan bekant med sökningen och ersätt funktionen i din favorittextredigerare eller IDE. Du kan söka efter exakta ord och fraser. Du kan också aktivera alternativ, t.ex. obestridlighet, så att en sökning efter ordet "färg" också hittar "Färg", "FÄRG" och "CoLoR". Men vad händer om du vill söka efter stavningsvariationerna av ordet "färg" (amerikansk stavning: färg, brittisk stavning: färg) utan att behöva utföra två separata sökningar?
Om det exemplet verkar för enkelt, hur är det med om du vill söka efter alla stavningsvariationer av det engelska namnet "Katherine" (Katharina, Katharine, Kathreen, Kathryn, etc. för att nämna några). Mer allmänt kan du söka i ett dokument för alla strängar som liknade hexadecimala nummer, datum, telefonnummer, e-postadresser, kreditkortsnummer osv..
Regelbundna uttryck är ett kraftfullt sätt att (delvis eller fullständigt) hantera dessa (och många andra) praktiska problem med text.
Strukturen i denna handledning är som följer. Jag kommer att introducera de grundläggande begreppen du behöver förstå genom att anpassa ett tillvägagångssätt som används i teoretiska läroböcker (efter att ha tagit bort all onödig rigor eller pedantri). Jag föredrar detta tillvägagångssätt, eftersom det gör det möjligt för dig att förstå kanske 70% av funktionaliteten som du behöver, i samband med några grundläggande principer. De återstående 30% är mer avancerade funktioner som du kan lära dig senare eller hoppa över, om du inte strävar efter att bli en regex maestro.
Det finns en riklig mängd syntax förknippade med reguljära uttryck, men det mesta är bara där för att du ska kunna tillämpa kärnidéerna så kortfattat som möjligt. Jag kommer att introducera dessa stegvis, snarare än att släppa ett stort bord eller en lista för att du ska kunna memorera.
I stället för att hoppa direkt till en Swift-implementering, kommer vi att utforska grunderna genom ett utmärkt onlineverktyg som hjälper dig att utforma och utvärdera reguljära uttryck med minsta friktion och onödigt bagage. När du väl är bekväm med huvudidéerna är skrivning av Swift-kod i princip ett problem med att kartlägga din förståelse för Swift API.
Vi kommer hela tiden att försöka hålla en pragmatisk tankegång. Regexes är inte det bästa verktyget för varje strängbearbetningssituation. I praktiken behöver vi identifiera situationer där regexes fungerar väldigt bra och situationer där de inte gör det. Det finns också en mellersta mark där regexes kan användas för att göra en del av jobbet (vanligtvis lite förbehandling och filtrering) och resten av jobbet kvar till algoritmisk logik.
Regelbundna uttryck har sina teoretiska underlag i "beräkningsteorin", ett av de ämnen som studeras av datavetenskap, där de spelar rollen som ingången som tillämpas på en specifik klass abstrakta datorer som kallas ändliga automater.
Koppla av, men du behöver inte studera den teoretiska bakgrunden för att använda vanliga uttryck i praktiken. Jag nämner bara dem eftersom det tillvägagångssätt jag ska använda för att initialt motivera reguljära uttryck från grunden speglar den metod som används i datavetenskapliga läroböcker för att definiera "teoretiska" reguljära uttryck.
Om du antar att du känner till rekursion, vill jag att du ska komma ihåg hur rekursiva funktioner definieras. En funktion definieras när det gäller enklare versioner av sig själv, och om du spårar genom en rekursiv definition måste du hamna i ett basfall som uttryckligen definieras. Jag tar upp detta eftersom vår definition nedan kommer att vara rekursiv också.
Observera att när vi pratar om strängar i allmänhet har vi implicit en karaktär i åtanke, såsom ASCII, Unicode, etc. Låt oss låtsas för det ögonblick som vi lever i ett universum där strängar består av de 26 bokstäverna i små bokstaven alfabetet (a, b, ... z) och inget annat.
Vi börjar med att hävda att varje tecken i denna uppsättning kan betraktas som ett regelbundet uttryck som matchar sig som en sträng. Så en
som ett vanligt uttryck matchar "a" (betraktas som en sträng), b
är en regex som matchar strängen "b" etc. Låt oss också säga att det finns ett "tomt" vanligt uttryck ɛ
som matchar den tomma strängen "". Sådana fall motsvarar de triviala "basfallen" av rekursionen.
Nu överväger vi följande regler som hjälper oss att göra nya reguljära uttryck från befintliga:
Låt oss göra detta konkret med flera enkla exempel med våra alfabetiska strängar.
Från regel 1, en
och b
att vara regelbundna uttryck som matchar "a" och "b", betyder ab
är ett vanligt uttryck som matchar strängen "ab". Eftersom ab
och c
är reguljära uttryck, abc
är ett vanligt uttryck som matchar strängen "abc" och så vidare. Fortsätt på detta sätt kan vi göra godtyckliga långa reguljära uttryck som matchar en sträng med identiska tecken. Inget intressant har hänt än.
Från regel 2, o
och en
vara reguljära uttryck, o | a
matchar "o" eller "a". Den vertikala linjen representerar växelverkan. c
och t
är reguljära uttryck och i kombination med regel 1 kan vi hävda det c (o | a) t
är ett vanligt uttryck. Parenteserna används för gruppering.
Vad matchar det? c
och t
bara matcha sig, vilket betyder att regexen c (o | a) t
matchar "c" följt av antingen "a" eller "o" följt av "t", till exempel strängen "katt" eller "barnsäng". Observera att det gör det inte matcha "coat" som o | a
matchar bara "a" eller "o", men inte båda samtidigt. Nu börjar saker bli intressanta.
Från regel 3, en*
matchar noll eller fler instanser av "a". Den matchar den tomma strängen eller strängarna "a", "aa", "aaa" och så vidare. Låt oss utöva denna regel i samband med de andra två reglerna.
Vad gör varm
match? Den matchar "ht" (med noll instanser av "o"), "hot", "hoot", "hooot" och så vidare. Vad sägs om b (o | a) *
? Det kan matcha "b" följt av ett antal instanser av "o" och "a" (inklusive ingen av dem). "b", "boa", "baa", "bao", "baooaoaoaoo" är bara några av det oändliga antalet strängar som detta vanliga uttryck matchar. Notera igen att parenteserna används för att gruppera den del av det reguljära uttrycket som *
tillämpas.
Låt oss försöka upptäcka reguljära uttryck som matchar strängar vi redan har i åtanke. Hur skulle vi göra ett regelbundet uttryck som erkänner fårblödning, vilket jag betraktar som ett antal repeteringar av grundljudet "baa" ("baa", "baabaa", "baabaabaa" etc.)
Om du sa, (bä)*
, då är du nästan rätt. Men märka att det här regelbundna uttrycket skulle matcha den tomma strängen, som vi inte vill ha. Med andra ord vill vi ignorera icke-blödande får. BAA (BAA) *
är det reguljära uttrycket vi letar efter. På samma sätt kan en cow mooing vara moo (moo) *
. Hur kan vi känna igen ljudet av något djur? Enkel. Använd växling. baa (baa) * | moo (moo) *
Om du har förstått ovanstående idéer, grattis, är du väl på väg.
Minns att vi lagt en dum restriktion på våra strängar. De kunde bara bestå av små bokstäver i alfabetet. Vi kommer nu att släppa denna begränsning och överväga alla strängar som består av ASCII-tecken.
Vi måste inse att för att regelbundna uttryck ska vara ett lämpligt verktyg måste de själva representeras som strängar. Så, till skillnad från tidigare, kan vi inte längre använda tecken som *
, |
, (
, )
, etc. utan att på något sätt signalera om vi använder dem som "speciella" tecken som representerar växling, gruppering etc. eller om vi behandlar dem som vanliga tecken som måste matchas bokstavligen.
Lösningen är att behandla dessa och andra "metakarakter" som kan ha en speciell betydelse. För att växla mellan en användning och den andra måste vi kunna fly från dem. Detta liknar tanken på att använda "\ n" (escaping n) för att ange en ny rad i en sträng. Det är något mer komplicerat eftersom det, beroende på kontextkaraktären som vanligtvis är "meta", kan representera sitt bokstavliga själv utan räddning. Vi kommer se exempel på detta senare.
En annan sak vi värdesätter är korthet. Många reguljära uttryck som kan uttryckas med bara den föregående sektionens notation skulle vara tediously verbose. Antag att du bara vill hitta alla två teckensträngar som består av ett litet bokstav följt av ett tal (till exempel strängar som "a0", "b9", "z3" osv.). Med hjälp av den notation vi diskuterade tidigare skulle detta resultera i följande reguljära uttryck:
(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)
Bara skriva det monsteret torkade mig ut.
inte [Abcdefghijklmnopqrstuvwxyz] [0123456789]
ser ut som en bättre representation? Observera metateklarna [
och ]
som betecknar en uppsättning tecken, varav en ger en positiv matchning. Faktum är att om vi anser att bokstäverna a till z och siffrorna 0 till 9 uppträder i följd i ASCII-uppsättningen, kan vi whittle regex ner till en cool [A-z] [0-9]
.
Inom ramen för en teckenuppsättning, bindestreck, -
, är en annan metakarakter som indikerar ett intervall. Observera att du kan klämma flera intervall i samma par kvadratiska parenteser. Till exempel, [0-9a-zA-Z]
kan matcha alla alfanumeriska tecken. De 9 och en (och z och en)klämda mot varandra kan se roligt ut, men kom ihåg att reguljära uttryck handlar om korthet och meningen är tydlig.
Med tanke på korthet finns det ännu mer konkreta sätt att representera vissa klasser av relaterade tecken som vi kommer att se om en minut. Observera att växlingsfältet, |
, är fortfarande giltig och användbar syntax som vi kommer att se på ett ögonblick.
Innan vi börjar träna, låt oss ta en titt på lite mer syntax.
Perioden, .
, matchar alla enskilda tecken, med undantag för rader. Detta innebär att c.t
kan matcha "katt", "crt", "c9t", "c% t", "c.t", "c t" och så vidare. Om vi ville matcha perioden som en vanlig karaktär, till exempel för att matcha strängen "c.t", kunde vi antingen fly den (c \ .t
) eller sätta det i en karaktärsklass av sig själv (c [.] t
).
I allmänhet gäller dessa idéer för andra metateklar, t.ex. [
, ]
, (
, )
, *
, och andra vi inte har stött på ännu.
Parenteser ((
och )
) används för gruppering som vi såg tidigare. Vi ska använda ordet tecken att innebära antingen en enda karaktär eller ett parentesiserat uttryck. Anledningen är att många regex operatörer kan appliceras på antingen.
Parentes används också för att definiera fånga grupper, så att du kan ta reda på vilken del av din match som var fångad av en viss fångrupp i regexen. Jag kommer att prata mer om denna mycket användbara funktionalitet senare.
en +
Efter en token är en eller flera förekomster av det token. I vårt fårblodningsexempel, BAA (BAA) *
kunde representeras mer kortfattat som (bä)+
. Minnas det *
betyder noll eller fler förekomster. Anteckna det (bä)+
skiljer sig från bä+
, för i den förra +
tillämpas på bä
token medan den senare gäller endast för en
före det. I den senare matchar den strängar som "baa", "baaa" och "baaaa".
en ?
Efter en token avses noll eller en instans av den token.
RegExr är ett utmärkt onlineverktyg för att experimentera med reguljära uttryck. När du är bekväm att läsa och skriva regelbundna uttryck, blir det mycket lättare att använda Foundation Framework-regelverket. Även då blir det enklare att testa ditt reguljära uttryck i realtid på webbplatsen först.
Besök webbplatsen och fokusera på huvuddelen av sidan. Så här ser du:
Du anger ett vanligt uttryck i rutan längst upp och anger texten där du söker matchningar.
"/ G" i slutet av uttrycksfältet är inte en del av det reguljära uttrycket i sig. Det är en flagga som påverkar regexmotorens övergripande matchningsbeteende. Genom att lägga till "/ g" på det reguljära uttrycket söker motorn efter alla möjliga matchningar i det reguljära uttrycket i texten, vilket är det beteende vi vill ha. Den blå höjdpunkten indikerar en matchning. Att hänga med musen över det reguljära uttrycket är ett praktiskt sätt att påminna dig om betydelsen av dess delar.
Vet att reguljära uttryck kommer i olika smaker, beroende på vilket språk eller bibliotek du använder. Det betyder inte bara att syntaxen kan vara lite annorlunda bland de olika smakerna, utan också förmågan och funktionerna. Swift använder till exempel den mönstersyntax som anges av ICU. Jag är inte säker på vilken smak som används i RegExr (som körs på JavaScript), men inom ramen för denna handledning är de ganska lika, om inte identiska.
Jag uppmuntrar dig också att utforska rutan på vänster sida, som har mycket information presenterad på ett kortfattat sätt.
För att undvika potentiell förvirring borde jag nämna att när vi pratar med ordinarie uttryckssätt kan vi betyda något av två saker:
Standardbetydelsen med vilken regexmotorerna fungerar är (1). Det vi har pratat om hittills är (2). Lyckligtvis är det enkelt att genomföra betydelse (2) med hjälp av meta tecken som kommer att introduceras senare. Oroa dig inte för det här för tillfället.
Låt oss börja göra det enkla genom att testa vårt fårblötande exempel. Typ (bä)+
in i expressionrutan och några exempel för att testa för matcher enligt nedan.
Jag hoppas du förstår varför matcherna som lyckades lyckades och varför de andra misslyckades. Även i det här enkla exemplet finns det några intressanta saker att påpeka.
Innehåller strängen "baabaa" två matcher eller en? Med andra ord är varje individ "baa" en match eller är hela "baabaa" en enda match? Detta beror på huruvida en "girig match" söks eller inte. En girig match försöker matcha så mycket av en sträng som möjligt.
Just nu är regexmotorn matchande greedily, vilket betyder att "baabaa" är en enda match. Det finns sätt att göra lat matchning, men det är ett mer avancerat ämne och eftersom vi redan har våra tallrikar fulla kommer vi inte att täcka det i den här handledningen.
RegExr-verktyget lämnar ett litet men urskiljbart mellanrum i markeringen om två intilliggande delar av en sträng vardera individuellt (men inte kollektivt) matchar det reguljära uttrycket. Vi kommer att se ett exempel på detta beteende på lite.
"Baabaa" misslyckas på grund av versalen "B". Säg att du bara vill tillåta att den första "B" är stor, vad skulle motsvarande reguljära uttryck vara? Försök att räkna ut det själv först.
Ett svar är (B | b) aa (BAA) *
. Det hjälper om du läser det högt. En stor eller liten bokstav "b" följt av "aa" följt av noll eller fler instanser av "baa". Detta är användbart, men observera att detta snabbt kan bli obekvämt, särskilt om vi vill ignorera kapitaliseringen helt och hållet. Till exempel skulle vi behöva ange alternativ för varje enskilt fall, vilket skulle resultera i något obehagligt som ([Bb] [Aa] [Aa])+
.
Lyckligtvis har vanliga uttrycksmotorer ett alternativ att ignorera fallet. Vid RegExr, klicka på knappen som läser "flaggor" och markera kryssrutan "ignore case". Observera att bokstaven "i" är upplagd i listan över alternativ i slutet av det reguljära uttrycket. Prova några exempel med blandade bokstäver, till exempel "bAABaa".
Låt oss försöka skapa ett regelbundet uttryck som kan fånga varianter av namnet "Katherine". Hur skulle du närma dig detta problem? Jag skulle skriva ner så många variationer, titta på de gemensamma delarna, och försök sedan uttrycka uttrycken i variationerna (med betoning på alternativa och valfria bokstäver) som en sekvens. Därefter skulle jag försöka formulera det reguljära uttrycket som assimilerar alla dessa variationer.
Låt oss prova med den här listan över variationer: Katherine, Katharine, Katharine, Katrine, Katrine, Kathryn, Katrin, Katrin. Jag kommer lämna det till dig att skriva ner flera mer om du vill. Titta på dessa variationer kan jag grovt säga det:
Med den här tanken i åtanke kan jag komma med följande ordinarie uttryck:
[KC] ath [ae]?? (R | l) (i | ee | y) ne?
Observera att första raden "KatherineKatharine" har två matcher utan någon skillnad mellan dem. Om du tittar på det nära i RegExrs textredigerare kan du observera den lilla rasten i markeringen mellan de två matcherna, vilket är vad jag pratade om tidigare.
Observera att ovanstående reguljära uttryck också matchar namn som vi inte övervägde och det kanske inte ens existerar, till exempel "Cathalin". I det nuvarande sammanhanget påverkar detta oss inte alls negativt. Men i vissa applikationer, till exempel e-validering, vill du vara mer specifik om strängarna du matchar och de du avvisar. Detta bidrar vanligen till komplexiteten hos det reguljära uttrycket.
Innan vi går vidare till Swift vill jag diskutera några få aspekter av syntaxen med reguljära uttryck.
Flera klasser av relaterade tecken har en kortfattad representation:
\ w
alfanumerisk karaktär, inklusive understrykning, motsvarar [A-zA-Z0-9_]
\ d
representerar en siffra som motsvarar [0-9]
\ s
representerar blankutrymme, det vill säga utrymme, flik eller radbrytningDessa klasser har också motsvarande negativa klasser:
\ W
representerar en icke-alfanumerisk, icke-understrykningstecken\ D
en icke-siffrig\ S
en icke-rymd karaktärKom ihåg de uncapitalized klasserna och kom ihåg att motsvarande motsvarande kapitaliserad matchar vad den uncapitalized klassen inte matchar. Observera att dessa kan kombineras genom att inkludera inuti kvadratfästen om det behövs. Till exempel, [\ S \ S]
representerar alla tecken, inklusive radbrytningar. Minns den perioden .
matchar alla tecken förutom radbrytningar.
^
och $
är ankare som representerar början och slutet på en sträng respektive. Kom ihåg att jag skrev att du kanske vill matcha en hel sträng istället för att leta efter substring matchningar? Så här gör du det. ^ C [OAU] t $
matchar "katt", "barnsäng" eller "skära", men inte säg "fånga" eller "recut".
\ b
representerar en gräns mellan ord, till exempel på grund av utrymme eller skiljetecken, och även strängens start eller slut. Observera att det är lite annorlunda eftersom det matchar en position snarare än en explicit karaktär. Det kan hjälpa till att tänka på ett ordgräns som en osynlig dividerare som skiljer ett ord från föregående / nästa. Som du förväntar dig, \ B
representerar "inte ett ordgräns". \ Bcat \ b
hittar matchningar i "katt", "en katt", "hej katt" men inte i "acat" eller "catch".
Tanken om negation kan göras mer specifikt med hjälp av ^
metacharacter inuti en teckenuppsättning. Detta är en helt annan användning av ^
från "start av strängankare". Detta innebär att för negation, ^
måste användas i en teckenuppsättning direkt vid början. [^ A]
matchar alla tecken förutom bokstaven "a" och [^ A-z]
matchar alla tecken med undantag för små bokstäver.
Kan du representera \ W
använder negation och karaktärsintervall? Svaret är [^ A-Za-z0-9_]
. Vad tror du [A ^]
tändstickor? Svaret är antingen ett "a" eller ett "^" tecken eftersom det inte hände i början av teckenuppsättningen. Här matchar "^" sig själv bokstavligen.
Alternativt kan vi undvika det uttryckligen så här: [\ ^ A]
. Förhoppningsvis börjar du utveckla en del intuition om hur flykting fungerar.
Vi såg hur *
(och +
) kan användas för att matcha en token noll eller mer (och en eller flera) gånger. Denna idé om att matcha ett token flera gånger kan göras mer specifikt med hjälp av kvantifierare i lockiga axlar. Till exempel, 2, 4
betyder två till fyra matcher av föregående token. 2,
betyder två eller flera matcher och 2
betyder exakt två matcher.
Vi kommer att titta på detaljerade exempel som använder de flesta av dessa element i nästa handledning. Men för praktens skull uppmuntrar jag dig att skapa egna exempel och testa den syntax vi såg med RegExr-verktyget.
I denna handledning har vi främst fokuserat på teorin och syntaxen för reguljära uttryck. I nästa handledning lägger vi till Swift i mixen. Innan du fortsätter, se till att du förstår vad vi har täckt i denna handledning genom att leka med RegExr.