const axios = require('axios');
const cheerio = require('cheerio');
const XLSX = require('xlsx');
const PAGE_URL = 'https://schedule-cloud.cfuv.ru/index.php/s/DKNodfRxgf9c5Xc';
const DAYS = ['понедельник','вторник','среда','четверг','пятница', 'суббота'];
const PARITY_ORDER = ['odd','even'];
const ROOM_RX = /(?:аудитория|ауд)\.?\s*([0-9A-Za-zА-Яа-я]+)/i;
const TEACHER_RX = /^(ст\.|доц\.|проф\.|ас\.|вак\.)/i;
async function fetchSchedule() {
// скачать XLSX
const { data: html } = await axios.get(PAGE_URL);
const $ = cheerio.load(html);
const rel = $('a[href$=".xlsx"]').first().attr('href');
if (!rel) throw new Error('xlsx не найден');
const xlsxUrl = new URL(rel, PAGE_URL).href;
const { data: buf } = await axios.get(xlsxUrl, { responseType: 'arraybuffer' });
// прочитать первый лист(т.к листы разделены по курсам)
const wb = XLSX.read(buf, { type: 'buffer' });
const ws = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' });
// найти блоки odd/even
let hdr = -1, blocks = [];
for (let i = 0; i < rows.length; i++) {
const lc = rows[i].map(c => String(c).toLowerCase().trim());
const idxsDay = lc.reduce((a,v,i) => v === 'дни недели' ? a.concat(i) : a, []);
if (idxsDay.length >= 2 && lc.includes('пара') && lc.includes('вид занятий')) {
const found = [];
for (let col of idxsDay) {
const ci = lc.indexOf('пара', col+1);
const ti = lc.indexOf('вид занятий', col+1);
if (ci > col && ti > ci) found.push({ colDay: col, colPair: ci, colType: ti });
}
if (found.length >= 2) {
hdr = i;
found.forEach((f, b) => {
const start = f.colDay;
const end = found[b+1]?.colDay ?? lc.length;
blocks.push({ ...f, startCol:start, endColExcl:end, parity: b===0?'odd':'even' });
});
break;
}
}
}
if (hdr < 0) throw new Error('Не найдены блоки недель');
// строка с подгруппами
let subg = hdr + 1;
while (subg < rows.length && !rows[subg].some(c=>/\([12]\)$/.test(String(c).trim()))) subg++;
if (subg >= rows.length) throw new Error('Не найдена строка подгрупп');
blocks.forEach(b => {
b.subjCols = [];
for (let i = b.startCol; i < b.endColExcl; i++) {
const m = String(rows[subg][i]).trim().match(/\((\d)\)$/);
if (m) b.subjCols.push({ idx:i, subgroup:m[1] });
}
b.col1 = b.subjCols.find(x=>x.subgroup==='1').idx;
b.col2 = b.subjCols.find(x=>x.subgroup==='2').idx;
});
const slots = {};
const state = blocks.map(() => ({ day:null, pair:null }));
for (let r = hdr+1; r < rows.length; r++) {
const row = rows[r];
blocks.forEach((b,i) => {
const d = String(row[b.colDay]||'').trim().toLowerCase();
const p = String(row[b.colPair]||'').trim();
if (DAYS.includes(d)) { state[i].day = d; state[i].pair = null; }
if (/^[1-7]$/.test(p)) state[i].pair = +p;
});
blocks.forEach((b,i) => {
const { day, pair } = state[i];
if (!day || !pair) return;
const rawType = String(row[b.colType]||'').trim().toUpperCase();
const type = ['ЛК','ПЗ'].includes(rawType) ? rawType : '';
[ {idx:b.col1, sub:'1'}, {idx:b.col2, sub:'2'} ].forEach(({idx,sub}) => {
const raw = String(row[idx]||'').trim();
if (!raw || /^\d+$/.test(raw)) return;
const key = [b.parity, day, pair, sub].join('|');
if (!slots[key]) slots[key] = { parity:b.parity, day, pair, subgroup:sub, type, subject:'', teacher:'', room:'' };
const parts = raw.split(/\r?\n| {2,}/).map(s=>s.trim()).filter(Boolean);
parts.forEach(p => {
const slot = slots[key];
if (!slot.room && ROOM_RX.test(p)) slot.room = ROOM_RX.exec(p)[1];
else if (!slot.teacher && TEACHER_RX.test(p)) slot.teacher = p;
else if (!slot.subject) slot.subject = p;
});
});
});
}
// сортировка
const raw = Object.values(slots).sort((a,b) => {
const pa = PARITY_ORDER.indexOf(a.parity), pb = PARITY_ORDER.indexOf(b.parity);
if (pa !== pb) return pa - pb;
const dayOrder = { понедельник:1, вторник:2, среда:3, четверг:4, пятница:5 };
if (dayOrder[a.day] !== dayOrder[b.day]) return dayOrder[a.day] - dayOrder[b.day];
return a.pair - b.pair;
});
// группировка по времени
const byTime = {};
raw.forEach(slot => {
const key = [slot.parity, slot.day, slot.pair].join('|');
if (!byTime[key]) byTime[key] = {};
byTime[key][slot.subgroup] = slot;
});
// распределение по массивам
const all = [];
const sub1 = [];
const sub2 = [];
Object.values(byTime).forEach(group => {
const s1 = group['1'];
const s2 = group['2'];
if (s1 && s2 && s1.subject === s2.subject) {
// общая пара
all.push({ ...s1, subgroup: 'all' });
sub1.push({ ...s1 });
sub2.push({ ...s2 });
} else {
if (s1) sub1.push(s1);
if (s2) sub2.push(s2);
}
});
// фильтр
function finalize(arr) {
const dayOrder = { понедельник:1, вторник:2, среда:3, четверг:4, пятница:5 };
return arr
.filter(s => s.subject)
.sort((a,b) => {
const pa = PARITY_ORDER.indexOf(a.parity),
pb = PARITY_ORDER.indexOf(b.parity);
if (pa !== pb) return pa - pb;
if (dayOrder[a.day] !== dayOrder[b.day]) {
return dayOrder[a.day] - dayOrder[b.day];
}
return a.pair - b.pair;
})
.map((s,i) => ({ id: i + 1, ...s }));
}
return {
all: finalize(all),
sub1: finalize(sub1),
sub2: finalize(sub2),
};
}
const fs = require('fs');
if (require.main === module) {
fetchSchedule()
.then(data => {
fs.writeFileSync('bd.json', JSON.stringify(data, null, 2), 'utf-8');
console.log('Расписание сохранено');
})
.catch(err => {
console.error('Ошибка', err.message);
});
}
module.exports = fetchSchedule;