zdu-student-api 1.1.5 → 1.1.7

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +242 -18
  3. package/dist/audience.d.ts +44 -44
  4. package/dist/audience.js +50 -48
  5. package/dist/cabinet/cabinet.d.ts +79 -0
  6. package/dist/cabinet/cabinet.js +162 -0
  7. package/dist/cabinet/data.d.ts +66 -0
  8. package/dist/cabinet/data.js +115 -0
  9. package/dist/cabinet/disciplines.d.ts +19 -0
  10. package/dist/cabinet/disciplines.js +87 -0
  11. package/dist/cabinet/index.d.ts +3 -2
  12. package/dist/cabinet/index.js +3 -2
  13. package/dist/cabinet/parsers.d.ts +37 -0
  14. package/dist/cabinet/parsers.js +284 -0
  15. package/dist/cabinet/scores.d.ts +12 -0
  16. package/dist/cabinet/scores.js +101 -0
  17. package/dist/cabinet/sesId.d.ts +10 -9
  18. package/dist/cabinet/sesId.js +40 -39
  19. package/dist/cabinet/session.d.ts +33 -0
  20. package/dist/cabinet/session.js +99 -0
  21. package/dist/cabinet/types.d.ts +2 -2
  22. package/dist/cabinet/utils.d.ts +8 -0
  23. package/dist/cabinet/utils.js +19 -0
  24. package/dist/cabinet/validSession.d.ts +8 -0
  25. package/dist/cabinet/validSession.js +29 -0
  26. package/dist/cabinetStudent/cabinetStudent.d.ts +87 -0
  27. package/dist/cabinetStudent/cabinetStudent.js +181 -0
  28. package/dist/cabinetStudent/disciplines.d.ts +19 -0
  29. package/dist/cabinetStudent/disciplines.js +86 -0
  30. package/dist/cabinetStudent/index.d.ts +4 -0
  31. package/dist/cabinetStudent/index.js +4 -0
  32. package/dist/cabinetStudent/scores.d.ts +12 -0
  33. package/dist/cabinetStudent/scores.js +101 -0
  34. package/dist/cabinetStudent/types.d.ts +137 -0
  35. package/dist/cabinetStudent/types.js +1 -0
  36. package/dist/cabinetTeacher/academicGroups.d.ts +10 -0
  37. package/dist/cabinetTeacher/academicGroups.js +36 -0
  38. package/dist/cabinetTeacher/cabinetTeacher.d.ts +71 -0
  39. package/dist/cabinetTeacher/cabinetTeacher.js +132 -0
  40. package/dist/cabinetTeacher/index.d.ts +3 -0
  41. package/dist/cabinetTeacher/index.js +3 -0
  42. package/dist/cabinetTeacher/types.d.ts +68 -0
  43. package/dist/cabinetTeacher/types.js +1 -0
  44. package/dist/constants.d.ts +49 -49
  45. package/dist/constants.js +55 -52
  46. package/dist/examples.d.ts +1 -1
  47. package/dist/examples.js +70 -15
  48. package/dist/index.d.ts +12 -10
  49. package/dist/index.js +12 -10
  50. package/dist/schedule.d.ts +62 -62
  51. package/dist/schedule.js +74 -68
  52. package/dist/types.d.ts +17 -16
  53. package/dist/types.js +0 -1
  54. package/package.json +35 -33
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Отримати ймовірний семестр поточної дати
3
+ * @category Cabinet
4
+ * @remarks
5
+ * З 2 вересня по кінець січня - перший семестр
6
+ * З 1 лютого по 1 вересня - другий семестр
7
+ */
8
+ export function getSemester() {
9
+ const year = new Date().getFullYear();
10
+ const febStart = new Date(year, 1, 1); // лютий = 1
11
+ const sepStart = new Date(year, 8, 1); // вересень = 8
12
+ const date = new Date();
13
+ if (!(date < febStart || date >= sepStart)) {
14
+ return 2;
15
+ }
16
+ else {
17
+ return 1;
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Перевірка на валідність сесії
3
+ * @category Cabinet
4
+ * @param sesId - ID сесії користувача
5
+ * @param sessGUID - GUID сесії з cookie
6
+ * @returns boolean значення.
7
+ */
8
+ export declare function isValidSession(sesId: string, sessGUID: string): Promise<boolean>;
@@ -0,0 +1,29 @@
1
+ import fetch from 'cross-fetch';
2
+ import iconv from 'iconv-lite';
3
+ import { generateCookieString, isLoginPage } from './utils.js';
4
+ /**
5
+ * Перевірка на валідність сесії
6
+ * @category Cabinet
7
+ * @param sesId - ID сесії користувача
8
+ * @param sessGUID - GUID сесії з cookie
9
+ * @returns boolean значення.
10
+ */
11
+ export async function isValidSession(sesId, sessGUID) {
12
+ try {
13
+ const cookieString = generateCookieString(sessGUID);
14
+ const response = await fetch(`https://dekanat.zu.edu.ua/cgi-bin/classman.cgi?n=3&sesID=${sesId}`, {
15
+ headers: {
16
+ Cookie: cookieString,
17
+ },
18
+ });
19
+ const buffer = await response.arrayBuffer();
20
+ const html = iconv.decode(Buffer.from(buffer), 'windows-1251');
21
+ if (!isLoginPage(html)) {
22
+ return true;
23
+ }
24
+ return false;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
@@ -0,0 +1,87 @@
1
+ import { Discipline, Disciplines, DataStudent, Scores } from './types.js';
2
+ /**
3
+ * Клас кабінету студента
4
+ * @category CabinetStudent
5
+ */
6
+ export declare class CabinetStudent {
7
+ /**
8
+ * Прізвище користувача
9
+ */
10
+ login: string;
11
+ /**
12
+ * Пароль від кабінету студента
13
+ */
14
+ password: string;
15
+ /**
16
+ * ID сесії користувача
17
+ */
18
+ sesID?: string;
19
+ /**
20
+ * GUID сесії з cookie
21
+ */
22
+ sessGUID?: string;
23
+ /**
24
+ * Семестр для пошуку оцінок (1 - перший, 2 - другий)
25
+ */
26
+ semester: 1 | 2;
27
+ /**
28
+ * Список дисциплін {@link Discipline}
29
+ */
30
+ disciplines: Discipline[];
31
+ /**
32
+ * Данні студента {@link Data}
33
+ */
34
+ data?: DataStudent;
35
+ /**
36
+ * Оціки з усіх предметів {@link Scores}
37
+ */
38
+ allScores?: Scores[];
39
+ /**
40
+ * Айді в системі оцінювання
41
+ */
42
+ private id?;
43
+ /**
44
+ * Конструктор
45
+ * @param login - Прізвище користувача
46
+ * @param password - Пароль
47
+ */
48
+ constructor(login: string, password: string);
49
+ /**
50
+ * Авторизація
51
+ */
52
+ auth(login?: string, password?: string): Promise<boolean>;
53
+ /**
54
+ * Базове значення семестру
55
+ */
56
+ private setSemester;
57
+ /**
58
+ * Отримання всіх данних
59
+ */
60
+ loadData(): Promise<boolean>;
61
+ /**
62
+ * Відновлення сесії
63
+ */
64
+ setSession(sesID: string, sessGUID: string): Promise<boolean>;
65
+ /**
66
+ * Перевірка валідності сесії
67
+ */
68
+ isValidSession(): Promise<boolean>;
69
+ /**
70
+ * Отримати дисципліни поточного семестру студента
71
+ * @returns Об'єкт дисциплін {@link Disciplines}
72
+ */
73
+ getDisciplines(): Promise<Disciplines>;
74
+ /**
75
+ * Отримати анкетні данні студента
76
+ * @returns Об'єкт {@link Data}
77
+ */
78
+ getData(): Promise<DataStudent>;
79
+ /**
80
+ * Отримати оцінки з всіх предметів
81
+ */
82
+ getAllScores(semester?: 1 | 2): Promise<Scores[]>;
83
+ /**
84
+ * Отримати айді в системі оцінювання
85
+ */
86
+ getId(): Promise<string | undefined>;
87
+ }
@@ -0,0 +1,181 @@
1
+ import { getSesId, isValidSession } from '../cabinet/session.js';
2
+ import { getСurrentDisciplines } from './disciplines.js';
3
+ import { getScores } from './scores.js';
4
+ import { getSemester } from '../cabinet/utils.js';
5
+ import { getDataStudent } from '../cabinet/data.js';
6
+ /**
7
+ * Клас кабінету студента
8
+ * @category CabinetStudent
9
+ */
10
+ export class CabinetStudent {
11
+ /**
12
+ * Прізвище користувача
13
+ */
14
+ login;
15
+ /**
16
+ * Пароль від кабінету студента
17
+ */
18
+ password;
19
+ /**
20
+ * ID сесії користувача
21
+ */
22
+ sesID;
23
+ /**
24
+ * GUID сесії з cookie
25
+ */
26
+ sessGUID;
27
+ /**
28
+ * Семестр для пошуку оцінок (1 - перший, 2 - другий)
29
+ */
30
+ semester = 1;
31
+ /**
32
+ * Список дисциплін {@link Discipline}
33
+ */
34
+ disciplines = [];
35
+ /**
36
+ * Данні студента {@link Data}
37
+ */
38
+ data;
39
+ /**
40
+ * Оціки з усіх предметів {@link Scores}
41
+ */
42
+ allScores;
43
+ /**
44
+ * Айді в системі оцінювання
45
+ */
46
+ id;
47
+ /**
48
+ * Конструктор
49
+ * @param login - Прізвище користувача
50
+ * @param password - Пароль
51
+ */
52
+ constructor(login, password) {
53
+ this.login = login;
54
+ this.password = password;
55
+ }
56
+ /**
57
+ * Авторизація
58
+ */
59
+ async auth(login, password) {
60
+ const accountData = await getSesId(login ?? this.login, password ?? this.password, 'student');
61
+ this.setSemester();
62
+ if (accountData.ok) {
63
+ this.sesID = accountData.sesID;
64
+ this.sessGUID = accountData.sessGUID;
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ /**
70
+ * Базове значення семестру
71
+ */
72
+ setSemester() {
73
+ this.semester = getSemester();
74
+ }
75
+ /**
76
+ * Отримання всіх данних
77
+ */
78
+ async loadData() {
79
+ const one = (await this.getDisciplines()).ok;
80
+ const two = (await this.getData()).ok;
81
+ const three = (await this.getAllScores()).length > 0;
82
+ return one && two && three;
83
+ }
84
+ /**
85
+ * Відновлення сесії
86
+ */
87
+ async setSession(sesID, sessGUID) {
88
+ this.setSemester();
89
+ const ses = await isValidSession(sesID, sessGUID, 'student');
90
+ if (ses) {
91
+ this.sesID = sesID;
92
+ this.sessGUID = sessGUID;
93
+ }
94
+ return ses;
95
+ }
96
+ /**
97
+ * Перевірка валідності сесії
98
+ */
99
+ async isValidSession() {
100
+ if (!this.sesID || !this.sessGUID)
101
+ return false;
102
+ return await isValidSession(this.sesID, this.sessGUID, 'student');
103
+ }
104
+ /**
105
+ * Отримати дисципліни поточного семестру студента
106
+ * @returns Об'єкт дисциплін {@link Disciplines}
107
+ */
108
+ async getDisciplines() {
109
+ if (!this.sesID || !this.sessGUID)
110
+ return { ok: false, disciplines: [] };
111
+ try {
112
+ const disciplinesData = await getСurrentDisciplines(this.sesID, this.sessGUID);
113
+ if (disciplinesData.ok) {
114
+ this.disciplines = disciplinesData.disciplines;
115
+ }
116
+ return disciplinesData;
117
+ }
118
+ catch {
119
+ return { ok: false, disciplines: [] };
120
+ }
121
+ }
122
+ /**
123
+ * Отримати анкетні данні студента
124
+ * @returns Об'єкт {@link Data}
125
+ */
126
+ async getData() {
127
+ if (!this.sesID || !this.sessGUID)
128
+ return { ok: false };
129
+ try {
130
+ const data = await getDataStudent(this.sesID, this.sessGUID);
131
+ if (data.ok) {
132
+ this.data = data;
133
+ }
134
+ return data;
135
+ }
136
+ catch {
137
+ return { ok: false };
138
+ }
139
+ }
140
+ /**
141
+ * Отримати оцінки з всіх предметів
142
+ */
143
+ async getAllScores(semester) {
144
+ if (!this.sesID || !this.sessGUID)
145
+ return [];
146
+ if (!this.disciplines?.length)
147
+ return [];
148
+ const targetSemester = semester ?? this.semester;
149
+ try {
150
+ const scorePromises = this.disciplines.map((discipline) => getScores(this.sesID, this.sessGUID, discipline.prId, targetSemester));
151
+ const results = await Promise.all(scorePromises);
152
+ const allScores = results.filter((scores) => scores.ok);
153
+ this.allScores = allScores;
154
+ return allScores;
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ }
160
+ /**
161
+ * Отримати айді в системі оцінювання
162
+ */
163
+ async getId() {
164
+ if (this.id)
165
+ return this.id;
166
+ if (!this.sesID || !this.sessGUID)
167
+ return undefined;
168
+ if (!this.disciplines?.length) {
169
+ await this.getDisciplines();
170
+ }
171
+ if (!this.disciplines?.length)
172
+ return undefined;
173
+ try {
174
+ const result = await getScores(this.sesID, this.sessGUID, this.disciplines[0].prId, this.semester);
175
+ return result.studentId;
176
+ }
177
+ catch {
178
+ return undefined;
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,19 @@
1
+ import { Disciplines } from './types.js';
2
+ /**
3
+ * Отримати всі дисципліни студента
4
+ * @category CabinetStudent
5
+ * @param sesId - ID сесії користувача
6
+ * @param sessGUID - GUID сесії з cookie
7
+ * @throws {Error} Якщо виникають проблеми з запитом або дані некоректні.
8
+ * @returns Масив дисциплін {@link Disciplines}
9
+ */
10
+ export declare function getDisciplines(sesId: string, sessGUID: string): Promise<Disciplines>;
11
+ /**
12
+ * Отримати поточні дисципліни студента
13
+ * @category CabinetStudent
14
+ * @param sesId - ID сесії користувача
15
+ * @param sessGUID - GUID сесії з cookie
16
+ * @throws {Error} Якщо виникають проблеми з запитом або дані некоректні.
17
+ * @returns Масив дисциплін {@link Disciplines}
18
+ */
19
+ export declare function getСurrentDisciplines(sesId: string, sessGUID: string): Promise<Disciplines>;
@@ -0,0 +1,86 @@
1
+ import fetch from 'cross-fetch';
2
+ import iconv from 'iconv-lite';
3
+ import { parseDisciplinesPageN6, parseDisciplinesPageN7 } from '../cabinet/parsers.js';
4
+ import { generateCookieString, isLoginPage } from '../cabinet/session.js';
5
+ /**
6
+ * Отримати всі дисципліни студента
7
+ * @category CabinetStudent
8
+ * @param sesId - ID сесії користувача
9
+ * @param sessGUID - GUID сесії з cookie
10
+ * @throws {Error} Якщо виникають проблеми з запитом або дані некоректні.
11
+ * @returns Масив дисциплін {@link Disciplines}
12
+ */
13
+ export async function getDisciplines(sesId, sessGUID) {
14
+ try {
15
+ const result = { ok: false, disciplines: [] };
16
+ const cookieString = generateCookieString(sessGUID);
17
+ const response1 = await fetch(`https://dekanat.zu.edu.ua/cgi-bin/classman.cgi?n=3&sesID=${sesId}`, { headers: { Cookie: cookieString } });
18
+ const buffer1 = await response1.arrayBuffer();
19
+ const html1 = iconv.decode(Buffer.from(buffer1), 'windows-1251');
20
+ if (isLoginPage(html1))
21
+ return result;
22
+ let data = extractStrongValue(html1, 'Семестрові бали');
23
+ if (data === undefined)
24
+ return result;
25
+ const response2 = await fetch(`https://dekanat.zu.edu.ua/cgi-bin/${data.slice(2)}`, {
26
+ headers: { Cookie: cookieString },
27
+ });
28
+ const buffer2 = await response2.arrayBuffer();
29
+ const html2 = iconv.decode(Buffer.from(buffer2), 'windows-1251');
30
+ if (isLoginPage(html2))
31
+ return result;
32
+ result.disciplines = parseDisciplinesPageN6(html2);
33
+ result.ok = true;
34
+ return result;
35
+ }
36
+ catch (e) {
37
+ console.error('Error in getDisciplines:', e);
38
+ throw e;
39
+ }
40
+ }
41
+ /**
42
+ * Витягує значення з HTML після label до кінця li тегу
43
+ */
44
+ function extractStrongValue(html, label) {
45
+ const textIdx = html.indexOf(label);
46
+ if (textIdx === -1)
47
+ return undefined;
48
+ const liStart = html.lastIndexOf('<li', textIdx);
49
+ if (liStart === -1)
50
+ return undefined;
51
+ const liEnd = html.indexOf('</li>', textIdx);
52
+ if (liEnd === -1)
53
+ return undefined;
54
+ const liContent = html.substring(liStart, liEnd);
55
+ const hrefMatch = liContent.match(/href\s*=\s*["']([^"']+)["']/i);
56
+ if (!hrefMatch)
57
+ return undefined;
58
+ const href = hrefMatch[1].trim();
59
+ return href || undefined;
60
+ }
61
+ /**
62
+ * Отримати поточні дисципліни студента
63
+ * @category CabinetStudent
64
+ * @param sesId - ID сесії користувача
65
+ * @param sessGUID - GUID сесії з cookie
66
+ * @throws {Error} Якщо виникають проблеми з запитом або дані некоректні.
67
+ * @returns Масив дисциплін {@link Disciplines}
68
+ */
69
+ export async function getСurrentDisciplines(sesId, sessGUID) {
70
+ try {
71
+ const result = { ok: false, disciplines: [] };
72
+ const cookieString = generateCookieString(sessGUID);
73
+ const response1 = await fetch(`https://dekanat.zu.edu.ua/cgi-bin/classman.cgi?n=7&sesID=${sesId}`, { headers: { Cookie: cookieString } });
74
+ const buffer1 = await response1.arrayBuffer();
75
+ const html1 = iconv.decode(Buffer.from(buffer1), 'windows-1251');
76
+ if (isLoginPage(html1))
77
+ return result;
78
+ result.disciplines = parseDisciplinesPageN7(html1);
79
+ result.ok = true;
80
+ return result;
81
+ }
82
+ catch (e) {
83
+ console.error('Error in getСurrentDisciplines:', e);
84
+ throw e;
85
+ }
86
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './disciplines.js';
3
+ export * from './scores.js';
4
+ export * from './cabinetStudent.js';
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './disciplines.js';
3
+ export * from './scores.js';
4
+ export * from './cabinetStudent.js';
@@ -0,0 +1,12 @@
1
+ import { Scores } from './types.js';
2
+ /**
3
+ * Отримати оцінки пвибраного предмета студента
4
+ * @category CabinetStudent
5
+ * @param sesId - ID сесії користувача
6
+ * @param sessGUID - GUID сесії з cookie
7
+ * @param prId - ID дисципліни
8
+ * @param semester - Семестр
9
+ * @throws {Error} Якщо виникають проблеми з запитом або дані некоректні.
10
+ * @returns Масив дисциплін {@link Disciplines}
11
+ */
12
+ export declare function getScores(sesId: string, sessGUID: string, prId: string, semester: 1 | 2): Promise<Scores>;
@@ -0,0 +1,101 @@
1
+ import fetch from 'cross-fetch';
2
+ import iconv from 'iconv-lite';
3
+ import { generateCookieString, isLoginPage } from '../cabinet/session.js';
4
+ /**
5
+ * Отримати оцінки пвибраного предмета студента
6
+ * @category CabinetStudent
7
+ * @param sesId - ID сесії користувача
8
+ * @param sessGUID - GUID сесії з cookie
9
+ * @param prId - ID дисципліни
10
+ * @param semester - Семестр
11
+ * @throws {Error} Якщо виникають проблеми з запитом або дані некоректні.
12
+ * @returns Масив дисциплін {@link Disciplines}
13
+ */
14
+ export async function getScores(sesId, sessGUID, prId, semester) {
15
+ try {
16
+ const result = {
17
+ ok: false,
18
+ prId: prId,
19
+ studentId: '',
20
+ scheduleItem: [],
21
+ studentScores: [],
22
+ };
23
+ const cookieString = generateCookieString(sessGUID);
24
+ const formData = `n=7&sesID=${sesId}&teacher=0&irc=0&tid=0&CYKLE=-1&prt=${prId}&hlf=${semester === 1 ? 0 : 1}&grade=0&m=-1`;
25
+ const encodedFormData = iconv.encode(formData, 'windows-1251');
26
+ const response1 = await fetch(`https://dekanat.zu.edu.ua/cgi-bin/classman.cgi?n=7&sesID=${sesId}`, {
27
+ method: 'POST',
28
+ body: new Uint8Array(encodedFormData),
29
+ redirect: 'manual',
30
+ headers: { Cookie: cookieString },
31
+ });
32
+ const buffer1 = await response1.arrayBuffer();
33
+ let html1 = iconv.decode(Buffer.from(buffer1), 'windows-1251');
34
+ if (isLoginPage(html1))
35
+ return result;
36
+ result.scheduleItem = parseSchedule(html1);
37
+ result.studentScores = parseScores(html1);
38
+ result.studentId = result.studentScores[0].id;
39
+ result.studentScores.sort((a, b) => a.id.localeCompare(b.id));
40
+ result.ok = true;
41
+ return result;
42
+ }
43
+ catch (e) {
44
+ console.error('Error in getQuestionnaireData:', e);
45
+ throw e;
46
+ }
47
+ }
48
+ /**
49
+ * Витягує список пар з HTML
50
+ */
51
+ function parseSchedule(html) {
52
+ let idx = html.indexOf('<th></th>');
53
+ html = idx === -1 ? html : html.slice(idx + '<th></th>'.length);
54
+ idx = html.indexOf('<th class="dh">');
55
+ html = idx === -1 ? html : html.slice(0, idx);
56
+ const regex = /<th[^>]*data-hth="([^"]+)"[^>]*>[\s\S]*?<a[^>]*data-ind="([^"]+)"[^>]*>[^<]+<\/a>[\s\S]*?<br>([^<]+)<\/th>/g;
57
+ const result = [];
58
+ let match;
59
+ while ((match = regex.exec(html)) !== null) {
60
+ const [_, dataHth, index, time] = match;
61
+ const parts = dataHth.split(',');
62
+ const teacher = parts[0].trim();
63
+ const dateType = parts[1]?.trim().split(' ') || [];
64
+ const date = dateType[0] || '';
65
+ const type = dateType[1] || '';
66
+ result.push({ teacher, date, type, time: time.trim(), index });
67
+ }
68
+ return result;
69
+ }
70
+ /**
71
+ * Витягує список оцінок з HTML
72
+ */
73
+ function parseScores(html) {
74
+ const studentRows = html.match(/<tr\s+[^>]*id="s\d+"[\s\S]*?<\/tr>/g) || [];
75
+ const result = [];
76
+ for (const row of studentRows) {
77
+ const idMatch = row.match(/id="(s\d+)"/);
78
+ const id = idMatch ? idMatch[1] : '';
79
+ const tdMatches = [...row.matchAll(/<td[^>]*data-item="(\d+)"[^>]*>([\s\S]*?)<\/td>/g)];
80
+ const scoresMap = new Map();
81
+ for (const td of tdMatches) {
82
+ const dataItem = Number(td[1]);
83
+ const score = td[2].trim() || '';
84
+ if (!scoresMap.has(dataItem)) {
85
+ scoresMap.set(dataItem, [score]);
86
+ }
87
+ else {
88
+ scoresMap.get(dataItem).push(score);
89
+ }
90
+ }
91
+ const scores = Array.from(scoresMap.values());
92
+ scores.shift();
93
+ const finalMatch = row.match(/<td[^>]*class="f f1"[^>]*>([\s\S]*?)<\/td>/);
94
+ const finalScore = finalMatch ? finalMatch[1].trim() : '';
95
+ const absenceMatches = [...row.matchAll(/<td[^>]*class="f f2"[^>]*>([\s\S]*?)<\/td>/g)];
96
+ const absences = absenceMatches[0] ? Number(absenceMatches[0][1].trim() || 0) : 0;
97
+ const uabsences = absenceMatches[1] ? Number(absenceMatches[1][1].trim() || 0) : 0;
98
+ result.push({ id, scores, absences, uabsences, finalScore });
99
+ }
100
+ return result;
101
+ }