Files
crm.clientright.ru/court_parser_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

483 lines
19 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 events = await page.evaluate((divId) => {
const clean = (str) => (str ? str.replace(/\s+/g, ' ').trim() : '');
const div = document.querySelector(`#${divId}`);
if (!div) return [];
const rows = Array.from(div.querySelectorAll('tr'));
const events = [];
rows.forEach((row) => {
const tds = row.querySelectorAll('td');
if (tds.length < 2) return;
const event_name = clean(tds[0]?.textContent || '');
const event_date = clean(tds[1]?.textContent || '');
const event_time = clean(tds[2]?.textContent || '');
const location = clean(tds[3]?.textContent || '');
const event_result = clean(tds[4]?.textContent || '');
const event_basis = clean(tds[5]?.textContent || '');
const note = clean(tds[6]?.textContent || '');
const publication_date = clean(tds[7]?.textContent || '');
// Пропускаем записи, если название события не указано или дата неверная
if (!event_name || !event_date || event_date === '1970-01-01') {
return;
}
events.push({
event_name,
event_date,
event_time,
location,
event_result,
event_basis,
note,
publication_date
});
});
return events;
}, divId);
// Возвращаем последнее событие (аналогично parscourt.php)
if (events.length > 0) {
const lastEvent = events[events.length - 1];
return {
url,
source: new URL(url).hostname,
court_type: 'regional',
status: 'success',
last_event: {
event_name: lastEvent.event_name,
event_date: formatDate(lastEvent.event_date),
event_time: lastEvent.event_time,
location: lastEvent.location,
event_result: lastEvent.event_result,
event_basis: lastEvent.event_basis,
note: lastEvent.note,
publication_date: formatDate(lastEvent.publication_date),
// Для совместимости с parscourt.php (кириллические ключи)
Наименование: lastEvent.event_name,
Дата: formatDateDisplay(lastEvent.event_date),
Время: lastEvent.event_time,
Место: lastEvent.location,
Результат: lastEvent.event_result,
Основание: lastEvent.event_basis,
Примечание: lastEvent.note,
'Дата размещения': formatDateDisplay(lastEvent.publication_date)
},
all_events: events
};
}
// Проверяем, может быть страница пустая или битая ссылка
const pageText = await page.evaluate(() => document.body?.textContent || '');
const isEmpty = !pageText || pageText.trim().length < 100;
return {
url,
source: new URL(url).hostname,
court_type: 'regional',
status: isEmpty ? 'error' : 'no_events',
error_type: isEmpty ? 'empty_page' : 'no_events_found',
error_message: isEmpty ? 'Страница пустая или битая ссылка' : 'События не найдены',
last_event: null,
message: isEmpty ? 'Ссылка на дело оказалась битой' : 'События не найдены'
};
}
// ========================================
// ПАРСИНГ МОСКОВСКИХ СУДОВ (mos-gorsud.ru)
// ========================================
if (isMoscowCourt) {
// Ждём карточку
await page.waitForSelector(
".detail-cart .row_card, .case-card, .case-details, .content, main .wrapper_innercontent",
{ timeout: 20000 }
);
// Активируем вкладки
try {
for (const id of ["#ui-id-1", "#ui-id-2", "#ui-id-3"]) {
if (await page.$(id)) await page.click(id);
}
const tabLinks = await page.$$(`a[href^="#tabs-"], .tabs_wrapper a.ui-tabs-anchor`);
if (tabLinks.length) for (const a of tabLinks) await a.click();
await page.waitForTimeout(300);
} catch (_) {}
const data = await page.evaluate(() => {
const norm = (el) => (el ? el.textContent.replace(/\s+/g, " ").trim() : "");
const qsa = (sel) => Array.from(document.querySelectorAll(sel));
function collectRows() {
const rows = [];
qsa(".detail-cart .row_card").forEach((r) => {
const left = norm(r.querySelector(".left"));
const right = norm(r.querySelector(".right"));
if (left && right) rows.push({ left, right });
});
if (!rows.length) {
qsa("table, .case-card, .case-details").forEach((tbl) => {
qsa("tr", tbl).forEach((tr) => {
const tds = tr.querySelectorAll("td, th");
if (tds.length === 2) {
const left = norm(tds[0]);
const right = norm(tds[1]);
if (left && right) rows.push({ left, right });
}
});
});
}
if (!rows.length) {
qsa(".case-card__row, .kv-row").forEach((row) => {
const left = norm(row.querySelector(".case-card__key, .kv-key, .left"));
const right = norm(row.querySelector(".case-card__val, .kv-val, .right"));
if (left && right) rows.push({ left, right });
});
}
return rows;
}
const rows = collectRows();
const byLeft = (start) => {
const row = rows.find((r) =>
r.left.toLowerCase().startsWith(start.toLowerCase())
);
return row ? row.right : null;
};
// Таблицы
function tableToRows(tbody) {
return Array.from(tbody.querySelectorAll("tr")).map((tr) => {
const tds = tr.querySelectorAll("td");
return Array.from(tds).map((td) => norm(td.querySelector("div") || td));
});
}
const sessionsTbody = document.querySelector("#tabs-2 table tbody");
const hearingsRows = sessionsTbody ? tableToRows(sessionsTbody) : [];
const hearings = hearingsRows.map((cols) => ({
datetime: cols[0] || null,
hall: cols[1] || null,
stage: cols[2] || null,
result: cols[3] || null,
basis: cols[4] || null,
}));
const stTbody = document.querySelector("#tabs-1 #state-history table tbody");
const stateRows = stTbody ? tableToRows(stTbody) : [];
const states = stateRows.map((cols) => ({
date: cols[0] || null,
state: cols[1] || null,
basis_doc: cols[2] || null,
}));
return {
hearings,
history: { states },
};
});
// Извлекаем последнее событие (аналогично MoscowCourtParser)
let lastEvent = null;
// Проверяем заседания (hearings)
if (data.hearings && data.hearings.length > 0) {
const hearing = data.hearings[data.hearings.length - 1];
if (hearing.datetime) {
const datetime = hearing.datetime;
let event_date = '';
let event_time = '';
// Формат: "27.10.2025 09:30" или "27.10.2025"
const match1 = datetime.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2})/);
if (match1) {
event_date = match1[1];
event_time = match1[2];
} else {
const match2 = datetime.match(/(\d{2}\.\d{2}\.\d{4})/);
if (match2) {
event_date = match2[1];
}
}
if (event_date) {
lastEvent = {
event_name: hearing.stage || 'Судебное заседание',
event_date: formatDate(event_date),
event_time: event_time,
location: hearing.hall || '',
event_result: hearing.result || '',
event_basis: hearing.basis || '',
note: '',
publication_date: formatDate(event_date),
// Для совместимости с parscourt.php
Наименование: hearing.stage || 'Судебное заседание',
Дата: formatDateDisplay(event_date),
Время: event_time,
Место: hearing.hall || '',
Результат: hearing.result || '',
Основание: hearing.basis || '',
Примечание: '',
'Дата размещения': formatDateDisplay(event_date)
};
}
}
}
// Если заседаний нет, проверяем историю состояний
if (!lastEvent && data.history?.states && data.history.states.length > 0) {
const state = data.history.states[data.history.states.length - 1];
if (state.date && state.state) {
lastEvent = {
event_name: state.state,
event_date: formatDate(state.date),
event_time: '',
location: '',
event_result: '',
event_basis: state.basis_doc || '',
note: '',
publication_date: formatDate(state.date),
// Для совместимости с parscourt.php
Наименование: state.state,
Дата: formatDateDisplay(state.date),
Время: '',
Место: '',
Результат: '',
Основание: state.basis_doc || '',
Примечание: '',
'Дата размещения': formatDateDisplay(state.date)
};
}
}
// Если событие не найдено, проверяем на ошибки
if (!lastEvent) {
const pageText = await page.evaluate(() => document.body?.textContent || '');
const isEmpty = !pageText || pageText.trim().length < 100;
return {
url,
source: new URL(url).hostname,
court_type: 'moscow',
status: isEmpty ? 'error' : 'no_events',
error_type: isEmpty ? 'empty_page' : 'no_events_found',
error_message: isEmpty ? 'Страница пустая или битая ссылка' : 'События не найдены',
last_event: null,
message: isEmpty ? 'Ссылка на дело оказалась битой' : 'События не найдены',
all_hearings: data.hearings,
all_states: data.history?.states || []
};
}
return {
url,
source: new URL(url).hostname,
court_type: 'moscow',
status: 'success',
last_event: lastEvent,
all_hearings: data.hearings,
all_states: data.history?.states || []
};
}
// Если тип суда не определён
return {
url,
source: new URL(url).hostname,
court_type: 'unknown',
status: 'error',
error_type: 'unknown_court',
error_message: `Неизвестный тип суда для URL: ${url}`,
last_event: null,
message: 'Неизвестный тип суда'
};
}