Визуальный конструктор для парсинга каталогов в ZennoPoster

  • Автор темы Автор темы OlegR
  • Дата начала Дата начала

OlegR

Client
Регистрация
11.06.2015
Сообщения
258
Реакции
257
Баллы
63
шапка2.JPG



Уважаемые пользователи ZennoPoster!​

Представленный визуальный конструктор профилей (ВКП) для парсинга каталогов:

  1. генерирует несколько кандидатов селекторов;
  2. проверяет их на карточках текущей и следующей страницы;
  3. выбирает рабочий вариант и резервные селекторы;
  4. сохраняет профиль в JSON;
  5. передаёт профиль сборщику, который извлекает и валидирует данные.

Всё работает локально, без LLM, внешних API и передачи данных на сторонние серверы.


архитектура.JPG

Разметка (JS-оверлей)
↓
Генерация кандидатов (C#)
↓
Тестирование на выборке (C#)
↓
Выбор active + fallback (C#)
↓
Сохранение профиля (JSON)
↓
Сбор данных (C# + XPath)
↓
Валидация полей
↓
Экспорт в CSV


работа.JPG

ШАГ 1. Разметка мышкой

Пользователь открывает сайт в ZennoPoster, запускает мастер. Справа появляется панель с ролями:

  • Карточка товара
  • Название
  • Цена
  • Ссылка
  • Следующая страница

Пользователь кликает по элементам — 2 раза по каждой роли (кроме «Следующая» — там достаточно 1 раза). Два образца нужны, чтобы система могла сравнить их и найти общие признаки. Один образец не даёт понять, какие классы постоянны, а какие случайны.

ШАГ 2. Генерация селекторов

Система:

  • собирает общие классы и атрибуты двух образцов;
  • отбрасывает хэш-классы (css-1x2y3z, _a8f3d, sc-bdVaJa);
  • отбрасывает товарные ID (product-12345);
  • формирует список кандидатов.

Как выбирается активный селектор

Для каждой роли генерируется несколько кандидатов. Все они проходят тестирование на выборке (см. шаг 3), после чего оцениваются по формуле:

score = coverage × 0.4
+ field_success × 0.3
+ (1 - ambiguous_ratio) × 0.2
+ source_priority × 0.1

Где:

  • coverage — доля карточек, где кандидат нашёл ровно 1 элемент;
  • field_success — доля карточек с валидным значением;
  • ambiguous_ratio — доля карточек с несколькими совпадениями;
  • source_priority — приоритет источника:

data-attr → 1.0
class-combo → 0.9
class → 0.8
tag-class → 0.7
attr-name → 0.6
path-class → 0.5
tag → 0.1

Active = кандидат с максимальным score. Fallback = два следующих по score.

Кандидаты с score ниже порога (coverage < 0.8) отбрасываются. Если ни один кандидат не прошёл порог — профиль не сохраняется, пользователь видит ошибку.

Про fallback-селекторы — честно

В текущей версии ВПК fallback = 2 следующих кандидата по score. Они генерируются из того же источника, что active. Пример для .product-card:

active: .product-card (score 80)
fallback1: div.product-card (score 70)
fallback2: html > body > ... (score 50)

Это слабое резервирование. Если сайт переименует .product-card в .item, ни active, ни fallback не сработают.

Настоящий fallback должен быть из независимого источника:

  • [data-testid='product-card'] — атрибут, не класс;
  • article.product — семантический тег;
  • [itemprop='itemListElement'] — микроразметка.

В текущем ВПК это не реализовано. В планах — генерировать fallback из независимых источников.

Пока это означает: профиль переживает переименование одного класса, но не полную смену разметки. Это ограничение, а не универсальное восстановление.

ШАГ 3. Тест на выборке

Каждый кандидат проверяется:

  • на всех карточках текущей страницы;
  • на первой следующей странице;
  • на повторном запуске после перезагрузки.

Считаются метрики:

  • coverage — доля карточек, где найден ровно 1 элемент;
  • field_success — доля карточек с валидным значением;
  • AMBIGUOUS% — доля карточек с несколькими совпадениями.

ШАГ 4. Сохранение профиля

Реальный профиль (упрощённый, без полей, которых нет в ВПК):

{
"card": {"active": ".product-card", "fallbacks": ["div.product-card"]},
"title": {"active": ".product-title", "fallbacks": ["div.product-title"]},
"price": {"active": ".current-price", "fallbacks": ["div.current-price"]},
"url": {"active": ".product-link", "fallbacks": ["a.product-link"]},
"next_page": {"active": ".next-page", "fallbacks": []}
}

Профиль намеренно упрощён. В нём нет:

  • типа поля (text/price/url) — определяется на этапе сбора;
  • required — все три поля считаются обязательными;
  • extract — извлечение текста или href зашито в сборщик;
  • version, site, created_at — в планах.

Добавлять поля, которых нет, — плохая идея. Профиль показывает реальное состояние версии ВКП.

ШАГ 5. Сбор данных

Второй шаблон — ВПК_Collector:

  • читает профиль;
  • конвертирует CSS-селекторы в XPath;
  • находит все карточки по card.active;
  • внутри каждой карточки ищет title, price, url;
  • для url берёт атрибут href;
  • формирует CSV с колонками: page;title;price;url;status.

Для title:

  • элемент найден ровно один раз внутри карточки;
  • InnerText не пустой;
  • длина текста ≥ 3 символов;
  • текст не состоит только из пробелов/знаков препинания.

Для price:

  • элемент найден ровно один раз внутри карточки;
  • InnerText не пустой;
  • содержит цифры;
  • соответствует паттерну валюты: ₽ | руб | $ | € | £ | ¥;
  • значение > 0.

Для url:

  • элемент найден ровно один раз внутри карточки;
  • содержит атрибут href;
  • href не пустой;
  • href не начинается с javascript:, mailto:, tel:, #;
  • после resolve относительно base URI — валидный http/https URL.

Что НЕ считается валидным:

  • пустая строка;
  • текст из одних пробелов;
  • цена без цифр («Цена по запросу»);
  • отрицательная цена;
  • ссылка на якорь (#);
  • ссылка на javascript.

Если поле не валидно: карточка получает статус MISSING_<FIELD>, не попадает в успешные результаты, в CSV пишется пустое значение и статус.

Система не понимает семантику .current-price vs .old-price. Она выбирает то, что пользователь разметил мышкой на шаге 1.

Что система делает:

  • проверяет, что элемент найден ровно один раз в карточке;
  • проверяет, что значение соответствует паттерну валюты;
  • проверяет, что значение > 0.

Что система НЕ делает (в текущей версии):

  • не отсеивает .old-price по имени класса;
  • не анализирует зачёркивание (text-decoration: line-through);
  • не сравнивает цены между образцами;
  • не проверяет позицию элемента внутри карточки.

Как это работает на практике:

На тестовом сайте с .old-price + .current-price пользователь кликнул по .current-price. Система выбрала .current-price, потому что пользователь разметил именно его, оба образца содержат .current-price, общий класс — .current-price.

Если бы пользователь кликнул по .old-price, система выбрала бы его. Различение старой/текущей цены — ответственность пользователя.

Планы:
добавить эвристику отсева классов old, discount, previous, was

В текущей версии:

  • роль next_page размечается пользователем на шаге 1;
  • селектор сохраняется в профиль;
  • сборщик умеет находить кнопку «Следующая»;
  • переход выполняется через nextBtn.Click();
  • после перехода — ожидание 2 секунды.

Что реализовано:

  • ✓ разметка кнопки «Следующая»;
  • ✓ сохранение селектора в профиль;
  • ✓ поиск кнопки через XPath;
  • ✓ клик и ожидание загрузки;
  • ✓ счётчик страниц.

Что НЕ реализовано в текущей версии:

  • ✗ автоматический цикл по страницам;
  • ✗ защита от повторного сбора одной страницы;
  • ✗ определение конца списка (последняя страница);
  • ✗ стоп-условия (disabled кнопка, отсутствие кнопки);
  • ✗ обработка AJAX-пагинации.

Почему: для этой версии достаточно одной страницы, чтобы доказать работоспособность. Пагинация — следующая итерация.

В планах:

  • обернуть сбор в цикл while (hasNextPage);
  • стоп по отсутствию кнопки или disabled;
  • защита от циклов через сравнение URL;
  • сравнение хэша идентификаторов карточек между страницами.


пример.JPG

Разметка демо-сайта → профиль:

card: .product-card
title: .product-title
price: .current-price
url: .product-link

Сбор данных → CSV (разделитель ;, кодировка UTF-8):

page;title;price;url;status
1;"Наушники Sony WH-1000XM5";"7 990 ₽";"http://localhost:8081/product/1";VALID
1;"Клавиатура Logitech MX Keys";"6 490 ₽";"http://localhost:8081/product/2";VALID
1;"Мышка Razer DeathAdder";"4 290 ₽";"http://localhost:8081/product/3";VALID
1;"Монитор Dell 27""";"24 990 ₽";"http://localhost:8081/product/4";VALID
1;"Веб-камера Logitech C920";"5 990 ₽";"http://localhost:8081/product/5";VALID

Пояснение про кавычки: Монитор Dell 27""" — это корректное экранирование CSV. Внутренняя кавычка в названии удваивается — стандарт RFC 4180. Excel открывает такой файл корректно.

Цены сохраняются как raw-строка ("7 990 ₽"). Нормализация до числа — в планах.


тесты.JPG

Проект протестирован на трёх версиях демо-сайта:

Версия A — обычная разметка

.product-card, .product-title, .current-price, .product-link
→ VALID = 5/5

Версия B — изменённые классы

.card, .name, .cost, .link
→ VALID = 5/5

→ Доказательство: мастер работает с любыми классами. Профиль для версии A не подходит — система генерирует новый на основе разметки.

Версия C — две цены в карточке (.old-price + .current-price)


→ Система выбрала .current-price, потому что пользователь кликнул именно по нему.

Честно про тесты: три теста на демо-сайте не доказывают универсальность. Они доказывают, что мастер работает с разными классами и собирает данные корректно.

Для проверки на реальном сайте использовался books.toscrape.com. Результат: после правильной разметки (card=article.product_pod, title=h3 a, price=.price_color, url=h3 a) система собрала 20 книг. Тест подтвердил, что эта версия работает вне демо-сайта, но требует корректной разметки — неверный селектор .btn вместо ссылки на книгу дал MISSING_URL.

  • ✓ Готов ВПК_Master — визуальная разметка и генерация профиля
  • ✓ Готов ВПК_Collector — сбор данных по профилю
  • ✓ Реализована визуальная разметка (JS-оверлей)
  • ✓ Реализована генерация кандидатов на C#
  • ✓ Работает scoring и выбор active + 2 fallback
  • ✓ Работает проверка coverage, field_success, AMBIGUOUS%
  • ✓ Работает валидация title/price/url
  • ✓ Работает XPath-конвертер (CSS → XPath)
  • ✓ Работает экспорт в CSV с экранированием
  • ✓ Реализован переход по кнопке «Следующая» (один шаг, без цикла)
  • ✓ Подготовлено демо — 3 версии сайта, 9 страниц
  • ✓ Проведены тесты — A, B, C + books.toscrape.com

О том, что не работает или работает частично:

Технические:

  • требуется доступ к HTML-элементам в браузере (не для closed shadow DOM);
  • динамический контент должен успеть загрузиться до разметки;
  • SPA (React/Vue с динамической загрузкой) — не поддерживается;
  • iframe — не поддерживается;
  • бесконечная прокрутка — не поддерживается;
  • авторизация — не поддерживается;
  • обход капчи — вне задачи.

Логические:

  • fallback-селекторы слабо отличаются от active;
  • семантики нет — пользователь сам отвечает за выбор элемента;
  • система не восстанавливается после полностью изменённой структуры;
  • автоматическая адаптация между сайтами — в планах.


развитие.JPG

Ближайшее:

  • автоматический цикл по страницам;
  • стоп-условия пагинации (disabled, отсутствие);
  • защита от циклов через URL и хэш карточек;
  • обработка STRUCTURE_CHANGED при поломке селектора;
  • нормализация цены до числа (7990 вместо "7 990 ₽");
  • fallback из независимых источников (data-*, article, itemprop).

Среднесрочное:

  • экспорт в JSON и Excel;
  • клонирование профилей между сайтами одной CMS;
  • уведомления в Telegram при поломке селектора;
  • история изменений профиля с откатом;
  • визуальный отчёт о качестве сбора.

Долгосрочное:

  • семантические эвристики для отсева .old-price;
  • автоматическая адаптация при изменении вёрстки;
  • поддержка SPA через ожидание состояния;
  • обработка карточек без цены или ссылки;
  • импорт/экспорт профилей между пользователями.



Вопросы для обратной связи:

  • какие сайты стоит поддержать в первую очередь?
  • какие поля кроме title/price/url нужны?
  • нужна ли интеграция с Telegram/Email?
  • что стоит доработать в первую очередь?

Спасибо за внимание!
 

Вложения

  • vkp.zip
    vkp.zip
    22 MB · Просмотры: 0
Последнее редактирование модератором:
  • Оценить
Реакции: Алексей Козлов

Кто просматривает тему: (Всего: 2, Пользователи: 0, Гости: 2)