jupiterweb-scraper 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jupiterweb/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ import importlib.metadata
2
+
3
+ from .disciplina import Disciplina, HorarioAula, Oferecimento, Requisito
4
+ from .instituto import Instituto
5
+ from .jupiterweb import obter_institutos
6
+
7
+ try:
8
+ __version__ = importlib.metadata.version("your_package")
9
+ except importlib.metadata.PackageNotFoundError:
10
+ __version__ = "0.0.0"
11
+
12
+ __all__ = [
13
+ "Disciplina",
14
+ "HorarioAula",
15
+ "Oferecimento",
16
+ "Requisito",
17
+ "Instituto",
18
+ "obter_institutos",
19
+ ]
@@ -0,0 +1,382 @@
1
+ {
2
+ "1": {
3
+ "nome": "Pró-Reitoria de Graduação - Cursos Interunidades",
4
+ "campus": "Butantã",
5
+ "abrev": "PRG"
6
+ },
7
+ "2": {
8
+ "nome": "Faculdade de Direito",
9
+ "campus": "Quadrilátero",
10
+ "abrev": "FD"
11
+ },
12
+ "3": {
13
+ "nome": "Escola Politécnica",
14
+ "campus": "Butantã",
15
+ "abrev": "POLI"
16
+ },
17
+ "4": {
18
+ "nome": "Instituto de Energia e Ambiente",
19
+ "campus": "Butantã",
20
+ "abrev": "IEE"
21
+ },
22
+ "5": {
23
+ "nome": "Faculdade de Medicina",
24
+ "campus": "Quadrilátero",
25
+ "abrev": "FM"
26
+ },
27
+ "6": {
28
+ "nome": "Faculdade de Saúde Pública",
29
+ "campus": "Quadrilátero",
30
+ "abrev": "FSP"
31
+ },
32
+ "7": {
33
+ "nome": "Escola de Enfermagem",
34
+ "campus": "Quadrilátero",
35
+ "abrev": "EE"
36
+ },
37
+ "8": {
38
+ "nome": "Faculdade de Filosofia, Letras e Ciências Humanas",
39
+ "campus": "Butantã",
40
+ "abrev": "FFLCH"
41
+ },
42
+ "9": {
43
+ "nome": "Faculdade de Ciências Farmacêuticas",
44
+ "campus": "Butantã",
45
+ "abrev": "FCF"
46
+ },
47
+ "10": {
48
+ "nome": "Faculdade de Medicina Veterinária e Zootecnia",
49
+ "campus": "Butantã",
50
+ "abrev": "FMVZ"
51
+ },
52
+ "11": {
53
+ "nome": "Escola Superior de Agricultura \"Luiz de Queiroz\"",
54
+ "campus": "Piracicaba",
55
+ "abrev": "ESALQ"
56
+ },
57
+ "12": {
58
+ "nome": "Faculdade de Economia, Administração, Contabilidade e Atuária",
59
+ "campus": "Butantã",
60
+ "abrev": "FEA"
61
+ },
62
+ "13": {
63
+ "nome": "Escola Politécnica e Faculdade de Medicina",
64
+ "campus": "Butantã",
65
+ "abrev": "POLI/FM"
66
+ },
67
+ "14": {
68
+ "nome": "Instituto de Astronomia, Geofísica e Ciências Atmosféricas",
69
+ "campus": "Butantã",
70
+ "abrev": "IAG"
71
+ },
72
+ "15": {
73
+ "nome": "Faculdade de Odontologia, Instituto de Ciências Biomédicas, Instituto de Química e Instituto de Biociências",
74
+ "campus": "Butantã",
75
+ "abrev": "FO/ICB/IQ/IB"
76
+ },
77
+ "16": {
78
+ "nome": "Faculdade de Arquitetura e Urbanismo e de Design",
79
+ "campus": "Butantã",
80
+ "abrev": "FAU"
81
+ },
82
+ "17": {
83
+ "nome": "Faculdade de Medicina de Ribeirão Preto",
84
+ "campus": "Ribeirão Preto",
85
+ "abrev": "FMRP"
86
+ },
87
+ "18": {
88
+ "nome": "Escola de Engenharia de São Carlos",
89
+ "campus": "São Carlos",
90
+ "abrev": "EESC"
91
+ },
92
+ "20": {
93
+ "nome": "Escola Politécnica, Instituto de Matemática, Estatística e Ciência da Computação, Instituto de Física",
94
+ "campus": "Butantã",
95
+ "abrev": "POLI/IME/IF"
96
+ },
97
+ "21": {
98
+ "nome": "Instituto Oceanográfico",
99
+ "campus": "Butantã",
100
+ "abrev": "IO"
101
+ },
102
+ "22": {
103
+ "nome": "Escola de Enfermagem de Ribeirão Preto",
104
+ "campus": "Ribeirão Preto",
105
+ "abrev": "EERP"
106
+ },
107
+ "23": {
108
+ "nome": "Faculdade de Odontologia",
109
+ "campus": "Butantã",
110
+ "abrev": "FO"
111
+ },
112
+ "24": {
113
+ "nome": "Faculdade de Medicina de Bauru",
114
+ "campus": "Bauru",
115
+ "abrev": "FMBRU"
116
+ },
117
+ "25": {
118
+ "nome": "Faculdade de Odontologia de Bauru",
119
+ "campus": "Bauru",
120
+ "abrev": "FOB"
121
+ },
122
+ "26": {
123
+ "nome": "Instituto de Astronomia, Geofísica e Ciências Atmosféricas e Instituto de Geociências",
124
+ "campus": "Butantã",
125
+ "abrev": "IAG/IGc"
126
+ },
127
+ "27": {
128
+ "nome": "Escola de Comunicações e Artes",
129
+ "campus": "Butantã",
130
+ "abrev": "ECA"
131
+ },
132
+ "30": {
133
+ "nome": "Centro de Biologia Marinha",
134
+ "campus": "São Sebastião",
135
+ "abrev": "CEBIMar"
136
+ },
137
+ "31": {
138
+ "nome": "Instituto de Estudos Brasileiros",
139
+ "campus": "Butantã",
140
+ "abrev": "IEB"
141
+ },
142
+ "32": {
143
+ "nome": "Museu de Arte Contemporânea",
144
+ "campus": "Butantã",
145
+ "abrev": "MAC"
146
+ },
147
+ "33": {
148
+ "nome": "Museu Paulista",
149
+ "campus": "Ipiranga",
150
+ "abrev": "MP"
151
+ },
152
+ "37": {
153
+ "nome": "Instituto de Estudos Avançados",
154
+ "campus": "Butantã",
155
+ "abrev": "IEA"
156
+ },
157
+ "38": {
158
+ "nome": "Museu de Zoologia",
159
+ "campus": "Ipiranga",
160
+ "abrev": "MZUSP"
161
+ },
162
+ "39": {
163
+ "nome": "Escola de Educação Física e Esporte",
164
+ "campus": "Butantã",
165
+ "abrev": "EEFE"
166
+ },
167
+ "41": {
168
+ "nome": "Instituto de Biociências",
169
+ "campus": "Butantã",
170
+ "abrev": "IB"
171
+ },
172
+ "42": {
173
+ "nome": "Instituto de Ciências Biomédicas",
174
+ "campus": "Butantã",
175
+ "abrev": "ICB"
176
+ },
177
+ "43": {
178
+ "nome": "Instituto de Física",
179
+ "campus": "Butantã",
180
+ "abrev": "IF"
181
+ },
182
+ "44": {
183
+ "nome": "Instituto de Geociências",
184
+ "campus": "Butantã",
185
+ "abrev": "IGc"
186
+ },
187
+ "45": {
188
+ "nome": "Instituto de Matemática, Estatística e Ciência da Computação",
189
+ "campus": "Butantã",
190
+ "abrev": "IME"
191
+ },
192
+ "46": {
193
+ "nome": "Instituto de Química",
194
+ "campus": "Butantã",
195
+ "abrev": "IQ"
196
+ },
197
+ "47": {
198
+ "nome": "Instituto de Psicologia",
199
+ "campus": "Butantã",
200
+ "abrev": "IP"
201
+ },
202
+ "48": {
203
+ "nome": "Faculdade de Educação",
204
+ "campus": "Butantã",
205
+ "abrev": "FE"
206
+ },
207
+ "55": {
208
+ "nome": "Instituto de Ciências Matemáticas e de Computação",
209
+ "campus": "São Carlos",
210
+ "abrev": "ICMC"
211
+ },
212
+ "58": {
213
+ "nome": "Faculdade de Odontologia de Ribeirão Preto",
214
+ "campus": "Ribeirão Preto",
215
+ "abrev": "FORP"
216
+ },
217
+ "59": {
218
+ "nome": "Faculdade de Filosofia, Ciências e Letras de Ribeirão Preto",
219
+ "campus": "Ribeirão Preto",
220
+ "abrev": "FFCLRP"
221
+ },
222
+ "60": {
223
+ "nome": "Faculdade de Ciências Farmacêuticas de Ribeirão Preto",
224
+ "campus": "Ribeirão Preto",
225
+ "abrev": "FCFRP"
226
+ },
227
+ "61": {
228
+ "nome": "Hospital de Reabilitação de Anomalias Craniofaciais",
229
+ "campus": "Bauru",
230
+ "abrev": "HRAC"
231
+ },
232
+ "64": {
233
+ "nome": "Centro de Energia Nuclear na Agricultura",
234
+ "campus": "Piracicaba",
235
+ "abrev": "CENA"
236
+ },
237
+ "65": {
238
+ "nome": "Pró-Reitoria de Graduação - Licenciatura em Ciências - Semipresencial",
239
+ "campus": "Butantã",
240
+ "abrev": "PRG-LC"
241
+ },
242
+ "66": {
243
+ "nome": "Física Médica - Instituto de Física e Faculdade de Medicina",
244
+ "campus": "Butantã",
245
+ "abrev": "IF/FM"
246
+ },
247
+ "67": {
248
+ "nome": "Faculdade de Medicina, Instituto de Ciências Biomédicas, Instituto de Química e Instituto de Biociências",
249
+ "campus": "Quadrilátero",
250
+ "abrev": "FM/ICB/IQ/IB"
251
+ },
252
+ "68": {
253
+ "nome": "Faculdade de Odontologia e Instituto de Ciências Biomédicas",
254
+ "campus": "Butantã",
255
+ "abrev": "FO/ICB"
256
+ },
257
+ "69": {
258
+ "nome": "Instituto de Biociências e Faculdade de Arquitetura e Urbanismo",
259
+ "campus": "Butantã",
260
+ "abrev": "IB/FAU"
261
+ },
262
+ "71": {
263
+ "nome": "Museu de Arqueologia e Etnologia",
264
+ "campus": "Butantã",
265
+ "abrev": "MAE"
266
+ },
267
+ "74": {
268
+ "nome": "Faculdade de Zootecnia e Engenharia de Alimentos",
269
+ "campus": "Pirassununga",
270
+ "abrev": "FZEA"
271
+ },
272
+ "75": {
273
+ "nome": "Instituto de Química de São Carlos",
274
+ "campus": "São Carlos",
275
+ "abrev": "IQSC"
276
+ },
277
+ "76": {
278
+ "nome": "Instituto de Física de São Carlos",
279
+ "campus": "São Carlos",
280
+ "abrev": "IFSC"
281
+ },
282
+ "81": {
283
+ "nome": "Faculdade de Economia, Administração e Contabilidade de Ribeirão Preto",
284
+ "campus": "Ribeirão Preto",
285
+ "abrev": "FEARP"
286
+ },
287
+ "83": {
288
+ "nome": "Instituto de Medicina Tropical de São Paulo",
289
+ "campus": "Quadrilátero",
290
+ "abrev": "IMT"
291
+ },
292
+ "85": {
293
+ "nome": "Instituto de Pesquisas Energéticas e Nucleares",
294
+ "campus": "Butantã",
295
+ "abrev": "IPEN"
296
+ },
297
+ "86": {
298
+ "nome": "Escola de Artes, Ciências e Humanidades",
299
+ "campus": "Leste",
300
+ "abrev": "EACH"
301
+ },
302
+ "87": {
303
+ "nome": "Instituto de Relações Internacionais",
304
+ "campus": "Butantã",
305
+ "abrev": "IRI"
306
+ },
307
+ "88": {
308
+ "nome": "Escola de Engenharia de Lorena",
309
+ "campus": "Lorena",
310
+ "abrev": "EEL"
311
+ },
312
+ "89": {
313
+ "nome": "Faculdade de Direito de Ribeirão Preto",
314
+ "campus": "Ribeirão Preto",
315
+ "abrev": "FDRP"
316
+ },
317
+ "90": {
318
+ "nome": "Licenciatura em Ciências Exatas - São Carlos",
319
+ "campus": "São Carlos",
320
+ "abrev": "LCE"
321
+ },
322
+ "91": {
323
+ "nome": "Faculdade de Filosofia, Ciências e Letras de Ribeirão Preto e Faculdade de Medicina de Ribeirão Preto",
324
+ "campus": "Ribeirão Preto",
325
+ "abrev": "FFCLRP/FMRP"
326
+ },
327
+ "92": {
328
+ "nome": "Instituto de Biociências e Centro de Biologia Marinha",
329
+ "campus": "Butantã",
330
+ "abrev": "IB/CEBIMar"
331
+ },
332
+ "93": {
333
+ "nome": "Instituto de Astronomia, Geofísica e Ciências Atmosféricas e Instituto Oceanográfico",
334
+ "campus": "Butantã",
335
+ "abrev": "IAG/IO"
336
+ },
337
+ "94": {
338
+ "nome": "Escola de Enfermagem de Ribeirão Preto e Faculdade de Economia, Administração e Contabilidade de Ribeirão Preto",
339
+ "campus": "Ribeirão Preto",
340
+ "abrev": "EERP/FEARP"
341
+ },
342
+ "95": {
343
+ "nome": "Faculdade de Odontologia de Ribeirão Preto e Faculdade de Ciências Farmacêuticas de Ribeirão Preto",
344
+ "campus": "Ribeirão Preto",
345
+ "abrev": "FORP/FCFRP"
346
+ },
347
+ "96": {
348
+ "nome": "Faculdade de Filosofia, Ciências e Letras de Ribeirão Preto e Faculdade de Economia, Administração e Contabilidade de Ribeirão Preto",
349
+ "campus": "Ribeirão Preto",
350
+ "abrev": "FFCLRP/FEARP"
351
+ },
352
+ "97": {
353
+ "nome": "Escola de Engenharia de São Carlos e Instituto de Ciências Matemáticas e de Computação",
354
+ "campus": "São Carlos",
355
+ "abrev": "EESC/ICMC"
356
+ },
357
+ "98": {
358
+ "nome": "Escola de Educação Física e Esporte de Ribeirão Preto",
359
+ "campus": "Ribeirão Preto",
360
+ "abrev": "EEFERP"
361
+ },
362
+ "99": {
363
+ "nome": "Instituto de Arquitetura e Urbanismo de São Carlos",
364
+ "campus": "São Carlos",
365
+ "abrev": "IAU"
366
+ },
367
+ "100": {
368
+ "nome": "Faculdade de Ciências Farmacêuticas e Instituto de Ciências Biomédicas",
369
+ "campus": "Butantã",
370
+ "abrev": "FCF/ICB"
371
+ },
372
+ "101": {
373
+ "nome": "Instituto de Química e Faculdade de Ciências Farmacêuticas",
374
+ "campus": "Butantã",
375
+ "abrev": "IQ/FCF"
376
+ },
377
+ "102": {
378
+ "nome": "Faculdade de Saúde Pública e Faculdade de Ciências Farmacêuticas",
379
+ "campus": "Quadrilátero",
380
+ "abrev": "FSP/FCF"
381
+ }
382
+ }
@@ -0,0 +1,373 @@
1
+ import re
2
+ import unicodedata
3
+ from typing import Any
4
+ from warnings import warn
5
+
6
+ from .urls import URLS
7
+ from .utils import obter_soup, truncate_string
8
+
9
+
10
+ class Disciplina:
11
+ """
12
+ Disciplina cadastrada no Jupiterweb.
13
+ """
14
+
15
+ def __init__(self, sigla: str) -> None:
16
+ self.sigla = str(sigla).upper()
17
+ self._dados: dict[str, Any] = {}
18
+ self._carregado = False
19
+
20
+ def __repr__(self) -> str:
21
+ return f"Disciplina(sigla='{self.sigla}')"
22
+
23
+ def __str__(self) -> str:
24
+ return self.sigla
25
+
26
+ def __getitem__(self, key: str) -> Any:
27
+ return self.obter_dados()[key]
28
+
29
+ def obter_dados(self) -> dict[str, Any]:
30
+ """
31
+ Retorna dados da disciplina no Jupiterweb. Se disciplina ainda nao foi carregada,
32
+ faz o scraping antes de retornar.
33
+ """
34
+
35
+ if not self._carregado:
36
+ self._carregar()
37
+ return self._dados
38
+
39
+ @property
40
+ def url_principal(self) -> str:
41
+ """
42
+ URL da pagina principal da disciplina no Jupiterweb.
43
+ """
44
+
45
+ return URLS["disciplina"].format(sigla=self.sigla)
46
+
47
+ @property
48
+ def url_oferecimento(self) -> str:
49
+ """
50
+ URL da pagina de oferecimentos da disciplina no Jupiterweb.
51
+ """
52
+
53
+ return URLS["oferecimento"].format(sigla=self.sigla)
54
+
55
+ @property
56
+ def url_requisitos(self) -> str:
57
+ """
58
+ URL da pagina de requisitos da disciplina no Jupiterweb.
59
+ """
60
+
61
+ return URLS["requisitos"].format(sigla=self.sigla)
62
+
63
+ def _normalizar_titulo(self, title: str) -> str:
64
+ """
65
+ Converte titulo ao formato padrao para chaves de dicionario.
66
+ """
67
+
68
+ title = title.strip().rstrip(":")
69
+ title = unicodedata.normalize("NFKD", title)
70
+ title = title.encode("ascii", "ignore").decode()
71
+ title = title.lower()
72
+ title = re.sub(r" +", " ", title)
73
+ return title
74
+
75
+ def _carregar_principal(self) -> None:
76
+ """
77
+ Faz scraping da pagina principal da disciplina e armazena os dados obtidos.
78
+ """
79
+
80
+ soup = obter_soup(self.url_principal)
81
+
82
+ table = soup.select_one("form[name='form1'] > table")
83
+ if not table:
84
+ warn(f"Nao foi possivel carregar pagina principal da disciplina {self.sigla}")
85
+ return
86
+
87
+ # ----- Texto centralizado -----
88
+ centered_text = [i.get_text(strip=True) for i in table.select("td[align='CENTER']")]
89
+ centered_text = [(centered_text[i] if len(centered_text) > i else "") for i in range(4)]
90
+
91
+ self._dados["instituto"] = centered_text[0]
92
+ self._dados["departamento"] = centered_text[1]
93
+ self._dados["nome"] = centered_text[2].removeprefix("Disciplina:").split("-", 1)[1].strip()
94
+ self._dados["nome ingles"] = centered_text[3]
95
+
96
+ # ----- Texto livre -----
97
+ span_text = table.select("span.txt_arial_8pt_gray, span.txt_arial_8pt_black")
98
+
99
+ title = "" # guardar texto em self._dados[title] (se subtitle == "")
100
+ subtitle = "" # guardar texto em self._dados[title][subtitle] (se subtitle != "")
101
+ subtitle_tab = None # tabela em que subtitle foi encontrado
102
+ added_text = False # se texto ja foi adicionado ao titulo atual (nao pode ter subtitulos)
103
+
104
+ for span in span_text:
105
+ text = span.get_text(strip=True, separator="\n")
106
+
107
+ if span.has_attr("class") and "txt_arial_8pt_black" in span["class"]: # titulo ou subtitulo
108
+ text = self._normalizar_titulo(text)
109
+ tab = span.find_parent("table")
110
+
111
+ if title and ((not subtitle and not added_text) or (subtitle and tab == subtitle_tab)): # subtitulo
112
+ subtitle = text
113
+ subtitle_tab = tab
114
+
115
+ if (title not in self._dados) or (not isinstance(self._dados[title], dict)):
116
+ self._dados[title] = {}
117
+ self._dados[title][subtitle] = ""
118
+ else: # titulo
119
+ title = text
120
+ subtitle = ""
121
+ self._dados[title] = ""
122
+ added_text = False
123
+ else: # texto
124
+ if subtitle:
125
+ self._dados[title][subtitle] += "\n" + text if self._dados[title][subtitle] and text else text
126
+ else:
127
+ self._dados[title] += "\n" + text if self._dados[title] and text else text
128
+ added_text = True # mesmo se text = "" o titulo nao pode ter subtitulos
129
+
130
+ def _carregar_requisitos(self) -> None:
131
+ """
132
+ Faz scraping da pagina de requisitos da disciplina e armazena os dados obtidos.
133
+ """
134
+
135
+ # dados["requisitos"][curso] = [["x"], ["y", "z"]] significa que, para fazer a
136
+ # disciplina, alunos de 'curso' precisam ter feito a disciplina "x", ou ter
137
+ # feito ambas as disciplinas "y" e "z".
138
+
139
+ self._dados["requisitos"] = {}
140
+ self._dados["periodo ideal"] = {}
141
+
142
+ soup = obter_soup(self.url_requisitos)
143
+ table = soup.select_one("form[name='form1'] > table")
144
+
145
+ if not table:
146
+ return # sem requisitos
147
+
148
+ rows = table.select("tr.txt_verdana_8pt_gray")
149
+ curso = ""
150
+ index = 0 # adicionar em self._dados["requisitos"][curso][index]
151
+
152
+ for row in rows:
153
+ td = row.find_all("td")
154
+ if not td:
155
+ continue
156
+
157
+ txt = " ".join(td[0].text.strip().split())
158
+ if not txt:
159
+ continue
160
+
161
+ if txt.startswith("Curso"):
162
+ sep = txt.removeprefix("Curso:").split(" - Período ideal:", 1)
163
+
164
+ curso = sep[0].strip()
165
+ index = 0
166
+ self._dados["requisitos"][curso] = [[]]
167
+
168
+ if len(sep) > 1:
169
+ self._dados["periodo ideal"][curso] = int(sep[1])
170
+ elif curso and txt.lower() == "ou":
171
+ index += 1
172
+ self._dados["requisitos"][curso].append([])
173
+ elif curso:
174
+ sigla = txt.split("-", 1)[0].strip().upper()
175
+ tipo = td[1].get_text(strip=True)
176
+ if not tipo:
177
+ tipo = "requisito"
178
+
179
+ req = Requisito(sigla, tipo)
180
+ self._dados["requisitos"][curso][index].append(req)
181
+
182
+ def _carregar_oferecimento(self) -> None:
183
+ """
184
+ Faz scraping da pagina de oferecimento da disciplina e armazena os dados obtidos.
185
+ """
186
+
187
+ self._dados["oferecimento"] = []
188
+
189
+ soup = obter_soup(self.url_oferecimento)
190
+ table = soup.select_one("div#layout_principal > table:nth-of-type(4)")
191
+
192
+ if not table:
193
+ return # sem oferecimentos
194
+
195
+ boxes = table.select_one("td").find_all("div", recursive=False)
196
+
197
+ for box in boxes:
198
+ box_tables = box.find_all("table", recursive=False)
199
+
200
+ # ----- Informacao basica -----
201
+ info_text = [i.get_text(strip=True) for i in box_tables[0].select("span.txt_arial_8pt_gray")]
202
+ info_text = [(info_text[i] if len(info_text) > i else "") for i in range(5)]
203
+
204
+ oferecimento = Oferecimento(
205
+ codigo=info_text[0],
206
+ data_inicio=info_text[1],
207
+ data_fim=info_text[2],
208
+ tipo_turma=info_text[3],
209
+ observacoes=info_text[4],
210
+ sigla_disciplina=self.sigla,
211
+ )
212
+
213
+ # ----- Horarios -----
214
+ horarios_rows = box_tables[1].find_all("tr", recursive=False)[1:]
215
+
216
+ for row in horarios_rows:
217
+ row_text = [i.get_text(strip=True) for i in row.find_all("td", recursive=False)]
218
+ row_text = [(row_text[i] if len(row_text) > i else "") for i in range(4)]
219
+
220
+ oferecimento.adicionar_horario(row_text[0], row_text[1], row_text[2], row_text[3])
221
+
222
+ # ----- Vagas -----
223
+ vagas_rows = box_tables[2].find_all("tr", recursive=False)
224
+ vagas_labels = [i.get_text(strip=True).lower() for i in vagas_rows[0].find_all("td", recursive=False)][1:]
225
+ vagas_labels = [self._normalizar_titulo(i) for i in vagas_labels]
226
+
227
+ tipo_vaga = ""
228
+
229
+ for row in vagas_rows[1:]:
230
+ row_text = [i.get_text(strip=True) for i in row.find_all("td", recursive=False)]
231
+
232
+ istitle = row_text[0] != ""
233
+ if not istitle:
234
+ row_text = row_text[1:]
235
+
236
+ row_name = row_text[0]
237
+ row_vals = [(int(i) if i.isnumeric() else "-") for i in row_text[1:]]
238
+ row_vals = [(row_vals[i] if len(row_vals) > i else "-") for i in range(len(vagas_labels))]
239
+ row_items = {vagas_labels[i]: row_vals[i] for i in range(len(vagas_labels))}
240
+
241
+ if istitle: # novo tipo de vaga
242
+ tipo_vaga = self._normalizar_titulo(row_name)
243
+ oferecimento.vagas[tipo_vaga] = row_items
244
+ oferecimento.vagas[tipo_vaga]["cursos"] = {}
245
+ else:
246
+ oferecimento.vagas[tipo_vaga]["cursos"][row_name] = row_items
247
+
248
+ self._dados["oferecimento"].append(oferecimento)
249
+
250
+ def _carregar(self) -> None:
251
+ """
252
+ Faz scraping da disciplina e armazena os seus dados.
253
+ """
254
+
255
+ self._dados = {
256
+ "sigla": self.sigla,
257
+ }
258
+
259
+ self._carregar_principal()
260
+ self._carregar_requisitos()
261
+ self._carregar_oferecimento()
262
+ self._carregado = True
263
+
264
+ def possui_oferecimento(self) -> bool:
265
+ """
266
+ Verifica se disciplina tem algum oferecimento no semestre atual.
267
+ """
268
+
269
+ return bool(self.obter_dados().get("oferecimento"))
270
+
271
+ def mostrar(self, trunc_str: bool = True) -> None:
272
+ """
273
+ Mostra dados da disciplina de forma legivel. Utilizada principalmente
274
+ para debug. Se trunc_str = True, strings longas serao truncadas.
275
+ """
276
+
277
+ LARGURA = 120
278
+
279
+ for key, val in self.obter_dados().items():
280
+ print(f"\n{key}{'─'*max(0, LARGURA-len(key))}")
281
+
282
+ if not val:
283
+ print(" (vazio)")
284
+ elif isinstance(val, dict):
285
+ for subkey, subval in val.items():
286
+ print(f" {subkey}{'─'*max(0, LARGURA-len(subkey)-1)}")
287
+
288
+ if isinstance(subval, str) and trunc_str:
289
+ subval = truncate_string(subval, LARGURA - 4)
290
+ print(f" {subval}")
291
+ else:
292
+ if isinstance(val, str) and trunc_str:
293
+ val = truncate_string(val, LARGURA - 2)
294
+ print(f" {val}")
295
+
296
+
297
+ class Requisito:
298
+ """
299
+ Requisito de disciplina no Jupiterweb.
300
+ """
301
+
302
+ def __init__(self, sigla: str, tipo: str = "requisito") -> None:
303
+ self.sigla = str(sigla)
304
+ self.tipo = str(tipo).lower() # requisito fraco, indicacao de conjunto, etc.
305
+
306
+ def __repr__(self) -> str:
307
+ return f"Requisito(sigla='{self.sigla}',tipo='{self.tipo}')"
308
+
309
+ def __str__(self) -> str:
310
+ return self.sigla
311
+
312
+ def obter_disciplina(self) -> Disciplina:
313
+ """
314
+ Retorna objeto Disciplina correspondente ao requisito.
315
+ """
316
+
317
+ return Disciplina(self.sigla)
318
+
319
+
320
+ class Oferecimento:
321
+ """
322
+ Oferecimento de turma no Jupiterweb.
323
+ """
324
+
325
+ def __init__(
326
+ self,
327
+ codigo: str,
328
+ data_inicio: str,
329
+ data_fim: str,
330
+ tipo_turma: str,
331
+ observacoes: str = "",
332
+ sigla_disciplina: str = "",
333
+ ) -> None:
334
+ self.codigo = str(codigo).upper()
335
+ self.data_inicio = data_inicio
336
+ self.data_fim = data_fim
337
+ self.tipo_turma = str(tipo_turma).lower()
338
+ self.observacoes = observacoes
339
+ self.sigla_disciplina = str(sigla_disciplina).upper()
340
+ self.horarios: list[HorarioAula] = []
341
+ self.vagas = {}
342
+
343
+ def __repr__(self) -> str:
344
+ return f"Oferecimento(codigo='{self.codigo}',data_inicio='{self.data_inicio}',data_fim='{self.data_fim}',tipo_turma='{self.tipo_turma}',observacoes='{self.observacoes}',sigla_disciplina='{self.sigla_disciplina}')"
345
+
346
+ def __str__(self) -> str:
347
+ return f"Turma {self.codigo}"
348
+
349
+ def adicionar_horario(self, dia_semana: str, hora_inicio: str, hora_fim: str, professor: str) -> None:
350
+ """
351
+ Adiciona horario de aula ao oferecimento.
352
+ """
353
+
354
+ horario = HorarioAula(dia_semana, hora_inicio, hora_fim, professor)
355
+ self.horarios.append(horario)
356
+
357
+
358
+ class HorarioAula:
359
+ """
360
+ Horario de aula no Jupiterweb.
361
+ """
362
+
363
+ def __init__(self, dia_semana: str, hora_inicio: str, hora_fim: str, professor: str) -> None:
364
+ self.dia_semana = str(dia_semana).lower()
365
+ self.hora_inicio = hora_inicio
366
+ self.hora_fim = hora_fim
367
+ self.professor = professor
368
+
369
+ def __repr__(self) -> str:
370
+ return f"HorarioAula(dia_semana='{self.dia_semana}',hora_inicio='{self.hora_inicio}',hora_fim='{self.hora_fim}',professor='{self.professor}')"
371
+
372
+ def __str__(self) -> str:
373
+ return f"{self.dia_semana} ({self.hora_inicio} - {self.hora_fim}) Prof(a). {self.professor}"
@@ -0,0 +1,61 @@
1
+ from .disciplina import Disciplina
2
+ from .urls import URLS
3
+ from .utils import obter_soup
4
+
5
+
6
+ class Instituto:
7
+ """
8
+ Unidade de ensino cadastrada no Jupiterweb.
9
+ """
10
+
11
+ def __init__(self, codigo: str, nome: str, campus: str, abrev: str) -> None:
12
+ self.codigo = str(codigo)
13
+ self.nome = nome
14
+ self.campus = campus
15
+ self.abrev = abrev
16
+ self.disciplinas = []
17
+ self._carregado = False
18
+
19
+ def __repr__(self) -> str:
20
+ return f"Instituto(codigo='{self.codigo}',nome='{self.nome}',campus='{self.campus}',abrev='{self.abrev}')"
21
+
22
+ def __str__(self) -> str:
23
+ return self.nome
24
+
25
+ def _carregar(self) -> None:
26
+ """
27
+ Faz scraping da pagina com as disciplinas do instituto e armazena
28
+ os objetos do tipo Disciplina correspondentes (delega o scraping das
29
+ disciplinas, que é feito sob demanda).
30
+ """
31
+
32
+ if self._carregado:
33
+ return
34
+
35
+ soup = obter_soup(self.url_listagem)
36
+ disciplina_rows = soup.select("tr[bgcolor='#658CCF'] ~tr")
37
+
38
+ for row in disciplina_rows:
39
+ tds = row.find_all("td")
40
+ sigla = tds[0].find("span").get_text(strip=True)
41
+ self.disciplinas.append(Disciplina(sigla))
42
+
43
+ self._carregado = True
44
+
45
+ @property
46
+ def url_listagem(self) -> str:
47
+ """
48
+ URL do Jupiterweb com todas as disciplinas oferecidas pela unidade de ensino.
49
+ """
50
+
51
+ return URLS["listagem"].format(codigo=self.codigo)
52
+
53
+ def obter_disciplinas(self) -> list[Disciplina]:
54
+ """
55
+ Retorna lista de disciplinas oferecidas no instituto.
56
+ """
57
+
58
+ if not self._carregado:
59
+ self._carregar()
60
+
61
+ return self.disciplinas
@@ -0,0 +1,15 @@
1
+ import json
2
+
3
+ from .instituto import Instituto
4
+ from .paths import PATHS
5
+
6
+
7
+ def obter_institutos() -> list[Instituto]:
8
+ """
9
+ Retorna lista com todas as unidades de ensino cadastradas no Jupiterweb (delega
10
+ o scraping da pagina da unidade e de suas disciplinas, que é feito sob demanda).
11
+ """
12
+
13
+ with open(PATHS["institutos"], "r", encoding="utf-8") as f:
14
+ data = json.load(f)
15
+ return [Instituto(codigo, data[codigo]["nome"], data[codigo]["campus"], data[codigo]["abrev"]) for codigo in data]
jupiterweb/paths.py ADDED
@@ -0,0 +1,8 @@
1
+ from pathlib import Path
2
+
3
+ ROOT_DIR = Path(__file__).parent.resolve()
4
+ DATA_DIR = ROOT_DIR / "data"
5
+
6
+ PATHS = {
7
+ "institutos": DATA_DIR / "institutos.json",
8
+ }
jupiterweb/urls.py ADDED
@@ -0,0 +1,10 @@
1
+ from urllib.parse import urljoin
2
+
3
+ URL_BASE = "https://uspdigital.usp.br/jupiterweb/"
4
+ URLS: dict[str, str] = {
5
+ "listagem": urljoin(URL_BASE, "jupDisciplinaLista?codcg={codigo}&letra=0-Z&tipo=D"),
6
+ "disciplina": urljoin(URL_BASE, "obterDisciplina?sgldis={sigla}"),
7
+ "oferecimento": urljoin(URL_BASE, "obterTurma?sgldis={sigla}"),
8
+ "requisitos": urljoin(URL_BASE, "listarCursosRequisitos?coddis={sigla}"),
9
+ "institutos": urljoin(URL_BASE, "jupColegiadoLista?tipo=D"),
10
+ }
jupiterweb/utils.py ADDED
@@ -0,0 +1,25 @@
1
+ import requests
2
+ from bs4 import BeautifulSoup
3
+
4
+
5
+ def obter_soup(url: str) -> BeautifulSoup:
6
+ """
7
+ Faz request para URL e retorna objeto BeautifulSoup com o texto da resposta.
8
+ """
9
+
10
+ response = requests.get(url, timeout=10)
11
+ response.raise_for_status()
12
+ response.encoding = "iso-8859-1"
13
+ soup = BeautifulSoup(response.text, "html.parser")
14
+
15
+ return soup
16
+
17
+
18
+ def truncate_string(s: str, max_length: int) -> str:
19
+ """
20
+ Trunca string para um comprimento máximo, adicionando "..." no final se necessário.
21
+ """
22
+
23
+ if len(s) <= max_length:
24
+ return s
25
+ return s[: max(max_length - 3, 0)] + "..."
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: jupiterweb-scraper
3
+ Version: 0.1.0
4
+ Summary: Extração de informações sobre disciplinas da Universidade de São Paulo a partir do Jupiterweb
5
+ Author: Davi Golebiovski, Isaque Nascimento, Lucas Kevin Silva Muniz
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 IME Jr
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Repository, https://github.com/davigole/jupiterweb-scraper
29
+ Project-URL: Issues, https://github.com/davigole/jupiterweb-scraper/issues
30
+ Keywords: usp,jupiterweb,scraper,universidade,web,university
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Requires-Python: >=3.9
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: requests>=2.34.2
43
+ Requires-Dist: beautifulsoup4>=4.15.0
44
+ Dynamic: license-file
45
+
46
+ # Jupiterweb Scraper
47
+
48
+ ![Python Version](https://img.shields.io/pypi/pyversions/jupiterweb-scraper)
49
+ ![License](https://img.shields.io/github/license/davigole/jupiterweb-scraper)
50
+ [![PyPI](https://img.shields.io/pypi/v/jupiterweb-scraper)](https://pypi.org/project/jupiterweb-scraper/)
51
+
52
+ Biblioteca para extração de informações sobre disciplinas da Universidade de São Paulo a partir do [Jupiterweb](https://uspdigital.usp.br/jupiterweb/).
53
+
54
+ ## 📖 Sobre o projeto
55
+
56
+ O **Jupiterweb Scraper** é uma biblioteca Python que permite a extração de informações sobre disciplinas da Universidade de São Paulo a partir do [Jupiterweb](https://uspdigital.usp.br/jupiterweb/), o sistema oficial de gestão acadêmica da universidade.
57
+
58
+ A biblioteca foi desenvolvida por alunos do IME-USP, inicialmente para atender às demandas de um projeto interno da [IME Jr](https://imejr.com/) — a empresa júnior do instituto. No entanto, percebemos que a obtenção de dados do Jupiterweb é uma necessidade recorrente em projetos voltados à comunidade USP. Por isso, decidimos disponibilizar o scraper como projeto open-source, com o intuito de facilitar o desenvolvimento de novas ferramentas destinadas à universidade.
59
+
60
+ > ⚠️ **Aviso:** O Jupiterweb é um site antigo, com uma estrutura HTML complexa e por vezes inconsistente, o que torna o processo de scraping muito desafiador. É esperado que a biblioteca contenha erros que passaram despercebidos. Se você encontrar algum problema ou comportamento inesperado, pedimos que abra uma [Issue](https://github.com/davigole/jupiterweb-scraper/issues) descrevendo o ocorrido.
61
+
62
+ ## 🚀 Instalação
63
+
64
+ Para instalar a biblioteca, utilize o comando
65
+ ```bash
66
+ pip install jupiterweb-scraper
67
+ ```
68
+
69
+ Ou, para instalar a partir do repositório:
70
+ ```bash
71
+ git clone https://github.com/davigole/jupiterweb-scraper.git
72
+ cd jupiterweb-scraper
73
+ pip install -e .
74
+ ```
75
+
76
+ ## 📚 Como usar
77
+
78
+ *A fazer*
79
+
80
+ ## 🤝 Como contribuir
81
+
82
+ Caso queira contribuir mas não saiba por onde começar, aqui estão algumas melhorias e funcionalidades que ainda não foram implementadas:
83
+
84
+ - Buscar disciplinas por parte do nome, horário, vagas remanescentes, etc. (o Jupiterweb já tem essas funcionalidades)
85
+ - Obter os cursos oferecidos por cada unidade e informações sobre cada curso (descrição, objetivos, grade curricular, etc.), disponíveis na seção "Cursos de ingresso" do Jupiterweb
86
+ - Obter informações sobre docentes (nome, instituto, departamento, disciplinas que ministra/ministrou, etc.)
87
+ - Obter informações do calendário escolar, disponível em PDF no Jupiterweb
88
+ - Testes automáticos para verificar o funcionamento do scraping
89
+ - Documentação mais completa e exemplos de uso
90
+ - Qualquer alteração nas funções de scraping que torne a biblioteca mais robusta
91
+
92
+ Contribuições são muito bem-vindas!
93
+
94
+ ## 📄 Licença
95
+
96
+ MIT © [IME Jr](https://imejr.com/)
@@ -0,0 +1,13 @@
1
+ jupiterweb/__init__.py,sha256=kBBu3Li9m9nr4NOz7VXC7Cc1MbBHhVtYjON4dR95hLA,464
2
+ jupiterweb/disciplina.py,sha256=TAHCPYmzzUs0bIFMFEvDxHy0nXzFLh3k4kqBYo1snGM,13478
3
+ jupiterweb/instituto.py,sha256=i3HL80A_QkK7y2hUgSDUNZOoAi0nbbv8NA_btk6hyjQ,1789
4
+ jupiterweb/jupiterweb.py,sha256=MhRTNCrv8xTgD218OK1aES02hQn4NIKVqa8x1FtSDyc,529
5
+ jupiterweb/paths.py,sha256=AjNzlYqYteYdh-Lc3wWyutbbfhbNJAHP2bMsprTmZb8,167
6
+ jupiterweb/urls.py,sha256=B4SPM5W8E_J6MG7CZhS7L0BOufsDHZjW0AoLb07IDi4,493
7
+ jupiterweb/utils.py,sha256=ZwUQe864hfT2Y6H-oeTXe5aYHacgqtHKxy-altG7I8A,655
8
+ jupiterweb/data/institutos.json,sha256=cJ6Gy8Us60A7PXtQ66JW6ZPtBv5vyXs4jSNuln_4J7s,11402
9
+ jupiterweb_scraper-0.1.0.dist-info/licenses/LICENSE,sha256=GQFN4qoFW8zH3u6PJvlOwiP7y8BV-mf-1YMIFRoqfv8,1084
10
+ jupiterweb_scraper-0.1.0.dist-info/METADATA,sha256=ET7fG-8nX645cNXvu6j2Lj3gkAZ0HoHFttAWA2N-N1A,5229
11
+ jupiterweb_scraper-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ jupiterweb_scraper-0.1.0.dist-info/top_level.txt,sha256=q2V6URFa8JKMOWGH2ZKBR_A6Rv5Le0qc7y40PuxFgvQ,11
13
+ jupiterweb_scraper-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IME Jr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ jupiterweb