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="Например: гинкго билоба аргинин витамин д"></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})();