// 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 pageData = await page.evaluate((divId) => { const clean = (str) => (str ? str.replace(/\s+/g, ' ').trim() : ''); // ======================================== // ПАРСИНГ РАЗДЕЛА "ДЕЛО" // ======================================== const caseInfo = {}; // Название суда - ищем в заголовке или в начале страницы const courtNameEl = document.querySelector('h1, .court-name, [class*="court"] [class*="name"], title'); if (courtNameEl) { let courtName = clean(courtNameEl.textContent); // Убираем лишнее из title if (courtName.includes('|')) { courtName = courtName.split('|')[0].trim(); } caseInfo.court_name = courtName; } // Номер дела - ищем в заголовке или в тексте "ДЕЛО №" const caseNumberMatch = document.body?.textContent?.match(/ДЕЛО\s*№\s*([^\s~]+(?:\s*~\s*[^\s]+)?)/i); if (caseNumberMatch) { caseInfo.case_number = clean(caseNumberMatch[1]); } // Ищем таблицу с информацией о деле (раздел "ДЕЛО") // Ищем таблицу, которая находится после заголовка "ДЕЛО" const allTables = document.querySelectorAll('table'); let foundCaseTable = false; allTables.forEach((table) => { // Проверяем, есть ли в таблице заголовок "ДЕЛО" или ключевые поля const tableText = clean(table.textContent || '').toLowerCase(); if (tableText.includes('уникальный идентификатор') || tableText.includes('судья') || tableText.includes('дата рассмотрения') || tableText.includes('результат рассмотрения')) { foundCaseTable = true; const rows = Array.from(table.querySelectorAll('tr')); rows.forEach((row) => { const cells = Array.from(row.querySelectorAll('td, th')); if (cells.length >= 2) { const label = clean(cells[0]?.textContent || '').toLowerCase(); const value = clean(cells[1]?.textContent || ''); if ((label.includes('уникальный идентификатор') || label.includes('uid')) && !caseInfo.uid) { caseInfo.uid = value; } else if (label.includes('судья') && !caseInfo.judge) { caseInfo.judge = value; } else if (label.includes('дата рассмотрения') && !caseInfo.consideration_date) { caseInfo.consideration_date = value; } else if (label.includes('результат рассмотрения') && !caseInfo.consideration_result) { caseInfo.consideration_result = value; } else if (label.includes('категория дела') && !caseInfo.category) { caseInfo.category = value; } else if (label.includes('дата поступления') && !caseInfo.intake_date) { caseInfo.intake_date = value; } } }); } }); // Если не нашли в таблице, ищем в тексте страницы if (!caseInfo.uid) { const uidMatch = document.body?.textContent?.match(/Уникальный\s+идентификатор\s+дела[\s\S]{0,200}?([A-Z0-9\-]+)/i); if (uidMatch) { caseInfo.uid = clean(uidMatch[1]); } } // ======================================== // ПАРСИНГ РАЗДЕЛА "СТОРОНЫ ПО ДЕЛУ" // ======================================== const parties = []; const PARTY_TYPES = ['истец', 'ответчик', 'третье лицо', 'представитель', 'соистец', 'соответчик']; // Ищем таблицу по строкам: первая колонка = ИСТЕЦ/ОТВЕТЧИК/ТРЕТЬЕ ЛИЦО/ПРЕДСТАВИТЕЛЬ const allTbl = document.querySelectorAll('table'); allTbl.forEach((table) => { const rows = Array.from(table.querySelectorAll('tr')); rows.forEach((row) => { const cells = Array.from(row.querySelectorAll('td, th')); if (cells.length < 2) return; const col0 = clean(cells[0]?.textContent || ''); const col1 = clean(cells[1]?.textContent || ''); const typeLower = col0.toLowerCase(); // Проверяем: первая ячейка — известный тип стороны, вторая — имя const isPartyType = PARTY_TYPES.some(t => typeLower === t || typeLower.startsWith(t + ' ')); const hasName = col1 && col1.length > 1; if (isPartyType && hasName) { // Не заголовок: "Фамилия", "наименование", "вид лица" и т.п. const nameLower = col1.toLowerCase(); if (nameLower.includes('фамилия') || nameLower.includes('наименование') || nameLower.includes('вид лица') || nameLower === 'инн' || nameLower === 'кпп') { return; } const inn = cells[2] ? clean(cells[2].textContent || '') : ''; const kpp = cells[3] ? clean(cells[3].textContent || '') : ''; const ogrn = cells[4] ? clean(cells[4].textContent || '') : ''; const ogrnip = cells[5] ? clean(cells[5].textContent || '') : ''; parties.push({ type: col0, name: col1, inn: inn || null, kpp: kpp || null, ogrn: ogrn || null, ogrnip: ogrnip || null }); } }); }); // ======================================== // ПАРСИНГ РАЗДЕЛА "СУДЕБНЫЕ АКТЫ" // ======================================== const courtActs = []; // Функция для очистки текста от JavaScript кода const cleanActText = (text) => { // Обрезаем текст до маркеров конца акта const endMarkers = [ /опубликовано\s+\d{2}\.\d{2}\.\d{4}/i, /изменено\s+\d{2}\.\d{2}\.\d{4}/i, /судья\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.\s*[А-ЯЁа-яё]+/i, /function\s+\w+\s*\(/i, /var\s+\w+\s*=/i, /document\./i, /getElementById/i, /addEventListener/i ]; let cleanText = text; let minIndex = text.length; // Находим самое раннее вхождение маркера конца endMarkers.forEach(marker => { const match = text.match(marker); if (match && match.index < minIndex) { minIndex = match.index; } }); // Если нашли маркер, обрезаем до него if (minIndex < text.length) { cleanText = text.substring(0, minIndex).trim(); } // Дополнительно удаляем JavaScript код, если он остался cleanText = cleanText.replace(/\s*function\s+\w+[^]*$/i, ''); cleanText = cleanText.replace(/\s*var\s+\w+[^]*$/i, ''); cleanText = cleanText.replace(/\s*document\.[^]*$/i, ''); cleanText = cleanText.replace(/\s*getElementById[^]*$/i, ''); return cleanText.trim(); }; // Ищем все тексты, начинающиеся с "Именем Российской Федерации" const bodyText = document.body?.textContent || ''; // Ищем все вхождения "Именем Российской Федерации" (с учетом возможных пробелов/без пробелов) const actPattern = /(Именем\s*Российской\s*Федерации[\s\S]{1,100000}?)(?=Именем\s*Российской\s*Федерации|$)/gi; const actMatches = bodyText.matchAll(actPattern); for (const match of actMatches) { let actText = match[1]; if (actText.length > 100) { // Очищаем текст от JavaScript кода actText = cleanActText(actText); if (actText.length < 100) continue; // Пропускаем слишком короткие тексты actText = clean(actText); // Определяем тип акта по тексту let actType = 'Судебный акт'; if (actText.match(/ЗАОЧНОЕ\s+РЕШЕНИЕ/i)) { actType = 'Заочное решение'; } else if (actText.match(/РЕШЕНИЕ/i)) { actType = 'Решение'; } else if (actText.match(/ОПРЕДЕЛЕНИЕ/i)) { actType = 'Определение'; } else if (actText.match(/ПОСТАНОВЛЕНИЕ/i)) { actType = 'Постановление'; } // Извлекаем номер дела и дату из текста акта const caseNumberMatch = actText.match(/№\s*([^\s]+(?:\s*~\s*[^\s]+)?)/i); const dateMatch = actText.match(/(\d{2}\.\d{2}\.\d{4})/); const uidMatch = actText.match(/УИД\s*([^\s]+)/i); // Извлекаем резолютивную часть (что суд решил) let decision = null; // Ищем "РЕШИЛ:" или "РЕШИЛ" и извлекаем текст до конца решения const decisionPatterns = [ /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*Судья\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.\s*[А-ЯЁа-яё]+)/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*опубликовано\s+\d{2}\.\d{2}\.\d{4})/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*изменено\s+\d{2}\.\d{2}\.\d{4})/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*Ответчик\s+вправе)/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*Иными\s+лицами)/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=function\s+\w+\s*\()/i, /РЕШИЛ\s*:?\s*([\s\S]+?)$/i ]; for (const pattern of decisionPatterns) { const decisionMatch = actText.match(pattern); if (decisionMatch && decisionMatch[1]) { decision = clean(decisionMatch[1]).trim(); // Убираем лишние пробелы и переносы decision = decision.replace(/\s+/g, ' ').trim(); // Обрезаем до разумной длины if (decision.length > 10000) { decision = decision.substring(0, 10000) + '...'; } break; } } courtActs.push({ type: actType, title: `${actType}${caseNumberMatch ? ' № ' + caseNumberMatch[1] : ''}${dateMatch ? ' от ' + dateMatch[1] : ''}`, text: actText, decision: decision, case_number: caseNumberMatch ? caseNumberMatch[1] : null, date: dateMatch ? dateMatch[1] : null, uid: uidMatch ? uidMatch[1] : null, link: '' }); } } // Если не нашли через паттерн, ищем ссылки на акты и текст рядом if (courtActs.length === 0) { // Ищем ссылки на акты const actLinks = document.querySelectorAll('a[href*="#"], a[href*="act"], a[href*="document"], a'); actLinks.forEach((link) => { const linkText = clean(link.textContent || ''); if (linkText.match(/судебный\s*акт|решение|определение|постановление/i)) { // Пытаемся найти текст акта после ссылки или в родительском элементе let actText = ''; // Ищем в родительском элементе и следующих let searchEl = link.closest('div, section, article, li'); if (!searchEl) searchEl = link.parentElement; let depth = 0; while (searchEl && depth < 15) { const text = clean(searchEl.textContent || ''); // Ищем текст с "Именем" и достаточной длиной if ((text.includes('Именем') || text.includes('ИменемРоссийской')) && text.length > 1000) { // Извлекаем текст акта const actMatch = text.match(/(Именем\s*Российской\s*Федерации[\s\S]{1,100000})/i); if (actMatch) { actText = clean(actMatch[1]); break; } } searchEl = searchEl.nextElementSibling || searchEl.parentElement; depth++; } if (actText || linkText) { // Очищаем текст от JavaScript кода if (actText) { actText = cleanActText(actText); actText = clean(actText); } let actType = 'Судебный акт'; if (actText && actText.length > 100) { if (actText.match(/ЗАОЧНОЕ\s+РЕШЕНИЕ/i)) { actType = 'Заочное решение'; } else if (actText.match(/РЕШЕНИЕ/i)) { actType = 'Решение'; } else if (actText.match(/ОПРЕДЕЛЕНИЕ/i)) { actType = 'Определение'; } else if (actText.match(/ПОСТАНОВЛЕНИЕ/i)) { actType = 'Постановление'; } } if (actText && actText.length > 100) { // Извлекаем резолютивную часть (что суд решил) let decision = null; const decisionPatterns = [ /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*Судья\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.\s*[А-ЯЁа-яё]+)/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*опубликовано\s+\d{2}\.\d{2}\.\d{4})/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*изменено\s+\d{2}\.\d{2}\.\d{4})/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*Ответчик\s+вправе)/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*Иными\s+лицами)/i, /РЕШИЛ\s*:?\s*([\s\S]+?)(?=function\s+\w+\s*\()/i, /РЕШИЛ\s*:?\s*([\s\S]+?)$/i ]; for (const pattern of decisionPatterns) { const decisionMatch = actText.match(pattern); if (decisionMatch && decisionMatch[1]) { decision = clean(decisionMatch[1]).trim(); decision = decision.replace(/\s+/g, ' ').trim(); if (decision.length > 10000) { decision = decision.substring(0, 10000) + '...'; } break; } } courtActs.push({ type: actType, title: linkText, text: actText, decision: decision, link: link.href || '' }); } } } }); } // Если всё ещё пусто — пробуем найти документы в таблицах (часто для 2-й инстанции) if (courtActs.length === 0) { const actKeyword = /решение|определение|постановление|судебный\s*акт/i; const datePattern = /(\d{2}\.\d{2}\.\d{4})/; document.querySelectorAll('table').forEach((table) => { const rows = Array.from(table.querySelectorAll('tr')); rows.forEach((row) => { const rowText = clean(row.textContent || ''); if (!actKeyword.test(rowText)) return; const linkEl = row.querySelector('a[href]'); if (!linkEl) return; let link = linkEl.getAttribute('href') || ''; if (!link || link.includes('#')) { // Попробуем вытащить ссылку из onclick const onclick = linkEl.getAttribute('onclick') || ''; const urlMatch = onclick.match(/['"](https?:\/\/[^'"]+|\.\/[^'"]+|\/[^'"]+)['"]/); link = urlMatch ? urlMatch[1] : ''; } if (!link || link.includes('javascript:')) return; // Приводим ссылку к абсолютной if (link.startsWith('/')) { link = window.location.origin + link; } else if (!link.startsWith('http')) { const baseUrl = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')); link = baseUrl + '/' + link.replace(/^\.\//, ''); } let actType = 'Судебный акт'; if (rowText.match(/ЗАОЧНОЕ\s+РЕШЕНИЕ/i)) { actType = 'Заочное решение'; } else if (rowText.match(/РЕШЕНИЕ/i)) { actType = 'Решение'; } else if (rowText.match(/ОПРЕДЕЛЕНИЕ/i)) { actType = 'Определение'; } else if (rowText.match(/ПОСТАНОВЛЕНИЕ/i)) { actType = 'Постановление'; } const dateMatch = rowText.match(datePattern); courtActs.push({ type: actType, title: `${actType}${dateMatch ? ' от ' + dateMatch[1] : ''}`, text: '', decision: null, case_number: caseInfo.case_number || null, date: dateMatch ? dateMatch[1] : null, uid: caseInfo.uid || null, link: link }); }); }); } // Если всё ещё пусто — ищем текст акта в блоках cont_doc* (часто для 2-й инстанции) if (courtActs.length === 0) { const docBlocks = document.querySelectorAll('[id^="cont_doc"], .contentt div[id^="cont_doc"]'); docBlocks.forEach((block) => { const rawText = clean(block.textContent || ''); if (!rawText || rawText.length < 200) return; // Определяем тип акта по заголовку let actType = 'Судебный акт'; if (rawText.match(/АПЕЛЛЯЦИОННОЕ\s+ОПРЕДЕЛЕНИЕ/i)) { actType = 'Апелляционное определение'; } else if (rawText.match(/КАССАЦИОННОЕ\s+ОПРЕДЕЛЕНИЕ/i)) { actType = 'Кассационное определение'; } else if (rawText.match(/ОПРЕДЕЛЕНИЕ/i)) { actType = 'Определение'; } else if (rawText.match(/ЗАОЧНОЕ\s+РЕШЕНИЕ/i)) { actType = 'Заочное решение'; } else if (rawText.match(/РЕШЕНИЕ/i)) { actType = 'Решение'; } else if (rawText.match(/ПОСТАНОВЛЕНИЕ/i)) { actType = 'Постановление'; } // Пробуем вытащить дату let date = null; const dateNumeric = rawText.match(/(\d{2}\.\d{2}\.\d{4})/); if (dateNumeric) { date = dateNumeric[1]; } else { const dateText = rawText.match(/(\d{1,2}\s+[а-яё]+\s+\d{4}\s+года)/i); if (dateText) date = dateText[1]; } // Извлекаем резолютивную часть (РЕШИЛ / ОПРЕДЕЛИЛА / ПОСТАНОВИЛ) let decision = null; const decisionPatterns = [ /РЕШИЛ\s*:?\s*([\s\S]+?)(?=\s*Судья\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.\s*[А-ЯЁа-яё]+)/i, /ОПРЕДЕЛИЛА\s*:?\s*([\s\S]+?)(?=\s*Председательствующий|Судьи|Апелляционное определение)/i, /ПОСТАНОВИЛ\s*:?\s*([\s\S]+?)(?=\s*Председательствующий|Судьи|Постановление)/i, /РЕШИЛ\s*:?\s*([\s\S]+?)$/i, /ОПРЕДЕЛИЛА\s*:?\s*([\s\S]+?)$/i, /ПОСТАНОВИЛ\s*:?\s*([\s\S]+?)$/i ]; for (const pattern of decisionPatterns) { const decisionMatch = rawText.match(pattern); if (decisionMatch && decisionMatch[1]) { decision = clean(decisionMatch[1]).trim(); decision = decision.replace(/\s+/g, ' ').trim(); if (decision.length > 10000) { decision = decision.substring(0, 10000) + '...'; } break; } } const actText = cleanActText(rawText); if (actText.length > 100) { courtActs.push({ type: actType, title: `${actType}${date ? ' от ' + date : ''}`, text: actText, decision: decision, case_number: caseInfo.case_number || null, date: date, uid: caseInfo.uid || null, link: '' }); } }); } // ======================================== // ПАРСИНГ СОБЫТИЙ (ДВИЖЕНИЕ ДЕЛА) // ======================================== const div = document.querySelector(`#${divId}`); const events = []; if (div) { const rows = Array.from(div.querySelectorAll('tr')); 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 { caseInfo, parties, courtActs, events }; }, divId); const caseInfoText = JSON.stringify(pageData.caseInfo || {}).toLowerCase(); const isSecondInstance = /апелляц|кассац|надзор|втор(ой|ая)\s+инстанц|апелляцион|кассацион/.test(caseInfoText); const decisionFound = pageData.courtActs?.some(act => { const type = (act?.type || act?.title || '').toLowerCase(); // Если есть любое решение if (type.includes('решение')) return true; // Если тип акта сам по себе апелляционный/кассационный — это 2-я инстанция const isActSecondInstance = /апелляцион|кассацион/.test(type); // Если дело 2-й инстанции (по case_info или по типу акта) и есть определение/постановление if ((isSecondInstance || isActSecondInstance) && (type.includes('определение') || type.includes('постановление'))) return true; return false; }); // Формируем результат const result = { url, source: new URL(url).hostname, court_type: 'regional', status: 'success', case_info: pageData.caseInfo, parties: pageData.parties, court_acts: pageData.courtActs, all_events: pageData.events, decision_found: !!decisionFound }; // Добавляем последнее событие для обратной совместимости if (pageData.events.length > 0) { const lastEvent = pageData.events[pageData.events.length - 1]; result.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) }; } else { result.last_event = null; } // Проверяем, есть ли хотя бы какие-то данные const hasCaseInfo = pageData.caseInfo && Object.keys(pageData.caseInfo).length > 0; const hasParties = pageData.parties && pageData.parties.length > 0; const hasCourtActs = pageData.courtActs && pageData.courtActs.length > 0; const hasEvents = pageData.events && pageData.events.length > 0; // Если нет вообще никаких данных - возвращаем ошибку if (!hasCaseInfo && !hasParties && !hasCourtActs && !hasEvents) { // Проверяем, может быть страница пустая или битая ссылка 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_data', error_type: isEmpty ? 'empty_page' : 'no_data_found', error_message: isEmpty ? 'Страница пустая или битая ссылка' : 'Данные не найдены', last_event: null, message: isEmpty ? 'Ссылка на дело оказалась битой' : 'Данные не найдены', case_info: {}, parties: [], court_acts: [], all_events: [], decision_found: false }; } // Если есть хотя бы какие-то данные - возвращаем успех return result; } // ======================================== // ПАРСИНГ МОСКОВСКИХ СУДОВ (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) => { if (!el) return ""; if (typeof el === 'string') return el.replace(/\s+/g, " ").trim(); if (el.textContent === undefined || el.textContent === null) return ""; return String(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; }; // ======================================== // ПАРСИНГ ИНФОРМАЦИИ О ДЕЛЕ // ======================================== const caseInfo = {}; // Название суда const courtNameEl = document.querySelector('h1, .court-name, [class*="court"] [class*="name"], title'); if (courtNameEl) { let courtName = norm(courtNameEl); if (courtName.includes('|')) { courtName = courtName.split('|')[0].trim(); } caseInfo.court_name = courtName; } // Извлекаем данные из структурированных полей caseInfo.uid = byLeft("Уникальный идентификатор") || byLeft("УИД") || byLeft("уникальный идентификатор дела"); caseInfo.case_number = byLeft("Номер дела") || byLeft("№ дела") || byLeft("Номер дела ~ материала"); caseInfo.intake_date = byLeft("Дата поступления") || byLeft("Поступило"); caseInfo.judge = byLeft("Судья") || byLeft("Cудья") || byLeft("Председательствующий судья"); caseInfo.category = byLeft("Категория дела") || byLeft("Категория"); const statusRaw = byLeft("Текущее состояние") || byLeft("Состояние"); if (statusRaw) { const m = statusRaw.match(/^(.+?),\s*([\d.]{10})$/); caseInfo.current_status = m ? m[1].trim() : statusRaw; caseInfo.current_status_date = m ? m[2] : null; } caseInfo.consideration_date = byLeft("Дата рассмотрения") || byLeft("Дата рассмотрения дела в первой инстанции"); const decisionRaw = byLeft("Решение первой инстанции") || byLeft("Решение (1 инстанция)"); if (decisionRaw) { const m = decisionRaw.match(/^(.+?),\s*([\d.]{10})$/); caseInfo.consideration_result = m ? m[1].trim() : decisionRaw; caseInfo.consideration_result_date = m ? m[2] : null; } caseInfo.decision_effective_date = byLeft("Дата вступления решения в силу") || byLeft("Дата вступления в силу"); // ======================================== // ПАРСИНГ СТОРОН // ======================================== const parties = []; // Ищем текст со сторонами - ищем в структурированных полях и в тексте страницы const partiesFromRows = byLeft("Стороны"); const bodyText = document.body?.textContent || ''; // Пробуем извлечь из структурированного поля if (partiesFromRows) { const partiesText = partiesFromRows; // Ищем истцов const plaintiffMatches = partiesText.matchAll(/Истец\s*:?\s*([^\n\r]+?)(?=\s*Ответчик|$)/gi); for (const match of plaintiffMatches) { if (!match || !match[1]) continue; let name = norm(match[1]); // Убираем лишние символы вроде кавычек HTML name = name.replace(/"/g, '"').replace(/&/g, '&').trim(); if (name && name.length > 2 && !name.toLowerCase().includes('ответчик')) { parties.push({ type: 'ИСТЕЦ', name: name, inn: null, kpp: null, ogrn: null, ogrnip: null }); } } // Ищем ответчиков const defendantMatches = partiesText.matchAll(/Ответчик\s*:?\s*([^\n\r]+?)(?=\s*Истец|\s*Третье|\s*Представитель|$)/gi); for (const match of defendantMatches) { if (!match || !match[1]) continue; let name = norm(match[1]); name = name.replace(/"/g, '"').replace(/&/g, '&').trim(); if (name && name.length > 2) { parties.push({ type: 'ОТВЕТЧИК', name: name, inn: null, kpp: null, ogrn: null, ogrnip: null }); } } } // Если не нашли в структурированных полях, ищем в тексте страницы if (parties.length === 0) { const partiesSection = bodyText.match(/Стороны[\s\S]{0,2000}?(?=Cудья|Категория|Текущее|Дата|$)/i); if (partiesSection) { const partiesText = partiesSection[0]; // Ищем истцов const plaintiffMatches = partiesText.matchAll(/Истец\s*:?\s*([^\n\r]+?)(?=\s*Ответчик|$)/gi); for (const match of plaintiffMatches) { if (!match || !match[1]) continue; let name = norm(match[1]); name = name.replace(/"/g, '"').replace(/&/g, '&').trim(); if (name && name.length > 2 && !name.toLowerCase().includes('ответчик')) { parties.push({ type: 'ИСТЕЦ', name: name, inn: null, kpp: null, ogrn: null, ogrnip: null }); } } // Ищем ответчиков const defendantMatches = partiesText.matchAll(/Ответчик\s*:?\s*([^\n\r]+?)(?=\s*Истец|\s*Третье|\s*Представитель|$)/gi); for (const match of defendantMatches) { if (!match || !match[1]) continue; let name = norm(match[1]); name = name.replace(/"/g, '"').replace(/&/g, '&').trim(); if (name && name.length > 2) { parties.push({ type: 'ОТВЕТЧИК', name: name, inn: null, kpp: null, ogrn: null, ogrnip: null }); } } } } // Ищем третьих лиц const thirdPartyText = byLeft("Третье лицо") || bodyText.match(/Третье\s+лицо[\s\S]{0,500}/i)?.[0]; if (thirdPartyText) { const thirdPartyMatches = thirdPartyText.matchAll(/Третье\s+лицо\s*:?\s*([^\n\r]+)/gi); for (const match of thirdPartyMatches) { if (!match || !match[1]) continue; let name = norm(match[1]); name = name.replace(/"/g, '"').replace(/&/g, '&').trim(); if (name && name.length > 2) { parties.push({ type: 'ТРЕТЬЕ ЛИЦО', name: name, inn: null, kpp: null, ogrn: null, ogrnip: null }); } } } // Ищем представителей const repText = byLeft("Представитель") || bodyText.match(/Представитель[\s\S]{0,500}/i)?.[0]; if (repText) { const repMatches = repText.matchAll(/Представитель\s*:?\s*([^\n\r]+)/gi); for (const match of repMatches) { if (!match || !match[1]) continue; let name = norm(match[1]); name = name.replace(/"/g, '"').replace(/&/g, '&').trim(); if (name && name.length > 2) { parties.push({ type: 'ПРЕДСТАВИТЕЛЬ', name: name, inn: null, kpp: null, ogrn: null, ogrnip: null }); } } } // Таблицы - функция для преобразования tbody в массив строк function tableToRows(tbody) { return Array.from(tbody.querySelectorAll("tr")).map((tr) => { const tds = tr.querySelectorAll("td"); return Array.from(tds).map((td) => { if (!td) return ''; const div = td.querySelector("div"); return norm(div || td); }); }); } // ======================================== // ПАРСИНГ СУДЕБНЫХ АКТОВ // ======================================== const courtActs = []; // Функция для очистки текста от JavaScript кода const cleanActText = (text) => { const endMarkers = [ /опубликовано\s+\d{2}\.\d{2}\.\d{4}/i, /изменено\s+\d{2}\.\d{2}\.\d{4}/i, /судья\s+[А-ЯЁ]\.\s*[А-ЯЁ]\.\s*[А-ЯЁа-яё]+/i, /function\s+\w+\s*\(/i, /var\s+\w+\s*=/i, /document\./i, /getElementById/i, /addEventListener/i ]; let cleanText = text; let minIndex = text.length; endMarkers.forEach(marker => { const match = text.match(marker); if (match && match.index < minIndex) { minIndex = match.index; } }); if (minIndex < text.length) { cleanText = text.substring(0, minIndex).trim(); } cleanText = cleanText.replace(/\s*function\s+\w+[^]*$/i, ''); cleanText = cleanText.replace(/\s*var\s+\w+[^]*$/i, ''); cleanText = cleanText.replace(/\s*document\.[^]*$/i, ''); cleanText = cleanText.replace(/\s*getElementById[^]*$/i, ''); return cleanText.trim(); }; // Ищем таблицу документов (#tabs-3) const docsTbody = document.querySelector("#tabs-3 table tbody"); if (docsTbody) { const docsRows = Array.from(docsTbody.querySelectorAll("tr")); docsRows.forEach((row) => { const cells = row.querySelectorAll("td"); if (cells.length < 2) return; const docDate = cells[0] ? norm(cells[0]) : ''; const docType = cells[1] ? norm(cells[1]) : ''; const docTextCell = cells[2] || null; // Проверяем все ячейки на наличие ссылок (может быть ссылка в любой ячейке) let allCellsLinks = []; Array.from(cells).forEach((cell, idx) => { const cellLinks = cell.querySelectorAll('a[href]'); cellLinks.forEach(link => { const href = link.getAttribute('href'); if (href && !href.includes('#') && !href.includes('javascript:')) { allCellsLinks.push({href: href, cellIndex: idx, text: norm(link)}); } }); }); // Ищем только решения, определения, постановления, мотивированные решения if (docType && (docType.match(/решение|определение|постановление/i))) { // Ищем ссылку на файл во всей строке таблицы let docLink = ''; // Если не нашли явную ссылку, но есть текст "Готовится к публикации" - значит файла пока нет const docTextContent = docTextCell ? norm(docTextCell) : ''; const isPreparing = docTextContent && docTextContent.toLowerCase().includes('готовится к публикации'); // Сначала проверяем ссылки, найденные во всех ячейках if (allCellsLinks.length > 0) { // Берем первую подходящую ссылку for (const linkInfo of allCellsLinks) { const href = linkInfo.href; const linkText = linkInfo.text || ''; const hrefLower = href.toLowerCase(); // Игнорируем навигационные ссылки if (hrefLower.includes('javascript:') || linkText.toLowerCase().includes('вернуться') || linkText.toLowerCase().includes('назад') || linkText.toLowerCase().includes('следующ') || linkText.toLowerCase().includes('предыдущ')) { continue; } // Принимаем ссылку если она похожа на документ if (href.match(/\.(pdf|doc|docx|rtf|txt|html)$/i) || hrefLower.includes('/document/') || hrefLower.includes('/download/') || hrefLower.includes('/file/') || hrefLower.includes('/documents/') || hrefLower.includes('/files/') || linkText.match(/скачать|загрузить|открыть|просмотр|документ|файл/i) || (hrefLower.includes('mos-gorsud.ru') && !hrefLower.includes('details'))) { docLink = href; // Если ссылка относительная, делаем её абсолютной if (docLink.startsWith('/')) { docLink = window.location.origin + docLink; } else if (docLink.startsWith('./') || (!docLink.startsWith('http') && !docLink.startsWith('mailto') && !docLink.startsWith('#'))) { const baseUrl = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')); docLink = baseUrl + '/' + docLink.replace(/^\.\//, ''); } break; } } } // Если не нашли в ячейках, ищем все ссылки в строке if (!docLink) { const allLinks = row.querySelectorAll('a[href]'); for (const link of allLinks) { const href = link.getAttribute('href'); if (!href || href.trim() === '') continue; // Проверяем, что это ссылка на документ (не навигация) const linkText = norm(link) || ''; const hrefLower = href.toLowerCase(); // Игнорируем навигационные ссылки if (hrefLower.includes('#') || hrefLower.includes('javascript:') || linkText.toLowerCase().includes('вернуться') || linkText.toLowerCase().includes('назад') || linkText.toLowerCase().includes('следующ') || linkText.toLowerCase().includes('предыдущ')) { continue; } // Принимаем ссылку если: // 1. Это файл с расширением // 2. Содержит ключевые слова в пути // 3. Содержит ключевые слова в тексте ссылки // 4. Это любая ссылка в ячейке с документом (кроме навигации) if (href.match(/\.(pdf|doc|docx|rtf|txt|html)$/i) || hrefLower.includes('/document/') || hrefLower.includes('/download/') || hrefLower.includes('/file/') || hrefLower.includes('/documents/') || hrefLower.includes('/files/') || linkText.match(/скачать|загрузить|открыть|просмотр|документ|файл/i) || (docTextCell && docTextCell.contains(link))) { docLink = href; // Если ссылка относительная, делаем её абсолютной if (docLink.startsWith('/')) { docLink = window.location.origin + docLink; } else if (docLink.startsWith('./') || (!docLink.startsWith('http') && !docLink.startsWith('mailto') && !docLink.startsWith('#'))) { const baseUrl = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/')); docLink = baseUrl + '/' + docLink.replace(/^\.\//, ''); } break; } } } // Если не нашли ссылку в , проверяем data-атрибуты или onclick if (!docLink && !isPreparing) { // Проверяем data-атрибуты на ссылках const linksWithData = row.querySelectorAll('a[data-href], a[data-url], [data-document-id], [data-file-id]'); for (const link of linksWithData) { const dataHref = link.getAttribute('data-href') || link.getAttribute('data-url') || link.getAttribute('data-document-id') || link.getAttribute('data-file-id'); if (dataHref && !dataHref.includes('#')) { docLink = dataHref; if (docLink.startsWith('/')) { docLink = window.location.origin + docLink; } break; } } // Проверяем onclick для JavaScript-ссылок if (!docLink) { const linksWithOnclick = row.querySelectorAll('a[onclick]'); for (const link of linksWithOnclick) { const onclick = link.getAttribute('onclick') || ''; // Ищем URL в onclick const urlMatch = onclick.match(/['"](https?:\/\/[^'"]+|\.\/[^'"]+|\/[^'"]+)['"]/); if (urlMatch && !urlMatch[1].includes('#')) { docLink = urlMatch[1]; if (docLink.startsWith('/') || docLink.startsWith('./')) { docLink = window.location.origin + (docLink.startsWith('./') ? docLink.substring(1) : docLink); } break; } } } } // Определяем тип акта let actType = 'Судебный акт'; if (docType.match(/МОТИВИРОВАННОЕ\s+РЕШЕНИЕ/i) || docType.match(/мотивированное\s+решение/i)) { actType = 'Мотивированное решение'; } else if (docType.match(/ЗАОЧНОЕ\s+РЕШЕНИЕ/i)) { actType = 'Заочное решение'; } else if (docType.match(/РЕШЕНИЕ/i)) { actType = 'Решение'; } else if (docType.match(/ОПРЕДЕЛЕНИЕ/i)) { actType = 'Определение'; } else if (docType.match(/ПОСТАНОВЛЕНИЕ/i)) { actType = 'Постановление'; } // Извлекаем номер дела из текста ячейки, если есть const caseNumberMatch = docTextContent.match(/(\d{2}-\d+\/\d{4})/); // Если всё ещё не нашли ссылку, но документ не готовится - возможно ссылка в другом формате // Проверяем, может быть это кнопка или элемент с классом, указывающим на документ if (!docLink && !isPreparing && docTextCell) { // Ищем любые элементы с классами, связанными с документами const docElements = docTextCell.querySelectorAll('[class*="doc"], [class*="file"], [class*="download"], [id*="doc"], [id*="file"]'); for (const el of docElements) { const href = el.getAttribute('href') || el.getAttribute('data-href') || el.getAttribute('data-url'); if (href && !href.includes('#') && !href.includes('javascript:')) { docLink = href; if (docLink.startsWith('/')) { docLink = window.location.origin + docLink; } else if (!docLink.startsWith('http')) { docLink = window.location.origin + '/' + docLink.replace(/^\.\//, ''); } break; } } } // Добавляем акт только если есть ссылка или документ готовится к публикации if (docLink || isPreparing) { courtActs.push({ type: actType, title: `${actType}${caseNumberMatch ? ' № ' + caseNumberMatch[1] : ''}${docDate ? ' от ' + docDate : ''}`, text: '', // Текст не парсим, это файл - нужно скачивать отдельно decision: null, // Резолютивная часть не парсим, это файл date: docDate, case_number: caseNumberMatch ? caseNumberMatch[1] : null, uid: null, link: docLink || '', // Ссылка на файл для скачивания status: isPreparing ? 'preparing' : (docLink ? 'available' : 'unknown') // Статус документа }); } else { // Если нет ссылки и не готовится - всё равно добавляем, но со статусом unknown // Может быть ссылка появится позже или находится в другом месте courtActs.push({ type: actType, title: `${actType}${caseNumberMatch ? ' № ' + caseNumberMatch[1] : ''}${docDate ? ' от ' + docDate : ''}`, text: '', decision: null, date: docDate, case_number: caseNumberMatch ? caseNumberMatch[1] : null, uid: null, link: '', // Ссылка не найдена status: 'unknown' }); } } }); } // Для московских судов не ищем текст актов на странице - они в файлах // Если нужно будет парсить текст из файлов - это будет отдельная задача // Дополнительный поиск ссылок на документы по всей странице // Может быть ссылки находятся вне таблицы #tabs-3 if (courtActs.length > 0) { // Ищем все ссылки на странице, которые могут быть связаны с документами const allPageLinks = document.querySelectorAll('a[href]'); const documentLinks = []; for (const link of allPageLinks) { const href = link.getAttribute('href'); if (!href || href.includes('#') || href.includes('javascript:')) continue; const linkText = norm(link); const hrefLower = href.toLowerCase(); // Проверяем, похожа ли ссылка на документ if (href.match(/\.(pdf|doc|docx|rtf|txt|html)$/i) || hrefLower.includes('/document/') || hrefLower.includes('/download/') || hrefLower.includes('/file/') || hrefLower.includes('/documents/') || hrefLower.includes('/files/') || (hrefLower.includes('mos-gorsud.ru') && (hrefLower.includes('/document') || hrefLower.includes('/file') || hrefLower.includes('/download')))) { // Пытаемся сопоставить ссылку с документом по дате или номеру дела const linkDateMatch = linkText.match(/(\d{2}\.\d{2}\.\d{4})/); const linkCaseMatch = linkText.match(/(\d{2}-\d+\/\d{4})/); documentLinks.push({ href: href, text: linkText, date: linkDateMatch ? linkDateMatch[1] : null, caseNumber: linkCaseMatch ? linkCaseMatch[1] : null }); } } // Пытаемся сопоставить найденные ссылки с документами по дате documentLinks.forEach(docLink => { if (!docLink.date) return; courtActs.forEach(act => { if (act.link || act.status === 'preparing') return; // Уже есть ссылка или готовится // Если даты совпадают, добавляем ссылку if (act.date === docLink.date) { let finalLink = docLink.href; if (finalLink.startsWith('/')) { finalLink = window.location.origin + finalLink; } else if (!finalLink.startsWith('http')) { finalLink = window.location.origin + '/' + finalLink.replace(/^\.\//, ''); } act.link = finalLink; act.status = 'available'; } }); }); } 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 { caseInfo, parties, courtActs, 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) }; } } const caseInfoText = JSON.stringify(data.caseInfo || {}).toLowerCase(); const isSecondInstance = /апелляц|кассац|надзор|втор(ой|ая)\s+инстанц|апелляцион|кассацион/.test(caseInfoText); const decisionFound = data.courtActs?.some(act => { const type = (act?.type || act?.title || '').toLowerCase(); // Если есть любое решение if (type.includes('решение')) return true; // Если тип акта сам по себе апелляционный/кассационный — это 2-я инстанция const isActSecondInstance = /апелляцион|кассацион/.test(type); // Если дело 2-й инстанции (по case_info или по типу акта) и есть определение/постановление if ((isSecondInstance || isActSecondInstance) && (type.includes('определение') || type.includes('постановление'))) return true; return false; }); // Формируем результат const result = { url, source: new URL(url).hostname, court_type: 'moscow', status: 'success', case_info: data.caseInfo || {}, parties: data.parties || [], court_acts: data.courtActs || [], all_hearings: data.hearings || [], all_states: data.history?.states || [], decision_found: !!decisionFound }; // Добавляем последнее событие для обратной совместимости if (lastEvent) { result.last_event = lastEvent; } else { result.last_event = null; } // Проверяем, есть ли хотя бы какие-то данные const hasCaseInfo = data.caseInfo && Object.keys(data.caseInfo).length > 0; const hasParties = data.parties && data.parties.length > 0; const hasCourtActs = data.courtActs && data.courtActs.length > 0; const hasHearings = data.hearings && data.hearings.length > 0; const hasStates = data.history?.states && data.history.states.length > 0; // Если нет вообще никаких данных - возвращаем ошибку if (!hasCaseInfo && !hasParties && !hasCourtActs && !hasHearings && !hasStates) { 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_data', error_type: isEmpty ? 'empty_page' : 'no_data_found', error_message: isEmpty ? 'Страница пустая или битая ссылка' : 'Данные не найдены', last_event: null, message: isEmpty ? 'Ссылка на дело оказалась битой' : 'Данные не найдены', case_info: {}, parties: [], court_acts: [], all_hearings: [], all_states: [], decision_found: false }; } // Если есть хотя бы какие-то данные - возвращаем успех return result; } // Если тип суда не определён return { url, source: new URL(url).hostname, court_type: 'unknown', status: 'error', error_type: 'unknown_court', error_message: `Неизвестный тип суда для URL: ${url}`, last_event: null, message: 'Неизвестный тип суда' }; }