Files
crm.clientright.ru/court_parser_text_function.js
Fedor 01c4fe80b5 chore: snapshot current working tree changes
Save all currently accumulated repository changes as a backup snapshot for Gitea so no local work is lost.
2026-03-26 14:19:01 +03:00

1532 lines
71 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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: 'Неизвестный тип суда'
};
}