Ozon Product Parser

Парсер товаров с Ozon для анализа поисковой выдачи

Size

28.3 KB

Version

1.1.4

Created

Dec 11, 2025

Updated

4 days ago

1// ==UserScript==
2// @name Ozon Product Parser
3// @description Парсер товаров с Ozon для анализа поисковой выдачи
4// @version 1.1.4
5// @match https://*.ozon.ru/*
6// @grant GM.getValue
7// @grant GM.setValue
8// @grant GM.deleteValue
9// @icon 
10// ==/UserScript==
11
12(function() {
13    'use strict';
14    console.log('Ozon Product Parser: Extension loaded');
15
16    // Утилита для дебаунса
17    function debounce(func, wait) {
18        let timeout;
19        return function executedFunction(...args) {
20            const later = () => {
21                clearTimeout(timeout);
22                func(...args);
23            };
24            clearTimeout(timeout);
25            timeout = setTimeout(later, wait);
26        };
27    }
28
29    // Добавляем стили
30    function addStyles() {
31        const styles = `
32            .ozon-parser-container {
33                position: fixed;
34                top: 20px;
35                right: 20px;
36                z-index: 10000;
37                display: flex;
38                gap: 10px;
39            }
40            .ozon-parser-btn {
41                background: linear-gradient(135deg, #005bff 0%, #0043c7 100%);
42                color: white;
43                border: none;
44                padding: 12px 24px;
45                border-radius: 8px;
46                cursor: pointer;
47                font-size: 14px;
48                font-weight: 600;
49                box-shadow: 0 4px 12px rgba(0, 91, 255, 0.3);
50                transition: all 0.3s ease;
51            }
52            .ozon-parser-btn:hover {
53                background: linear-gradient(135deg, #0043c7 0%, #002f8f 100%);
54                box-shadow: 0 6px 16px rgba(0, 91, 255, 0.4);
55                transform: translateY(-2px);
56            }
57            .ozon-parser-btn:active {
58                transform: translateY(0);
59            }
60            .ozon-parser-btn.secondary {
61                background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
62                box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
63            }
64            .ozon-parser-btn.secondary:hover {
65                background: linear-gradient(135deg, #1e7e34 0%, #155724 100%);
66                box-shadow: 0 6px 16px rgba(40, 167, 69, 0.4);
67            }
68            .ozon-parser-modal {
69                position: fixed;
70                top: 0;
71                left: 0;
72                width: 100%;
73                height: 100%;
74                background: rgba(0, 0, 0, 0.7);
75                display: flex;
76                justify-content: center;
77                align-items: center;
78                z-index: 10001;
79            }
80            .ozon-parser-modal-content {
81                background: white;
82                padding: 30px;
83                border-radius: 12px;
84                max-width: 600px;
85                width: 90%;
86                max-height: 80vh;
87                overflow-y: auto;
88                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
89            }
90            .ozon-parser-modal-header {
91                font-size: 24px;
92                font-weight: 700;
93                margin-bottom: 20px;
94                color: #333;
95            }
96            .ozon-parser-modal-body {
97                margin-bottom: 20px;
98            }
99            .ozon-parser-textarea {
100                width: 100%;
101                min-height: 200px;
102                padding: 12px;
103                border: 2px solid #e0e0e0;
104                border-radius: 8px;
105                font-size: 14px;
106                font-family: inherit;
107                resize: vertical;
108                box-sizing: border-box;
109            }
110            .ozon-parser-textarea:focus {
111                outline: none;
112                border-color: #005bff;
113            }
114            .ozon-parser-modal-footer {
115                display: flex;
116                gap: 10px;
117                justify-content: flex-end;
118            }
119            .ozon-parser-progress {
120                margin-top: 20px;
121                padding: 15px;
122                background: #f8f9fa;
123                border-radius: 8px;
124                font-size: 14px;
125                color: #333;
126            }
127            .ozon-parser-progress-bar {
128                width: 100%;
129                height: 8px;
130                background: #e0e0e0;
131                border-radius: 4px;
132                margin-top: 10px;
133                overflow: hidden;
134            }
135            .ozon-parser-progress-fill {
136                height: 100%;
137                background: linear-gradient(90deg, #005bff 0%, #0043c7 100%);
138                transition: width 0.3s ease;
139            }
140            .ozon-parser-results-table {
141                width: 100%;
142                border-collapse: collapse;
143                margin-top: 20px;
144                font-size: 13px;
145            }
146            .ozon-parser-results-table th,
147            .ozon-parser-results-table td {
148                padding: 12px;
149                text-align: left;
150                border-bottom: 1px solid #e0e0e0;
151            }
152            .ozon-parser-results-table th {
153                background: #f8f9fa;
154                font-weight: 600;
155                color: #333;
156                position: sticky;
157                top: 0;
158            }
159            .ozon-parser-results-table tr:hover {
160                background: #f8f9fa;
161            }
162            .ozon-parser-highlight {
163                background: #fff3cd !important;
164                font-weight: 600;
165            }
166            .ozon-parser-sku-link {
167                color: #005bff;
168                text-decoration: none;
169                font-weight: 600;
170            }
171            .ozon-parser-sku-link:hover {
172                text-decoration: underline;
173            }
174            .ozon-parser-tabs {
175                display: flex;
176                gap: 5px;
177                margin-bottom: 20px;
178                flex-wrap: wrap;
179            }
180            .ozon-parser-tab {
181                padding: 10px 20px;
182                background: #f8f9fa;
183                border: none;
184                border-radius: 6px;
185                cursor: pointer;
186                font-size: 14px;
187                transition: all 0.2s ease;
188            }
189            .ozon-parser-tab:hover {
190                background: #e9ecef;
191            }
192            .ozon-parser-tab.active {
193                background: #005bff;
194                color: white;
195                font-weight: 600;
196            }
197            .ozon-parser-info {
198                padding: 10px;
199                background: #e7f3ff;
200                border-left: 4px solid #005bff;
201                border-radius: 4px;
202                margin-bottom: 15px;
203                font-size: 13px;
204                color: #333;
205            }
206            .ozon-parser-search {
207                width: 100%;
208                padding: 10px 12px;
209                border: 2px solid #e0e0e0;
210                border-radius: 8px;
211                font-size: 14px;
212                margin-bottom: 15px;
213                box-sizing: border-box;
214            }
215            .ozon-parser-search:focus {
216                outline: none;
217                border-color: #005bff;
218            }
219            .ozon-parser-search::placeholder {
220                color: #999;
221            }
222        `;
223        const styleElement = document.createElement('style');
224        styleElement.textContent = styles;
225        document.head.appendChild(styleElement);
226        console.log('Ozon Product Parser: Styles added');
227    }
228
229    // Создаем UI кнопок
230    function createUI() {
231        const container = document.createElement('div');
232        container.className = 'ozon-parser-container';
233        
234        const parseBtn = document.createElement('button');
235        parseBtn.className = 'ozon-parser-btn';
236        parseBtn.textContent = 'Парсинг';
237        parseBtn.addEventListener('click', showParseModal);
238        
239        const resultsBtn = document.createElement('button');
240        resultsBtn.className = 'ozon-parser-btn secondary';
241        resultsBtn.textContent = 'Посмотреть результаты';
242        resultsBtn.addEventListener('click', showResultsModal);
243        
244        container.appendChild(parseBtn);
245        container.appendChild(resultsBtn);
246        document.body.appendChild(container);
247        console.log('Ozon Product Parser: UI created');
248    }
249
250    // Показываем модальное окно для ввода запросов
251    function showParseModal() {
252        const modal = document.createElement('div');
253        modal.className = 'ozon-parser-modal';
254        
255        const content = document.createElement('div');
256        content.className = 'ozon-parser-modal-content';
257        content.innerHTML = `
258            <div class="ozon-parser-modal-header">Парсинг товаров Ozon</div>
259            <div class="ozon-parser-modal-body">
260                <div class="ozon-parser-info">
261                    Введите поисковые запросы (каждый с новой строки). Парсер извлечет топ-16 товаров для каждого запроса.
262                </div>
263                <textarea class="ozon-parser-textarea" placeholder="Например:&#10;гинкго билоба&#10;аргинин&#10;витамин д"></textarea>
264                <div class="ozon-parser-progress" style="display: none;">
265                    <div class="ozon-parser-progress-text">Обработка запросов...</div>
266                    <div class="ozon-parser-progress-bar">
267                        <div class="ozon-parser-progress-fill" style="width: 0%"></div>
268                    </div>
269                </div>
270            </div>
271            <div class="ozon-parser-modal-footer">
272                <button class="ozon-parser-btn" onclick="this.closest('.ozon-parser-modal').remove()">Отмена</button>
273                <button class="ozon-parser-btn" id="start-parsing-btn">Начать парсинг</button>
274            </div>
275        `;
276        
277        modal.appendChild(content);
278        document.body.appendChild(modal);
279        
280        // Закрытие по клику на фон
281        modal.addEventListener('click', (e) => {
282            if (e.target === modal) {
283                modal.remove();
284            }
285        });
286        
287        // Обработчик кнопки парсинга
288        const startBtn = content.querySelector('#start-parsing-btn');
289        startBtn.addEventListener('click', async () => {
290            const textarea = content.querySelector('.ozon-parser-textarea');
291            const queries = textarea.value.split('\n').filter(q => q.trim());
292            if (queries.length === 0) {
293                alert('Пожалуйста, введите хотя бы один запрос');
294                return;
295            }
296            startBtn.disabled = true;
297            startBtn.textContent = 'Парсинг...';
298            await startParsing(queries, content);
299        });
300        
301        console.log('Ozon Product Parser: Parse modal shown');
302    }
303
304    // Начинаем парсинг
305    async function startParsing(queries, modalContent) {
306        const progressDiv = modalContent.querySelector('.ozon-parser-progress');
307        const progressText = modalContent.querySelector('.ozon-parser-progress-text');
308        const progressFill = modalContent.querySelector('.ozon-parser-progress-fill');
309        progressDiv.style.display = 'block';
310        
311        // Сохраняем список запросов и начинаем парсинг
312        await GM.setValue('ozon_parser_queries', JSON.stringify(queries));
313        await GM.setValue('ozon_parser_current_index', 0);
314        await GM.setValue('ozon_parser_results', JSON.stringify({}));
315        await GM.setValue('ozon_parser_active', 'true');
316        console.log('Ozon Product Parser: Starting parsing process');
317        
318        // Переходим к первому запросу
319        const firstQuery = queries[0].trim();
320        const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(firstQuery)}&from_global=true`;
321        window.location.href = searchUrl;
322    }
323
324    // Продолжаем парсинг после загрузки страницы
325    async function continueParsingIfActive() {
326        const isActive = await GM.getValue('ozon_parser_active', 'false');
327        if (isActive !== 'true') {
328            return;
329        }
330        
331        console.log('Ozon Product Parser: Continuing parsing process');
332        
333        // Ждем появления таблицы
334        await waitForTable();
335        
336        // Получаем текущее состояние
337        const queriesJson = await GM.getValue('ozon_parser_queries', '[]');
338        const queries = JSON.parse(queriesJson);
339        const currentIndex = await GM.getValue('ozon_parser_current_index', 0);
340        const resultsJson = await GM.getValue('ozon_parser_results', '{}');
341        const allResults = JSON.parse(resultsJson);
342        
343        if (currentIndex >= queries.length) {
344            // Парсинг завершен
345            await GM.setValue('ozon_parser_active', 'false');
346            console.log('Ozon Product Parser: Parsing completed');
347            return;
348        }
349        
350        const currentQuery = queries[currentIndex].trim();
351        console.log(`Ozon Product Parser: Processing query ${currentIndex + 1}/${queries.length}: "${currentQuery}"`);
352        
353        // Парсим данные текущей страницы
354        const products = await parseProducts(currentQuery);
355        allResults[currentQuery] = products;
356        
357        // Сохраняем результаты
358        await GM.setValue('ozon_parser_results', JSON.stringify(allResults));
359        console.log(`Ozon Product Parser: Parsed ${products.length} products for "${currentQuery}"`);
360        
361        // Переходим к следующему запросу
362        const nextIndex = currentIndex + 1;
363        await GM.setValue('ozon_parser_current_index', nextIndex);
364        
365        if (nextIndex < queries.length) {
366            // Есть еще запросы - переходим к следующему
367            const nextQuery = queries[nextIndex].trim();
368            const searchUrl = `https://www.ozon.ru/search/?text=${encodeURIComponent(nextQuery)}&from_global=true`;
369            setTimeout(() => {
370                window.location.href = searchUrl;
371            }, 2000); // Небольшая задержка между запросами
372        } else {
373            // Все запросы обработаны
374            await GM.setValue('ozon_parser_active', 'false');
375            console.log('Ozon Product Parser: All queries processed');
376            
377            // Показываем результаты
378            setTimeout(() => {
379                showResultsModal();
380            }, 1000);
381        }
382    }
383
384    // Ждем появления таблицы
385    function waitForTable() {
386        return new Promise((resolve) => {
387            const checkTable = () => {
388                const table = document.querySelector('#mpstat-ozone-search-result table tbody');
389                if (table && table.querySelectorAll('tr').length > 0) {
390                    console.log('Ozon Product Parser: Table found');
391                    // Дополнительная задержка для полной загрузки данных
392                    setTimeout(resolve, 3000);
393                } else {
394                    setTimeout(checkTable, 1000);
395                }
396            };
397            checkTable();
398        });
399    }
400
401    // Парсим товары из таблицы
402    async function parseProducts(query) {
403        const table = document.querySelector('#mpstat-ozone-search-result table');
404        if (!table) {
405            console.error('Ozon Product Parser: Table not found');
406            return [];
407        }
408        
409        const rows = table.querySelectorAll('tbody tr');
410        const products = [];
411        const maxProducts = Math.min(16, rows.length);
412        let hasTargetBrand = false;
413        
414        for (let i = 0; i < maxProducts; i++) {
415            const row = rows[i];
416            const cells = row.querySelectorAll('td');
417            if (cells.length < 8) continue;
418            
419            const position = cells[0]?.textContent.trim() || '';
420            const sku = cells[2]?.textContent.trim() || '';
421            const brand = cells[3]?.textContent.trim() || '';
422            const priceText = cells[4]?.textContent.trim() || '';
423            const revenueText = cells[6]?.textContent.trim() || '';
424            const ordersText = cells[7]?.textContent.trim() || '';
425            
426            // Проверяем, есть ли товар от GLS или Skinphoria
427            const isTargetBrand = brand.includes('GLS Pharmaceuticals') || brand.includes('Skinphoria');
428            if (isTargetBrand) {
429                hasTargetBrand = true;
430            }
431            
432            // Парсим числовые значения
433            const price = parseFloat(priceText.replace(/[^\d]/g, '')) || 0;
434            const revenue = parseFloat(revenueText.replace(/[^\d]/g, '')) || 0;
435            const orders = parseInt(ordersText.replace(/[^\d]/g, '')) || 0;
436            
437            products.push({
438                position: parseInt(position) || (i + 1),
439                sku,
440                brand,
441                price,
442                revenue,
443                orders,
444                isTargetBrand
445            });
446        }
447        
448        // Если нет товаров от целевых брендов, продолжаем искать дальше
449        if (!hasTargetBrand && rows.length > maxProducts) {
450            for (let i = maxProducts; i < rows.length; i++) {
451                const row = rows[i];
452                const cells = row.querySelectorAll('td');
453                if (cells.length < 8) continue;
454                
455                const brand = cells[3]?.textContent.trim() || '';
456                const isTargetBrand = brand.includes('GLS Pharmaceuticals') || brand.includes('Skinphoria');
457                
458                if (isTargetBrand) {
459                    const position = cells[0]?.textContent.trim() || '';
460                    const sku = cells[2]?.textContent.trim() || '';
461                    const priceText = cells[4]?.textContent.trim() || '';
462                    const revenueText = cells[6]?.textContent.trim() || '';
463                    const ordersText = cells[7]?.textContent.trim() || '';
464                    
465                    const price = parseFloat(priceText.replace(/[^\d]/g, '')) || 0;
466                    const revenue = parseFloat(revenueText.replace(/[^\d]/g, '')) || 0;
467                    const orders = parseInt(ordersText.replace(/[^\d]/g, '')) || 0;
468                    
469                    products.push({
470                        position: parseInt(position) || (i + 1),
471                        sku,
472                        brand,
473                        price,
474                        revenue,
475                        orders,
476                        isTargetBrand: true
477                    });
478                    break; // Нашли хотя бы один товар от целевого бренда
479                }
480            }
481        }
482        
483        // Сортируем по убыванию выручки
484        products.sort((a, b) => b.revenue - a.revenue);
485        console.log(`Ozon Product Parser: Parsed ${products.length} products, ${products.filter(p => p.isTargetBrand).length} from target brands`);
486        return products;
487    }
488
489    // Показываем результаты
490    async function showResultsModal() {
491        const resultsJson = await GM.getValue('ozon_parser_results', '{}');
492        const results = JSON.parse(resultsJson);
493        let queries = Object.keys(results);
494        
495        if (queries.length === 0) {
496            alert('Нет сохраненных результатов. Сначала выполните парсинг.');
497            return;
498        }
499        
500        // Сортируем запросы по алфавиту
501        queries.sort((a, b) => a.localeCompare(b, 'ru'));
502        
503        const modal = document.createElement('div');
504        modal.className = 'ozon-parser-modal';
505        
506        const content = document.createElement('div');
507        content.className = 'ozon-parser-modal-content';
508        content.style.maxWidth = '1200px';
509        content.innerHTML = `
510            <div class="ozon-parser-modal-header">Результаты парсинга</div>
511            <div class="ozon-parser-modal-body">
512                <input type="text" class="ozon-parser-search" id="query-search" placeholder="Поиск по запросам">
513                <input type="text" class="ozon-parser-search" id="sku-search" placeholder="Поиск по SKU">
514                <div class="ozon-parser-tabs" id="query-tabs"></div>
515                <div id="results-container"></div>
516            </div>
517            <div class="ozon-parser-modal-footer">
518                <button class="ozon-parser-btn" id="close-results-btn">Закрыть</button>
519            </div>
520        `;
521        
522        modal.appendChild(content);
523        document.body.appendChild(modal);
524        
525        // Обработчик закрытия модального окна
526        const closeBtn = content.querySelector('#close-results-btn');
527        closeBtn.addEventListener('click', () => {
528            modal.remove();
529        });
530        
531        // Обработчик поиска по запросам
532        const querySearchInput = content.querySelector('#query-search');
533        querySearchInput.addEventListener('input', debounce(() => {
534            const searchValue = querySearchInput.value.trim().toLowerCase();
535            filterQueriesByQuery(searchValue, queries, results, content);
536        }, 300));
537        
538        // Обработчик поиска по SKU
539        const skuSearchInput = content.querySelector('#sku-search');
540        skuSearchInput.addEventListener('input', debounce(() => {
541            const searchValue = skuSearchInput.value.trim();
542            filterQueriesBySKU(searchValue, queries, results, content);
543        }, 300));
544        
545        // Создаем вкладки
546        createTabs(queries, results, content);
547        
548        // Показываем результаты первого запроса
549        displayResults(queries[0], results[queries[0]], content);
550        
551        // Закрытие по клику на фон
552        modal.addEventListener('click', (e) => {
553            if (e.target === modal) {
554                modal.remove();
555            }
556        });
557        
558        console.log('Ozon Product Parser: Results modal shown');
559    }
560
561    // Фильтруем запросы по названию запроса
562    function filterQueriesByQuery(queryText, allQueries, results, modalContent) {
563        if (!queryText) {
564            // Если поиск пустой, показываем все запросы
565            createTabs(allQueries, results, modalContent);
566            displayResults(allQueries[0], results[allQueries[0]], modalContent);
567            return;
568        }
569        
570        // Ищем запросы, содержащие введенный текст
571        const filteredQueries = allQueries.filter(q => 
572            q.toLowerCase().includes(queryText)
573        );
574        
575        if (filteredQueries.length === 0) {
576            const container = modalContent.querySelector('#results-container');
577            container.innerHTML = '<p>Запросы не найдены</p>';
578            const tabsContainer = modalContent.querySelector('#query-tabs');
579            tabsContainer.innerHTML = '';
580            return;
581        }
582        
583        // Создаем вкладки только для отфильтрованных запросов
584        createTabs(filteredQueries, results, modalContent);
585        displayResults(filteredQueries[0], results[filteredQueries[0]], modalContent);
586    }
587
588    // Фильтруем запросы по SKU
589    function filterQueriesBySKU(sku, allQueries, results, modalContent) {
590        if (!sku) {
591            // Если поиск пустой, показываем все запросы
592            createTabs(allQueries, results, modalContent);
593            displayResults(allQueries[0], results[allQueries[0]], modalContent);
594            return;
595        }
596        
597        // Ищем запросы, в которых есть товары с указанным SKU
598        const filteredQueries = allQueries.filter(query => {
599            const products = results[query];
600            return products.some(product => product.sku.includes(sku));
601        });
602        
603        if (filteredQueries.length === 0) {
604            const container = modalContent.querySelector('#results-container');
605            container.innerHTML = '<p>Товары с таким SKU не найдены ни в одном запросе</p>';
606            const tabsContainer = modalContent.querySelector('#query-tabs');
607            tabsContainer.innerHTML = '';
608            return;
609        }
610        
611        // Создаем вкладки только для отфильтрованных запросов
612        createTabs(filteredQueries, results, modalContent);
613        displayResults(filteredQueries[0], results[filteredQueries[0]], modalContent);
614    }
615
616    // Создаем вкладки для запросов
617    function createTabs(queries, results, modalContent) {
618        const tabsContainer = modalContent.querySelector('#query-tabs');
619        tabsContainer.innerHTML = '';
620        
621        queries.forEach((query, index) => {
622            const tab = document.createElement('button');
623            tab.className = 'ozon-parser-tab' + (index === 0 ? ' active' : '');
624            tab.textContent = query;
625            tab.addEventListener('click', () => {
626                // Убираем активный класс со всех вкладок
627                tabsContainer.querySelectorAll('.ozon-parser-tab').forEach(t => t.classList.remove('active'));
628                tab.classList.add('active');
629                // Показываем результаты для этого запроса
630                displayResults(query, results[query], modalContent);
631            });
632            tabsContainer.appendChild(tab);
633        });
634    }
635
636    // Отображаем результаты для конкретного запроса
637    function displayResults(query, products, modalContent) {
638        const container = modalContent.querySelector('#results-container');
639        if (!products || products.length === 0) {
640            container.innerHTML = '<p>Нет данных для этого запроса</p>';
641            return;
642        }
643        
644        let tableHTML = `
645            <table class="ozon-parser-results-table">
646                <thead>
647                    <tr>
648                        <th>Позиция</th>
649                        <th>SKU</th>
650                        <th>Бренд</th>
651                        <th>Цена</th>
652                        <th>Выручка</th>
653                        <th>Заказы</th>
654                    </tr>
655                </thead>
656                <tbody>
657        `;
658        
659        products.forEach(product => {
660            const rowClass = product.isTargetBrand ? ' class="ozon-parser-highlight"' : '';
661            const skuLink = `https://www.ozon.ru/product/${product.sku}`;
662            tableHTML += `
663                <tr${rowClass}>
664                    <td>${product.position}</td>
665                    <td><a href="${skuLink}" target="_blank" class="ozon-parser-sku-link">${product.sku}</a></td>
666                    <td>${product.brand}</td>
667                    <td>${product.price.toLocaleString('ru-RU')} ₽</td>
668                    <td>${product.revenue.toLocaleString('ru-RU')} ₽</td>
669                    <td>${product.orders.toLocaleString('ru-RU')}</td>
670                </tr>
671            `;
672        });
673        
674        tableHTML += `
675                </tbody>
676            </table>
677        `;
678        
679        container.innerHTML = tableHTML;
680    }
681
682    // Инициализация
683    function init() {
684        console.log('Ozon Product Parser: Initializing...');
685        
686        // Ждем загрузки DOM
687        if (document.readyState === 'loading') {
688            document.addEventListener('DOMContentLoaded', () => {
689                addStyles();
690                createUI();
691                continueParsingIfActive();
692            });
693        } else {
694            addStyles();
695            createUI();
696            continueParsingIfActive();
697        }
698    }
699
700    // Запускаем
701    init();
702})();
Ozon Product Parser | Robomonkey