Unele site-uri găzduite de //licee.edu.ro expun și orare școlare. Aceste orare au fost generate cu programul "aSc Orare
", furnizat de SIVECO; de fapt, acest program integrează în așa numitul "laborator AEL" aplicaţia creată de ascTimeTables (care percepe preţuri rezonabile, în funcție de facilitățile dorite: 99€, 149€, ș.a.m.d.).
Desigur, exista posibilitatea de a propune nu ascTimeTables, ci FET - care însă este gratuit și este creat de Liviu Lalescu; în plus - FET este disponibil și pe sisteme Linux (nu numai pe Windows) și… nu depinde de "laboratorul AEL".
Pentru FET nu ar fi fost necesară "integrarea în laboratorul AEL": nefiind o aplicaţie comercială, FET poate fi redistribuită ca atare, cu toate facilităţile proprii ei - inclusiv aceea de a "exporta" orarul generat într-un format acceptabil (rămânând eventual să conectezi FET la baza de date constituită în cadrul "laboratorului AEL", pentru a refolosi unele date - clase și profesori - înregistrate acolo).
În schimb, ascTimeTables a trebuit să fie "integrat": o aplicaţie comercială nu poate fi redistribuită legal decât cel mult ca un "Demo" - ceea ce exclude unele facilităţi; pentru a putea publica orarul generat prin aSc Orare
într-un format decent (HTML
, nu PNG sau JPG), trebuie obţinut de la SIVECO un "cod de înregistrare" (bazat pe "Certificatul de licenţă AeL" furnizat școlii respective).
Dar de ce să avem nevoie ca orarul final să fie în "format HTML"? În principiu - pentru a permite căutarea și extragerea automată - datele trebuie reprezentate textual și nu pozate; de pe o imagine grafică nu poţi extrage "datele" (decât apelând la "programe de recunoaștere a formei", mult prea complexe pentru necesitățile obișnuite). Pe de altă parte, HTML-ul standard produs de ascTimeTables pentru prezentarea orarului final vizează mai degrabă hârtia în format "landscape", iar folosirea prin intermediul unui browser cam lasă de dorit (chiar dacă utilizatorii nu sunt pretențioși).
Etalăm aici următorul experiment: în cadrul unui program Python, accesăm pagina //ler.licee.edu.ro/sum_teachers.html - sau pagina cu "orarul profesorilor" de la oricare alt liceu - și extragem datele orarului, creând un dicţionar convenabil (profesor - discipline - orar zilnic
); pe baza acestui dicţionar de date, vom constitui prin scripturi Python anumite fișiere ".rst" (text folosind limbajul de marcare reStructuredText) pe care le vom utiliza apoi prin modulul python-sphinx. Putem obține astfel un site consistent, cu posibilități și deschideri superioare (față de pagina HTML inițială a orarului).
În [1] avem un experiment similar, dar orarul era furnizat sub forma unui fișier Excel; iar în [1] aveam de-a face cu un orar lucrat manual - ceea ce ne impunea și grija unei verificări minimale (semnalam uneori, coincidenţe de ore); în cazul de faţă nu se mai pune problema corectitudinii, orarul inițial fiind acum unul generat automat. În [1] aveam de-a face cu două orare: pentru schimbul I, cu orele în intervalul 8-15 și respectiv pentru al II-lea schimb, cu orele în intervalul 13-20; aSc Orare
"unifică" schimburile, acoperind într-un singur orar intervalul orar 8-20.
Accesăm și obţinem sursa paginii menţionate mai sus, folosind modulul Python urllib2
; listând sursa pe ecran ne dăm seama că datele care ne interesează sunt conţinute în al doilea element <table>. Extragem ceea ce ne interesează, folosind modulul BeautifulSoup
și constituim un dicţionar Python cu datele respective, salvându-l în final în format JSON (în scopul întrebuinţării ulterioare a acestor date, fără a mai fi nevoie să reaccesăm pagina originală):
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 | # -*- coding: utf-8 -*- import urllib2, codecs, json from bs4 import BeautifulSoup as BS URL = "http://ler.licee.edu.ro/sum_teachers.html" header = {'User-Agent': 'Mozilla/5.0'} # to prevent 403 error (Forbidden) req = urllib2.Request(URL, headers=header) page = urllib2.urlopen(req) # print page (avem de vizat al doilea tabel) soup = BS(page) table = soup.find_all('table')[2] rows = table.find_all('tr')[2:] orar = {} # orar[prof] = [ [ore_zi0], ..., [ore_zi4] ] for tr in rows: cols = tr.find_all('td') prof = cols[0].get_text() # numele profesorului if prof not in orar: ore60 = [] for td in cols[1:]: text = td.get_text(separator='|') if text: di_cl = text.split('|') # disciplină | clasă ore60.append([di_cl[1], di_cl[0]]) else: ore60.append('') orar[prof] = [ore60[z*12:(z+1)*12] for z in range(0, 5)] fp = codecs.open('_orar.json', 'w', 'utf-8') # print orar json.dump(orar, fp) |
Testul din linia 18 a devenit necesar când am observat că există uneori câte un profesor "uitat" în tabel: el apare pe un anumit rând al tabelului HTML (cu orarul zilnic aferent) - dar pe la sfârşitul tabelului el figurează a doua oară, de data aceasta fără orar (iar acest rând trebuie ignorat, pentru a nu suprascrie în dicţionar datele înscrise la prima întâlnire a respectivului).
Pe sursa HTML (obținută prin liniile 5-8) vedem că fiecărui profesor îi corespunde un element <tr>
care are în primul <td>
numele profesorului (extras în linia 17); urmează 60 de elemente <td>
, indicând clasele la care intră (și pe ce disciplină) în orele 1..12, în fiecare dintre zilele 1..5:
<tr> <td align="center" width="50" height="60" class="purehtml_headers">URSU GABRIELA</td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>11C<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>12A<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>11A<br><br>Clasă intreagă</td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" bgcolor="#CC9900">in<br>12D<br><br>Clasă intreagă</td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" bgcolor="#CC9900">in<br>11A<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>12A<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>10B<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>11E<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>10B<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>12C<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>9A<br><br>Grupa 3</td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" bgcolor="#CC9900">in<br>12A<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>11A<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>11E<br><br>Clasă intreagă</td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>11C<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>11A<br><br>Clasă intreagă</td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" bgcolor="#CC9900">in<br>12A<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>12G<br><br>Clasă intreagă</td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" bgcolor="#CC9900">in<br>9A<br><br>Grupa 3</td> <td width="60" height="60" bgcolor="#CC9900">in<br>10C<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>9A<br><br>Clasă intreagă</td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" C0C0C0><br></td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>9A<br><br>Clasă intreagă</td> <td width="60" height="60" bgcolor="#CC9900">in<br>9A<br><br>Grupa 3</td> <td width="60" height="60" bgcolor="#CC9900">in<br>9A<br><br>Grupa 3</td> <td width="60" height="60" bgcolor="#CC9900">TIC<br>10C<br><br>Clasă intreagă</td> <td width="60" height="60" C0C0C0><br></td> </tr>
În linia 21 extragem textul conținut de elementul <td>
curent - de exemplu, pentru al doilea dintre cele redate mai sus: "TIC|11C|Clasă întreagă
". Considerăm că putem ignora al treilea atribut ("Clasă întreagă" - care apare la marea majoritate a orelor - și respectiv "Grupă"); dar ne-am crea numai dificultăți, dacă - în scopul de evita redundanța - am separa disciplina de clasă…
Linia 24 adaugă în lista ore60
(introdusă la linia 19) perechea "clasă, disciplină": ["11C", "TIC"], ["12A", "in"], ["11A", "in"], "", ["12D", "in"]
etc. Inițial, evitasem în mod elegant repetarea disciplinei, instituind orar[prof]['disc'] = ['TIC', 'in']
(lista disciplinelor) și orar[prof]['sapt'] = [['11C', '12A', '11A', '', '12D', ...], ...]
; dar astfel rezultă numai dificultăți, când va fi de formulat orarul clasei: nu vom ști care dintre aceste discipline trebuie înscrisă într-o oră și care în alta, în situația când profesorul face la clasa respectivă mai multe discipline.
În final (linia 27), orar[prof]
este o listă cu cinci subliste - câte una pentru fiecare zi:
"URSU GABRIELA": [ [["11C", "TIC"], ["12A", "in"], ["11A", "in"], "", ["12D", "in"], "", "", "", "", "", "", ""], [["11A", "in"], ["12A", "in"], ["10B", "in"], ["11E", "TIC"], ["10B", "TIC"], ["12C", "TIC"], ["9A", "in"], "", "", "", "", ""], [["12A", "in"], ["11A", "in"], ["11E", "TIC"], "", "", "", "", "", "", "", "", ""], [["11C", "TIC"], ["11A", "in"], "", ["12A", "in"], ["12G", "in"], "", "", "", "", ["9A", "in"], ["10C", "in"], ["9A", "TIC"]], ["", "", "", "", "", "", "", ["9A", "TIC"], ["9A", "in"], ["9A", "in"], ["10C", "TIC"], ""] ],
Fiecare sublistă are 12 elemente, rangul fiecăruia indicând în mod implicit câte o oră din intervalul orar 8-20, valoarea fiind șirul vid dacă profesorul este liber în acea oră, sau o pereche reprezentând clasa la care trebuie să intre profesorul la acea oră și disciplina corespunzătoare.
Ministrul zice "bac" în loc de "bacalaureat" și de atuncea încoace, elevii zic și ei - întărind nuanța de persiflare - "bac", "profa", "diriga" și "mate", iar în listele oficiale publicate după examenele naționale numele proprii apar cu majuscule și fără diacriticele specifice limbii române - altfel, toți susținem sau clamăm că în documentele adresate elevilor avem de respectat normele limbii literare.
Desigur că în orar nu o să scriem "Etalarea mărfurilor pe rafturi" (am ajuns să avem ca "discipline de studiu" și acte normative); dar nici nu se cade să indicăm "mate", pentru ora de "Matematică" și "ss" (bine măcar că nu "SS"!) pentru (probabil) "Științe Sociale".
Creem și salvăm în format JSON un dicționar al disciplinelor școlare, în care cheile sunt prescurtările întâlnite frecvent pe orarele școlare vizate aici, iar valorile sunt (pe cât posibil) denumirile standard ale disciplinelor respective:
# -*- coding: utf-8 -*- import codecs, json materie = { 'ro': u'Română', # "Rom\u00e2n\u0103" 'sp': u'Spaniolă', # ... etc., 'm': u'Matematică', 'fi': u'Fizică', # ... etc., 'ge': u'Geografie', 'l': u'Logică', 'p': u'Psihologie', # ... etc., 'lu': u'lu ???', 'ss': u'ss ???', # ... etc., } json.dump(materie, codecs.open('_discipline.json', 'w', 'utf-8'))
Ulterior, când vom avea nevoie, vom putea "importa" ca structură de date recunoscută de Python, dicționarul păstrat în acest fișier - folosind metoda .load()
din modulul json
.
Creem un subdirector LER/
, în care vom constitui fișierele ".rst" necesare pentru construcția cu Sphinx a site-ului. Următorul script folosește fișierele obținute mai sus, _orar.json
și _discipline.json
pentru a formula fișierul LER/orar.rst
, reprezentând orarul profesorilor:
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 | # -*- coding: utf-8 -*- import codecs, json materie = json.load(codecs.open('_discipline.json', 'r', 'utf-8')) orar = json.load(codecs.open('_orar.json', 'r', 'utf-8')) def discip(sapt): disc = [] for zi in sapt: for ql in zi: if ql: if ql[1] not in disc: disc.append(ql[1]) return ' | '.join([materie[ob] for ob in disc]) out = codecs.open('../LER/orar.rst', 'w', 'utf-8') out.write(u'Orarul profesorilor\n' + '-'*20 + '\n') out.write(u'AM: 8-14 - și PM: 14-20 ~\n\n') for prof in sorted(orar): nume = prof.title() # nu 'NUME PREN', ci 'Nume Pren' sapt = orar[prof] disc = discip(sapt) out.write('.. index:: ' + nume + '\n\n') out.write(nume + ' - ' + disc + ' ::\n\n') am = [sapt[z][:6] for z in range(0,5)] pm = [sapt[z][6:] for z in range(0,5)] for zi in range(5): ore = am[zi] so = ' ' for o in ore: if o: so += o[0].ljust(4) else: so += '-'.ljust(4) so += ' '.ljust(3) ore = pm[zi] for o in ore: if o: so += o[0].ljust(4) else: so += '~'.ljust(4) out.write(so + '\n') out.write('\n') out.close() |
În liniile 53 și 54 se reconstituie din fișierele JSON, dicționarul disciplinelor și respectiv dicționarul de bază al orarului (rezultat după scriptul cu liniile 1-29 de mai sus) - folosind metoda json.load()
.
Funcția discip()
definită pe liniile 56-63 și apelată din linia 71, analizează cele 60 de elemente (fie șiruri vide, fie perechi de clasă și disciplină) din orarul unui profesor și returnează un șir conținând disciplinele acoperite de acel profesor, separate (când este cazul) prin "|".
În linia 73 se înscrie în "orar.rst" numele profesorului (transformat în linia 69, prin metoda .title()
a șirurilor Python), împreună cu șirul disciplinelor acoperite de acesta. În liniile 74 și 75 am separat orele pe cele două schimburi; ciclul 76-87 constituie și înscrie pe câte un rând, orele din fiecare zi, în ordinea firească, începând cu cele din primul schimb și continuând cu cele din al doilea schimb:
.. index:: Ursu Gabriela Ursu Gabriela - TIC | Informatică :: 11C 12A 11A - 12D - ~ ~ ~ ~ ~ ~ 11A 12A 10B 11E 10B 12C 9A ~ ~ ~ ~ ~ 12A 11A 11E - - - ~ ~ ~ ~ ~ ~ 11C 11A - 12A 12G - ~ ~ ~ 9A 10C 9A - - - - - - ~ 9A 9A 9A 10C ~
Orele libere au fost marcate prin '-' (linia 81) pentru primul schimb și cu '~' (linia 86) pentru al doilea schimb.
Sphinx va constitui și o pagină de căutare ("General Index"), prin care (angajând anumite scripturi javaScript încorporate) se vor putea căuta termenii indicați sub directiva .. index::
(înscrisă prin linia 72); numele profesorului va fi adăugat în pagina de căutare sub forma unui link la orarul său.
În fișierul HTML pe care îl va crea plecând de la fișierul "orar.rst" constituit prin scriptul de mai sus, Sphinx nu va folosi o structură <table>
pentru a reda orarul unui profesor, ci va folosi un element <pre>
(ambalat într-un <div>
, permițând diverse ajustări stilistice), păstrând și în browser forma de redare foarte simplâ, care se vede mai sus.
Desigur, orarul profesorului este redat cu zilele "una sub alta" (12 "coloane", plus una de separare a schimburilor) și nu "una după alta" (pe un rând cu 60 de coloane - neadecvat pentru browser, fiind mult prea lung) cum apare în fișierul HTML generat inițial de aSc Orare
.
Plecând de la cele două dicționare păstrate în fișierele "_orar.json
" și "_discipline.json
", constituim un dicționar klas
(liniile 108-124) având drept chei clasele și drept valori - câte o listă cu 5 subliste, fiecare conținând disciplinele repartizate în câte o zi a orarului; apoi (liniile 126-156) formulăm fișierul text LER/clase.rst
, conținând orarele claselor:
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 | # -*- coding: utf-8 -*- import codecs, json def FRM(nume): return "%-15s" % nume # câmp de lungime 15, pentru 'nume' materie = json.load(codecs.open('_discipline.json', 'r', 'utf-8')) orar = json.load(codecs.open('_orar.json', 'r', 'utf-8')) klas = {} # clasă: [(zi, rang_oră, disciplină), ...] for prof in orar: sapt = orar[prof] for zi in range(5): for ix, ql in enumerate(sapt[zi]): if ql: if ql[0] not in klas: klas[ql[0]] = [] klas[ql[0]].append((zi, ix, materie[ql[1]])) for ql in klas: # sublista orelor din prima zi, din a doua, etc. ore = klas[ql] or_ql = [] for zi in range(5): or_z = [(t[1], t[2]) for t in ore if t[0]==zi] or_ql.append(sorted(or_z, key=lambda x: int(x[0]))) klas[ql] = or_ql out = codecs.open('../LER/clase.rst', 'w', 'utf-8') out.write(u'Orarul claselor\n' + '-'*15 + '\n') out.write(u'AM: 8-14/15 și PM: 13/14-20\n\n') for ql in sorted(klas, key = lambda nume: (int(nume[:-1]), nume[-1])): out.write('\n.. index:: ' + ql + '\n\n') ore = klas[ql] first = min([int(ore[z][0][0]) for z in range(5)]) last = max([int(ore[z][-1][0]) for z in range(5)]) sch = 'AM' # în care schimb funcționează clasa if first > 0: sch = 'PM' out.write(ql + ' (`' + sch + '`)' + '::\n\n ') qor = {ora: list(['']*5) for ora in range(first, last+1)} for zi in range(5): # ora I din fiecare zi, a II-a, etc. for oz in ore[zi]: if len(qor[oz[0]][zi]) > 0: if oz[1] not in qor[oz[0]][zi]: qor[oz[0]][zi] += '|' + oz[1] else: qor[oz[0]][zi] = oz[1] for ora in qor: ora_z = qor[ora] for zi in ora_z: if zi: out.write(FRM(zi)) else: out.write(FRM('--')) out.write('\n ') out.close() |
Funcția FRM()
din linia 103 definește formatul în care vom scrie disciplinele (în liniile 152, sau 154) - anume, pe câte un câmp de 15 poziții de caracter, cu aliniere la stânga; lungimea maximă a unui rând scris în fișierul text "clase.rst
" va fi de 5*15 = 75 caractere.
15 poziții pentru înscrierea disciplinei sunt suficiente; în testul din linia 143 verificăm dacă este cazul de a concatena (prin "|") disciplinele: dacă ora se desfășoară "pe grupe", dar disciplina este denumită la fel în orarul de bază, atunci o înscriem o singură dată (nu "Informatică | Informatică", ci simplu "Informatică"); altfel, înscriem de exemplu "Desen|Muzică".
Ciclul inițiat în linia 130 formulează orarul fiecărei clase (și îl înscrie în final prin subciclul 148-155, în fișierul "clase.rst") parcurgând dicționarul klas
în ordinea "alfabetică" a cheilor sale (clasele a 9-a în ordine alfabetică, apoi la fel clasele a 10-a, etc.)
În mod implicit, prima oră dintr-o zi are rangul 0, iar ultima oră are rangul 11; în liniile 133-137 se stabilește schimbul ("AM: 8-14/15", sau "PM: 13/14-20" - precizate în linia 128) în care funcționează clasa curentă, iar apoi rândurile orarului se formulează (vezi linia 139) corespunzător intervalului definit de valorile first
și last
obținute în liniile 133 și 134. Redăm ca exemplu:
.. index:: 9D 9D (PM):: -- Biologie -- -- -- Biologie Istorie Engleză Logică Matematică Chimie Franceză Ed_Fizică Biologie Matematică Fizică Fizică Română Desen|Muzică Religie Fizică Geografie Română Franceză Română Matematică Informatică Chimie Engleză Română -- Matematică -- TIC Informatică
Bineînțeles că am indexat fiecare clasă (linia 131), încât orarul ei va putea fi obținut prin click pe link-ul adăugat de Sphinx în pagina de căutare; suplimentar față de ceea ce va produce Sphinx, vom putea avea în vedere să încorporăm o funcție javaScript prin care orarul clasei să poată fi extras și tipărit (deși nu este neapărat cazul, fiindcă - fiind în final un simplu <pre>
- conținutul respectiv va putea fi selectat și copiat direct din browser, păstrând formatul).
În exemplul redat mai sus, clasa a IX-a D funcționează în schimbul "PM"; în prima zi (se subânțelege ușor că "Luni") are 5 ore, începând de la ora 14 (și la fel în a treia zi); în a doua zi (adică marți) are 7 ore, începând de la ora 13 (cu "Biologia"); joi ora a treia (de la ora 16) are "Desen" sau "Muzică" (probabil, în funcție de paritatea săptămânii).
Ar fi de așteptat ca tastând (în "pagina de căutare") numele unei discipline, să obținem orarele care o acoperă. Ne propunem să obținem un fișier "obiecte.rst
" conținând câte o declarație .. index::
pe fiecare obiect, împreună cu orarele acelora dintre profesori care acoperă acel obiect; în cazul când profesorul acoperă mai multe obiecte, orarul său va trebui inserat la fiecare dintre acestea.
Fiindcă deja avem orarele profesorilor în fișierul orar.rst
, putem proceda mai simplu decât plecând iarăși de la cele două dicționare din fișierele JSON. Folosim modulul Python pyparsing
pentru a analiza fișierul orar.rst
, grupând după discipline orarele existente:
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 | import codecs, re from pyparsing import Group, OneOrMore, Suppress, SkipTo from pyparsing import restOfLine, lineEnd, originalTextFor f = codecs.open("../LER/orar.rst", "r", "utf-8") orar_text = f.read() f.close() begin = ".. index:: " + restOfLine + lineEnd parser = OneOrMore(Group(Suppress(originalTextFor(begin)) + SkipTo(begin))) results = parser.scanString(orar_text + '.. index:: \n', True) obiect = {} disc_re = re.compile('.*-\s+(.*)\s+::') for res in results: for hh in res[0]: lst = hh[0] disc = disc_re.match(''.join(lst)).group(1) dsc = disc.split(' | ') for ob in dsc: if ob not in obiect: obiect[ob] = [lst] else: obiect[ob].append(lst) out = codecs.open('../LER/obiecte.rst', 'w', 'utf-8') out.write(u'Orarul disciplinelor\n' + '-'*20 + '\n') for ob in sorted(obiect): out.write('.. index:: ' + ob + '\n\n') out.write('**' + ob + '** ::\n\n') for prof in obiect[ob]: out.write(' ' + prof) out.close() |
În orar.rst
orarul fiecărui profesor este delimitat de două linii care încep cu ".. index::" - încât a rezultat definiția de grupare a liniilor fișierului din liniile de program 208-209; excepție face doar ultimul orar de profesor din orar.rst
(lipsește a doua linie de încadrare, ".. index::") și pentru a nu-l omite, am alipit șirului orar_text
în care am citit conținutul fișierului "orar.rst" (în linia 205), o asemenea "linie de separare" finală - vezi linia 211.
În orar.rst
disciplinele urmează după numele profesorului, delimitate de caracterele '-' și respectiv '::' - rezultând șablonul din linia 214, folosit în linia 218 pentru extragerea disciplinelor de pe rândul curent. În liniile 219-224 se constituie dicționarul obiect
prin care fiecărei discipline i se asociază o listă conținând orarele din grupul curent al rezultatelor obținute în linia 211; în final, acest dicționar este înscris în fișierul obiecte.rst
, prin liniile 228-232.
Ne luăm permisiunea de a da și aici, un exemplu de pe fișierul obiecte.rst
obținut:
.. index:: Spaniolă **Spaniolă** :: Nedelcu Bogdan - Engleză | Spaniolă :: 11D 12I 11I 12I - - ~ ~ ~ ~ ~ ~ - - 12I 11I 11G - ~ ~ ~ ~ ~ ~ - - 11G 12I - 10I 9D 10D ~ ~ ~ ~ 12I 12I - - - - ~ ~ 10I 10D 9D ~ - 12I - 11I 11D - ~ ~ ~ ~ ~ ~ Tofan Gabriela - Spaniolă :: - - - - - - ~ 10I ~ ~ ~ ~ - - - - - - ~ ~ ~ ~ ~ 9H - - - - - - 10I 10I 9H ~ ~ ~ 11I 11I - - - - 9H ~ ~ ~ ~ ~ 11I 11I - - - - 10I 9H ~ ~ ~ ~
Având și "Engleză", orarul primului dintre acești doi profesori va figura și la indexul "Engleză".
În linia 230, disciplina a fost înscrisă marcând numele respectiv cu "**" - marca prevăzută de reST pentru "bold"; astfel, **Spaniolă**
din exemplul redat va fi înlocuit cu <strong>Spaniolă</strong>
atunci când Sphinx va crea pagina HTML corespunzătoare fișierului obiecte.rst
.
Care profesori au ore și care este orarul acestora, într-o anumită zi? De data aceasta ni se pare mai simplu de rezolvat folosind direct dicționarul din fișierul "_orar.json
" (în loc de a prelucra "orar.rst
", cum am făcut mai sus pentru constituirea orarului disciplinelor):
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 | # -*- coding: utf-8 -*- import codecs, json orar = json.load(codecs.open('_orar.json', 'r', 'utf-8')) def FRM(nume): return "%-26s" % nume zile = [u'Luni', u'Marți', u'Miercuri', u'Joi', u'Vineri'] def are_ore(orar_zi): for sbl in orar_zi: if sbl: return True return False # toate valorile sunt '', deci nu are ore for zi in range(5): file_ = '../LER/zi_' + str(zi) + '.rst' out = codecs.open(file_, 'w', 'utf-8') ziua = zile[zi] out.write(ziua + '\n' + '-'*len(ziua) + '\n\n') out.write(u'AM: 8-14 - și PM: 14-20 ~ ::\n\n') free = [] # profesorii "liberi" în ziua curentă for prof in sorted(orar): nume = prof.title() or_zi = orar[prof][zi] if are_ore(or_zi): so = ' ' for o in or_zi[:6]: if o: so += o[0].ljust(4) else: so += '-'.ljust(4) so += ' '.ljust(3) for o in or_zi[6:]: if o: so += o[0].ljust(4) else: so += '~'.ljust(4) out.write(' ' + FRM(nume) + so + '\n') else: free.append(nume) out.write('\n') for prof in free: out.write(' ' + prof + '\n') out.close() |
Pentru fiecare zi, în linia 266 se creează câte un fișier ".rst"; ciclul inițiat în linia 271 parcurge în ordinea alfabetică a cheilor dicționarul orar
(reconstituit în linia 252 din fișierul "_orar.json") și înscrie în fișierul creat numele profesorului (cu formatul din linia 254) și orarul său pe ziua respectivă - exceptând (prin testul din linia 274) cazul când profesorul nu are ore în acea zi, caz în care el este adăugat (linia 285) într-o listă înscrisă în final (liniile 287-288) la sfârșitul fișierului zilei.
În reST paragrafele de text trebuie separate prin câte un rând liber, iar dacă vrem să grupăm niște rânduri - de exemplu, într-o listă - atunci rândurile respective trebuie să fie (la fel) indentate; am preferat să indentăm rândurile, prefixându-le cu trei spații - vezi liniile 283 și 288.
Bineînțeles că în toate fișierele create mai sus păstrăm ordinea alfabetică; obiceiul neprincipial de a prezenta orarul, statele de plată, etc. folosind ordinea funcțiilor administrative - doar îngreunează inutil căutarea informațiilor.
Sphinx este un instrument de tip "generator de documentație": convertește un grup de fișiere text cu formatul de marcare reST, într-un grup de pagini HTML interconectate (într-un web-site).
Am creat deja mai sus, fișierele reST necesare - în directorul LER/
; desigur, vom putea adăuga ulterior și alte asemenea fișiere (dacă servesc tema comună, "orarul școlii"). Nu rămâne decât să invocăm din directorul LER/
, scriptul executabil sphinx-quickstart
- acesta creează în mod interactiv scriptul de configurare "conf.py
" și un șablon de fișier "index.rst
".
În "conf.py
" putem modifica eventual, unele configurări; de exemplu, preferăm html_theme =
'sphinxdoc'
în locul temei "default
", înscrise în mod implicit; o temă este o colecție de șabloane HTML, definiții de stil și scripturi javaScript, servind pentru prezentarea într-o manieră unitară a paginilor.
Ceea ce avem de făcut este să rescriem fișierul "index.rst
"; iată un exemplu, minimal:
Orarul şcolii ============= .. toctree:: Orare Profesori <orar> Orare Clase <clase> Orare Discipline <obiecte> .. toctree:: Luni <zi_0> Marţi <zi_1> Miercuri <zi_2> Joi <zi_3> Vineri <zi_4> :ref:`genindex` (profesori, clase, discipline)
Directiva .. toctree::
definește "Cuprins"-ul site-ului ("toc
" = "Table Of Content"); itemurile respective vor fi convertite în link-uri (ca <a href="clase.html">Orare Clase</a>
). Directiva :ref:`genindex`
creează o referință la viitoarea "pagină de căutare", genindex.html
.
Obținem paginile HTML corespunzătoare fișierelor "*.rst" existente în /LER
, prin comanda:
vb@vb:~/orar/Doc/LER$ make html
sphinx-build -b html -d _build/doctrees . _build/html
Running Sphinx v1.2.3 ... (informează asupra etapelor execuției)
Build finished. The HTML pages are in _build/html.
Directorul _build/html
conține acum toate fișierele care împreună, definesc site-ul: fișierele HTML (pagina "de bază" index.html
, orar.html
, clase.html
, obiecte.html
, genindex.html
, zi_0.html
, etc.) rezultate prin convertirea și analiza fișierelor reST inițiale; în subdirectorul _static/
sunt grupate fișierele CSS care asigură stilarea conținutului HTML (conform temei alese în conf.py
) și fișierele javaScript care asigură navigarea între pagini și operațiile de căutare a termenilor.
Avem de remarcat că fișierele HTML respective sunt independente: nu există fragmente HTML (de încărcat prin AJAX, drept conținut al unui anumit element dintr-o pagină deja constituită în browser), ci numai pagini HTML complete - care în <head>
prevăd încărcarea acelorași fișiere ".css" și ".js" și care încorporează - pe "deasupra" conținutului specific - aceleași bare și elemente de navigare.
Cu alte cuvinte, site-ul va putea fi accesat indicând în bara de adresă a browserului oricare dintre aceste fișiere HTML; index.html
este totuși, "pagina de bază" - având în vedere și faptul că numai aici apar (inițial) declarațiile <meta />
pentru description
și keywords
- dacă le-am prevăzut în "index.rst", sub directiva .. meta::
(aceste declarații vor fi utilizate de "motoarele de căutare", pentru indexarea paginilor respective în bazele de date globale asupra informațiilor circulate pe Internet).
Bineînțeles că putem modifica paginile HTML obținute, înainte de a și publica site-ul; dar dacă orarul școlii se va modifica, vor trebui relansate scripturile Python de mai sus pentru producerea noilor dicționare "de bază" și apoi a fișierelor ".rst", iar în final comanda make html
va recrea fișierele HTML (încât modificările efectuate între timp asupra acestora vor fi pierdute).
Totuși, pentru unele modificări finale, putem prevedea un mic script pe care să-l executăm imediat după obținerea fișierelor HTML (după make html
). De exemplu, putem "moderniza" declarația <!DOCTYPE ...>
(fixată de Sphinx pe "DTD XHTML 1.0 Transitional
"), pentru a folosi HTML5:
sed -i -e '/DOCTYPE/ {N; s/.*/<!doctype html>/;}' *.html sed -i -e '/html xmlns/ {s/.*/<html>/;}' *.html
Prima comandă va înlocui în toate fișierele HTML (din directorul în care am lansat-o, LER/_build/html
) linia care conține "DOCTYPE" și pe cea următoare acesteia, cu <!doctype html>
; a doua comandă va elimina atributul "xmlns
" din tagul <html>
- rezultând fișiere conforme HTML5 (devenind important în site-uri - generate de Sphinx și modificate manual - în care am folosi baze de date "offline").
Punând la punct scripturile redate mai sus, am crezut un timp că aș putea reformula astfel, orarul oricărei școli - neavând de făcut altceva decât să înlocuiesc adresa URL înscrisă pe linia 5 cu adresa la care școala respectivă și-a postat orarul (generat prin aSc Orare
). Dar am și constatat între timp, că de fapt multe școli și-au postat orarele (incompatibil noțiunii de informație) ca o secvență de cadre de film - fișiere Adobe Flash (application/vnd.adobe.flash.movie)
.
SWF
este folosit pentru grafică vectorială, pentru realizarea animațiilor, a jocurilor "prin Internet", a filmelor și rulează în browser prin intermediul unor anumite programe externe (Adobe Flash Player). Este chiar caraghios, să prezinți un orar școlar folosind asemenea instrumente - sofisticate, cu restricții comerciale și produse în cu totul alte scopuri (decât acela de a furniza informație).
Prezentarea orarului are menirea evidentă, de a furniza informații utile celor interesați; în mod firesc, trebuie să ai posibilitatea de a identifica informația care interesează, de a o extrage (măcar prin procedeul elementar, Copy&Paste) și chiar de a o refolosi (de exemplu, ca în experimentul evocat mai sus). O poză, sau un "movie" - nu-ți poate oferi decât posibilitatea de a admira "informația" pe ecran, sau pe hârtie.
Am avut deci noroc, să dau peste adresa menționată în linia 5 (ar mai fi vreo două) și să pot astfel derula experimentul redat mai sus. Nu-i cazul să pun la dispoziție (cu o anumită posibilitate de actualizare, când s-ar schimba orarul), site-ul rezultat (am înțeles că "profesorii sunt foarte mulțumiți așa"); dar pentru completarea lucrurilor, postez rezultatul sub titlul de exemplu (neactualizabil) - mulțumindu-mă să consider în final că am produs încă un exemplu didactic de elaborare a unei aplicații Web (ca și în alte locuri, pe aici), cu acest rezultat: orar școlar (exemplu).
vezi Cărţile mele (de programare)