From 5a3a5b3f0b0a9739cd1d99531b9207a103721fd8 Mon Sep 17 00:00:00 2001 From: dimaromsa Date: Tue, 20 Oct 2026 12:38:52 +0300 Subject: [PATCH] feat: update script add copy on excel format --- parse/collect_promo_links.js | 373 ++++++++++++++--------------------- 1 file changed, 151 insertions(+), 222 deletions(-) diff --git a/parse/collect_promo_links.js b/parse/collect_promo_links.js index 76ef67e..85b161b 100644 --- a/parse/collect_promo_links.js +++ b/parse/collect_promo_links.js @@ -1,229 +1,158 @@ // ==UserScript== -// @name Yandex Promo Links Collector (Multi-Query Console Version) -// @namespace yandex.console.collector.promo.multi -// @version 1.2 -// @description Собирает промо ссылки из выдачи Яндекса для нескольких запросов — версия для Tampermonkey (единый вывод) -// @match https://yandex.ru/search/* -// @match https://www.yandex.ru/search/* -// @match https://yandex.com/search/* -// @grant GM_xmlhttpRequest -// @connect yandex.ru -// @connect yandex.com -// @run-at document-idle +// @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 QUERIES = [ +(function () { + 'use strict'; - 'купить Belgee в Нижнем Новгороде', - 'купить бу Belgee в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Belgee ', - 'купить в Нижнем Новгороде Belgee ', - - 'купить Haval в Нижнем Новгороде', - 'купить бу Haval в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Haval ', - 'купить в Нижнем Новгороде Haval ', - - 'купить Chery в Нижнем Новгороде', - 'купить бу Chery в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Chery ', - 'купить в Нижнем Новгороде Chery ', - - 'купить Geely в Нижнем Новгороде', - 'купить бу Geely в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Geely ', - 'купить в Нижнем Новгороде Geely ', - - 'купить Changan в Нижнем Новгороде', - 'купить бу Changan в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Changan ', - 'купить в Нижнем Новгороде Changan ', - - 'купить Exeed в Нижнем Новгороде', - 'купить бу Exeed в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Exeed ', - 'купить в Нижнем Новгороде Exeed ', - - 'купить Omoda в Нижнем Новгороде', - 'купить бу Omoda в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Omoda ', - 'купить в Нижнем Новгороде Omoda ', - - 'купить Jaecoo в Нижнем Новгороде', - 'купить бу Jaecoo в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Jaecoo ', - 'купить в Нижнем Новгороде Jaecoo ', - - 'купить Tank в Нижнем Новгороде', - 'купить бу Tank в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Tank ', - 'купить в Нижнем Новгороде Tank ', - - 'купить Jetour в Нижнем Новгороде', - 'купить бу Jetour в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Jetour ', - 'купить в Нижнем Новгороде Jetour ', - - 'купить Forthing в Нижнем Новгороде', - 'купить бу Forthing в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Forthing ', - 'купить в Нижнем Новгороде Forthing ', - - 'купить BAIC в Нижнем Новгороде', - 'купить бу BAIC в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу BAIC ', - 'купить в Нижнем Новгороде BAIC ', - - 'купить Dongfeng в Нижнем Новгороде', - 'купить бу Dongfeng в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Dongfeng ', - 'купить в Нижнем Новгороде Dongfeng ', - - 'купить Hongqi в Нижнем Новгороде', - 'купить бу Hongqi в Нижнем Новгороде', - 'купить в Нижнем Новгороде бу Hongqi ', - 'купить в Нижнем Новгороде Hongqi ', - ]; - const MAX_PAGES_PER_QUERY = 3; - const PAGE_DELAY_MS = 500; - const QUERY_DELAY_MS = 1000; - - var allLinks = []; - var uniqueLinks = new Set(); - var baseUrl = window.location.origin; - - function cleanUrl(url) { - try { - var urlObj = new URL(url); - urlObj.searchParams.delete('primary_reqid'); - urlObj.searchParams.delete('rnd'); - urlObj.searchParams.delete('reqid'); - return urlObj.toString(); - } catch (e) { - return url; - } - } - function processPage(doc, queryLinks) { - var nodes = doc.querySelectorAll('li.serp-item a[href]'); - var pageLinks = []; - for (var i = 0; i < nodes.length; i++) { - var href = nodes[i].getAttribute('href'); - if (href && href.startsWith('https://yabs.yandex.ru')) { - var fullUrl = href.startsWith('http') ? href : baseUrl + href; - var cleanedUrl = cleanUrl(fullUrl); - if (!uniqueLinks.has(cleanedUrl)) { - uniqueLinks.add(cleanedUrl); - pageLinks.push(cleanedUrl); - queryLinks.push(cleanedUrl); - } - } - } - return pageLinks.length; - } - function getNextPageUrl(doc) { - var nextBtn = doc.querySelector('a.Pager-Item_type_next'); - if (!nextBtn) { - nextBtn = doc.querySelector('a[aria-label="Следующая страница"]'); - } - if (nextBtn) { - var href = nextBtn.getAttribute('href'); - if (href) { - return href.startsWith('http') ? href : baseUrl + href; - } - } - return null; - } - async function processQuery(query) { - var queryLinks = []; - var pagesProcessed = 0; - var searchUrl = `${baseUrl}/search/?text=${encodeURIComponent(query)}`; - try { - var html = await gmFetch(searchUrl); - var parser = new DOMParser(); - var doc = parser.parseFromString(html, 'text/html'); - processPage(doc, queryLinks); - pagesProcessed++; - var nextUrl = getNextPageUrl(doc); - while (nextUrl && pagesProcessed < MAX_PAGES_PER_QUERY) { - await new Promise(r => setTimeout(r, PAGE_DELAY_MS)); - html = await gmFetch(nextUrl); - doc = parser.parseFromString(html, 'text/html'); - processPage(doc, queryLinks); - pagesProcessed++; - nextUrl = getNextPageUrl(doc); - } - console.log(`[Запрос: "${query}"] Обработано страниц: ${pagesProcessed}, промо ссылок: ${queryLinks.length}`); - allLinks.push(queryLinks.join('\n')); - } catch (error) { - console.error(`❌ Ошибка для запроса "${query}":`, error); - } - return queryLinks.length; - } - function gmFetch(url) { - return new Promise((resolve, reject) => { - GM_xmlhttpRequest({ - method: 'GET', - url: url, - headers: { - 'User-Agent': navigator.userAgent, - 'Referer': baseUrl - }, - onload: (response) => { - if (response.status >= 200 && response.status < 300) { - resolve(response.responseText); - } else { - reject(new Error(`HTTP ${response.status}`)); - } - }, - onerror: (err) => reject(err), - ontimeout: (err) => reject(new Error('Timeout')) - }); - }); - } - function copyToClipboard(text) { - if (navigator.clipboard && window.isSecureContext) { - return navigator.clipboard.writeText(text); - } - return new Promise((resolve, reject) => { - try { - var textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.left = '-9999px'; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - var success = document.execCommand('copy'); - document.body.removeChild(textarea); - success ? resolve() : reject(); - } catch (e) { - reject(e); - } - }); + 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 (это клик по рекламе!) - console.log(`🚀 Запуск сбора промо ссылок для ${QUERIES.length} запросов (макс. страниц на запрос: ${MAX_PAGES_PER_QUERY})...`); - (async function run() { - var totalLinks = 0; - for (let i = 0; i < QUERIES.length; i++) { - if (i > 0) await new Promise(r => setTimeout(r, QUERY_DELAY_MS)); - totalLinks += await processQuery(QUERIES[i]); - } + const baseUrl = location.origin; + const rows = []; + const seen = new Set(); - console.log('\n' + '='.repeat(50)); - console.log(`✅ СБОР ЗАВЕРШЁН`); - console.log(`📄 Запросов обработано: ${QUERIES.length}`); - console.log(`🔗 Промо ссылок собрано всего: ${totalLinks}`); - console.log('='.repeat(50) + '\n'); - if (totalLinks > 0) { - var outputText = allLinks.join('\n'); - console.log(outputText); - await copyToClipboard(outputText); - console.log('\n✅ Все промо ссылки (с разделителями по запросам) скопированы в буфер обмена!'); - console.log('💡 Нажмите Ctrl+V в любом месте для вставки'); - } else { - console.log('⚠️ Промо ссылки не найдены'); - } - })(); + 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('⚠️ Рекламных блоков не найдено'); + })(); })(); \ No newline at end of file