utils/parse/collect_promo_links.js

158 lines
6.8 KiB
JavaScript
Raw Normal View History

2026-03-16 12:58:57 +03:00
// ==UserScript==
// @name Yandex Promo + Direct Links Collector
// @namespace yandex.console.collector.promo.multi
// @version 2.0
// @description Собирает промо-ссылки и прямые ссылки на сайты рекламодателей из выдачи Яндекса
// @match https://yandex.ru/search/*
// @match https://www.yandex.ru/search/*
// @match https://yandex.com/search/*
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @connect yandex.ru
// @connect yandex.com
// @connect *
// @run-at document-idle
2026-03-16 12:58:57 +03:00
// ==/UserScript==
(function () {
'use strict';
const BRANDS = ['Belgee', 'Haval', 'Chery', 'Geely', 'Changan', 'Exeed', 'Omoda',
'Jaecoo', 'Tank', 'Jetour', 'Forthing', 'BAIC', 'Dongfeng', 'Hongqi'];
const REGIONS = ['Спб', 'Ленинградской области', 'Нижнем Новгороде'];
const QUERIES = [];
for (const r of REGIONS) {
for (const b of BRANDS) {
QUERIES.push(
`купить ${b} в ${r}`,
`купить бу ${b} в ${r}`,
`купить в ${r} бу ${b}`,
`купить в ${r} ${b}`,
);
2026-03-16 12:58:57 +03:00
}
}
const MAX_PAGES_PER_QUERY = 3;
const PAGE_DELAY_MS = 800;
const QUERY_DELAY_MS = 1500;
const RESOLVE_REDIRECTS = false; // true = переходить по yabs (это клик по рекламе!)
const baseUrl = location.origin;
const rows = [];
const seen = new Set();
const sleep = ms => new Promise(r => setTimeout(r, ms));
function cleanUrl(url) {
try {
const u = new URL(url, baseUrl);
['primary_reqid', 'rnd', 'reqid'].forEach(p => u.searchParams.delete(p));
return u.toString();
} catch { return url; }
}
function gmFetch(url, extra = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET', url, timeout: 15000,
headers: { 'User-Agent': navigator.userAgent, 'Referer': baseUrl },
...extra,
onload: r => (r.status >= 200 && r.status < 400) ? resolve(r) : reject(new Error('HTTP ' + r.status)),
onerror: reject,
ontimeout: () => reject(new Error('Timeout')),
});
});
}
// Рекламный ли блок: старое слово «Промо/Реклама», новый значок-стрелка или yabs-ссылка
function isPromo(item) {
if (item.querySelector('.AdvLabel, .OrganicAdvLabel, .AdvLabel-Icon, [aria-label="Промо"], [aria-label="Реклама"]')) return true;
const t = item.querySelector('.AdvLabel-Text, .Label');
return !!(t && /^(Промо|Реклама)$/i.test(t.textContent.trim()));
}
// Прямой адрес сайта из видимой строки адреса под заголовком
function extractDirect(item) {
const pathItem = item.querySelector('.Organic-Path .Path-Item, .Path .Path-Item');
const pathText = (item.querySelector('.Organic-Path, .Path') || pathItem || {}).textContent || '';
const display = pathText.replace(/\s+/g, ' ').trim();
// домен — первая часть строки адреса до « › »
const host = ((pathItem ? pathItem.textContent : display).split('›')[0] || '').trim().toLowerCase();
if (/^([a-zа-яё0-9-]+\.)+[a-zа-яё]{2,}$/i.test(host)) return { direct: 'https://' + host, display };
// запасной вариант: прямая не-яндексовая ссылка в блоке
for (const a of item.querySelectorAll('a[href^="http"]')) {
const h = a.getAttribute('href');
if (!/yandex\.|yabs\.|ya\.ru/.test(h)) return { direct: h, display };
2026-03-16 12:58:57 +03:00
}
return { direct: '', display };
}
function getDomain(url) {
try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; }
}
async function resolveYabs(url) {
try { const r = await gmFetch(url); return r.finalUrl || ''; } catch { return ''; }
}
async function processPage(doc, query, page) {
const items = doc.querySelectorAll('li.serp-item');
let pos = 0, found = 0;
for (const item of items) {
pos++;
if (!isPromo(item)) continue;
const promoA = item.querySelector('a[href*="yabs.yandex."]');
const promo = promoA ? cleanUrl(promoA.getAttribute('href')) : '';
let { direct, display } = extractDirect(item);
if (!direct && promo && RESOLVE_REDIRECTS) {
direct = await resolveYabs(promo);
await sleep(500);
}
const key = getDomain(direct) || promo;
if (!key || seen.has(query + '|' + key)) continue;
seen.add(query + '|' + key);
const title = (item.querySelector('h2, .OrganicTitle-LinkText') || {}).textContent || '';
rows.push([query, page, pos, getDomain(direct), title.trim(), display, direct, promo]);
found++;
2026-03-16 12:58:57 +03:00
}
return found;
}
function getNextPageUrl(doc) {
const a = doc.querySelector('a.Pager-Item_type_next, a[aria-label="Следующая страница"]');
const h = a && a.getAttribute('href');
return h ? new URL(h, baseUrl).toString() : null;
}
async function processQuery(query) {
let url = `${baseUrl}/search/?text=${encodeURIComponent(query)}`;
let page = 0, total = 0;
const parser = new DOMParser();
try {
while (url && page < MAX_PAGES_PER_QUERY) {
if (page) await sleep(PAGE_DELAY_MS);
const html = (await gmFetch(url)).responseText;
if (/showcaptcha|checkcaptcha/i.test(html)) { console.warn('⚠️ Капча на запросе:', query); break; }
const doc = parser.parseFromString(html, 'text/html');
page++;
total += await processPage(doc, query, page);
url = getNextPageUrl(doc);
}
console.log(`[${query}] страниц: ${page}, рекламных сайтов: ${total}`);
} catch (e) { console.error(`❌ ${query}:`, e); }
return total;
}
(async function run() {
console.log(`🚀 Запросов: ${QUERIES.length}, страниц на запрос: ${MAX_PAGES_PER_QUERY}`);
for (let i = 0; i < QUERIES.length; i++) {
if (i) await sleep(QUERY_DELAY_MS);
await processQuery(QUERIES[i]);
2026-03-16 12:58:57 +03:00
}
const header = ['Запрос', 'Стр', 'Поз', 'Домен', 'Заголовок', 'Видимый адрес', 'Прямая ссылка', 'Промо-ссылка'];
const tsv = [header, ...rows].map(r => r.map(c => String(c).replace(/[\t\n]/g, ' ')).join('\t')).join('\n');
console.table(rows.map(r => ({ Запрос: r[0], Домен: r[3], Прямая: r[6] })));
if (rows.length) {
GM_setClipboard(tsv, 'text');
console.log(`✅ Найдено ${rows.length}. Таблица в буфере, вставьте в Excel/Google Таблицы (Ctrl+V)`);
} else console.log('⚠️ Рекламных блоков не найдено');
})();
2026-03-16 12:58:57 +03:00
})();