Save all currently accumulated repository changes as a backup snapshot for Gitea so no local work is lost.
483 lines
19 KiB
JavaScript
483 lines
19 KiB
JavaScript
// 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: 'Неизвестный тип суда'
|
||
};
|
||
}
|
||
|