Itt vagy: Kezdőlap Ugorj fejest a Python 3-ba

Nehézségi szint: ♦♦♦♢♢

Reguláris kifejezések

Egyesek amikor találkoznak egy problémával, azt gondolják: „Tudom, reguláris kifejezéseket fogok használni.” Innentől két problémájuk van.
Jamie Zawinski

 

Ugorj fejest

Egy kisebb szövegdarab kinyerése egy nagy szövegblokkból mindig kihívást jelent. Pythonban a karakterláncok rendelkeznek a keresésre és cserére szolgáló metódusokkal: index(), find(), split(), count(), replace() stb. De ezek a metódusok a legegyszerűbb esetekre vannak korlátozva. Az index() metódus például egyetlen bedrótozott részkarakterláncot keres, és a keresés mindig megkülönbözteti a kis- és nagybetűket. A kis- és nagybetűket meg nem különböztető kereséshez az s karaktersorozatban, meg kell hívnod az s.lower() vagy s.upper() metódust, és meg kell győződnöd, hogy a keresőkifejezések ennek megfelelően kis- vagy nagybetűsek. A replace() és split() metódusok ugyanilyen korlátozásokkal rendelkeznek.

Ha a célod elérhető karakterlánc-metódusokkal, akkor használd azokat. Ezek gyorsak, egyszerűek és könnyen olvashatók, a gyors, egyszerű és könnyen olvasható kódról pedig rengeteget lehetne beszélni. De ha azt veszed észre, hogy rengeteg különböző karakterláncfüggvényt használsz if utasításokkal a speciális esetek kezelésére, vagy a split() és join() hívások láncolataival aprítod a karakterláncaid, akkor szükséges lehet a reguláris kifejezésekre váltás.

A reguláris kifejezések hatékony és (nagyrészt) szabványosított lehetőséget adnak bonyolult karaktermintákat tartalmazó szövegek keresésére, cseréjére és feldolgozására. Ugyanakkor a reguláris kifejezések szintaxisa szoros, és a normál kóddal ellentétben az eredmény olvashatóbb lehet, mint a karakterláncfüggvények hosszú láncolatát használó barkácsmegoldás. A reguláris kifejezéseken belül még megjegyzések is elhelyezhetők, így részletes dokumentációt mellékelhetsz.

Ha használtál reguláris kifejezéseket más nyelveken (mint a Perl, JavaScript vagy PHP), akkor a Python szintaxisa nagyon ismerős lesz. Olvasd el a re modul összefoglalását az elérhető függvények és azok paramétereinek áttekintéséhez.

Esettanulmány: lakcímek

Ezen példák sorozatát egy valós életbeli probléma inspirálta, amellyel napi munkám során találkoztam évekkel ezelőtt, amikor egy örökölt rendszerből származó lakcímeket kellett kitisztítanom és szabványosítanom, mielőtt egy újabb rendszerbe importálhattam volna azokat. (Látod, ezeket a dolgokat nem csak úgy kitalálom, ez tényleg hasznos.) Ez a példa bemutatja, hogyan közelítettem meg a problémát.

>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')                
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')                
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')  
'100 NORTH BROAD RD.'
>>> import re                               
>>> re.sub('ROAD$', 'RD.', s)               
'100 NORTH BROAD RD.'
  1. A célom a lakcím egységesítése, hogy a 'ROAD' mindig 'RD.'-ként legyen rövidítve. Első ránézésre ez elég egyszerűnek tűnt ahhoz, hogy a replace() karakterlánc-metódust használjam. Végül is minden adat már nagybetűs volt, így a kis- és nagybetűk eltérése nem okozhatott gondot. És a keresett kifejezés, a 'ROAD' konstans volt. És ebben a megtévesztően egyszerű példában az s.replace() tényleg működik.
  2. Az élet ellenben tele van ellenpéldákkal, és gyorsan fel is fedeztem egyet. Itt az a probléma, hogy a 'ROAD' kétszer jelenik meg a címben, egyszer a 'BROAD' utcanév részeként, egyszer pedig önálló szóként. A replace() metódus mindkét előfordulást látja, és vakon kicseréli mindkettőt, emiatt a címek szépen megsemmisülnek.
  3. A több 'ROAD' rész-karakterláncot tartalmazó címek problémájának megoldásához próbálkozhatsz valami ilyesmivel: a 'ROAD' szót csak a cím utolsó négy karakterében (s[-4:]) keresed és cseréled, a karakterlánc többi részét (s[:-4]) pedig békén hagyod. De az már most látszik, hogy ez így nyögvenyelős lesz. A minta például függ a lecserélt karakterlánc hosszától. (Ha a 'STREET' szót cserélnéd az 'ST.'-re, akkor a s[:-6] és s[-6:].replace(...) hívásokat kellene használnod.) Szeretnél hat hónap múlva visszatérni ehhez, és hibákat keresni benne? Azt tudom, hogy én biztosan nem.
  4. Ideje feljebb lépni a reguláris kifejezésekhez. A Pythonban a reguláris kifejezésekhez kapcsolódó minden funkcionalitás a re modulban van.
  5. Vess egy pillantást az első paraméterre: 'ROAD$'. Ez egy egyszerű reguláris kifejezés, amely csak akkor illeszkedik a'ROAD' szóra, ha az a karakterlánc végén van. A $ a „karakterlánc végét” jelenti. (Ennek párja a ^, amely a „karakterlánc elejét” jelzi.) A re.sub() függvény használatakor az s karakterláncban a 'ROAD$' reguláris kifejezést keresed, és helyettesíted az'RD.'-vel. Ez illeszkedik az s karakterlánc végén lévő ROAD előfordulásra, de nem illeszkedik a BROAD szóban lévő ROAD előfordulásra, mert ez az s közepén van.

A címtisztítós történetet folytatva, hamarosan észrevettem, hogy az előző példa, a cím végére illeszkedő 'ROAD' nem elég jó, mert nem minden címben van az utca megjelölve. Néhány cím egyszerűen csak az utca nevével végződik. Legtöbbször nem okozott gondot, de ha az utcanév a 'BROAD' volt, akkor a reguláris kifejezés illeszkedett volna a karakterlánc végén lévő, a 'BROAD' részét képező 'ROAD' szövegre, de én nem ezt akartam.

>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)   
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)   
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)   
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)  
'100 BROAD RD. APT 3'
  1. Amit igazán akartam, az olyan 'ROAD' előfordulások megtalálása, amelyek a karakterlánc végén és önálló szóként voltak jelen (nem pedig egy hosszabb szó részeiként). Ezt reguláris kifejezésként a \b használatával lehet kifejezni, amely azt jelenti, hogy „pontosan itt szóhatárnak kell lennie”. Pythonban ezt az a tény bonyolítja, hogy a karakterláncban lévő '\' karaktert escape-elni kell. Ezt néha backslash járványnak hívják, és az egyik oka annak, hogy a reguláris kifejezések egyszerűbbek Perlben, mint Pythonban. A hátulütő ott van, hogy a Perl a reguláris kifejezéseket más szintaktikai elemekkel keveri, így hiba esetén nehéz megállapítani, hogy az a szintaktikai elemek vagy a reguláris kifejezés hibája-e.
  2. A backslash járvány megkerülése érdekében használhatsz úgynevezett nyers karakterláncot, a karakterlánc elé egy r beszúrásával. Ezzel megmondod a Pythonnak, hogy a karakterláncban semmit nem kell escape-elni: a '\t' egy tab karakter, de az r'\t' valójában egy fordított törtvonal karakter (\), amelyet a t betű követ. Reguláris kifejezések használatakor mindig a nyers karakterláncok használatát javaslom; ellenkező esetben a dolgok túl gyorsan válnak túl zavarossá (és a reguláris kifejezések egyébként is elég zavarosak).
  3. *sóhaj* Sajnos hamarosan további eseteket találtam, amelyek legyűrték a kódot. Ebben az esetben a lakcím a 'ROAD' szót önállóan tartalmazta, de nem a végén, mert a cím az utca megadása után az ajtószámot is tartalmazta. Mivel a 'ROAD' nem a karakterlánc legvégén van, nem illeszkedik, így az egész re.sub() hívás nem cserél le semmit, és visszakapod az eredeti karakterláncot, de nem ezt akartad.
  4. Ennek a problémának a megoldásához eltávolítottam a $ karaktert, és egy újabb \b-t adtam hozzá. A reguláris kifejezés most ezt jelenti: „illeszkedj a 'ROAD' szóra, ha az önállóan fordul elő a karakterláncban bárhol”, legyen az a végén, az elején vagy valahol középen.

Esettanulmány: római számok

A római számokkal gyakran találkozhatsz, még ha nem is veszed mindig a fáradságot a visszafejtésükhöz. Előfordulnak régi filmek és TV-műsorok szerzői jog sorában („Copyright MCMXLVI” a „Copyright 1946” helyett), vagy a könyvtárak és egyetemek dedikációs falán („alapítva MDCCCLXXXVIII” az „alapítva 1888” helyett). Láthatsz ilyeneket vázlatokban és bibliográfiai hivatkozásokban. Ez a számábrázolási rendszer tényleg az ókori római birodalomban született (ezért ez a neve).

A római számok alatt hét karaktert értünk, amelyek különböző módokon ismétlődve és kombinálódva ábrázolják a számokat.

A következő néhány általános szabály vonatkozik a római számok előállítására:

Ezresek keresése

Mi kellene egy tetszőleges karakterlánc érvényes római szám mivoltának ellenőrzéséhez?Nézzük meg számjegyenként. Mivel a római számok mindig a legnagyobbtól a legkisebb felé íródnak, kezdjük a legmagasabbal: az ezres hellyel. Az 1000 és nagyobb számok esetén az ezreseket M karakterek sorozata képviseli.

>>> import re
>>> minta = '^M?M?M?$'        
>>> re.search(minta, 'M')     
<_sre.SRE_Match object at 0106FB58>
>>> re.search(minta, 'MM')    
<_sre.SRE_Match object at 0106C290>
>>> re.search(minta, 'MMM')   
<_sre.SRE_Match object at 0106AA38>
>>> re.search(minta, 'MMMM')  
>>> re.search(minta, '')      
<_sre.SRE_Match object at 0106F4A8>
  1. Ez a minta három részből áll. A ^ hatására az azt követő elemeket csak a karakterlánc elejére illeszti. Ha ez nem lenne megadva, akkor a minta mindenképp illeszkedne, bárhol is legyenek az M karakterek, de most nem ezt akarjuk. Arról kell meggyőződni, hogy ha vannak M karakterek, akkor azok a karakterlánc elején vannak. Az M? egyetlen elhagyható M karakterre illeszkedik. Mivel ez háromszor ismétlődik, ez a minta 0 - 3 egymást követő M karakterre illeszkedik. És a $ a karakterlánc végére illeszkedik. Az elején lévő ^ karakterrel együtt ez azt jelenti, hogy a mintának a teljes karakterláncra kell illeszkednie, az M karakterek előtt vagy után semmi sem állhat.
  2. A re modul lényege a search() függvény, amely egy reguláris kifejezést (pattern) és egy karakterláncot ('M') vár, amelyre megpróbálja a reguláris kifejezést illeszteni. Ha egyezést talál, akkor a search() egy objektumot ad vissza, amely különböző metódusokkal rendelkezik a találat leírására; ha nincs találat, akkor a search() a Python null értékét, a None objektumot adja vissza. Jelenleg csak azzal foglalkozunk, hogy a minta illeszkedik-e, amit a search() kimenetéből ránézésre meg lehet állapítani. Az 'M' illeszkedik a reguláris kifejezésre, mert az első elhagyható M illeszkedik, a második és harmadik elhagyható M karakter pedig figyelmen kívül marad.
  3. Az 'MM' illeszkedik, mert az első és második elhagyható M karakter illeszkedik, a harmadik M pedig figyelmen kívül marad.
  4. Az 'MMM' illeszkedik, mert mind a három M karakter illeszkedik.
  5. Az 'MMMM' nem illeszkedik. Mind a három M karakter illeszkedik, de a reguláris kifejezés ragaszkodik a karakterlánc végéhez, (a $ karakter miatt), ám a karakterláncnak még nincs vége (a negyedik M miatt). Így a search() a None objektumot adja vissza.
  6. Érdekes, hogy az üres karakterlánc is illeszkedik erre a reguláris kifejezésre, mert az összes M karakter elhagyható.

Százasok keresése

A százasok helye bonyolultabb az ezresekénél, mert több egymást kizáró módon fejezhető ki az értékétől függően.

Így négy lehetséges minta van:

Az utolsó két minta kombinálható:

Ez a példa bemutatja, hogyan ellenőrizhető a százas helyiérték római szám mivolta.

>>> import re
>>> minta = '^M?M?M?(CM|CD|D?C?C?C?)$'  
>>> re.search(minta, 'MCM')             
<_sre.SRE_Match object at 01070390>
>>> re.search(minta, 'MD')              
<_sre.SRE_Match object at 01073A50>
>>> re.search(minta, 'MMMCCC')          
<_sre.SRE_Match object at 010748A8>
>>> re.search(minta, 'MCMC')            
>>> re.search(minta, '')                
<_sre.SRE_Match object at 01071D98>
  1. Ez a minta ugyanúgy kezdődik, mint az előző: a karakterlánc elejét keresi (^), majd az ezres helyiértéket (M?M?M?). Ezután következik a zárójelben lévő új rész, amely három, egymást kölcsönösen kizáró minta függőleges vonalakkal elválasztott halmazát adja meg: CM, CD és D?C?C?C? (ez egy elhagyható D, amelyet 0 - 3 elhagyható C karakter követ). A reguláris kifejezések feldolgozása sorban (balról jobbra) ellenőrzi ezeket a mintákat, veszi az első illeszkedőt, és figyelmen kívül hagyja a többit.
  2. Az 'MCM' illeszkedik, mert az első M illeszkedik, a második és harmadik M karakterek figyelmen kívül maradnak, és a CM illeszkedik (így a CD és D?C?C?C? minták nem is lesznek figyelembe véve). Az MCM az 1900 római számokkal leírt változata.
  3. Az 'MD' illeszkedik, mert az első M illeszkedik, a második és harmadik M karakterek figyelmen kívül maradnak, és a D?C?C?C? minta illeszkedik a D-re (a három C karakter mindegyike elhagyható, és figyelmen kívül marad). Az MD az 1500 római számokkal leírt változata.
  4. Az 'MMMCCC' illeszkedik, mert mind a három M karakter illeszkedik, és a D?C?C?C? minta illeszkedik a CCC-re (a D elhagyható és figyelmen kívül marad). Az MMMCCC a 3300 római számokkal leírt változata.
  5. Az 'MCMC' nem illeszkedik. Az első M illeszkedik, a második és harmadik M karakterek figyelmen kívül maradnak, a CM illeszkedik, de ezután a $ nem illeszkedik, mert még nincs vége a karakterláncnak (még van egy illeszkedés nélküli C karakter). A C nem illeszkedik a D?C?C?C? minta részeként, mert a kölcsönösen kizáró CM minta már illeszkedett.
  6. Érdekes módon az üres karakterlánc továbbra is illeszkedik erre a mintára, mert az összes M karakter elhagyható és figyelmen kívül marad, és az üres karakterlánc illeszkedik a D?C?C?C? mintára, amelynek minden karaktere elhagyható és figyelmen kívül marad.

Huh! Látod, a reguláris kifejezések milyen gyorsan válnak nagyon bonyolulttá? És még csak a római számok ezres és százas helyiértékeit fedtük le. De ha ezt az egészet sikerült követned, akkor a tizesek és az egyesek könnyűek lesznek, mert pontosan ugyanezt a mintát követik. De nézzünk egy másik módszert a minta kifejezésére.

A {n,m} szintaxis használata

Az előző szakaszban egy olyan mintával foglalkoztunk, amelyben ugyanaz a karakter legfeljebb háromszor ismétlődhetett. Ezt reguláris kifejezésekkel máshogy is ki lehet fejezni, amelyet egyesek olvashatóbbnak ítélnek. Először nézzük meg az előző példában már használt metódust.

>>> import re
>>> minta = '^M?M?M?$'
>>> re.search(minta, 'M')     
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(minta, 'MM')    
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MMM')   
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(minta, 'MMMM')  
>>> 
  1. Ez a karakterlánc elejére illeszkedik, majd az első elhagyható M karakterre, de a második és harmadik M-re nem (de ez nem gond, ezek elhagyhatók), majd a karakterlánc végére.
  2. Ez a karakterlánc elejére illeszkedik, majd az első és második elhagyható M karakterre, de a harmadik M-re nem (de ez nem gond, ez elhagyható), majd a karakterlánc végére.
  3. Ez a karakterlánc elejére illeszkedik, majd mind a három elhagyható M karakterre, majd a karakterlánc végére.
  4. Ez a karakterlánc elejére illeszkedik, majd mind a három elhagyható M karakterre, de ezután nem illeszkedik a karakterlánc végére (mert még van egy nem illeszkedő M), így a minta nem illeszkedik és a None objektumot adja vissza.
>>> minta = '^M{0,3}$'        
>>> re.search(minta, 'M')     
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MM')    
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(minta, 'MMM')   
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(minta, 'MMMM')  
>>> 
  1. Ez a minta azt mondja: „Illeszkedj a karakterlánc elejére, majd 0 és 3 közt (ezeket is beleértve) tetszőleges számú M karakterre, majd a karakterlánc végére.” A 0 és 3 tetszőleges szám lehet, ha legalább egy és legfeljebb 3 M karaktert szeretnél illeszteni, akkor ezt mondhatnád:M{1,3}.
  2. Ez a karakterlánc elejére illeszkedik, majd a három lehetséges M karakterből egyre, majd a karakterlánc végére.
  3. Ez a karakterlánc elejére illeszkedik, majd a három lehetséges M karakterből kettőre, majd a karakterlánc végére.
  4. Ez a karakterlánc elejére illeszkedik, majd a három lehetséges M karakterből háromra, majd a karakterlánc végére.
  5. Ez a karakterlánc elejére illeszkedik, majd a három lehetséges M karakterből háromra, de ezután nem illeszkedik a karakterlánc végére. A reguláris kifejezés legfeljebb csak három M karaktert engedélyez a karakterlánc vége előtt, de itt négy van, így a minta nem illeszkedik, és a None objektumot adja vissza.

Tizesek és egyesek keresése

Bővítsük ki a római számos reguláris kifejezést a tizes és egyes helyiértékekkel. Ez a példa bemutatja a tizes helyiértékek ellenőrzését.

>>> minta = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(minta, 'MCMXL')     
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MCML')      
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MCMLX')     
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MCMLXXX')   
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MCMLXXXX')  
>>> 
  1. Ez a karakterlánc elejére illeszkedik, majd az első elhagyható M karakterre, majd a CM-re, majd az XL-re, végül a karakterlánc végére. Ne feledd, a (A|B|C) szintaxis azt jelenti: „illeszkedjen az A, B vagy C közül pontosan egyre”. Az XL illeszkedik, így az XC és L?X?X?X? lehetőségek figyelmen kívül maradnak, és az illesztés a karakterlánc végével folytatódik. Az MCMXL az 1940 római számokkal leírt változata.
  2. Ez a karakterlánc elejére illeszkedik, majd az első elhagyható M karakterre, majd a CM-re, majd az L?X?X?X?-re. Az L?X?X?X?-ből az L-re illeszkedik, és kihagyja mind a három elhagyható X karaktert. Ezután a karakterlánc végére lép. Az MCML az 1950 római számokkal leírt változata.
  3. Ez a karakterlánc elejére illeszkedik, majd az első elhagyható M karakterre, majd a CM-re, majd az elhagyható L-re és az első elhagyható X-re, kihagyja a második és harmadik elhagyható X-et, végül a karakterlánc végére is illeszkedik. Az MCMLX az 1960 római számokkal leírt változata.
  4. Ez a karakterlánc elejére illeszkedik, majd az első elhagyható M karakterre, majd a CM-re, majd az elhagyható L-re és mind a három elhagyható X-re, végül a karakterlánc végére is illeszkedik. Az MCMLXXX az 1980 római számokkal leírt változata.
  5. Ez a karakterlánc elejére illeszkedik, majd az első elhagyható M karakterre, majd a CM-re, majd az elhagyható L-re és mind a három elhagyható X-re, végül a karakterlánc végére nem illeszkedik, mert még hátra van egy nem illeszkedő X. Így végül a teljes minta nem fog illeszkedni, és a None objektumot adja vissza. Az MCMLXXXX nem érvényes római szám.

Az egyes helyiértékek kifejezése ugyanazt a mintát követi. Megkíméllek a részletektől, és csak a végeredményt mutatom.

>>> minta = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'

Hogy fog ez kinézni az alternatív {n,m} szintaxis használatával? Ez a példa bemutatja az új szintaxist.

>>> minta = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(minta, 'MDLV')              
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MMDCLXVI')          
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MMMDCCCLXXXVIII')   
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'I')                 
<_sre.SRE_Match object at 0x008EEB48>
  1. Ez a karakterlánc elejére illeszkedik, majd a három lehetséges M karakterből egyre, majd a D?C{0,3} kifejezésre. Ebből az elhagyható D karakterre illeszkedik, és a három lehetséges C karakterből nullára. Továbbhaladva illeszkedik a L?X{0,3} kifejezésre is, ebből az elhagyható L karakterre, és a három lehetséges X karakterből nullára. Ezután illeszkedik a V?I{0,3} kifejezésre, ebből az elhagyható V karakterre és a három lehetséges I karakterből nullára, végül a karakterlánc végére. Az MDLV az 1555 római számokkal leírt változata.
  2. Ez a karakterlánc elejére illeszkedik, majd a három lehetséges M karakterből kettőre, majd a D?C{0,3} kifejezésből a D-re és a lehetséges három C karakterből egyre, majd az L?X{0,3} kifejezésből az L-re és a lehetséges három X karakterből egyre, majd a V?I{0,3} kifejezésből a V-re és a három lehetséges I karakterből egyre, végül a karakterlánc végére is illeszkedik. Az MMDCLXVI a 2666 római számokkal leírt változata.
  3. Ez a karakterlánc elejére illeszkedik, majd a három M karakterből háromra, majd a D?C{0,3} kifejezésből a D-re és a három C karakterből háromra, majd az L?X{0,3} kifejezésből az L-re és a három X karakterből háromra, majd a V?I{0,3} kifejezésből a V-re és a három lehetséges I karakterből háromra, végül a karakterlánc végére is illeszkedik. Az MMMDCCCLXXXVIII a 3888 római számokkal leírt változata, és a kiterjesztett szintaxis nélkül leírható leghosszabb római szám.
  4. Nagyon figyelj. (Mintha egy bűvész lennék. „Nagyon figyeljetek gyerekek, mindjárt előhúzok egy nyuszit a kalapomból.”) Ez a karakterlánc elejére illeszkedik, majd a három M-ből nullára illeszkedik, majd a D?C{0,3} kifejezésből kihagyja az elhagyható D-t és a három C-ből nullára illeszkedik, majd az L?X{0,3} kifejezésből kihagyja az elhagyható L-et és a három X-ből nullára illeszkedik, majd a V?I{0,3} kifejezésből kihagyja az elhagyható V-t és a három I-ből egyre illeszkedik. Ezután a karakterlánc végére illeszkedik. Bámulatos, hol tart már a tudomány.

Ha ezt mind követted és elsőre megértetted, akkor jobb vagy mint én voltam. Most képzeld el, hogy valaki más reguláris kifejezéseit próbálod megérteni, egy nagy program kritikus függvényének a közepén. Vagy akár csak a saját reguláris kifejezéseid továbbfejlesztését képzeld el pár hónap múlva. Csináltam ilyet, és nem szép látvány.

Most pedig fedezzünk fel egy alternatív szintaxist, amely segíthet a kifejezések karbantarthatóan megírni.

Részletes reguláris kifejezések

Amikkel eddig foglalkoztunk, azokat „tömör” reguláris kifejezéseknek hívom. Amint láthattad, nehezen olvashatók és még ha rá is jössz, hogy mit csinálnak, nincs rá garancia, hogy hat hónap múlva is meg fogod érteni őket. Amire igazán szükség van, az a beágyazott dokumentáció.

A Python ezt lehetővé teszi az úgynevezett részletes reguláris kifejezésekkel. A részletes reguláris kifejezés két tekintetben tér el a tömör reguláris kifejezéstől:

Egy példán keresztül mindjárt világosabb lesz. Dolgozzuk át a korábban használt tömör reguláris kifejezést, és alakítsuk részletes reguláris kifejezéssé. Ez a példa bemutatja ennek módját.

>>> minta = '''
    ^                   # karakterlánc eleje
    M{0,3}              # ezresek - 0 és 3 közti M
    (CM|CD|D?C{0,3})    # százasok - 900 (CM), 400 (CD), 0-300 (0 és 3 közti C),
                        #            vagy 500-800 (D, amelyet 0 és 3 közti C követ)
    (XC|XL|L?X{0,3})    # tizesek - 90 (XC), 40 (XL), 0-30 (0 és 3 közti X),
                        #        vagy 50-80 (L, amelyet 0 és 3 közti X követ)
    (IX|IV|V?I{0,3})    # egyesek - 9 (IX), 4 (IV), 0-3 (0 és 3 közti I),
                        #        vagy 5-8 (V, amelyet 0 és 3 közti I követ)
    $                   # karakterlánc vége
    '''
>>> re.search(minta, 'M', re.VERBOSE)                 
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MCMLXXXIX', re.VERBOSE)         
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'MMMDCCCLXXXVIII', re.VERBOSE)   
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(minta, 'M')                             
  1. A részletes reguláris kifejezések használatával kapcsolatban a legfontosabb megjegyeznivaló, hogy át kell adnod egy extra argumentumot: ez a re.VERBOSE konstans. Ezt a re modul definiálja, és ez jelzi, hogy a mintát részletes reguláris kifejezésként kell kezelni. Amint láthatod, a minta meglehetősen sok üres helyet tartalmaz (mind figyelmen kívül marad), és jónéhány megjegyzés is van benne (mind figyelmen kívül marad). Az üres helyek és a megjegyzések figyelmen kívül hagyása után ez pontosan ugyanaz a reguláris kifejezés, mint amit az előző szakaszban láttál, de sokkal olvashatóbb.
  2. Ez a karakterlánc elejére illeszkedik, majd a lehetséges három M karakter egyikére, majd a CM-re, majd az L-re és a három lehetséges X-re, majd az IX-re, végül a karakterlánc végére is illeszkedik.
  3. Ez a karakterlánc elejére illeszkedik, majd a három lehetséges M mindegyikére, majd a D-re és a három lehetséges C mindegyikére, majd az L-re és a három lehetséges X mindegyikére, majd a V-re és a három lehetséges I mindegyikére, végül a karakterlánc végére is illeszkedik.
  4. Ez nem illeszkedik. Miért? Mert nem rendelkezik a re.VERBOSE jelzővel, így a re.search függvény a mintát tömör reguláris kifejezésként kezeli, amelyben sok üres hely és kettőskereszt jelek vannak. A Python nem tudja automatikusan felismerni, hogy egy reguláris kifejezés részletes-e vagy sem. A Python feltételezi, hogy minden reguláris kifejezés tömör, hacsak nem mondod azt kifejezetten, hogy részletes.

Esettanulmány: telefonszámok értelmezése

Eddig teljes minták illesztésére koncentráltál. A minta vagy illeszkedik, vagy nem. De a reguláris kifejezések ennél sokkal hatékonyabbak. Amikor egy reguláris kifejezés illeszkedik, akkor az egyes részeit kiemelheted. Meghatározhatod, hogy mi hol illeszkedett.

Ez a példa egy másik valós életbeli problémából jön, amellyel egy korábbi munkahelyemen találkoztam. A probléma: egy amerikai telefonszám értelmezése. Az ügyfél a szám szabad formátumú megadására kért lehetőséget (egyetlen mezőben), de ezután a körzetszámot, törzset, számot és egy elhagyható melléket külön-külön szerette volna tárolni a cég adatbázisában. Átkutattam a webet, és sok példát találtam olyan reguláris kifejezésekre, amelyek erre szolgáltak, de egyik sem volt elég megengedő.

Az alábbi telefonszámok elfogadására kellett képesnek lennie a kódnak:

Micsoda változatosság! Ezen esetek mindegyikében tudnom kellett, hogy a körzetszám a 800, a törzs az 555 a telefonszám többi része pedig az 1212 volt. A mellékkel rendelkezők esetén tudnom kellett, hogy a mellék az 1234 volt.

Haladjunk végig a telefonszám-értelmezés megoldásának kifejlesztésén. Ez a példa bemutatja az első lépést.

>>> telefonMinta = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')  
>>> telefonMinta.search('800-555-1212').groups()             
('800', '555', '1212')
>>> telefonMinta.search('800-555-1212-1234')                 
>>> telefonMinta.search('800-555-1212-1234').groups()        
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'groups'
  1. A reguláris kifejezéseket mindig balról jobbra olvassuk. Ez illeszkedik a karakterlánc elejére, majd a (\d{3})-ra. Mi az a \d{3}? Nos, a \d azt jelenti: „tetszőleges numerikus számjegy” (0-tól 9-ig). A {3} azt jelenti: „illessz pontosan három numerikus számjegyet”; ez a korábban látott {n,m} szintaxis egy változata. Az egész zárójelek közé tétele azt jelenti: „illessz pontosan három numerikus számjegyet, és jegyezd meg azokat később lekérdezhető csoportként”. Ezután egy literális kötőjelre illeszkedik. Ezután egy újabb, három számjegyből álló csoportra illeszkedik. Ezután egy újabb literális kötőjelre illeszkedik. Ezután egy újabb, pontosan négy számjegyből álló csoportra. Ezután a karakterlánc végére illeszkedik.
  2. A reguláris kifejezések értelmezője által megjegyzett csoportok eléréséhez használd a groups() metódust a search() metódus által visszaadott objektumon. Ez a reguláris kifejezésben megadott számú csoportot tartalmazó tuple-t ad vissza. Ebben az esetben három csoportot adtál meg, egy háromjegyűt, egy másik háromjegyűt és egy négyjegyűt.
  3. Ez a reguláris kifejezés nem a végső válasz, mert nem kezeli a végén mellékkel rendelkező telefonszámokat. Ehhez ki kell terjeszteni a reguláris kifejezést.
  4. Ez pedig az, amiért soha nem szabad „összeláncolnod” a search() és groups() metódusokat az éles kódban. Ha a search() metódus nem ad vissza találatot, akkor a None objektumot adja vissza, nem pedig egy reguláris kifejezés találati objektumot. A None.groups() hívása egy teljesen nyilvánvaló kivételt dob: a None nem rendelkezik groups() metódussal. (Természetesen ennél egy picivel kevésbé nyilvánvaló, amikor a kódod mélyéről kapod ezt a kivételt. Igen, ezt tapasztalatból mondom.)
>>> telefonMinta = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')  
>>> telefonMinta.search('800-555-1212-1234').groups()              
('800', '555', '1212', '1234')
>>> telefonMinta.search('800 555 1212 1234')                       
>>> 
>>> telefonMinta.search('800-555-1212')                            
>>> 
  1. Ez a reguláris kifejezés majdnem azonos az előzővel. Ahogy az előbb, a karakterlánc elejét illeszti, majd egy három számjegyből álló megjegyzett csoportot, majd egy kötőjelet, majd egy három számjegyből álló megjegyzett csoportot, majd egy kötőjelet, majd egy négy számjegyből álló megjegyzett csoportot. Ami új, az egy újabb kötőjel és egy legalább egy számjegyből álló megjegyzett csoport illesztése, végül pedig a karakterlánc végének illesztése.
  2. A groups() metódus most egy négy elemű tuple-t ad vissza, mivel a reguláris kifejezés most négy megjegyzendő csoportot definiál.
  3. Sajnos még ez a reguláris kifejezés sem a végső válasz, mert feltételezi, hogy a telefonszám különböző részeit kötőjelek választják el. Mi van, ha ezeket szóközök, vesszők vagy pontok választják el? Általánosabb megoldásra lesz szükség, hogy több különböző elválasztótípusra is illeszkedhessen a kifejezés.
  4. Hoppá! Ez a reguláris kifejezés nem csak hogy nem old meg mindent, de valójában még visszalépés is, mert most már nem tudja értelmezni a mellék nélküli telefonszámokat. Ez egyáltalán nem az, ami nekünk kell. Ha van mellék, akkor azt ismerni akarjuk, de ha nincs, a szám különböző részeit ugyanúgy ismerni akarjuk.

A következő példa bemutatja azt a reguláris kifejezést, amely képes a telefonszám különböző részei közti elválasztók kezelésére.

>>> telefonMinta = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')  
>>> telefonMinta.search('800 555 1212 1234').groups()  
('800', '555', '1212', '1234')
>>> telefonMinta.search('800-555-1212-1234').groups()  
('800', '555', '1212', '1234')
>>> telefonMinta.search('80055512121234')              
>>> 
>>> telefonMinta.search('800-555-1212')                
>>> 
  1. Kapaszkodj. Ez a karakterlánc elejére illeszkedik, majd három számjegyből álló csoportra, majd a \D+-re. Ez meg mi? Nos a \D bármely karakterre illeszkedik, kivéve a numerikus számjegyeket, a + jelentése pedig „legalább 1”. Így a \D+ legalább egy nem számjegy karakterre illeszkedik. Ezt kell használnod a kötőjel helyett a különböző elválasztók illesztésére.
  2. A \D+ használata a - helyett azt jelenti, hogy mostantól azok a telefonszámok is illeszkednek, amelyek részeit kötőjelek helyett szóközök választják el.
  3. Természetesen a kötőjelekkel elválasztott telefonszámok továbbra is illeszkednek.
  4. Sajnos ez még mindig nem a végső válasz, mert feltételezi, hogy van elválasztó. Mi van, ha a telefonszámot szóközök vagy kötőjelek nélkül adják meg?
  5. Hoppá! Ez még mindig nem javította a mellék megkövetelésének problémáját. Most két problémád van, de mindkettő megoldható ugyanazzal az eljárással.

A következő példa bemutatja azt a reguláris kifejezést, amely képes az elválasztók nélküli telefonszámok kezelésére.

>>> telefonMinta = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  
>>> telefonMinta.search('80055512121234').groups()      
('800', '555', '1212', '1234')
>>> telefonMinta.search('800.555.1212 x1234').groups()  
('800', '555', '1212', '1234')
>>> telefonMinta.search('800-555-1212').groups()        
('800', '555', '1212', '')
>>> telefonMinta.search('(800)5551212 x1234')           
>>> 
  1. A legutóbbi lépés óta egyetlen változás történt: az összes + lecserélése * karakterekre. A telefonszám részei közti \D+ helyett mostantól a \D* áll. Emlékszel, hogy a + jelentése „legalább 1”? Nos, a * jelentése „nulla vagy több”. Mostantól a reguláris kifejezés képes olyan telefonszámok értelmezésére is, amelyek egyáltalán nem tartalmaznak elválasztó karaktereket.
  2. Idesüss, ez tényleg működik. Miért? A karakterlánc elejét illesztetted, majd egy három számjegyből álló megjegyzett csoportot (800), majd nulla nem számjegy karaktert, majd egy három számjegyből álló megjegyzett csoportot (555), majd nulla nem számjegy karaktert, majd egy négy számjegyből álló megjegyzett csoportot (1212), majd nulla nem számjegy karaktert, majd egy tetszőleges számú számjegyből álló megjegyzett csoportot (1234), majd a karakterlánc végét.
  3. Most más változatok is működnek: pontok a kötőjelek helyett, szóköz és x a mellék előtt.
  4. Végre megoldottuk a másik régi problémát: a mellékek újra elhagyhatók. Ha nem található mellék, akkor a groups() metódus továbbra is egy négy elemű tuple-t ad vissza, de a negyedik elem egy üres karakterlánc.
  5. Utálok a rossz hírek hordozója lenni, de még nem vagy kész. Mi most a probléma? A körzetszám előtt egy extra karakter van, de a reguláris kifejezés feltételezi, hogy a körzetszám az első dolog a karakterlánc elején. Semmi gond, használhatod ugyanazt a „nulla vagy több nem numerikus karakter” eljárást a körzetszám előtti bevezető karakterek kihagyására.

A következő példa bemutatja a telefonszámok bevezető karaktereinek kezelésének módját.

>>> telefonMinta = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  
>>> telefonMinta.search('(800)5551212 ext. 1234').groups()                  
('800', '555', '1212', '1234')
>>> telefonMinta.search('800-555-1212').groups()                            
('800', '555', '1212', '')
>>> telefonMinta.search('work 1-(800) 555.1212 #1234')                      
>>> 
  1. Ez ugyanaz, mint az előző példában, kivéve hogy most a \D* kifejezést, a nulla vagy több nem numerikus karaktert illeszted az első megjegyzett csoport (a körzetszám) előtt. Vedd észre, hogy ezeket a nem numerikus karaktereket nem jegyezteted meg (nincsenek zárójelek közt). Ha a kifejezés talál ilyeneket, akkor kihagyja őket, és a körzetszámot csak akkor kezdi el megjegyezni, amikor megtalálja.
  2. A telefonszám sikeresen értelmezhető, még a körzetszám előtti nyitó zárójellel is. (A körzetszám utáni záró zárójel nem numerikus elválasztónak számít, és mint ilyet, az első megjegyzett csoport utáni \D* már kezeli.)
  3. Csak egy épségellenőrzés, győződjünk meg róla, hogy a korábban működő dolgok még mindig működnek. Mivel a kezdő karakterek teljesen elhagyhatók, ez illeszkedik a karakterlánc elejére, majd nulla nem numerikus karakterre, majd egy három számjegyből álló megjegyzett csoportra (800), majd egy nem számjegy karakterre (a kötőjelre), majd egy három számjegyből álló megjegyzett csoportra (555), majd egy nem számjegy karakterre (a kötőjelre), majd egy négy számjegyből álló megjegyzett csoportra (1212), majd nulla nem számjegy karakterre, majd egy nulla számjegyből álló megjegyzett csoportra, majd a karakterlánc végére.
  4. Itt jön az, amikor a reguláris kifejezések hatására ki akarom kaparni a szemem egy tompa tárggyal. Miért nem illeszkedik ez a telefonszám? Mert a körzetszám előtt van egy 1-es, de feltételeztük, hogy a körzetszám előtti összes kezdő karakter nem számjegy (\D*). Aargh.

Tegyünk egy lépést hátra. Eddig a reguláris kifejezések mind a karakterlánc elejétől illeszkedtek. De most már látod, hogy a karakterlánc elején meghatározhatatlan mennyiségű, figyelmen kívül hagyandó szöveg állhat. Ahelyett, hogy ezt mindet megpróbálnád illeszteni, hogy aztán átléphesd, próbálkozzunk egy másik megközelítéssel: egyáltalán ne illesszük a karakterlánc elejét. A következő példa ezt a megközelítést mutatja be.

>>> telefonMinta = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')  
>>> telefonMinta.search('work 1-(800) 555.1212 #1234').groups()         
('800', '555', '1212', '1234')
>>> telefonMinta.search('800-555-1212').groups()                        
('800', '555', '1212', '')
>>> telefonMinta.search('80055512121234').groups()                      
('800', '555', '1212', '1234')
  1. Figyeld meg a ^ hiányát ebben a reguláris kifejezésben. Már nem illeszted a karakterlánc elejét. Semmi sem mondja, hogy a reguláris kifejezésednek a teljes bemenetre illeszkednie kell. A reguláris kifejezéseket kezelő alrendszer elvégzi a munka nehezét, meghatározza, hogy a beviteli karakterlánc hol kezd el illeszkedni, és onnan halad tovább.
  2. Most már sikeresen értelmezhetsz egy bevezető karaktereket és bevezető számjegyet, valamint a telefonszám egyes részei körül tetszőleges számú tetszőleges elválasztót tartalmazó telefonszámot.
  3. Épségellenőrzés. Ez továbbra is működik.
  4. Még ez is működik.

Látod, hogy a reguláris kifejezések milyen gyorsan tudnak kikerülni az ellenőrzés alól? Vess egy gyors pillantást az előző iterációk bármelyikére. Meg tudod mondani a különbséget az egyik és a következő között?

Amíg még érted a végső választ (és ez a végső válasz, ha találtál egy olyan esetet, amit nem kezel, nem akarok róla tudni), írjuk ki részletes reguláris kifejezésként, mielőtt elfelejted a meghozott döntések okait.

>>> telefonMinta = re.compile(r'''
                # ne illessze a karakterlánc elejére, a szám bárhol elkezdődhet
    (\d{3})     # a körzetszám 3 számjegy (például: '800')
    \D*         # elhagyható elválasztó, ez tetszőleges számú nem számjegy lehet
    (\d{3})     # a törzs 3 számjegy (például: '555')
    \D*         # elhagyható elválasztó
    (\d{4})     # a szám többi része 4 számjegy (például: '1212')
    \D*         # elhagyható elválasztó
    (\d*)       # a mellék elhagyható és tetszőleges számú számjegy lehet
    $           # a karakterlánc vége
    ''', re.VERBOSE)
>>> telefonMinta.search('work 1-(800) 555.1212 #1234').groups()  
('800', '555', '1212', '1234')
>>> telefonMinta.search('800-555-1212')                          
('800', '555', '1212', '')
  1. Attól eltekintve, hogy több sort foglal, ez pontosan ugyanaz a reguláris kifejezés, mint ami az utolsó lépésben volt, így nem meglepetés, hogy képes értelmezni ugyanazokat a bemeneteket.
  2. Végső épségellenőrzés. Igen, ez még mindig működik. Kész vagyunk.

Összegzés

Ez csak a reguláris kifejezések képességeit illusztráló jéghegy legapróbb csúcsa. Más szavakkal, noha mostanra már teljesen elborítottak a reguláris kifejezések, higgy nekem, még semmit sem láttál.

Mostanra ismerned kell a következő kifejezéseket:

A reguláris kifejezések nagyon hatékonyak, de nem jelentenek helyes megoldást minden problémára. Eleget kell tanulnod róluk, hogy tudd, mikor megfelelők, mikor oldják meg a problémáid és mikor okoznak több problémát, mint ahányat megoldanak.

© 2001–11 Mark Pilgrim