// JavaScript функция для парсинга судов (аналог parscourt.php) // Используется в n8n workflow ноде "парсим суд" // Поддерживает: региональные суды (*.sudrf.ru) и московские суды (mos-gorsud.ru) export default async function ({ page, context }) { // Получаем данные из переменных n8n workflow const url = '{{ $json.link }}'; const status = '{{ $json.status }}'; if (!url) throw new Error('❌ Не передан url'); const sleep = ms => new Promise(r => setTimeout(r, ms)); // Определяем тип суда по URL const isMoscowCourt = /mos-(gorsud|sud)\.ru/.test(url); const isRegionalCourt = /\.sudrf\.ru/.test(url) && !isMoscowCourt; // Установка заголовков и поведения браузера await page.setViewport({ width: 1920, height: 1080 }); await page.setExtraHTTPHeaders({ "Referer": isMoscowCourt ? "https://mos-sud.ru/" : "https://sudrf.ru/", "Origin": isMoscowCourt ? "https://mos-sud.ru" : "https://sudrf.ru", "Accept-Language": "ru,en;q=0.9", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Upgrade-Insecure-Requests": "1", }); await page.setUserAgent( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" ); await page.goto(url, { waitUntil: "networkidle2", timeout: 60000 }); // Проверяем на ошибки страницы (битая ссылка, неверный формат запроса и т.д.) // Проверяем более точно - ищем конкретные сообщения об ошибках, а не просто слова const pageText = await page.evaluate(() => { const body = document.body?.textContent || ''; const title = document.title || ''; const h1 = document.querySelector('h1')?.textContent || ''; return (body + ' ' + title + ' ' + h1).toLowerCase(); }); // Более точные паттерны ошибок - ищем конкретные сообщения const errorPatterns = [ /неверный формат запроса/i, // Точное сообщение об ошибке /ошибка.*формат.*запрос/i, // Сообщение с ошибкой и форматом запроса /страница не найдена/i, // 404 ошибка /404.*not found/i, // 404 в английском /дело не найдено/i, // Дело не найдено /информация.*не.*найдена/i, // Информация не найдена /ошибка доступа/i, // Ошибка доступа /access.*denied/i, // Доступ запрещён /forbidden/i, // Запрещено /internal.*server.*error/i, // Внутренняя ошибка сервера /ошибка.*сервера/i // Ошибка сервера ]; // Проверяем, что это действительно ошибка, а не просто упоминание слова "ошибка" в тексте // Ищем паттерны в заголовках или в начале страницы const pageTitle = await page.evaluate(() => document.title || ''); const pageH1 = await page.evaluate(() => document.querySelector('h1')?.textContent || ''); const pageH2 = await page.evaluate(() => document.querySelector('h2')?.textContent || ''); // Проверяем заголовки на ошибки (более надёжно) const titleHasError = errorPatterns.some(pattern => pattern.test(pageTitle) || pattern.test(pageH1) || pattern.test(pageH2) ); // Также проверяем, если страница очень короткая (меньше 200 символов) - вероятно ошибка const pageLength = pageText.length; const isVeryShort = pageLength < 200; // Если в заголовках есть ошибка ИЛИ страница очень короткая с ошибкой в тексте if (titleHasError || (isVeryShort && errorPatterns.some(pattern => pattern.test(pageText)))) { return { url, source: new URL(url).hostname, court_type: isMoscowCourt ? 'moscow' : (isRegionalCourt ? 'regional' : 'unknown'), status: 'error', error_type: 'invalid_request', error_message: 'НЕВЕРНЫЙ ФОРМАТ ЗАПРОСА или битая ссылка', last_event: null, message: 'Ссылка на дело оказалась битой или запрос неверный' }; } // Закрыть баннеры cookies, если есть try { await page.waitForSelector("#cookie-disclaimer .cd-close-button, .cookie-accept, .cookie__close", { timeout: 3000 }); const btns = await page.$$("#cookie-disclaimer .cd-close-button, .cookie-accept, .cookie__close"); if (btns[0]) await btns[0].click(); } catch (_) {} await sleep(2000); // Универсальная функция для форматирования даты в YYYY-MM-DD (для БД) // Поддерживает форматы: DD.MM.YYYY, YYYY-MM-DD, DD-MM-YYYY const formatDate = (dateStr) => { if (!dateStr) return ''; // Если уже в формате YYYY-MM-DD if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { return dateStr; } // Формат DD.MM.YYYY (российский формат) const matchDDMMYYYY = dateStr.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); if (matchDDMMYYYY) { const day = matchDDMMYYYY[1]; const month = matchDDMMYYYY[2]; const year = matchDDMMYYYY[3]; return `${year}-${month}-${day}`; } // Формат DD-MM-YYYY const matchDDMMYYYY_dash = dateStr.match(/^(\d{2})-(\d{2})-(\d{4})$/); if (matchDDMMYYYY_dash) { const day = matchDDMMYYYY_dash[1]; const month = matchDDMMYYYY_dash[2]; const year = matchDDMMYYYY_dash[3]; return `${year}-${month}-${day}`; } // Пытаемся использовать Date, но только если формат распознан try { // Если дата в формате, который Date может распознать (YYYY-MM-DD или ISO) const date = new Date(dateStr); if (!isNaN(date.getTime())) { return date.toISOString().split('T')[0]; } } catch (_) {} // Если ничего не подошло, возвращаем как есть return dateStr; }; // Функция для форматирования даты в DD.MM.YYYY (для отображения, кириллические ключи) const formatDateDisplay = (dateStr) => { if (!dateStr) return ''; // Если уже в формате DD.MM.YYYY, возвращаем как есть if (/^\d{2}\.\d{2}\.\d{4}$/.test(dateStr)) { return dateStr; } // Если в формате YYYY-MM-DD, преобразуем в DD.MM.YYYY const matchYYYYMMDD = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (matchYYYYMMDD) { const year = matchYYYYMMDD[1]; const month = matchYYYYMMDD[2]; const day = matchYYYYMMDD[3]; return `${day}.${month}.${year}`; } // Если в формате DD.MM.YYYY (с точками), возвращаем как есть if (/^\d{2}\.\d{2}\.\d{4}$/.test(dateStr)) { return dateStr; } // Пытаемся преобразовать через formatDate и обратно const dbDate = formatDate(dateStr); if (dbDate && dbDate !== dateStr) { const match = dbDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (match) { return `${match[3]}.${match[2]}.${match[1]}`; } } // Если ничего не подошло, возвращаем как есть return dateStr; }; // ======================================== // ПАРСИНГ РЕГИОНАЛЬНЫХ СУДОВ (*.sudrf.ru) // ======================================== if (isRegionalCourt) { // Определяем div для парсинга (аналогично parscourt.php) const divId = (status === 'представительство в суде 1й инстанции' || status === 'выдача листа' || status === 'исполнительное производство' || status === 'заявление на лист') ? 'cont2' : 'cont3'; const events = await page.evaluate((divId) => { const clean = (str) => (str ? str.replace(/\s+/g, ' ').trim() : ''); const div = document.querySelector(`#${divId}`); if (!div) return []; const rows = Array.from(div.querySelectorAll('tr')); const events = []; rows.forEach((row) => { const tds = row.querySelectorAll('td'); if (tds.length < 2) return; const event_name = clean(tds[0]?.textContent || ''); const event_date = clean(tds[1]?.textContent || ''); const event_time = clean(tds[2]?.textContent || ''); const location = clean(tds[3]?.textContent || ''); const event_result = clean(tds[4]?.textContent || ''); const event_basis = clean(tds[5]?.textContent || ''); const note = clean(tds[6]?.textContent || ''); const publication_date = clean(tds[7]?.textContent || ''); // Пропускаем записи, если название события не указано или дата неверная if (!event_name || !event_date || event_date === '1970-01-01') { return; } events.push({ event_name, event_date, event_time, location, event_result, event_basis, note, publication_date }); }); return events; }, divId); // Возвращаем последнее событие (аналогично parscourt.php) if (events.length > 0) { const lastEvent = events[events.length - 1]; return { url, source: new URL(url).hostname, court_type: 'regional', status: 'success', last_event: { event_name: lastEvent.event_name, event_date: formatDate(lastEvent.event_date), event_time: lastEvent.event_time, location: lastEvent.location, event_result: lastEvent.event_result, event_basis: lastEvent.event_basis, note: lastEvent.note, publication_date: formatDate(lastEvent.publication_date), // Для совместимости с parscourt.php (кириллические ключи) Наименование: lastEvent.event_name, Дата: formatDateDisplay(lastEvent.event_date), Время: lastEvent.event_time, Место: lastEvent.location, Результат: lastEvent.event_result, Основание: lastEvent.event_basis, Примечание: lastEvent.note, 'Дата размещения': formatDateDisplay(lastEvent.publication_date) }, all_events: events }; } // Проверяем, может быть страница пустая или битая ссылка const pageText = await page.evaluate(() => document.body?.textContent || ''); const isEmpty = !pageText || pageText.trim().length < 100; return { url, source: new URL(url).hostname, court_type: 'regional', status: isEmpty ? 'error' : 'no_events', error_type: isEmpty ? 'empty_page' : 'no_events_found', error_message: isEmpty ? 'Страница пустая или битая ссылка' : 'События не найдены', last_event: null, message: isEmpty ? 'Ссылка на дело оказалась битой' : 'События не найдены' }; } // ======================================== // ПАРСИНГ МОСКОВСКИХ СУДОВ (mos-gorsud.ru) // ======================================== if (isMoscowCourt) { // Ждём карточку await page.waitForSelector( ".detail-cart .row_card, .case-card, .case-details, .content, main .wrapper_innercontent", { timeout: 20000 } ); // Активируем вкладки try { for (const id of ["#ui-id-1", "#ui-id-2", "#ui-id-3"]) { if (await page.$(id)) await page.click(id); } const tabLinks = await page.$$(`a[href^="#tabs-"], .tabs_wrapper a.ui-tabs-anchor`); if (tabLinks.length) for (const a of tabLinks) await a.click(); await page.waitForTimeout(300); } catch (_) {} const data = await page.evaluate(() => { const norm = (el) => (el ? el.textContent.replace(/\s+/g, " ").trim() : ""); const qsa = (sel) => Array.from(document.querySelectorAll(sel)); function collectRows() { const rows = []; qsa(".detail-cart .row_card").forEach((r) => { const left = norm(r.querySelector(".left")); const right = norm(r.querySelector(".right")); if (left && right) rows.push({ left, right }); }); if (!rows.length) { qsa("table, .case-card, .case-details").forEach((tbl) => { qsa("tr", tbl).forEach((tr) => { const tds = tr.querySelectorAll("td, th"); if (tds.length === 2) { const left = norm(tds[0]); const right = norm(tds[1]); if (left && right) rows.push({ left, right }); } }); }); } if (!rows.length) { qsa(".case-card__row, .kv-row").forEach((row) => { const left = norm(row.querySelector(".case-card__key, .kv-key, .left")); const right = norm(row.querySelector(".case-card__val, .kv-val, .right")); if (left && right) rows.push({ left, right }); }); } return rows; } const rows = collectRows(); const byLeft = (start) => { const row = rows.find((r) => r.left.toLowerCase().startsWith(start.toLowerCase()) ); return row ? row.right : null; }; // Таблицы function tableToRows(tbody) { return Array.from(tbody.querySelectorAll("tr")).map((tr) => { const tds = tr.querySelectorAll("td"); return Array.from(tds).map((td) => norm(td.querySelector("div") || td)); }); } const sessionsTbody = document.querySelector("#tabs-2 table tbody"); const hearingsRows = sessionsTbody ? tableToRows(sessionsTbody) : []; const hearings = hearingsRows.map((cols) => ({ datetime: cols[0] || null, hall: cols[1] || null, stage: cols[2] || null, result: cols[3] || null, basis: cols[4] || null, })); const stTbody = document.querySelector("#tabs-1 #state-history table tbody"); const stateRows = stTbody ? tableToRows(stTbody) : []; const states = stateRows.map((cols) => ({ date: cols[0] || null, state: cols[1] || null, basis_doc: cols[2] || null, })); return { hearings, history: { states }, }; }); // Извлекаем последнее событие (аналогично MoscowCourtParser) let lastEvent = null; // Проверяем заседания (hearings) if (data.hearings && data.hearings.length > 0) { const hearing = data.hearings[data.hearings.length - 1]; if (hearing.datetime) { const datetime = hearing.datetime; let event_date = ''; let event_time = ''; // Формат: "27.10.2025 09:30" или "27.10.2025" const match1 = datetime.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})/); if (match1) { event_date = match1[1]; event_time = match1[2]; } else { const match2 = datetime.match(/(\d{2}\.\d{2}\.\d{4})/); if (match2) { event_date = match2[1]; } } if (event_date) { lastEvent = { event_name: hearing.stage || 'Судебное заседание', event_date: formatDate(event_date), event_time: event_time, location: hearing.hall || '', event_result: hearing.result || '', event_basis: hearing.basis || '', note: '', publication_date: formatDate(event_date), // Для совместимости с parscourt.php Наименование: hearing.stage || 'Судебное заседание', Дата: formatDateDisplay(event_date), Время: event_time, Место: hearing.hall || '', Результат: hearing.result || '', Основание: hearing.basis || '', Примечание: '', 'Дата размещения': formatDateDisplay(event_date) }; } } } // Если заседаний нет, проверяем историю состояний if (!lastEvent && data.history?.states && data.history.states.length > 0) { const state = data.history.states[data.history.states.length - 1]; if (state.date && state.state) { lastEvent = { event_name: state.state, event_date: formatDate(state.date), event_time: '', location: '', event_result: '', event_basis: state.basis_doc || '', note: '', publication_date: formatDate(state.date), // Для совместимости с parscourt.php Наименование: state.state, Дата: formatDateDisplay(state.date), Время: '', Место: '', Результат: '', Основание: state.basis_doc || '', Примечание: '', 'Дата размещения': formatDateDisplay(state.date) }; } } // Если событие не найдено, проверяем на ошибки if (!lastEvent) { const pageText = await page.evaluate(() => document.body?.textContent || ''); const isEmpty = !pageText || pageText.trim().length < 100; return { url, source: new URL(url).hostname, court_type: 'moscow', status: isEmpty ? 'error' : 'no_events', error_type: isEmpty ? 'empty_page' : 'no_events_found', error_message: isEmpty ? 'Страница пустая или битая ссылка' : 'События не найдены', last_event: null, message: isEmpty ? 'Ссылка на дело оказалась битой' : 'События не найдены', all_hearings: data.hearings, all_states: data.history?.states || [] }; } return { url, source: new URL(url).hostname, court_type: 'moscow', status: 'success', last_event: lastEvent, all_hearings: data.hearings, all_states: data.history?.states || [] }; } // Если тип суда не определён return { url, source: new URL(url).hostname, court_type: 'unknown', status: 'error', error_type: 'unknown_court', error_message: `Неизвестный тип суда для URL: ${url}`, last_event: null, message: 'Неизвестный тип суда' }; }