Ozon Scraper

Агрессивный скрапер для Ozon

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Ozon Scraper
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  Агрессивный скрапер для Ozon
// @author       torch
// @match        https://www.ozon.ru/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=ozon.ru
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- НАСТРОЙКИ ---
    const SCROLL_STEP = 400;       // Меньше шаг = надежнее триггер загрузки
    const SCROLL_INTERVAL = 100;   // Частота скролла (быстро)
    const WAIT_FOR_LOAD = 4000;    // Сколько ждать, если страница перестала расти (4 сек)
    const MAX_RETRIES = 3;         // Сколько раз пробовать "дернуть" скролл перед остановкой
    const BUTTON_ID = 'ozon-hardcore-scraper';
    // -----------------

    let isActive = false;
    let productsMap = new Map();
    let mainInterval;
    let parserInterval;

    // Состояние скролла
    let lastScrollHeight = 0;
    let stagnateCounter = 0;
    let retryAttempt = 0;

    // --- ЛОГИРОВАНИЕ ---
    function status(msg, color = 'white') {
        const el = document.getElementById(BUTTON_ID + '_status');
        if (el) {
            el.innerText = msg;
            el.style.color = color;
        }
        console.log(`[OzonScraper] ${msg}`);
    }

    // --- ИНТЕРФЕЙС ---
    setInterval(() => {
        const targetPage = /ozon\.ru\/(category|search|brand|seller|highlight)/.test(location.href);
        if (targetPage && !document.getElementById(BUTTON_ID)) {
            renderUI();
        }
    }, 1500);

    function renderUI() {
        const container = document.createElement('div');
        container.id = BUTTON_ID;
        Object.assign(container.style, {
            position: 'fixed', bottom: '80px', right: '20px', zIndex: '9999999',
            background: 'rgba(0, 0, 0, 0.85)', padding: '15px', borderRadius: '12px',
            color: 'white', fontFamily: 'Arial', boxShadow: '0 5px 15px rgba(0,0,0,0.5)',
            display: 'flex', flexDirection: 'column', gap: '8px', minWidth: '200px'
        });

        const btn = document.createElement('button');
        btn.innerText = '⬇️ СТАРТ (Hardcore)';
        Object.assign(btn.style, {
            background: '#005bff', border: 'none', padding: '10px', color: 'white',
            borderRadius: '6px', cursor: 'pointer', fontWeight: 'bold', fontSize: '14px'
        });
        btn.onclick = toggleScraper;

        const info = document.createElement('div');
        info.id = BUTTON_ID + '_status';
        info.innerText = 'Готов к работе';
        info.style.fontSize = '12px';

        const count = document.createElement('div');
        count.id = BUTTON_ID + '_count';
        count.innerText = 'Товаров: 0';
        count.style.fontWeight = 'bold';
        count.style.color = '#00ff00';

        container.appendChild(count);
        container.appendChild(info);
        container.appendChild(btn);
        document.body.appendChild(container);
    }

    // --- ЛОГИКА ---
    function toggleScraper() {
        if (isActive) {
            finishScraping();
        } else {
            isActive = true;
            productsMap.clear();
            document.querySelector(`#${BUTTON_ID} button`).innerText = '⏹ СТОП';
            document.querySelector(`#${BUTTON_ID} button`).style.background = '#ff0040';

            lastScrollHeight = document.body.scrollHeight;
            stagnateCounter = 0;
            retryAttempt = 0;

            // Запускаем парсер (он работает независимо от скролла)
            parserInterval = setInterval(parseVisibleCards, 800);

            // Запускаем скроллер
            mainInterval = setInterval(scrollingLoop, SCROLL_INTERVAL);
        }
    }

    function scrollingLoop() {
        if (!isActive) return;

        const currentScroll = window.scrollY + window.innerHeight;
        const totalHeight = document.body.scrollHeight;

        // 1. Попытка нажать кнопки "Показать еще" (Ozon иногда меняет Infinite Scroll на кнопку)
        const moreButtons = Array.from(document.querySelectorAll('button, div[role="button"]'));
        const loadMoreBtn = moreButtons.find(b => b.innerText.includes('Показать еще') || b.innerText.includes('Загрузить'));
        if (loadMoreBtn) {
            status('Нажимаю кнопку подгрузки...', 'yellow');
            loadMoreBtn.click();
            stagnateCounter = 0; // Сброс таймера застоя
            return;
        }

        // 2. Если мы еще не внизу - просто крутим
        if (currentScroll < totalHeight - 300) {
            window.scrollBy(0, SCROLL_STEP);
            status('Скроллим вниз...');
            stagnateCounter = 0;
        } else {
            // 3. Мы уперлись в дно. Ждем подгрузки.
            stagnateCounter += SCROLL_INTERVAL;
            status(`Ждем подгрузку: ${(stagnateCounter/1000).toFixed(1)} сек...`, 'orange');

            // 4. Если долго нет изменений
            if (stagnateCounter > WAIT_FOR_LOAD) {
                if (totalHeight > lastScrollHeight) {
                    // Ура, страница выросла!
                    lastScrollHeight = totalHeight;
                    stagnateCounter = 0;
                    retryAttempt = 0;
                    status('Страница выросла! Продолжаем.', 'green');
                } else {
                    // Страница не выросла. Пробуем "ПИНАТЬ" скролл
                    if (retryAttempt < MAX_RETRIES) {
                        retryAttempt++;
                        stagnateCounter = 0; // Сбрасываем ожидание, даем шанс после пинка
                        kickScroll();
                    } else {
                        // Все попытки исчерпаны
                        status('Похоже, это конец.', 'red');
                        finishScraping();
                    }
                }
            }
        }
    }

    // Эмуляция поведения "человек дергает скролл вверх-вниз", чтобы разбудить Lazy Load
    function kickScroll() {
        status(`ПИНАЕМ СКРОЛЛ (Попытка ${retryAttempt}/${MAX_RETRIES})`, 'magenta');

        // Резко вверх на 700px
        window.scrollBy(0, -700);

        setTimeout(() => {
            // И сразу вниз
            window.scrollTo(0, document.body.scrollHeight);
        }, 300);
    }

    function parseVisibleCards() {
        if (!isActive) return;

        // Самый надежный селектор для карточек на Ozon
        const cards = document.querySelectorAll('div[data-index]');

        cards.forEach(card => {
            try {
                // Ищем ссылку на товар
                const linkEl = card.querySelector('a[href^="/product/"]');
                if (!linkEl) return;

                // Чистый ID товара из ссылки
                const cleanUrl = 'https://www.ozon.ru' + linkEl.getAttribute('href').split('?')[0];

                // Если уже есть - не тратим время
                if (productsMap.has(cleanUrl)) return;

                // --- ПАРСИНГ ---
                // 1. Цена (ищем класс c35_3... или просто tsHeadline)
                // Ozon часто меняет классы, ищем по символу рубля
                let price = 'Нет цены';
                // Специфичный селектор цены
                const priceNode = card.querySelector('div > span:first-child');
                if (priceNode && priceNode.innerText.includes('₽')) {
                    price = priceNode.innerText;
                } else {
                    // Фоллбек: перебор всех спанов
                    const spans = card.querySelectorAll('span');
                    for (let s of spans) {
                        if (s.innerText.includes('₽') && s.innerText.length < 15) {
                            price = s.innerText;
                            break;
                        }
                    }
                }

                // 2. Название
                let title = linkEl.innerText;
                const titleNode = card.querySelector('.tsBody500Medium');
                if (titleNode) title = titleNode.innerText;

                // Очистка
                price = price.replace(/[^\d]/g, '');
                if (!price) return; // Не берем товары без цены (например, "нет в наличии")

                productsMap.set(cleanUrl, {
                    title: title.trim(),
                    price: price,
                    link: cleanUrl
                });

                // Обновляем счетчик
                const counter = document.getElementById(BUTTON_ID + '_count');
                if (counter) counter.innerText = `Товаров: ${productsMap.size}`;

            } catch (e) {}
        });
    }

    function finishScraping() {
        isActive = false;
        clearInterval(mainInterval);
        clearInterval(parserInterval);

        const btn = document.querySelector(`#${BUTTON_ID} button`);
        if(btn) {
            btn.innerText = '⬇️ СТАРТ (Hardcore)';
            btn.style.background = '#005bff';
        }
        status('Сбор завершен');
        showTable();
    }

    // --- ВЫВОД РЕЗУЛЬТАТОВ ---
    function showTable() {
        const old = document.getElementById('ozon_table_overlay');
        if (old) old.remove();

        const overlay = document.createElement('div');
        overlay.id = 'ozon_table_overlay';
        Object.assign(overlay.style, {
            position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
            background: 'rgba(0,0,0,0.7)', zIndex: '10000000', display: 'flex',
            justifyContent: 'center', alignItems: 'center'
        });

        const modal = document.createElement('div');
        Object.assign(modal.style, {
            background: 'white', width: '90%', height: '90%', borderRadius: '8px',
            display: 'flex', flexDirection: 'column', padding: '20px', fontFamily: 'Arial'
        });

        modal.innerHTML = `
            <div style="display:flex; justify-content:space-between; margin-bottom:15px;">
                <h2>Собрано уникальных товаров: ${productsMap.size}</h2>
                <div>
                    <button id="dl_csv" style="padding:10px 20px; background:#28a745; color:white; border:none; cursor:pointer; margin-right:10px;">Скачать CSV</button>
                    <button id="cl_btn" style="padding:10px 20px; background:#ccc; border:none; cursor:pointer;">Закрыть</button>
                </div>
            </div>
            <div style="flex:1; overflow:auto; border:1px solid #ddd;">
                <table style="width:100%; border-collapse:collapse;">
                    <thead style="background:#f0f0f0; position:sticky; top:0;">
                        <tr>
                            <th style="padding:10px; border:1px solid #ddd; text-align:left;">Название</th>
                            <th style="padding:10px; border:1px solid #ddd; text-align:left;">Цена (₽)</th>
                            <th style="padding:10px; border:1px solid #ddd; text-align:left;">Ссылка</th>
                        </tr>
                    </thead>
                    <tbody id="res_tbody"></tbody>
                </table>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        const tbody = document.getElementById('res_tbody');
        productsMap.forEach(p => {
            const tr = document.createElement('tr');
            tr.innerHTML = `
                <td style="padding:5px; border:1px solid #ddd;">${p.title}</td>
                <td style="padding:5px; border:1px solid #ddd;">${p.price}</td>
                <td style="padding:5px; border:1px solid #ddd;"><a href="${p.link}" target="_blank">Ссылка</a></td>
            `;
            tbody.appendChild(tr);
        });

        document.getElementById('cl_btn').onclick = () => overlay.remove();
        document.getElementById('dl_csv').onclick = () => {
            let csv = '\uFEFFНазвание;Цена;Ссылка\n';
            productsMap.forEach(p => {
                csv += `"${p.title.replace(/"/g, '""')}";${p.price};${p.link}\n`;
            });
            const blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'});
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = 'ozon_full_scan.csv';
            link.click();
        };
    }

})();