utils/parse/collect_promo_links.js

158 lines
6.8 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.

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