-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathmwebcrawler.py
566 lines (468 loc) · 22.4 KB
/
mwebcrawler.py
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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# -*- coding: utf-8 -*-
# @package: mwebcrawler.py
# @author: Guilherme N. Ramos ([email protected])
#
# Funções de web-crawling para buscar informações de cursos da UnB. O programa
# busca as informações com base em expressões regulares que, assume-se,
# representam a estrutura de uma página do Matrícula Web. Caso esta estrutura
# seja alterada, as expressões aqui precisam ser atualizadas de acordo.
#
# Erros em requests são ignorados silenciosamente.
import requests
import re
# Renomeando funções/classes para maior clareza de código.
busca = re.findall
RequestException = requests.exceptions.RequestException
def mweb(nivel, pagina, params, timeout=1):
'''Retorna a página no Matrícula Web referente às especificações dadas.'''
try:
pagina = 'https://matriculaweb.unb.br/%s/%s.aspx' % (nivel, pagina)
html = requests.get(pagina, params=params, timeout=timeout)
return html.content
except RequestException: # as e:
pass
return ''
class Nivel:
'''Enumeração de níveis de cursos oferecidos.'''
GRADUACAO = 'graduacao'
POS = 'posgraduacao'
class Campus:
'''Enumeração dos códigos de cada campus.'''
DARCY_RIBEIRO = 1
PLANALTINA = 2
CEILANDIA = 3
GAMA = 4
class Departamento:
'''Enumeração dos códigos de cada departamento.'''
CIC = 116
ENE = 163
ENM = 164
EST = 115
GAMA = 650
IFD = 550 # Instituto de Física
MAT = 113
class Habilitacoes:
'''Enumeração das habilitações de cada curso.'''
BCC = 1856 # Ciência da Computação
LIC = 1899 # Computação
ENC = 1741 # Engenharia de Computação
ENM = 6912 # Engenharia de Controle e Automação
class Cursos:
'''Métodos de busca associados a informações de cursos.'''
@staticmethod
def curriculo(curso, nivel=Nivel.GRADUACAO, verbose=False):
'''Acessa o Matrícula Web e retorna um dicionário com a lista de
disciplinas definidas no currículo do curso.
Argumentos:
curso -- o código do curso
nivel -- nível acadêmico do curso
(default Nivel.GRADUACAO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
No caso de disciplinas de cadeias seletivas, o resultado é uma lista em
que cada item tem uma relação 'OU' com os demais, e cada item é um
dicionário cujos itens têm uma relação 'E' entre si. Por exemplo:
o resultado da busca por 6912 (Engenharia Mecatrônica) tem como uma das
cadeias resultantes (a cadeia '2'), a seguinte lista:
[{'114014': 'QUIMICA GERAL'}, {'114634': 'QUI GERAL EXPERIMENTAL',
'114626': 'QUIMICA GERAL TEORICA'}]
que deve ser interpretado como
114014 OU (114626 E 114634)
Ou seja, para graduação na habilitação 6912, é preciso ter sido
aprovado na disciplina QUIMICA GERAL ou ter sido aprovado em ambas as
disciplinas QUI GERAL EXPERIMENTAL e QUIMICA GERAL TEORICA.
'''
OBR_OPT = 'DISCIPLINAS OBRIGATÓRIAS (.*?)</table></td>(.*?)' \
'DISCIPLINAS OPTATIVAS (.*?)</table></td>'
CADEIAS = 'CADEIA: (\d+)(.*?)</table>'
DISCIPLINA = 'disciplina.aspx\?cod=(\d+)>.*?</b> - (.*?)</a></td>' \
'<td><b>(.*?)</b></td><td>(\d+) (\d+) (\d+) (\d+)</td>' \
'<td>(.*?)</td></tr>'
curso = str(curso)
if verbose:
log('Buscando currículo do curso ' + curso)
pagina_html = mweb(nivel, 'curriculo', {'cod': curso})
obr_e_opts = busca(OBR_OPT, pagina_html)
disciplinas = {'obrigatórias': {}, 'cadeias': {}, 'optativas': {}}
for obrigatorias, cadeias, optativas in obr_e_opts:
disciplinas['obrigatórias'] = {}
for (cod, nome, e_ou, teor,
prat, ext, est, area) in busca(DISCIPLINA, obrigatorias):
creditos = {'Teoria': int(teor), 'Prática': int(prat),
'Extensão': int(ext), 'Estudo': int(est)}
disciplinas['obrigatórias'][cod] = {'Nome': nome.strip(),
'Créditos': creditos,
'Área': area.strip()}
for ciclo, discs in busca(CADEIAS, cadeias):
disciplinas['cadeias'][ciclo] = []
current = {}
for (cod, nome, e_ou, teor,
prat, ext, est, area) in busca(DISCIPLINA, discs):
creditos = {'Teoria': int(teor), 'Prática': int(prat),
'Extensão': int(ext), 'Estudo': int(est)}
current[cod] = {'Nome': nome.strip(),
'Créditos': creditos,
'Área': area.strip()}
if e_ou.strip() != 'E':
disciplinas['cadeias'][ciclo].append(current)
current = {}
for (cod, nome, e_ou, teor,
prat, ext, est, area) in busca(DISCIPLINA, optativas):
creditos = {'Teoria': int(teor), 'Prática': int(prat),
'Extensão': int(ext), 'Estudo': int(est)}
disciplinas['optativas'][cod] = {'Nome': nome.strip(),
'Créditos': creditos,
'Área': area.strip()}
return disciplinas
@staticmethod
def fluxo(habilitacao, nivel=Nivel.GRADUACAO, verbose=False):
'''Acessa o Matrícula Web e retorna um dicionário com a lista de
disciplinas por período definidas no fluxo da habilitação.
Argumentos:
habilitacao -- o código da habilitação do curso
nivel -- nível acadêmico do curso
(default Nivel.GRADUACAO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
'''
PERIODO = '<b>PERÍODO: (\d+).*?CRÉDITOS:</b> (\d+)</td>' \
'(.*?)</tr></table>'
DISCIPLINA = 'disciplina.aspx\?cod=\d+>(\d+)</a>'
habilitacao = str(habilitacao)
if verbose:
log('Buscando disciplinas no fluxo da habilitação ' +
habilitacao)
pagina_html = mweb(nivel, 'fluxo', {'cod': habilitacao})
oferta = busca(PERIODO, pagina_html)
disciplinas = {}
for periodo, creditos, dados in oferta:
periodo = int(periodo)
disciplinas[periodo] = {}
disciplinas[periodo]['Créditos'] = creditos
disciplinas[periodo]['Disciplinas'] = busca(DISCIPLINA, dados)
return disciplinas
@staticmethod
def habilitacoes(curso, nivel=Nivel.GRADUACAO,
campus=Campus.DARCY_RIBEIRO, verbose=False):
'''Acessa o Matrícula Web e retorna um dicionário com a lista de
informações referentes a cada habilitação no curso.
Argumentos:
curso -- o código do curso
nivel -- nível acadêmico do curso
(default Nivel.GRADUACAO)
campus -- o campus onde o curso é oferecido
(default DARCY_RIBEIRO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
'''
OPCAO = '<a name=\d+></a><tr .*?><td colspan=3><b>(\d+) - (.*?)' \
'</b></td></tr>.*?' \
'Grau: </td><td .*?>(.*?)</td></tr>.*?' \
'Limite mínimo de permanência: </td>' \
'<td align=right>(\d+)</td>.*?' \
'Limite máximo de permanência: </td>.*?' \
'<td align=right>(\d+)</td>.*?' \
'Quantidade de Créditos para Formatura: </td>' \
'<td align=right>(\d+)</td>.*?' \
'Quantidade mínima de Créditos Optativos ' \
'na Área de Concentração: </td>' \
'<td align=right>(\d+)</td>.*?' \
'Quantidade mínima de Créditos Optativos na Área Conexa: ' \
'</td><td align=right>(\d+)</td>.*?' \
'Quantidade máxima de Créditos no Módulo Livre: </td>' \
'<td align=right>(\d+)</td>'
curso = str(curso)
if verbose:
log('Buscando informações da habilitação do curso ' + curso)
pagina_html = mweb(nivel, 'curso_dados', {'cod': curso})
habilitacoes = busca(OPCAO, pagina_html)
dados = {}
for (habilitacao, nome, grau, l_min, l_max,
formatura, obr, opt, livre) in habilitacoes:
dados[habilitacao] = {}
dados[habilitacao]['Nome'] = nome
dados[habilitacao]['Grau'] = grau
dados[habilitacao]['Limite mínimo de permanência'] = l_min
dados[habilitacao]['Limite máximo de permanência'] = l_max
dados[habilitacao]['Créditos para Formatura'] = formatura
dados[habilitacao]['Mínimo de Créditos Optativos na '
'Área de Concentração'] = obr
dados[habilitacao]['Quantidade mínima de Créditos Optativos '
'na Área Conexa'] = opt
dados[habilitacao]['Quantidade máxima de Créditos no '
'Módulo Livre'] = livre
return dados
@staticmethod
def relacao(nivel=Nivel.GRADUACAO, campus=Campus.DARCY_RIBEIRO,
verbose=False):
'''Acessa o Matrícula Web e retorna um dicionário com a relação de
cursos existentes.
Argumentos:
nivel -- nível acadêmico dos cursos
(default Nivel.GRADUACAO)
campus -- o campus onde o curso é oferecido
(default DARCY_RIBEIRO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
'''
CURSOS = '<tr CLASS=PadraoMenor bgcolor=.*?>'\
'<td>(.*?)</td>' \
'<td>\d+</td>' \
'.*?aspx\?cod=(\d+)>(.*?)</a></td>' \
'<td>(.*?)</td></tr>'
campus = str(campus)
if verbose:
log('Buscando lista de cursos para o campus ' + campus)
pagina_html = mweb(nivel, 'curso_rel', {'cod': campus})
cursos_existentes = busca(CURSOS, pagina_html)
lista = {}
for modalidade, codigo, denominacao, turno in cursos_existentes:
lista[codigo] = {}
lista[codigo]['Modalidade'] = modalidade
lista[codigo]['Denominação'] = denominacao
lista[codigo]['Turno'] = turno
return lista
class Disciplina:
'''Métodos de busca associados a informações de disciplinas.'''
@staticmethod
def informacoes(disciplina, nivel=Nivel.GRADUACAO, verbose=False):
'''Acessa o Matrícula Web e retorna um dicionário com as informações da
disciplina.
Argumentos:
disciplina -- o código da disciplina
nivel -- nível acadêmico da disciplina
(default Nivel.GRADUACAO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
'''
DISCIPLINAS = 'Órgão:</b> </td><td>(\w+) - (.*?)</td></tr>.*?' \
'Denominação:</b> </td><td>(.*?)</td></tr>.*?' \
'Nível:</b> </td><td>(.*?)</td></tr>.*?' \
'Vigência:</b> </td><td>(.*?)</td></tr>.*?' \
'Pré-req:</b> </td><td class=PadraoMenor>(.*?)' \
'</td></tr>.*?' \
'Ementa:</b> </td><td class=PadraoMenor>' \
'<p align=justify>(.*?)</P></td></tr>.*?' \
'(?:.*Programa:</b> </td><td class=PadraoMenor>' \
'<p align=justify>(.*?)</P></td></tr>)?.*?' \
'Bibliografia:</b> </td><td class=PadraoMenor>' \
'<p align=justify>(.*?)</P></td></tr>'
disciplina = str(disciplina)
if verbose:
log('Buscando informações da disciplina ' + disciplina)
pagina_html = mweb(nivel, 'disciplina', {'cod': disciplina})
informacoes = busca(DISCIPLINAS, pagina_html)
infos = {}
for (sigla, nome, denominacao, nivel, vigencia,
pre_req, ementa, programa, bibliografia) in informacoes:
infos['Sigla do Departamento'] = sigla
infos['Nome do Departamento'] = nome
infos['Denominação'] = denominacao
infos['Nível'] = nivel # sobrescreve o argumento da função
infos['Vigência'] = vigencia
infos['Pré-requisitos'] = pre_req.replace('<br>', ' ')
infos['Ementa'] = ementa.replace('<br />', '\n')
if programa:
infos['Programa'] = programa.replace('<br />', '\n')
infos['Bibliografia'] = bibliografia.replace('<br />', '\n')
return infos
@staticmethod
def pre_requisitos(disciplina, nivel=Nivel.GRADUACAO, verbose=False):
'''Dado o código de uma disciplina, acessa o Matrícula Web e retorna
uma lista com os códigos das disciplinas que são pré-requisitos para a
dada.
Argumentos:
disciplina -- o código da disciplina
nivel -- nível acadêmico da disciplina
(default Nivel.GRADUACAO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
Cada item da lista tem uma relação 'OU' com os demais, e cada item é
uma outra lista cujos itens têm uma relação 'E' entre si. Por exemplo:
o resultado da busca por 116424 (Transmissão de Dados) é:
[['117251'], ['116394', '113042']]
que deve ser interpretado como
['117251'] OU ['116394' E '113042']
Ou seja, para cursar a disciplina 116424, é preciso ter sido aprovado
na disciplina 117251 (ARQ DE PROCESSADORES DIGITAIS) ou ter sido
aprovado nas disciplinas 116394 (ORG ARQ DE COMPUTADORES) e 113042
(Cálculo 2).
'''
DISCIPLINAS = '<td valign=top><b>Pré-req:</b> </td>' \
'<td class=PadraoMenor>(.*?)</td></tr>'
CODIGO = '(\d{6})'
disciplina = str(disciplina)
if verbose:
log('Buscando a lista de pré-requisitos para a disciplina ' +
disciplina)
pagina_html = mweb(nivel, 'disciplina_pop', {'cod': disciplina})
requisitos = busca(DISCIPLINAS, pagina_html)
pre_reqs = []
for req in requisitos:
for disciplina in req.split(' OU<br>'):
pre_reqs.append(busca(CODIGO, disciplina))
return [codigo for codigo in pre_reqs if codigo]
class Oferta:
'''Métodos de busca associados a informações da oferta de disciplinas.'''
@staticmethod
def departamentos(nivel=Nivel.GRADUACAO, campus=Campus.DARCY_RIBEIRO,
verbose=False):
'''Acessa o Matrícula Web e retorna um dicionário com a lista de
departamentos com ofertas do semestre atual.
Argumentos:
nivel -- nível acadêmico do Departamento
(default Nivel.GRADUACAO)
campus -- o campus onde o curso é oferecido
(default Campus.DARCY_RIBEIRO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
'''
DEPARTAMENTOS = '<tr CLASS=PadraoMenor bgcolor=.*?>'\
'<td>\d+</td><td>(\w+)</td>' \
'.*?aspx\?cod=(\d+)>(.*?)</a></td></tr>'
if verbose:
log('Buscando a informações de departamentos com oferta')
pagina_html = mweb(nivel, 'oferta_dep', {'cod': str(campus)})
deptos_existentes = busca(DEPARTAMENTOS, pagina_html)
deptos = {}
for sigla, codigo, denominacao in deptos_existentes:
deptos[codigo] = {}
deptos[codigo]['Sigla'] = sigla
deptos[codigo]['Denominação'] = denominacao
return deptos
@staticmethod
def disciplinas(departamento, nivel=Nivel.GRADUACAO, verbose=False):
'''Acessa o Matrícula Web e retorna um dicionário com a lista de
disciplinas ofertadas por um departamento.
Argumentos:
departamento -- o código do Departamento que oferece as disciplinas
nivel -- nível acadêmico das disciplinas buscadas
(default Nivel.GRADUACAO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
Lista completa dos Departamentos da UnB:
matriculaweb.unb.br/matriculaweb/graduacao/oferta_dep.aspx?cod=1
'''
DISCIPLINAS = 'oferta_dados.aspx\?cod=(\d+).*?>(.*?)</a>'
departamento = str(departamento)
if verbose:
log('Buscando a informações de disciplinas do departamento ' +
departamento)
pagina_html = mweb(nivel, 'oferta_dis', {'cod': departamento})
ofertadas = busca(DISCIPLINAS, pagina_html)
oferta = {}
for codigo, nome in ofertadas:
oferta[codigo] = nome
return oferta
@staticmethod
def lista_de_espera(disciplina, turma='\w+', nivel=Nivel.GRADUACAO,
verbose=False):
'''Dado o código de uma disciplina, acessa o Matrícula Web e retorna um
dicionário com a lista de espera para turmas ofertadas da disciplina.
Argumentos:
disciplina -- o código da disciplina
turma -- identificador da turma
(default '\w+') (todas as disciplinas)
nivel -- nível acadêmico da disciplina buscada
(default Nivel.GRADUACAO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
O argumento 'turma' deve ser uma expressão regular.
'''
TABELA = '<td><b>Turma</b></td> ' \
'<td><b>Vagas<br>Solicitadas</b></td> </tr>' \
'<tr CLASS=PadraoMenor bgcolor=.*?> ' \
'.*?</tr><tr CLASS=PadraoBranco>'
TURMAS = '<td align=center >(%s)</td> ' \
'<td align=center >(\d+)</td></tr>' % turma
disciplina = str(disciplina)
if verbose:
log('Buscando turmas com lista de espera para a disciplina ' +
disciplina)
pagina_html = mweb(nivel, 'faltavaga_rel', {'cod': disciplina})
turmas_com_demanda = busca(TABELA, pagina_html)
demanda = {}
for tabela in turmas_com_demanda:
for turma, vagas_desejadas in busca(TURMAS, tabela):
vagas = int(vagas_desejadas)
if vagas > 0:
demanda[turma] = vagas
return demanda
@staticmethod
def oferta(disciplina, depto=None, nivel=Nivel.GRADUACAO,
verbose=False):
'''Dado o código de uma disciplina, e o do Departamento que a oferece,
acessa o Matrícula Web e retorna um dicionário com a lista de turmas
ofertadas para uma disciplina.
Argumentos:
disciplina -- o código da disciplina
depto -- o código do departamento que oferece a disciplina
(default Departamento.CIC)
nivel -- nível acadêmico da disciplina
(default Nivel.GRADUACAO)
verbose -- indicação dos procedimentos sendo adotados
(default False)
'''
INFORMACOES = 'Departamento: <strong><a href.*?>(.*?)</a></strong>' \
'.*?' \
'Nome: <a title=.*?>(.*?)<img .*?></a>' \
'.*?' \
'<b>Créditos</b><br>\(Teor-Prat-Ext-Est\)<br>' \
'<font.*?>(\d+)-(\d+)-(\d+)-(\d+)'
TURMAS = '<b>Turma</b>.*?<font size=4><b>(\w+)</b></font></div>' \
'.*?' \
'<td>Total</td><td>Vagas</td><td><b>(\d+)</b>' \
'.*?' \
'<td>Ocupadas</td>' \
'<td><b><font color=(?:red|green)>(\d+)</font></b></td>' \
'(.*?)' \
'<center>(.*?)(?:|<br>)</center>' \
'.*?' \
'(Reserva para curso(.*?))?' \
'<tr><td colspan=6 bgcolor=white height=20></td></tr>'
HORARIO = '<b>((?:Segunda|Terça|Quarta|Quinta|Sexta|Sábado|Domingo))' \
'</b>.*?' \
'<font size=1 color=black><b>(.*?)</font>.*?' \
'<font size=1 color=brown>(.*?)</b></font><br><i>' \
'<img src=/imagens/subseta_dir.gif align=top> (.*?)</i>'
RESERVA = '<td align=left>(.*?)</td>' \
'<td align=center>(\d+)</td>' \
'<td align=center>(\d+)</td>'
disciplina = str(disciplina)
if verbose:
log('Buscando as turmas da disciplina ' + disciplina)
params = {'cod': disciplina}
if depto:
params['dep'] = str(depto)
pagina_html = mweb(nivel, 'oferta_dados', params)
informacoes = busca(INFORMACOES, pagina_html)
oferta = {}
for (departamento, nome, teor, prat, ext, est) in informacoes:
oferta['Departamento'] = departamento
oferta['Nome'] = nome
oferta['Créditos'] = {'Teoria': int(teor), 'Prática': int(prat),
'Extensão': int(ext), 'Estudo': int(est)}
turmas = busca(TURMAS, pagina_html)
turmas_ofertadas = {}
for (t, vagas, ocupadas, horarios, docentes, aux, reservas) in turmas:
turma = {'Vagas': int(vagas),
'Alunos Matriculados': int(ocupadas),
'Professores': docentes.split('<br>')}
turma['Aulas'] = {}
for dia, inicio, fim, local in busca(HORARIO, horarios):
if dia not in turma['Aulas']:
turma['Aulas'][dia] = []
turma['Aulas'][dia].append({'Início': inicio,
'Fim': fim,
'Local': local})
if reservas:
turma['Turma Reservada'] = {curso: {'Vagas': int(vagas),
'Calouros': int(calouros)}
for curso, vagas, calouros
in busca(RESERVA, reservas)}
turmas_ofertadas[t] = turma
oferta['Turmas'] = turmas_ofertadas
return oferta
def log(msg):
'''Log de mensagens.'''
print(msg)