Simpel gezegd, reguliere expressies (in het kort regexes of regexps) zijn een manier om stringpatronen te specificeren. U bent ongetwijfeld bekend met de zoek- en vervangfunctie in uw favoriete teksteditor of IDE. U kunt zoeken naar exacte woorden en woordgroepen. U kunt ook opties activeren, zoals hoofdletterongevoeligheid, zodat een zoekopdracht naar het woord "kleur" ook "Kleur", "KLEUR" en "KoRoR" vindt. Maar wat als u wilde zoeken naar de spellingsvarianten van het woord 'kleur' (Amerikaanse spelling: kleur, Britse spelling: kleur) zonder twee afzonderlijke zoekopdrachten uit te voeren?
Als dat voorbeeld te simpel lijkt, wat dacht je ervan als je alle spellingvarianten van de Engelse naam "Katherine" (Catherine, Katharine, Kathreen, Kathryn, enz.) Wilt opzoeken om er maar een paar te noemen). Meer in het algemeen zou je een document willen doorzoeken op alle strings die op hexadecimale nummers, datums, telefoonnummers, e-mailadressen, creditcardnummers enz. Lijken.
Reguliere uitdrukkingen zijn een krachtige manier om (en vele andere) praktische problemen met tekst (gedeeltelijk of volledig) aan te pakken.
De structuur van deze tutorial is als volgt. Ik zal de kernbegrippen introduceren die je moet begrijpen door een benadering aan te passen die wordt gebruikt in theoretische studieboeken (na het verwijderen van onnodige striktheid of pedanterie). Ik geef de voorkeur aan deze benadering, omdat je 70% van de functionaliteit kunt gebruiken die je nodig hebt, in de context van een paar basisprincipes. De overige 30% zijn geavanceerdere functies die u later kunt leren of overslaan, tenzij u een regex maestro wilt worden.
Er is een grote hoeveelheid syntaxis gekoppeld aan reguliere expressies, maar het meeste is er gewoon om de kernideeën zo bondig mogelijk toe te passen. Ik zal deze stapsgewijs introduceren, in plaats van een grote tafel of lijst voor je te laten onthouden.
In plaats van direct in een Swift-implementatie te springen, verkennen we de basis door een uitstekende online tool waarmee u reguliere expressies kunt ontwerpen en evalueren met de minimale hoeveelheid wrijving en onnodige bagage. Als u eenmaal vertrouwd bent met de belangrijkste ideeën, is het schrijven van Swift-code in feite een probleem van het in kaart brengen van uw begrip voor de Swift-API.
We zullen proberen een pragmatische denkwijze te behouden. Regexes zijn niet het beste hulpmiddel voor elke string-verwerkingssituatie. In de praktijk moeten we situaties identificeren waarin regexes erg goed werken en situaties waarin dit niet het geval is. Er is ook een middenweg waar regexes kunnen worden gebruikt om een deel van de klus te klaren (meestal een voorbewerking en filtering) en de rest van de klus overgelaten aan algoritmische logica.
Reguliere expressies hebben hun theoretische onderbouwing in de 'theorie van de berekening', een van de onderwerpen die door de computerwetenschap worden bestudeerd, waarbij ze de rol spelen van de invoer die wordt toegepast op een specifieke klasse van abstracte computermachines die eindige automaten worden genoemd.
Ontspan echter, je bent niet verplicht om de theoretische achtergrond praktisch te bestuderen om reguliere uitdrukkingen te gebruiken. Ik noem ze alleen omdat de aanpak die ik zal gebruiken om vanaf het begin reguliere expressies in eerste instantie te motiveren, de benadering weerspiegelt die wordt gebruikt in computerwetenschappelijke studieboeken om 'theoretische' reguliere expressies te definiëren.
Ervan uitgaande dat u bekend bent met recursie, zou ik willen dat u in gedachten houdt hoe recursieve functies zijn gedefinieerd. Een functie wordt gedefinieerd in termen van eenvoudigere versies van zichzelf en, als u een recursieve definitie doorloopt, moet u eindigen op een basisscenario dat expliciet is gedefinieerd. Ik breng dit naar voren omdat onze definitie hieronder ook recursief zal zijn.
Merk op dat, wanneer we het hebben over strings in het algemeen, we impliciet een karakter in ons hoofd hebben, zoals ASCII, Unicode, etc. Laten we doen alsof we leven in een universum waarin strings zijn samengesteld uit de 26 letters van de kleine letters alfabet (a, b, ... z) en niets anders.
We beginnen met te beweren dat elk personage in deze set kan worden beschouwd als een reguliere expressie die overeenkomt met zichzelf als een tekenreeks. Zo een
als een reguliere expressie komt overeen met "a" (beschouwd als een string), b
is een regex die overeenkomt met de string "b", enz. Laten we ook zeggen dat er een "lege" reguliere expressie is Ɛ
die overeenkomt met de lege reeks "". Dergelijke gevallen komen overeen met de triviale "basegevallen" van de recursie.
Nu beschouwen we de volgende regels die ons helpen bij het maken van nieuwe reguliere expressies van bestaande:
Laten we dit concreet maken met verschillende eenvoudige voorbeelden met onze alfabetische reeksen.
Van regel 1, een
en b
reguliere uitdrukkingen zijn die overeenkomen met "a" en "b", betekent ab
is een reguliere expressie die overeenkomt met de tekenreeks "ab". Sinds ab
en c
zijn reguliere expressies, abc
is een reguliere expressie die overeenkomt met de tekenreeks "abc", enzovoort. Op deze manier kunnen we willekeurige lange reguliere expressies maken die overeenkomen met een reeks met identieke tekens. Er is nog niets interessants gebeurd.
Van regel 2, O
en een
reguliere uitdrukkingen zijn, o | a
komt overeen met "o" of "a". De verticale balk staat voor afwisseling. c
en t
zijn reguliere expressies en, in combinatie met regel 1, kunnen we dat beweren c (o | a) t
is een reguliere expressie. De haakjes worden gebruikt voor groeperen.
Wat komt overeen?? c
en t
alleen overeenkomen met zichzelf, wat betekent dat de regex c (o | a) t
komt overeen met "c" gevolgd door een "a" of een "o" gevolgd door "t", bijvoorbeeld de string "cat" of "cot". Merk op dat dat zo is niet match "jas" als o | a
komt alleen overeen met "a" of "o", maar niet allebei tegelijk. Nu beginnen de dingen interessant te worden.
Van regel 3, een*
komt overeen met nul of meer instanties van "a". Het komt overeen met de lege tekenreeks of de tekenreeksen "a", "aa", "aaa", enzovoort. Laten we deze regel toepassen in combinatie met de andere twee regels.
Wat doet ho * t
wedstrijd? Het komt overeen met "ht" (met nul instances van "o"), "hot", "hoot", "hooot", enzovoort. Hoe zit het met b (o | a) *
? Het kan overeenkomen met "b" gevolgd door een willekeurig aantal instanties van "o" en "a" (inclusief geen van hen). "b", "boa", "baa", "bao", "baooaoaoaoo" zijn slechts enkele van het oneindige aantal tekenreeksen dat overeenkomt met deze reguliere expressie. Merk nogmaals op dat de haakjes worden gebruikt om het gedeelte van de reguliere expressie te groeperen waarnaar het *
wordt toegepast.
Laten we proberen reguliere expressies te vinden die overeenkomen met strings die we al in gedachten hebben. Hoe zouden we een reguliere expressie kunnen maken die schapenblaten herkent, wat ik beschouw als een aantal herhalingen van het basisgeluid "baa" ("baa", "baabaa", "baabaabaa", enz.)
Zoals je zei, (BAA) *
, dan heb je bijna gelijk. Maar merk op dat deze reguliere expressie ook overeenkomt met de lege string, wat we niet willen. Met andere woorden, we willen niet-blaten van schapen negeren. BAA (BAA) *
is de reguliere uitdrukking waarnaar we op zoek zijn. Evenzo kan een koe loeien moo (moo) *
. Hoe kunnen we het geluid van een van beide dieren herkennen? Eenvoudig. Gebruik afwisseling. baa (baa) * | moo (moo) *
Als je de bovenstaande ideeën hebt begrepen, gefeliciteerd, ben je goed op weg.
Bedenk dat we een dwaze beperking op onze snaren hebben gezet. Ze kunnen alleen worden samengesteld uit kleine letters van het alfabet. We zullen nu deze beperking negeren en alle strings bestaande uit ASCII-tekens beschouwen.
We moeten ons realiseren dat reguliere expressies een handig hulpmiddel moeten zijn, zodat ze zelf als snaren moeten worden weergegeven. Dus, in tegenstelling tot vroeger, kunnen karakters zoals. Niet meer worden gebruikt *
, |
, (
, )
, etc. zonder op de een of andere manier te signaleren of we ze gebruiken als "speciale" karakters die afwisseling, groepering, enz. vertegenwoordigen of dat we ze behandelen als gewone karakters die letterlijk moeten worden geëvenaard.
De oplossing is om deze en andere "metatekens" te behandelen die een speciale betekenis kunnen hebben. Om te schakelen tussen het ene gebruik en het andere, moeten we eraan kunnen ontsnappen. Dit lijkt op het idee om "\ n" te gebruiken (ontsnappen aan de n) om een nieuwe regel in een string aan te geven. Het is iets ingewikkelder in die zin dat, afhankelijk van het contextkarakter dat gewoonlijk "meta" is, het zijn letterlijke zelf zonder echappement zou kunnen voorstellen. We zullen later voorbeelden hiervan zien.
Een ander ding dat we waarderen is bondigheid. Veel reguliere expressies die kunnen worden uitgedrukt met alleen de notatie van de vorige sectie zouden saai zijn. Stel dat u alleen alle tekenreeksen wilt zoeken die bestaan uit een kleine letter gevolgd door een cijfer (bijvoorbeeld tekenreeksen als "a0", "b9", "z3", enzovoort). Met behulp van de notatie die we eerder hebben besproken, zou dit resulteren in de volgende reguliere expressie:
(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)
Alleen al het typen van dat monster heeft me uitgeroeid.
Maakt niet [Abcdefghijklmnopqrstuvwxyz] [0123456789]
ziet eruit als een betere representatie? Let op de metatekens [
en ]
dat betekent een reeks tekens, waarvan elke een positieve match geeft. Als we bedenken dat de letters a tot z en de cijfers 0 tot 9 achtereenvolgens voorkomen in de ASCII-set, kunnen we de regex naar beneden laten koelen [Az] [0-9]
.
Binnen de grenzen van een tekenset, het streepje, -
, is een ander metateken dat een bereik aangeeft. Merk op dat u meerdere reeksen in hetzelfde paar vierkante haken kunt drukken. Bijvoorbeeld, [0-9a-zA-Z]
kan elk alfanumeriek teken matchen. De 9 en een (en z en EEN)tegen elkaar gedrukt kan er misschien grappig uitzien, maar vergeet niet dat reguliere expressies allemaal te maken hebben met beknoptheid en de betekenis duidelijk is.
Over beknoptheid gesproken, er zijn nog meer beknopte manieren om bepaalde klassen van verwante karakters te vertegenwoordigen, zoals we zo dadelijk zullen zien. Merk op dat de wisselbalk, |
, is nog steeds een geldige en bruikbare syntaxis zoals we zo dadelijk zullen zien.
Voordat we beginnen met oefenen, laten we een beetje meer syntaxis bekijken.
De periode, .
, komt overeen met een willekeurig teken, met uitzondering van regeleinden. Dit betekent dat c.t
kan overeenkomen met "cat", "crt", "c9t", "c% t", "c.t", "c t", enzovoort. Als we de periode als een gewoon teken zouden willen vergelijken, bijvoorbeeld om de tekenreeks "c.t" aan te passen, zouden we eraan kunnen ontsnappen (c \ .t
) of zet het in een eigen karakterklasse (c [.] t
).
Over het algemeen zijn deze ideeën van toepassing op andere metatekens, zoals [
, ]
, (
, )
, *
, en anderen die we nog niet zijn tegengekomen.
Haakjes ((
en )
) worden gebruikt om te groeperen zoals we eerder hebben gezien. We gaan het woord gebruiken blijk om ofwel een enkel teken of een expressie tussen haakjes te betekenen. De reden is dat veel regex-operators op beide kunnen worden toegepast.
Haakjes worden ook gebruikt om te definiëren groepen vastleggen, zodat je kunt uitvinden welk deel van je wedstrijd was gevangen genomen door een bepaalde capture-groep in de regex. Ik zal later meer vertellen over deze zeer nuttige functionaliteit.
EEN +
het volgen van een token is een of meer exemplaren van dat token. In ons voorbeeld over het blaten van schapen, BAA (BAA) *
zou beknopter kunnen worden weergegeven als (BAA)+
. Herhaal dat *
betekent nul of meer voorkomens. Let daar op (BAA)+
is verschillend van geblaat+
, omdat in de voormalige de +
wordt toegepast op de geblaat
token, terwijl het in het laatste alleen van toepassing is op de een
voor het. In het laatste komt het overeen met tekenreeksen als "baa", "baaa" en "baaaa".
EEN ?
het volgen van een token betekent nul of één instantie van dat token.
RegExr is een uitstekende online tool om te experimenteren met reguliere expressies. Wanneer u vertrouwd bent met het lezen en schrijven van reguliere expressies, is het veel eenvoudiger om de reguliere expressie-API van het Foundation-framework te gebruiken. Zelfs dan zal het gemakkelijker zijn om je reguliere expressie eerst in real-time op de website te testen.
Bezoek de website en focus op het hoofdgedeelte van de pagina. Dit is wat je ziet:
U voert een reguliere expressie in het vak bovenaan in en voert de tekst in waarnaar u zoekt.
De "/ g" aan het einde van het uitdrukkingvak maakt geen deel uit van de reguliere expressie op zich. Het is een vlag die het algemene zoekgedrag van de regex-engine beïnvloedt. Door "/ g" toe te voegen aan de reguliere expressie, zoekt de zoekmachine naar alle mogelijke overeenkomsten van de reguliere expressie in de tekst, wat het gedrag is dat we willen. De blauwe markering geeft een overeenkomst aan. Door met je muis over de reguliere expressie te zwaaien, kun je je op een handige manier herinneren aan de betekenis van de samenstellende delen.
Weet dat reguliere expressies verschillende smaken bevatten, afhankelijk van de taal of bibliotheek die u gebruikt. Dit betekent niet alleen dat de syntaxis een beetje anders kan zijn tussen de verschillende smaken, maar ook de mogelijkheden en functies. Swift maakt bijvoorbeeld gebruik van de patroonsyntaxis gespecificeerd door ICU. Ik weet niet zeker welke smaak wordt gebruikt in RegExr (die op JavaScript wordt uitgevoerd), maar in het kader van deze zelfstudie zijn ze redelijk vergelijkbaar, zo niet identiek.
Ik moedig je ook aan om de ruit aan de linkerkant te verkennen, die veel informatie op een beknopte manier bevat.
Om mogelijke verwarring te voorkomen, zou ik moeten vermelden dat, wanneer we praten over reguliere expressies, we twee dingen kunnen betekenen:
De standaardbetekenis waarmee regex-engines werken, is (1). Waar we tot nu toe over gesproken hebben is (2). Gelukkig is het gemakkelijk om betekenis (2) te implementeren door middel van metatekens die later zullen worden ingevoerd. Maak je daar nu geen zorgen over.
Laten we eenvoudig beginnen door ons voorbeeld over het blaten van schapen te testen. Type (BAA)+
in het uitdrukkingsvak en enkele voorbeelden om te testen op wedstrijden zoals hieronder getoond.
Ik hoop dat je begrijpt waarom de geslaagde wedstrijden echt gelukt zijn en waarom de anderen faalden. Zelfs in dit eenvoudige voorbeeld zijn er een paar interessante dingen om op te wijzen.
Bevat de string "baabaa" twee overeenkomsten of één? Met andere woorden, is elke individuele "baa" een match of is de hele "baabaa" een enkele match? Dit komt neer op het al dan niet zoeken naar een "hebzuchtige match". Een hebzuchtige match probeert zo veel mogelijk van een string te matchen.
Op dit moment komt de regex-engine gretig overeen, wat betekent dat "baabaa" een enkele match is. Er zijn manieren om luie overeenkomsten te doen, maar dat is een meer geavanceerd onderwerp en omdat we al onze borden vol hebben, zullen we dat in deze tutorial niet behandelen..
Het hulpprogramma RegExr laat een kleine maar waarneembare opening achter in de markering als twee aangrenzende delen van een tekenreeks elk afzonderlijk (maar niet samen) overeenkomen met de reguliere expressie. We zullen een voorbeeld van dit gedrag in een beetje zien.
"Baabaa" mislukt vanwege de hoofdletter "B". Stel dat u alleen de eerste 'B' als hoofdletter wilt gebruiken, wat zou de bijbehorende reguliere expressie dan zijn? Probeer het eerst zelf uit te zoeken.
Eén antwoord is (B | b) bis (BAA) *
. Het helpt als je het hardop voorleest. Een hoofdletter of kleine letters "b", gevolgd door "aa", gevolgd door nul of meer exemplaren van "baa". Dit is werkbaar, maar houd er rekening mee dat dit snel ongemakkelijk zou kunnen worden, vooral als we het hoofdlettergebruik helemaal zouden willen negeren. We zouden bijvoorbeeld voor elk geval alternatieven moeten opgeven, wat zou resulteren in iets logs zoals ([Bb] [Aa] [Aa])+
.
Gelukkig hebben reguliere expressiemotoren meestal een optie om case te negeren. Klik in het geval van RegExr op de knop met de tekst "flags" en vink het selectievakje "case negeren" aan. Merk op dat de letter "i" is toegevoegd aan de lijst met opties aan het einde van de reguliere expressie. Probeer enkele voorbeelden met gemengde hoofdletters, zoals "bAABaa".
Laten we proberen een reguliere expressie te ontwerpen die varianten van de naam "Katherine" kan vastleggen. Hoe zou u dit probleem benaderen? Ik zou zoveel variaties opschrijven, de gemeenschappelijke delen bekijken en vervolgens de variaties (met de nadruk op de alternatieve en optionele letters) in een rij proberen uit te drukken als een reeks. Vervolgens zou ik proberen de reguliere expressie te formuleren die al deze variaties assimileert.
Laten we het eens proberen met deze lijst met variaties: Katherine, Katharine, Catherine, Kathreen, Kathleen, Katryn en Catrin. Ik laat het aan jou over om er nog een paar op te schrijven als je wilt. Als ik naar deze variaties kijk, kan ik grofweg zeggen:
Met dit idee in gedachten kan ik de volgende reguliere expressie bedenken:
[Kc] ath [ae]? (R | l) (i | eo | y) ne?
Merk op dat de eerste regel "KatherineKatharine" twee overeenkomsten heeft zonder enige scheiding tussen beide. Als je het goed in de teksteditor van RegExr bekijkt, kun je de kleine pauze in de markering tussen de twee wedstrijden waarnemen, waar ik het eerder over had.
Merk op dat de bovenstaande reguliere expressie ook overeenkomt met namen die we niet hebben overwogen en die misschien niet eens bestaat, bijvoorbeeld "Cathalin". In de huidige context heeft dit helemaal geen negatief effect op ons. Maar in sommige toepassingen, zoals e-mailvalidatie, wil je specifieker zijn over de reeksen die overeenkomen en degenen die je afwijst. Dit draagt meestal bij aan de complexiteit van de reguliere expressie.
Voordat we verder gaan met Swift, wil ik graag nog een paar aspecten bespreken van de syntaxis van reguliere expressies.
Verschillende klassen verwante tekens hebben een beknopte weergave:
\ w
alfanumeriek teken, inclusief onderstrepingsteken, equivalent aan [A-zA-Z0-9_]
\ d
staat voor een cijfer, equivalent aan [0-9]
\ s
staat voor witruimte, dat wil zeggen spatie, tab of regeleindeDeze klassen hebben ook overeenkomstige negatieve klassen:
\ w
staat voor een niet-alfanumeriek, niet-onderstrepend karakter\ D
een niet-cijferig getal\ S
een niet-spatie karakterOnthoud de niet-geklasseerde klassen en onthoud vervolgens dat de corresponderende geactiveerde klasse overeenkomt met wat de niet-geklasseerde klasse niet overeenkomt. Merk op dat deze kunnen worden gecombineerd door, indien nodig, tussen vierkante haken op te nemen. Bijvoorbeeld, [\ S \ S]
vertegenwoordigt elk teken, inclusief regeleinden. Bedenk dat de periode .
komt overeen met elk teken behalve regeleinden.
^
en $
zijn ankers die respectievelijk het begin en het einde van een reeks voorstellen. Weet je nog dat ik schreef dat je misschien een hele reeks wilt matchen, in plaats van te zoeken naar substring-overeenkomsten? Dit is hoe je dat doet. ^ C [oau] t $
komt overeen met "kat", "kinderbedje" of "knippen", maar niet, laten we zeggen, "vangen" of "recuteren".
\ b
vertegenwoordigt een grens tussen woorden, bijvoorbeeld vanwege ruimte of interpunctie, en ook het begin of einde van de tekenreeks. Merk op dat het een beetje anders is omdat het overeenkomt met een positie in plaats van een expliciet teken. Het kan helpen om een woordgrens te beschouwen als een onzichtbare scheidingsteken die een woord van de vorige / volgende scheidt. Zoals je zou verwachten, \ B
staat voor "geen woordgrens". \ BCAT \ b
vindt overeenkomsten in "kat", "een kat", "hallo, kat", maar niet in "acat" of "vangst".
Het idee van ontkenning kan meer specifiek worden gemaakt met behulp van de ^
metateken in een tekenset. Dit is een heel ander gebruik van ^
van "begin van stringanker". Dit betekent dat, voor ontkenning, ^
moet bij het begin in een tekenset worden gebruikt. [^ A]
komt overeen met elk teken naast de letter "a" en [^ A-z]
komt overeen met elk teken behalve een kleine letter.
Kunt u vertegenwoordigen \ w
negatie- en tekenbereiken gebruiken? Het antwoord is [^ A-Za-z0-9_]
. Wat denk je [A ^]
wedstrijden? Het antwoord is een "a" of een "^" teken omdat het niet aan het begin van de tekenset voorkomt. Hier komt ^ ^ letterlijk overeen.
Als alternatief zouden we er expliciet aan kunnen ontsnappen: [\ ^ A]
. Hopelijk begin je wat intuïtie te ontwikkelen over hoe ontsnappen werkt.
We hebben gezien hoe *
(en +
) kan worden gebruikt om een token nul of meer (en één of meer) keer overeen te komen. Dit idee om meerdere keren een token te matchen, kan specifieker gemaakt worden met behulp van kwantoren in accolades. Bijvoorbeeld, 2, 4
betekent twee tot vier overeenkomsten van het voorgaande token. 2
betekent twee of meer overeenkomsten en 2
betekent precies twee overeenkomsten.
We zullen gedetailleerde voorbeelden bekijken die de meeste van deze elementen in de volgende tutorial gebruiken. Maar omwille van de praktijk, moedig ik u aan om uw eigen voorbeelden te verzinnen en de syntaxis uit te testen die we zojuist zagen met de RegExr-tool.
In deze zelfstudie hebben we ons voornamelijk geconcentreerd op de theorie en syntaxis van reguliere expressies. In de volgende tutorial voegen we Swift toe aan de mix. Zorg ervoor dat je, voordat je verder gaat, begrijpt wat we in deze tutorial hebben behandeld door met RegExr te spelen.