momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Reformularea orarului generat de "asc-orare", cu Python și Sphinx

JSON | Python | Sphinx | orar şcolar | pyparsing
2014 oct

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.

Constituirea dicţionarului de bază al orarului

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.

Dicţionarul disciplinelor

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.

Crearea fişierului "orar.rst" (orarul profesorilor)

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.

Crearea fişierului "clase.rst" (orarele claselor)

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).

Crearea fişierului "obiecte.rst" (orarele disciplinelor)

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.

Crearea fişierelor reST pentru orarele zilnice

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.

Procedura de constituire a site-ului, folosind Sphinx

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").

Ajustări…

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)

docerpro | Prev | Next