// ==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 // ==/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}`, ); } } 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 }; } 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++; } 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]); } 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('⚠️ Рекламных блоков не найдено'); })(); })();