summaryrefslogtreecommitdiff
path: root/python/problems/re/re_sl.html
blob: 44323369eb49c42c8210037446a23124a9768888 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
<!DOCTYPE html>
<html lang="sl">
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" type="text/css" href="/css/codeq.css" />
<link rel="stylesheet" type="text/css" href="../../style.css" />
</head>
<body>

<h2>Regularni izrazi</h2>

<p>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.</p>

<h3>Regularni izraz</h3>

<p>Če bi hoteli teoretizirati, bi rekli, da opisujejo <em>jezik</em>, 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:
<dl>
<dt>.</dt><dd>katerikoli znak</dd>
<dt>\w</dt><dd>katerakoli črka ali števka</dd>
<dt>\W</dt><dd>katerikoli znak, ki ni črka ali števka</dd>
<dt>\d</dt><dd>katerakoli števka</dd>
<dt>\D</dt><dd>katerakoli znak, ki ni števka</dd>
<dt>\s</dt><dd>beli prostor (presledek, tabulator, nova vrstica...)</dd>
<dt>\S</dt><dd>karkoli, kar ni beli prostor</dd>
<dt>^</dt><dd>začetek niza</dd>
<dt>$</dt><dd>konec niza</dd><
<dt>\</dt><dd>vzvratno poševnico uporabimo, ko bi radi v vzorec vpisali, recimo, piko. Znak <code>.</code> ima namreč poseben pomen (glej zgoraj), zato takrat, kadar v resnici želimo piko, napišemo <code>\.</code>. Enako velja za zvezdico, vprašaj, plus... in tudi vzvratno poševnico.</dd>
<dt>oglati oklepaji</dt><dd>mednje napišemo seznam znakov in opisujejo katerikoli znak s tega seznama; če je prvi znak <code>^</code>, 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.</dd>
</dl>
Poglejmo nekaj preprostih primerov vzorcev
<dl>
<dt>l.pa</dt><dd>Ta izraz opisuje "l", ki mu sledi karkoli in nato "pa". To je lahko "lipa", "lepa" ali "lopa", pa tudi "lrpa", "lmpa", "lgpa". In celo "l3pa", "l/pa" in "l pa". Vzorcu pa ne ustrezata, recimo, "sipa", saj se ne začne z "l" ali "lotupa", saj ima med "l" in "pa" več kot eno samo črko.</dd>
<dt>l.p.</dt><dd>Tole pa je lahko tudi "lipi" ali "lopi" ali "lspt" ali "l1p4" ali "l-p)"... Torej vse besede s štirimi znaki, pri čemer mora biti prvi znak "l", tretji pa "p".</dd>
<dt>l\wp\w</dt><dd>Ta vzorec je podoben prejšnjemu, le da zahteva, da sta drugi in četrti znak črki ali številki. "lipa" in "l1p4" sta še vedno spremenljivi, "l-p)" pa ne več.</dd>
<dt>lip[aieo]</dt><dd>Tole pa je lahko "lipa", "lipi", "lipe" ali "lipo". Ne pa "lipu" ali "lipm".</dd>
<dt>lip[^u t]</dt><dd>Ta vzorec opisuje "lip", ki mu sledi katerikoli znak, razen u-ja, presledka ali t-ja.</dd>
<dt>s[tpr]anje</dt><dd>"stanje", "spanje" in nekatere druge reči.</dd>
</dl>
Zdaj pa še par posebnih znakov.
<dl>
<dt>okrogli oklepaji</dt><dd>Z okroglimi oklepaji označimo skupino</dd>
<dt>*</dt><dd>Z zvezdico označimo, da se sme prejšnji znak ali prejšnja skupina (ki smo jo zaprli v okrogle oklepaje) poljubnokrat ponoviti - lahko tudi ničkrat.</dd>
<dt>+</dt><dd>Plus pomeni isto kot zvezdica, le da zahteva, da se prejšnja skupina ali znak pojavi vsaj enkrat.</dd>
<dt>?</dt><dd>Z vprašajem povemo, naj se prejšnja skupina pojavi enkrat ali nobenkrat.</dd>
</dl>
Spet si oglejmo nekaj primerov.
<dl>
<dt>Go*l!</dt><dd>Ta vzorec opisuje nize, kot so "Gol!", "Gooooooool!" in "Goooooooooooooooool!". Žal pa tudi "Gl!". :)</dd>
<dt>Go+l!</dt><dd>Popravljena verzija gornjega: zahteva, da se "o" pojavi vsaj enkrat.</dd>
<dt>Go+l?!</dt><dd>Tole pa je podobno kot zgoraj, le da se črka <code>l</code> 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!"  </dd>
</dl>
Pa še par bolj zapletenih.
<dl>
<dt>(brm)+</dt><dd>Opisuje motor, ki dela "brmbrmbrmbrmbrmbrm" ali "brmbrmbrmbrmbrmbrmbrmbrm" ali vsaj "brm".</dd>
<dt>ra(ta)*</dt><dd>Opisuje mitraljez (ki je lahko tudi pokvarjen in reče samo "ra" in razpade)</dd>
<dt>tr([aeiou]l)*[aeiou]</dt><dd>Opisuje prepevanje, ko ne poznamo besedila ("tralalelalalalilolololu")</dd>
<dt>jodl(dodl)*dii</dt><dd>Jodlanje (in jodldodlanje in jodldodldodldodldodlanje).</dd>
<dt>jodl(dodl)?dii</dt><dd>Tole pa je samo jodlanje in jodldodlanje, ne pa tudi jodldodldodldodldodlanje.</dd>
</dl>
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.
<dl>
<dt>*?, +?, ??</dt><dd>Če k zvezdici, plusu ali vprašaju dodamo še en vprašaj, zahtevamo, naj ta zvezdica, plus ali vprašaj "požre" čim manj znakov. Tole je na prvi pogled zelo praktično; v resnici bo začetnik, ko odkrije te vprašaje, naredil veliko naivnih napak, zato se jim, vsaj v začetku, raje izognite in poskusite to, kar želite povedati, povedati brez njih.</dd>
</dl>
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".</p>

<h3>Funkcije za uporabo regularnih izrazov</h3>

<p>Naučili smo se opisovati vzorce. Kaj pa lahko s temi opisi počnemo?</p>

<p>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 <code>re</code>, kar je okrajšava za <em>regular expression</em>.</p>

<p>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:
<pre>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

"""</pre>

<p>Vzorec, ki bo predstavljal ime - nekaj, kar se začne z veliko črko in nadaljuje s vsaj eno črko, je <code>[A-Z]\w+</code>.</p>

<p>Najpreprostejša za uporabo (a ne tudi najbolj uporabna) funkcija, na kateri lahko poženemo ta izraz, je <code>findall</code>, 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 <code>msg</code>, bomo seznam imen dobili z
<pre>>>> re.findall(r"[A-Z]\w+", msg)
['Dragi', 'Ahmed', 'Upam', 'Mustafa', 'Osama', 'Abdulah', 'Pesavar', 'Osama', 'Abdulah', 'Harun', 'Osama', 'Jibril', 'Skype', 'Husein', 'PS', 'Python']</pre></p>

<p>Opazka: pred narekovaj sem dodal <code>r</code>. Iz davnega začetka semestra se morda spomnite, kakšne posledice ima to: vse vzvratne poševnice (po domače: <em>backslashi</em>) po tem pomenijo le vzvratne poševnice, ne ubežnih zaporedij (torej, <code>\n</code> pomeni vzvratno poševnico in <code>n</code>, 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 <code>r</code>-jem pred narekovaji.</p>

<p>Metoda <code>findall</code> je najpreprostejša, ker vrne kar (pod)nize, ki ustrezajo vzorcu. Druge funkcije vračajo objekt, iz katerega lahko razberemo še kaj več. Vzemimo <code>search</code>, ki poišče prvo pojavitev vzorca.</p>

<pre>>>> mo_ime = re.search(r"[A-Z]\w+", msg)
>>> mo_ime
<_sre.SRE_Match object at 0x045812F8></pre>

<p><code>mo_ime</code> ni niz, tako kot prej, temveč objekt, ki vsebuje podatke o podnizu, ki se ujema z izrazom. (Ker gre za "<code>Match object</code>", zato sem mu pripel <code>mo_</code>. 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 <code>mo_</code>, je problem rešen.</p>)

<pre>>>> mo_ime.group()
'Dragi'
>>> mo_ime.start()
0
>>> mo_ime.end()
5
>>> mo_ime.span()
(0, 5)</pre>

<p>Tule <code>start</code> pove indeks črke v nizu, kjer se ujemanje začne in <code>end</code> indeks črke za ujetim podnizom; <code>span</code> pove oboje hkrati. Najdeni niz se torej nahaja v <code>msg[0:5]</code>. Razlog, da se metoda, ki vrne celotni podniz, imenuje <code>group()</code>, pa bo postal jasen malo kasneje, ko se bomo pogovarjali o skupinah.</p>

<p>Če želimo poiskati vse pojavitve vzorca in jih obdelati v zanki, bomo uporabili <code>finditer</code>, kot recimo tule:
<pre>for s in re.finditer(r"[A-Z]\w+", msg):
    print(s.group())</pre></p>
	
<p>Metoda <code>sub</code> je podobna metodi <code>replace</code>, ki smo jo spoznali pri nizih: z njo lahko zamenjamo vse pojavitve danega vzorca s čim drugim.</p>

<pre>>>> 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
</pre>

<p>V zvezi s <code>sub</code> lahko opazimo, da so argumenti funkcije obrnjeni nekoliko drugače kot pri <code>replace</code>: <code>replace</code> je metoda razreda <code>str</code>, torej je niz, katerega podnize želimo spreminjati že implicitno podan (kot <code>self</code>), 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

<pre>fname = fname.replace(" ", "_")</pre>

Metodi <code>sub</code> 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:
<pre>fname = re.sub(" ", "_", fname)</pre>
</p>

<p>Regularni izrazi so za tako preprosto zamenjavo prevelik kanon. <code>sub</code> 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 <code>Match object</code>, kot rezultat mora vrniti niz, ki naj zamenja ujemajoči se podniz. Podniz lahko zamenjamo z, recimo, enakim podnizom, vendar zapisanim z velikimi črkami.</p>

<pre>>>> 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

</pre>>

<p>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:
<pre>print(re_ime.sub(lambda mo: mo.group().upper(), msg))</pre>
Za primer obrnimo vsa imena po arabsko, z desne na levo:

<pre>>>> 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
</pre></p>

<p>Še ena (in zadnja, za danes) zanimiva funkcija, ki jo nudijo regularni izrazi, je <code>split</code>. 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 <code>split</code>, 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).</p>

<pre>
>>> re.split(r"[A-Z]\w+", msg)<br/>
['', ' ', ',\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']
</pre>

<p>V zvezi z <code>Match objecti</code> so nam ostale še skupine. Kaj so in kaj počnemo z njimi, pa bomo videli na primerih.</p>

<h2>Prevedeni regularni izrazi</h2>

<p>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 <em>končni avtomat</em>, ta pa je neke vrste usmerjen graf. (Tako da boste vsaj vedeli, čemu služi to, kar počnete pri Diskretnih strukturah.)</p>

<p>Za razumevanje tega, kar je pomembno tu, je bistveno le: regularni izraz se mora v nekaj <em>prevesti</em>. Če napišete program, ki vsebuje nekaj takšnega:
<pre>for i in range(10000):
    re.findall(r"[A-Z]\w+", msg[i])
	# in potem še kaj naprej</pre>
bo moral nesrečni računalnik deset tisočkrat prevesti regularni izraz <code>[A-Z]\w+</code> 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.</p>

<p><pre>re_ime = re.compile(r"[A-Z]\w+")
for i in range(10000):
    re_ime.findall(msg[i])
	# in potem še kaj naprej</pre>
Spremenljivka <code>re_ime</code> (spet madžarska notacija!) zdaj predstavlja prevedeni izraz. Objekt <code>re_ime</code> ima vse funkcije, ki smo jih poznali prej, le da gre zdaj pač za metode objekta in ne funkcije modula: namesto <code>re.findall</code>, <code>re.finditer</code>, <code>re.sub</code>, <code>re.split</code>, zdaj pišemo <code>re_ime.findall</code>, <code>re_ime.finditer</code>, <code>re_ime.sub</code>, <code>re_ime.split</code>. Poleg tega pa še izpustimo prvi argument, vzorec, saj je izraz že pripravljen.</p>
	
<p>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.</p>

<h2>Primeri</h2>

<h3>Primer: teroristi</h3>

<p>Rešimo, za začetek, kar staro nalogo, ki je zahtevala, da izpišemo, kolikokrat se v sporočilu pojavi posamezno ime.</p>

<pre>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))</pre>
	
<p>Gre pa, jasno, tudi hitreje: modul <code>collections</code> ima razred <code>Counter</code>, ki prešteje število ponovitev v poslanem seznamu, slovarju, množici... Takole:

<pre>>>> 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})</pre>

<p>To naš program precej poenostavi, saj nas odreši glavnega opravila, štetja.</p>


<pre>import re, collections
for ime, stevec in collections.Counter(re.findall(r"[A-Z]\w+", msg)).items():
    print("{} {}".format(ime, stevec))</pre>


<h3>Primer: branje vseh slik z določene strani</h3>

<p>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:
<xmp><img src="ime_slike.gif"/></xmp>
Z malo potencialnimi komplikacijami, seveda. Oklepaju sicer vedno sledi najprej <code>img</code>, a poleg <code>src</code> lahko naletimo še na kaj drugega. Lepo vzgojen HTML ima poleg <code>src</code> vsaj še <code>alt</code>, slabo vzgojen pa tudi <code>onclick</code> ali kaj hujšega. Poleg tega so lahko kjerkoli, razen sredi besed, tudi presledki.</p>

<p>Kar iščemo, lahko z besedami opišemo kot oklepaj (namreč tale: <code>&lt;</code>), ki mu lahko sledi kaj praznega prostora, nato pa <code>img</code>. Potem sme biti čisto karkoli razen zaklepaja, <code>&gt;</code> in nekoč mora priti na vrsto <code>src</code>, pred njim pa mora biti kak znak, ki ni črka (če bi, recimo, pisalo <code>mesrc</code>, to ni OK, če je pred <code>src</code> presledek, tabulator, nova vrsta ali, zaradi mene tudi narekovaj ali vejica (čeprav to ni povsem po predpisih) pa je OK. Besedi <code>src</code> 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.</p>

<p>Z regularnim izrazom taisto povemo tako:
<pre><\s*img[^>]*\Wsrc\s*=\s*"([^"]*)"</pre>
Se pravi
<table>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>&lt;</code></td><td>oklepaj, &lt;</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td>poljubno praznega prostora (lahko tudi nič)</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>img</code></td><td>beseda <code>img</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>[^&gt]*</code></td><td>poljubno dolga poljubna šara, razen zaključnega oklepaja</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\W</code></td><td>karkoli, kar ni črka</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>src</code></td><td>beseda <code>src</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td>poljubno praznega prostora (lahko tudi nič)</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>=</code></td><td>enačaj</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td>poljubno praznega prostora (lahko tudi nič)</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>"</code></td><td>narekovaj</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>([^"]*)</code></td><td>poljubne reči razen narekovaja; čemu oklepaji, bomo še videli</td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>"</code></td><td>narekovaj</td></tr>
</table>

Ko smo rekli <em>poljubno dolga poljubna šara, razen zaključnega zaklepaja</em>: kdo nam jamči, da ta "poljubna šara" ne bo požrla tudi <code>src</code>ja in URLja? To nam jamči sam regularni izraz! Če bi <code>[^&gt]*</code> požrl <code>src</code>, 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.</p>

<p>Podobno, a ravno obratno vprašanje je, kdo zagotavlja, da bo <code>([^"]*)</code>, to so, <em>poljubne reči razen narekovaja</em>, 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 <code>([^"]*)</code> namreč sledi <code>"</code>; če <code>([^"]*)</code> ne požre vsega do narekovaja, se tisto, kar sledi, ne bo ujemalo z narekovajem, kot zahteva izraz.</p>

<p>Je kdo opazil, da nikjer ne lovimo zaključnega zaklepaja, <code>&gt;</code>. 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<code>&lt;img src="ime_slike.gif"/&gt;</code></p>

<table>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>&lt;</code></td><td><code>&lt;</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>img</code></td><td><code>img</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>[^&gt]*</code></td><td></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\W</code></td><td><em>(presledek med <code>img</code> in <code>src</code>)</em></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>src</code></td><td><code>src</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>=</code></td><td><code>=</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>"</code></td><td><code>"</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>([^"]*)</code></td><td><code>ime_slike.gif</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>"</code></td><td><code>"</code></td></tr>
</table>

<p>Pa še bolj zapleten primer: kako regularni izraz zgrabi <code>&lt;   img alt="Tvoj brskalnik ne kaže slik" onclick="http://www.fri.uni-lj.si"   src=  "x.gif" class="psl" &gt;</code></p>

<table>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>&lt;</code></td><td><code>&lt;</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td><em>(presledka pred <code>img</code>)</em></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>img</code></td><td><code>img</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>[^&gt]*</code></td><td><code> alt="Tvoj brskalnik ne kaže slik" onclick="http://www.fri.uni-lj.si"  </code> <em>(pred <code>src</code> so trije presledki; "šara" pograbi prva dva)</em></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\W</code></td><td><em>(tretji presledek pred <code>src</code>)</em></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>src</code></td><td><code>src</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>=</code></td><td><code>=</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>\s*</code></td><td><em>(presledek)</em></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>"</code></td><td><code>"</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>([^"]*)</code></td><td><code>x.gif</code></td></tr>
<tr><td style="background-color: #cccccc; text-align: right; padding: 2px"><code>"</code></td><td><code>"</code></td></tr>
</table>

<p>Zdaj pa uporabimo izraz v funkciji <code>download_images</code>, 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.</p>

<pre>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)</pre>
        

<p>Regularni izraz smo tokrat enkrat za vselej prevedli pred funkcijo in ga shranili v <code>re_img</code>. Dodali smo še en argument, zastavice, s katerimi povemo, naj vzorec <code>.</code> požira tudi znake za novo vrstico,  <code>\n</code> (<code>re.DOTALL</code>) in da nam je vseeno za male in velike črke (<code>re.IGNORECASE</code>), tako da se bo, denimo, <code>img</code> ujemal tudi z <code>IMG</code>, <code>Img</code> in <code>iMg</code>...

<p>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.</p>

<p>Sledi pravo delo: poberemo podano stran. Kar dobimo z <code>read</code>, ni niz (<code>str</code>) temveč bajti (<code>bytes</code>). 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 <code>decode("utf-8")</code>

Potem poiščemo vse pojavitve značke <code>img</code>. Zdaj pride, kar sem zamolčal: <code>group</code>. Ko smo v prejšnjih primerih želeli iz <code>match object</code>a dobiti podniz, ki se je prilegal regularnemu izrazu, smo poklicali <code>group()</code>. Zdaj bi želeli priti do podniza, ki se prilega URLju slike, torej tistemu med narekovaji za <code>src</code>em. Zato smo ta del zaprli v oklepaje in s tem naredili novo skupino. Do njene vsebine pridemo z <code>group(1)</code>. Čemu <code>1</code>? 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.</p>

<p>"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
<pre>re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"([^"]*)"', re.DOTALL + re.IGNORECASE)</pre>
napišemo
<pre>re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"(?P<url>[^"]*)"', re.DOTALL + re.IGNORECASE)</pre>
in skupina je dobila ime, <code>url</code>. Do vsebine te skupine potem pridemo z <code>mo.group("url").</code></p>

<p>Sliko poberemo v niz z <code>urllib.request.urlopen</code>. Zdaj pa jo je potrebno še shraniti. URL slike bo, recimo, takšen: <code>http://i1.nyt.com/images/2011/01/01/arts/design/01moth_costa/01moth_costa-moth.jpg</code> in takšno sliko želimo shraniti pod imenom <code>01moth_costa-moth.jpg</code>. Da pridemo do njega, lahko za prvi približek posilimo kar funkcijo <code>os.path.split(p)</code>, ki kot argument (običajno) prejme datoteko skupaj s potjo do nje, na primer, <code>c:\Users\Janez\Desktop\desktop.ini</code>, kot rezultat pa vrne terko s potjo in datoteko, na primer <code>("c:\Users\Janez\Desktop", "desktop.ini")</code>. URL je dovolj podobna reč, da ga bo <code>os.path.split</code> pravilno razcepil.</p>

<p>Funkcija deluje, lahko jo preskusimo. Ker vaje ni nikoli preveč, pa poskusimo spremeniti izraz tako, da se izognemo klicu <code>os.path.join</code>. Pot vodi prek razmisleka, da je URL sestavljen iz dveh delov; prvi del je pot, drugi ime datoteke in slednji ne vsebuje poševnic, <code>/</code>. (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 <code>[^"]*</code>, karkoli razen narekovaja, v <code>[^"]*[^"/]*</code>, 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 <code>[^"]*/[^"/]*</code>, 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 <code>&lt;img src="ime_slike.gif"/&gt;</code>. Prava rešitev je, da prvemu delu pač zapovemo, naj požre čim manj, tako da zvezdici dodamo vprašaj, <code>[^"]*?[^"/]*</code>. 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.</p>

<p>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: <code>[^"]*?(?P<fname>[^"/]*)</code>. Vse skupaj pa zapremo še enkrat, saj celoten URL vsebuje tako prvi del kot tudi ime: <code>(?P<url>[^"]*?(?P<fname>[^"/]*))</code>. Funkcija s takšnim regularnim izrazom je taka:</p>

<pre>re_img = re.compile(r'<\s*img[^>]*\Wsrc\s*=\s*"(?P<url>[^"]*?(?P<fname>[^"/]*))"', 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) </pre>

</body>
</html>