Files
crm.clientright.ru/court_parser_text_function.js

1532 lines
71 KiB
JavaScript
Raw Normal View History

// 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(/&quot;/g, '"').replace(/&amp;/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(/&quot;/g, '"').replace(/&amp;/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(/&quot;/g, '"').replace(/&amp;/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(/&quot;/g, '"').replace(/&amp;/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(/&quot;/g, '"').replace(/&amp;/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(/&quot;/g, '"').replace(/&amp;/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;
}
}
}
// Если не нашли ссылку в <a>, проверяем 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: 'Неизвестный тип суда'
};
}