From 236001ec7563804f87a40c924681461bc8b2d764 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20Mo=C5=BEina?= Ena najbolj uporabnih stvari v skriptnih jezikih so "regularni izrazi". Če jih boste znali, boste glavni frajerji in boste lahko delali odlične reči. Po drugi strani pa niso prav nič zapleteni, čisto enostavno se jih je naučiti ... in, no, priznam, kar zoprno sestavljati. Ko sestavljamo kaj bolj zapletenega, se tudi stari mački nekoliko zafrkavamo, preden jih spravimo v delujoče stanje. Če bi hoteli teoretizirati, bi rekli, da opisujejo jezik, pri čemer z "jezikom" mislimo nekakšno množico nizov. So tudi v tesnem sorodu s končnimi avtomati, ki vam morda niso več neznani, če kaj sledite Uvodu v računalništvo.) A pustimo teorijo in uvode. Naučili se bomo pisati nize, s katerimi opišemo nekakšne vzorce. Recimo, da imamo na voljo naslednje oznake, ki jih lahko tlačimo v nize:
+Regularni izrazi
+
+Regularni izraz
+
+
+
+Poglejmo nekaj preprostih primerov vzorcev
+.
ima namreč poseben pomen (glej zgoraj), zato takrat, kadar v resnici želimo piko, napišemo \.
. Enako velja za zvezdico, vprašaj, plus... in tudi vzvratno poševnico.^
, pa to pomeni katerikoli znak, ki ni na tem seznamu. Znotraj oglatih oklepajev je pika samo pika in celo vzvratna poševnica samo vzvratna poševnica.
+
+Zdaj pa še par posebnih znakov.
+
+
+Spet si oglejmo nekaj primerov.
+
+
+Pa še par bolj zapletenih.
+l
lahko pojavi ali pa tudi ne. To je torej lahko karkoli od tega, kar smo našteli zgoraj, poleg tega pa tudi "Go!" ali "Gooooooooooo!"
+
+Pri plusu, zvezdici in vprašaju nas včasih zmoti, da požrejo preveč. Če hočemo, da požrejo čim manj, jim dodamo vprašaj.
+
+
+Vzemimo stavek "Ta nas Janez je en dolgocasnez, prav res". Če poskusimo v tem stavku poiskati podniz, ki ustreza vzorcu "J.*nez", bomo izvedeli, da "Janez je en dolgocasnez", kar ni ne lepo ne prav in nikakor ni tisto, kar smo hoteli. Težava je v tem, da je zvezdica požrla "nez je en dolgocas"; izraz namreč pravi, da hočemo "Ja", potem (karseda veliko) drugih črk in potem "nez". Če pa poiščemo "J.*?nez", bomo dobili samo "Janez", saj bo ".*" požrlo le toliko znakov, kolikor jih potrebujemo, da je iskanje uspešno, torej, v tem primeru, le črko "a".
Naučili smo se opisovati vzorce. Kaj pa lahko s temi opisi počnemo?
+ +Regularni izrazi, ki smo jih spoznali, so bolj ali manj enaki v vseh jezikih. Jeziki, ki imajo veliko opraviti z iskanjem po besedilih, na primer php in javascript, imajo regularne izraze tesno vdelane v jezik - tako kot ima Python vdelana števila in nize (za nize nimamo "posebnega modula", ki bi ga morali uvoziti, preden lahko delamo z njimi), imajo ti jeziki vdelane tudi regularne izraze. Ne da bi potrebovali poseben modul, lahko preverimo, ali ima določen niz določeno obliko, podano z regularnim izrazom. Python je splošnejši jezik, zato regularnih izrazov ne sili v ospredju, temveč so lepo v posebnem modulu. Imenuje se re
, kar je okrajšava za regular expression.
Za primer uporabimo rdečo nit predavanj izpred nekaj let: v besedilu iščemo imena potencialnih teroristov, pri čemer so nam sumljive kar vse besede, ki se začnejo z veliko črko. Besedilo nekega prestreženega elektronskega sporočila je bilo takšno: +
msg = """Dragi Ahmed, + +kako si kaj? Upam, da so otroci ze zdravi. + +Mustafa, Osama in jaz smo se sli danes malo razgledat in +kaze kar dobro. Abdulah pa ni mogel zraven, je sel v Pesavar +prodat se tri kamele. Osama sicer pravi, da se mu to pred +zimo ne splaca, ampak saj ves, kaksen je Abdulah. Harun in +on, nic jima ne dopoves, se Osama ne. Jibril me klice, +moram iti; oglasi se kaj na Skype! + +tvoj Husein + +"""+ +
Vzorec, ki bo predstavljal ime - nekaj, kar se začne z veliko črko in nadaljuje s vsaj eno črko, je [A-Z]\w+
.
Najpreprostejša za uporabo (a ne tudi najbolj uporabna) funkcija, na kateri lahko poženemo ta izraz, je findall
, ki, kot bi bili ob imenu nemara presenečeni (posebej, če ne bi znali angleško) poišče vse (neprekrivajoče se) podnize, ki jim ustreza izraz. Če imamo sporočilo shranjeno v nizu msg
, bomo seznam imen dobili z
+
>>> re.findall(r"[A-Z]\w+", msg) +['Dragi', 'Ahmed', 'Upam', 'Mustafa', 'Osama', 'Abdulah', 'Pesavar', 'Osama', 'Abdulah', 'Harun', 'Osama', 'Jibril', 'Skype', 'Husein', 'PS', 'Python']+ +
Opazka: pred narekovaj sem dodal r
. Iz davnega začetka semestra se morda spomnite, kakšne posledice ima to: vse vzvratne poševnice (po domače: backslashi) po tem pomenijo le vzvratne poševnice, ne ubežnih zaporedij (torej, \n
pomeni vzvratno poševnico in n
, ne pa prehoda v novo vrsto). V regularnih izrazih se bo vedno trlo vzvrtanih poševnic, medtem ko bodo nove vrste in tabulatorji strašno redki, zato bomo skoraj vedno uporabljali nize z r
-jem pred narekovaji.
Metoda findall
je najpreprostejša, ker vrne kar (pod)nize, ki ustrezajo vzorcu. Druge funkcije vračajo objekt, iz katerega lahko razberemo še kaj več. Vzemimo search
, ki poišče prvo pojavitev vzorca.
>>> mo_ime = re.search(r"[A-Z]\w+", msg) +>>> mo_ime +<_sre.SRE_Match object at 0x045812F8>+ +
mo_ime
ni niz, tako kot prej, temveč objekt, ki vsebuje podatke o podnizu, ki se ujema z izrazom. (Ker gre za "Match object
", zato sem mu pripel mo_
. To spet spominja na madžarsko notacijo, ki sem jo omenil pri Qtju in tudi tu izvira iz (mojih) izkušenj: če imaš spremenljivko, ki predstavlja objekt, ki se nanaša na najdeno ime, boš imel najbrž tudi spremenljivko, ki bo vsebovala niz z najdenim imenom in imeli bosta tendenco, da se bosta enaki. Če prvemu vedno pripnemo mo_
, je problem rešen.
>>> mo_ime.group() +'Dragi' +>>> mo_ime.start() +0 +>>> mo_ime.end() +5 +>>> mo_ime.span() +(0, 5)+ +
Tule start
pove indeks črke v nizu, kjer se ujemanje začne in end
indeks črke za ujetim podnizom; span
pove oboje hkrati. Najdeni niz se torej nahaja v msg[0:5]
. Razlog, da se metoda, ki vrne celotni podniz, imenuje group()
, pa bo postal jasen malo kasneje, ko se bomo pogovarjali o skupinah.
Če želimo poiskati vse pojavitve vzorca in jih obdelati v zanki, bomo uporabili finditer
, kot recimo tule:
+
for s in re.finditer(r"[A-Z]\w+", msg): + print(s.group())+ +
Metoda sub
je podobna metodi replace
, ki smo jo spoznali pri nizih: z njo lahko zamenjamo vse pojavitve danega vzorca s čim drugim.
>>> print(re.sub(r"[A-Z]\w+", "XXX", msg)) +XXX XXX, + +kako si kaj? XXX, da so otroci ze zdravi. + +XXX, XXX in jaz smo se sli danes malo razgledat in +kaze kar dobro. XXX pa ni mogel zraven, je sel v XXX +prodat se tri kamele. XXX sicer pravi, da se mu to pred +zimo ne splaca, ampak saj ves, kaksen je XXX. XXX in +on, nic jima ne dopoves, se XXX ne. XXX me klice, +moram iti; oglasi se kaj na XXX! + +tvoj XXX ++ +
V zvezi s sub
lahko opazimo, da so argumenti funkcije obrnjeni nekoliko drugače kot pri replace
: replace
je metoda razreda str
, torej je niz, katerega podnize želimo spreminjati že implicitno podan (kot self
), povedati je potrebno le, kakšen podniz zamenjamo s katerim. Za primer: na nekaterih operacijskih sistemih so presledki v imenih datotek precej tečna reč. Kako bi se jih znebili? Če bi poznali le nize, bi rekli
+
+
fname = fname.replace(" ", "_")+ +Metodi
sub
pa kot argument povemo vzorec, niz, po katerem naj išče in s čim naj zamenjuje. Če bi želeli gornjo zamenjavo opraviti z regularnimi izrazi, bi napisali:
+fname = re.sub(" ", "_", fname)+ + +
Regularni izrazi so za tako preprosto zamenjavo prevelik kanon. sub
namreč zmore še veliko več; namesto niza, s katerim naj zamenja ujemajoči se podniz, ji lahko podamo kar funkcijo, ki naj jo pokliče ob vsakem ujemanju: funkcija bo kot argument dobila Match object
, kot rezultat mora vrniti niz, ki naj zamenja ujemajoči se podniz. Podniz lahko zamenjamo z, recimo, enakim podnizom, vendar zapisanim z velikimi črkami.
>>> def velike_crke(mo): +... return mo.group().upper() +... +>>> print re.sub(r"[A-Z]\w+", velike_crke, msg) +DRAGI AHMED, + +kako si kaj? UPAM, da so otroci ze zdravi. + +MUSTAFA, OSAMA in jaz smo se sli danes malo razgledat in +kaze kar dobro. ABDULAH pa ni mogel zraven, je sel v PESAVAR +prodat se tri kamele. OSAMA sicer pravi, da se mu to pred +zimo ne splaca, ampak saj ves, kaksen je ABDULAH. HARUN in +on, nic jima ne dopoves, se OSAMA ne. JIBRIL me klice, +moram iti; oglasi se kaj na SKYPE! + +tvoj HUSEIN + +> + +
Da ne definiramo brez potrebe takšnih kratkih funkcij, si lahko pomagamo z lambda-funkcijami. O njih se pri Programiranju 1 nismo pogovarjali; primeren trenutek je bil kvečjemu prejšnji teden, saj je njihovo naravno mesto funkcijsko programiranje. Tule jih bomo samo uporabili, v izziv radovednim: +
print(re_ime.sub(lambda mo: mo.group().upper(), msg))+Za primer obrnimo vsa imena po arabsko, z desne na levo: + +
>>> print(re.sub(r"[A-Z]\w+", lambda mo: "".join(reversed(mo.group())), msg)) +... +igarD demhA, + +kako si kaj? mapU, da so otroci ze zdravi. + +afatsuM, amasO in jaz smo se sli danes malo razgledat in +kaze kar dobro. haludbA pa ni mogel zraven, je sel v ravaseP +prodat se tri kamele. amasO sicer pravi, da se mu to pred +zimo ne splaca, ampak saj ves, kaksen je haludbA. nuraH in +on, nic jima ne dopoves, se amasO ne. lirbiJ me klice, +moram iti; oglasi se kaj na epykS! + +tvoj niesuH ++ +
Še ena (in zadnja, za danes) zanimiva funkcija, ki jo nudijo regularni izrazi, je split
. Tej smo doslej podajali točen podniz, po katerem naj deli. Tako bo tudi poslej, modul za regularne izraze pa ima še eno, drugo funkcijo split
, ki ji povemo vzorec, po katerem naj deli. Lahko, recimo, rečemo, naj sporočilo razdeli ob vsakem imenu (kar ni posebej uporaben zgled, je pa preprost).
+>>> re.split(r"[A-Z]\w+", msg)+ +
+['', ' ', ',\n\nkako si kaj? ', ', da so otroci ze zdravi.\n\n', ', ', +' in jaz smo se sli danes malo razgledat in\nkaze kar dobro. ', +' pa ni mogel zraven, je sel v ', '\nprodat se tri kamele. ', +' sicer pravi, da se mu to pred\nzimo ne splaca, ampak saj ves, kaksen je ', +'. ', ' in\non, nic jima ne dopoves, se ', ' ne. ', +' me klice,\nmoram iti; oglasi se kaj na ', '!\n\ntvoj ', '\n\n'] +
V zvezi z Match objecti
so nam ostale še skupine. Kaj so in kaj počnemo z njimi, pa bomo videli na primerih.
V davnih časih smo se pri predmetih, kot je Uvod v računalništvo, učili Moorevoih in Mealyjevih avtomatov. Danes so menda iz mode, zato bo moral ta, ki ga zanima, kako delujejo regularni izrazi, najbrž počakati do Prevajalnikov in navideznih strojev, ki si jih lahko izbere v tretjem letniku. Za zdaj vam lahko povem le, da se izraz, preden lahko računalnik v resnici naredi karkoli z njim, prevede v nekaj, čemur se reče končni avtomat, ta pa je neke vrste usmerjen graf. (Tako da boste vsaj vedeli, čemu služi to, kar počnete pri Diskretnih strukturah.)
+ +Za razumevanje tega, kar je pomembno tu, je bistveno le: regularni izraz se mora v nekaj prevesti. Če napišete program, ki vsebuje nekaj takšnega: +
for i in range(10000): + re.findall(r"[A-Z]\w+", msg[i]) + # in potem še kaj naprej+bo moral nesrečni računalnik deset tisočkrat prevesti regularni izraz
[A-Z]\w+
v tisto, v kar ga pač prevaja. Pri tako kratkem in preprostem izrazu še gre, pri čem bolj zapletenem pa bi program tekel bistveno hitreje, če bi ga lahko prevedli enkrat za vselej. In res ga lahko.
+
+re_ime = re.compile(r"[A-Z]\w+") +for i in range(10000): + re_ime.findall(msg[i]) + # in potem še kaj naprej+Spremenljivka
re_ime
(spet madžarska notacija!) zdaj predstavlja prevedeni izraz. Objekt re_ime
ima vse funkcije, ki smo jih poznali prej, le da gre zdaj pač za metode objekta in ne funkcije modula: namesto re.findall
, re.finditer
, re.sub
, re.split
, zdaj pišemo re_ime.findall
, re_ime.finditer
, re_ime.sub
, re_ime.split
. Poleg tega pa še izpustimo prvi argument, vzorec, saj je izraz že pripravljen.
+
+Sam vedno eksplicitno pokličem prevajanje izraza, preden ga uporabim; tako sem se navadil in škoditi ne more. Tule pa bomo počeli, kakor bo naneslo.
+ +Rešimo, za začetek, kar staro nalogo, ki je zahtevala, da izpišemo, kolikokrat se v sporočilu pojavi posamezno ime.
+ +import re + +imena = {} +for ime in re.findall(r"[A-Z]\w+", msg): + imena.setdefault(ime, 0) + imena[ime] += 1 + +for ime, stevec in imena.items(): + print("{} {}".format(ime, stevec))+ +
Gre pa, jasno, tudi hitreje: modul collections
ima razred Counter
, ki prešteje število ponovitev v poslanem seznamu, slovarju, množici... Takole:
+
+
>>> collections.Counter(re.findall(r"[A-Z]\w+", msg)) +Counter({'Osama': 3, 'Abdulah': 2, 'PS': 1, 'Ahmed': 1, 'Mustafa': 1, 'Python': 1, +'Pesavar': 1, 'Upam': 1, 'Dragi': 1, 'Harun': 1, 'Husein': 1, 'Skype': 1, +'Jibril': 1})+ +
To naš program precej poenostavi, saj nas odreši glavnega opravila, štetja.
+ + +import re, collections +for ime, stevec in collections.Counter(re.findall(r"[A-Z]\w+", msg)).items(): + print("{} {}".format(ime, stevec))+ + +
Tisti, ki vsaj malo poznate HTML, veste, da so slike na spletnih straneh praviloma postavljene tako, da HTML vsebuje značko IMG, ki videti nekako tako: +
img
, a poleg src
lahko naletimo še na kaj drugega. Lepo vzgojen HTML ima poleg src
vsaj še alt
, slabo vzgojen pa tudi onclick
ali kaj hujšega. Poleg tega so lahko kjerkoli, razen sredi besed, tudi presledki.
+
+Kar iščemo, lahko z besedami opišemo kot oklepaj (namreč tale: <
), ki mu lahko sledi kaj praznega prostora, nato pa img
. Potem sme biti čisto karkoli razen zaklepaja, >
in nekoč mora priti na vrsto src
, pred njim pa mora biti kak znak, ki ni črka (če bi, recimo, pisalo mesrc
, to ni OK, če je pred src
presledek, tabulator, nova vrsta ali, zaradi mene tudi narekovaj ali vejica (čeprav to ni povsem po predpisih) pa je OK. Besedi src
mora slediti enačaj, pred in za njim sme biti bel prostor. Nato pa narekovaj, za katerim pride tisto, kar nas v resnici zanima: URL slike. Ta je sestavljen iz poljubnih znakov razen narekovajev... in na koncu pride še narekovaj.
Z regularnim izrazom taisto povemo tako: +
<\s*img[^>]*\Wsrc\s*=\s*"([^"]*)"+Se pravi +
< | oklepaj, < |
\s* | poljubno praznega prostora (lahko tudi nič) |
img | beseda img |
[^>]* | poljubno dolga poljubna šara, razen zaključnega oklepaja |
\W | karkoli, kar ni črka |
src | beseda src |
\s* | poljubno praznega prostora (lahko tudi nič) |
= | enačaj |
\s* | poljubno praznega prostora (lahko tudi nič) |
" | narekovaj |
([^"]*) | poljubne reči razen narekovaja; čemu oklepaji, bomo še videli |
" | narekovaj |
src
ja in URLja? To nam jamči sam regularni izraz! Če bi [^>]*
požrl src
, potem se regularni izraz ne bi mogel ujemati s sicer pravilno vstavljeno sliko. Regularni izraz (oziroma knjižnica, ki ga poganja) pa vedno naredi vse, da bi se izraz ujemal, torej tudi ukroti požrešne dele.
+
+Podobno, a ravno obratno vprašanje je, kdo zagotavlja, da bo ([^"]*)
, to so, poljubne reči razen narekovaja, res požrle ves URL in ne odnehale, tako, za hec, po treh znakih? Tako kot prej, nam tudi to zagotavlja preostanek izraza. V opisu regularnega izraza delu ([^"]*)
namreč sledi "
; če ([^"]*)
ne požre vsega do narekovaja, se tisto, kar sledi, ne bo ujemalo z narekovajem, kot zahteva izraz.
Je kdo opazil, da nikjer ne lovimo zaključnega zaklepaja, >
. Res je, ne omenjamo ga. Z razlogom. Vseeno nam je zanj. Ko ulovimo URL, nam je za vse, kar se dogaja naprej, vseeno. Da bo še jasneje, pripnimo h gornji tabeli še en preprost in en zapleten podniz, ki se ujame v izraz. Najprej<img src="ime_slike.gif"/>
< | < |
\s* | |
img | img |
[^>]* | |
\W | (presledek med img in src ) |
src | src |
\s* | |
= | = |
\s* | |
" | " |
([^"]*) | ime_slike.gif |
" | " |
Pa še bolj zapleten primer: kako regularni izraz zgrabi < img alt="Tvoj brskalnik ne kaže slik" onclick="http://www.fri.uni-lj.si" src= "x.gif" class="psl" >
< | < |
\s* | (presledka pred img ) |
img | img |
[^>]* | alt="Tvoj brskalnik ne kaže slik" onclick="http://www.fri.uni-lj.si" (pred src so trije presledki; "šara" pograbi prva dva) |
\W | (tretji presledek pred src ) |
src | src |
\s* | |
= | = |
\s* | (presledek) |
" | " |
([^"]*) | x.gif |
" | " |
Zdaj pa uporabimo izraz v funkciji download_images
, ki bo kot argument sprejela URL strani in ciljni direktorij. Prebrala bo s stran, nato pa z nje snela vse slike in jih shranila v ciljni direktorij.
import re +import urllib.request +import os + +re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"([^"]*)"', re.DOTALL + re.IGNORECASE) + +def download_images(url, target_dir): + if not os.path.exists(target_dir): + os.mkdir(target_dir) + elif not os.path.isdir(target_dir): + raise IOError("'%s' is not a directory" % target_dir) + + page = urllib.request.urlopen(url).read().decode("utf-8") + for mo in re_img.finditer(page): + img_url = mo.group(1) + img = urllib.request.urlopen(img_url).read() + img_name = os.path.split(img_url)[1] + target_name = os.path.join(target_dir, img_name) + open(target_name, "wb").write(img)+ + +
Regularni izraz smo tokrat enkrat za vselej prevedli pred funkcijo in ga shranili v re_img
. Dodali smo še en argument, zastavice, s katerimi povemo, naj vzorec .
požira tudi znake za novo vrstico, \n
(re.DOTALL
) in da nam je vseeno za male in velike črke (re.IGNORECASE
), tako da se bo, denimo, img
ujemal tudi z IMG
, Img
in iMg
...
+
+
Funkcija nato preveri ali ciljni direktorij obstaja; če ga ni, ga naredi, če je, pa preveri, ali je v resnici direktorij in v nasprotnem primeru - kot znamo od prejšnjič - sproži izjemo.
+ +Sledi pravo delo: poberemo podano stran. Kar dobimo z read
, ni niz (str
) temveč bajti (bytes
). Ker regularni izrazi brskajo le po nizih, pretvorimo bajte v niz. Če bi hoteli narediti, kot se šika, bi ugotovili, kako je stran v resnici kodirana, tule pa bomo predpostavili, da gre za utf-8 in poklicali decode("utf-8")
+
+Potem poiščemo vse pojavitve značke img
. Zdaj pride, kar sem zamolčal: group
. Ko smo v prejšnjih primerih želeli iz match object
a dobiti podniz, ki se je prilegal regularnemu izrazu, smo poklicali group()
. Zdaj bi želeli priti do podniza, ki se prilega URLju slike, torej tistemu med narekovaji za src
em. Zato smo ta del zaprli v oklepaje in s tem naredili novo skupino. Do njene vsebine pridemo z group(1)
. Čemu 1
? Zato pač, ker gre za prvo skupino. Če bi jih imeli več, bi jih lepo prešteli in povedali, ali želimo vsebino tretje ali šeste.
"Lepo prešteli"? Tole štetje ni prav nič lepo in prikladno. Regularni izrazi so zapleteni in kaj lahko se uštejemo. Še huje: ko izraz spremenimo, bomo morda dodali novo skupino in vse bo treba preštevilčiti. Zato je skupine bolj praktično poimenovati. Namesto +
re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"([^"]*)"', re.DOTALL + re.IGNORECASE)+napišemo +
re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"(?P+in skupina je dobila ime,[^"]*)"', re.DOTALL + re.IGNORECASE)
url
. Do vsebine te skupine potem pridemo z mo.group("url").
+
+Sliko poberemo v niz z urllib.request.urlopen
. Zdaj pa jo je potrebno še shraniti. URL slike bo, recimo, takšen: http://i1.nyt.com/images/2011/01/01/arts/design/01moth_costa/01moth_costa-moth.jpg
in takšno sliko želimo shraniti pod imenom 01moth_costa-moth.jpg
. Da pridemo do njega, lahko za prvi približek posilimo kar funkcijo os.path.split(p)
, ki kot argument (običajno) prejme datoteko skupaj s potjo do nje, na primer, c:\Users\Janez\Desktop\desktop.ini
, kot rezultat pa vrne terko s potjo in datoteko, na primer ("c:\Users\Janez\Desktop", "desktop.ini")
. URL je dovolj podobna reč, da ga bo os.path.split
pravilno razcepil.
Funkcija deluje, lahko jo preskusimo. Ker vaje ni nikoli preveč, pa poskusimo spremeniti izraz tako, da se izognemo klicu os.path.join
. Pot vodi prek razmisleka, da je URL sestavljen iz dveh delov; prvi del je pot, drugi ime datoteke in slednji ne vsebuje poševnic, /
. (Tole je le zmerno res: pri, denimo, slikah, ki se generirajo sproti, URL navadno ne bo vseboval imena datoteke. Ta detajl za zdaj pozabimo.) Ime, torej tisto med narekovaji, bi zato spremenili iz [^"]*
, karkoli razen narekovaja, v [^"]*[^"/]*
, to je, karkoli razen narekovaja, ki mu sledi karkoli razen narekovaja in poševnice. No, tole še ne bo povsem dobro, saj prvemu delu nič ne preprečuje, da ne bi kar vedno požrl celotnega imena in za drugi del ne bi ostalo nič. Prva rešitev, ki nam pride na misel, je [^"]*/[^"/]*
, torej, karkoli razen narekovaja, ki mu sledi poševnica in potem karkoli razen narekovaja in poševnice. Tudi to ne bo povsem dobro, saj zdaj zahtevamo, da URL vsebuje poševnico in zdaj ne bomo več prepoznali povsem legalne slike <img src="ime_slike.gif"/>
. Prava rešitev je, da prvemu delu pač zapovemo, naj požre čim manj, tako da zvezdici dodamo vprašaj, [^"]*?[^"/]*
. Zdaj vse deluje, kot mora: prvi del lahko požira karkoli (vendar čim manj), drugi pa ne mara poševnic. Če obstaja (vsaj ena) poševnica, bo prvi del požrl vse do (zadnje) poševnice in tako omogočil drugemu, da se ujame (a požrl ne bo nič več, kot je nujno treba, saj požira čim manj). Če pa poševnice sploh ni, bo vse požrl drugi del, saj prvemu ni potrebno požreti ničesar.
In zdaj določimo in poimenujmo še skupine. Dve bosta in ena bo del druge. Najprej poskrbimo za ime datoteke, se pravi to, kar sledi poševnici: [^"]*?(?P
. Vse skupaj pa zapremo še enkrat, saj celoten URL vsebuje tako prvi del kot tudi ime: (?P
. Funkcija s takšnim regularnim izrazom je taka:
re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"(?P+ + + -- cgit v1.2.1[^"]*?(?P [^"/]*))"', re.DOTALL+re.IGNORECASE) + +def download_images(url, target_dir): + if not os.path.exists(target_dir): + os.mkdir(target_dir) + elif not os.path.isdir(target_dir): + raise IOError("{}' is not a directory".format(target_dir)) + page = urllib.request.urlopen(url).read().decode("utf-8") + for mo in re_img.finditer(page): + img_url, img_name = mo.group("url", "fname") + img = urllib.request.urlopen(img_url).read() + target_name = os.path.join(target_dir, img_name) + open(target_name, "wb").write(img)