Как правильно обрабатывать объединённые ячейки?
По вводным: решил сделать бота в Telegram для более удобного пользования расписанием университета. (Забираю xlsx файл из сайта университета, распаршиваю его в bd.json)
Сейчас ситуация такая: возникла проблема, которую не могу решить: есть три массива all, sub1, sub2 в расписании присутсвует две подгруппы с разными парами, и общие пары для этих подгрупп, которые находятся в объединении ячеек). И сейчас общие пары кладутся в массив sub1 вместе с парами для первой подгруппы, в массив sub2 кладутся только пары для второй подгруппы.
Как бы хотелось:
- В all попадут общие пары
По вводным: в sub1 попадут только пары для первой подгруппы
По вводным: sub2 работает без перебоев - All исчезнет
Sub1 не изменится
По вводным: в sub2 попадут еще и общие пары
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;
Опишите проблему, и специалист поможет с настройкой, исправлением ошибки или доработкой сайта. Подберём понятный план работ без лишней переписки.
Пока нет других ответов. Будьте первым, кто поможет автору.
Ответить на вопрос
Объединенные ячейки лучше не обрабатывать как обычные ячейки. В Excel значение физически хранится только в верхней левой ячейке диапазона merge, а остальные ячейки внутри объединения пустые. Поэтому при парсинге нужно сначала получить список merged ranges, а потом для каждой пустой ячейки понимать, входит ли она в объединенный диапазон.
Если используете JavaScript и библиотеку ExcelJS, логика примерно такая: читаете worksheet, смотрите модель объединений, строите функцию, которая по координате возвращает master cell. После этого при обходе строк берете значение либо из самой ячейки, либо из master cell объединенного диапазона.
function getMergedValue(worksheet, rowNumber, colNumber) { const cell = worksheet.getCell(rowNumber, colNumber); if (cell.value !== null && cell.value !== undefined) { return cell.value; } for (const range of worksheet.model.merges || []) { const merged = worksheet.getCell(range.split(':')[0]); const master = merged.master || merged; // В реальном коде лучше распарсить range в координаты и проверить попадание. if (cell.master && cell.master.address === master.address) { return master.value; } } return null; }function getMergedValue(worksheet, rowNumber, colNumber) { const cell = worksheet.getCell(rowNumber, colNumber); if (cell.value !== null && cell.value !== undefined) { return cell.value; } for (const range of worksheet.model.merges || []) { const merged = worksheet.getCell(range.split(':')[0]); const master = merged.master || merged; // В реальном коде лучше распарсить range в координаты и проверить попадание. if (cell.master && cell.master.address === master.address) { return master.value; } } return null; }
В SheetJS/xlsx данные о merge лежат в
worksheet['!merges']. Там нужно проверять диапазон по r/c координатам:const merges = ws['!merges'] || []; // merge: { s: { r, c }, e: { r, c } } // значение находится в ячейке merge.sconst merges = ws['!merges'] || []; // merge: { s: { r, c }, e: { r, c } } // значение находится в ячейке merge.s
Самая надежная стратегия — перед импортом привести Excel к плоской таблице: одна строка — одна сущность, одна колонка — одно поле. Если файл приходит от пользователей, добавьте нормализатор объединенных ячеек и валидацию, иначе импорт будет давать пропуски там, где визуально в Excel значение есть.