Осьо! — каталог українських продуктів
Осьо!/Open Graph, JSON-LD і трохи болю: як отримати інформацію про товар із будь-якого магазину

Open Graph, JSON-LD і трохи болю: як отримати інформацію про товар із будь-якого магазину

На перший погляд задача здається дуже простою.

Користувач вставляє посилання на товар. Я отримую сторінку магазину, дістаю назву, фотографію, ціну та валюту — і створюю бажання.

Що тут може піти не так?

Як виявилося — приблизно все.

Саме з цією задачею я зіткнувся в Bazhannya. Хотілося, щоб людині не доводилося вручну копіювати назву товару, завантажувати фотографію та вводити ціну. В ідеальному сценарії достатньо просто вставити URL.

На практиці ж кожен магазин може описувати один і той самий товар абсолютно по-різному.

Ідеальний сценарій

Базова схема виглядає доволі просто.

Bazhannya отримує URL, сервер робить HTTP-запит до сторінки, отримує HTML і намагається знайти в ньому потрібні дані.

У результаті мені потрібно отримати хоча б чотири речі:

  1. назву;
  2. зображення;
  3. ціну;
  4. валюту

Важливий момент: я не хочу писати окремий парсер під кожен інтернет-магазин.

Умовно, сьогодні це Rozetka, завтра якийсь невеликий український магазин, післязавтра — сайт з Європи чи США.

Тому потрібні не сотні правил під конкретні сайти, а нормальний каскад джерел даних.

Перше, що можна перевірити — структуровані дані

Найприємніший варіант — коли магазин сам уже описав товар у машинозчитуваному форматі.

Для цього існує Schema.org, а на сторінках товарів часто можна знайти тип Product.

У HTML це нерідко виглядає як JSON-LD:

{ "@type": "Product", "name": "Nike Air Max", "image": "...", "offers": { "@type": "Offer", "price": "4999", "priceCurrency": "UAH" } }

Для парсера це майже подарунок.

У Product можна знайти назву та зображення, а інформація про продаж зазвичай лежить в Offer: ціна, валюта, наявність та інші параметри. Саме такі поля описані Schema.org і використовуються, зокрема, пошуковими системами для роботи з товарними сторінками.

Тому логічно спочатку шукати application/ld+json, розбирати JSON і намагатися знайти об'єкт Product.

Але навіть тут усе не завжди красиво.

На одній сторінці може бути декілька JSON-LD блоків. Дані можуть лежати всередині @graph. offers може бути одним об'єктом або масивом. А замість звичайного Offer може використовуватися AggregateOffer, де є, наприклад, lowPrice і highPrice.

Тому правило «знайшов price — готово» швидко перестає працювати.

А яка з двох цін правильна?

Це одна з найцікавіших частин.

На сторінці товару легко можуть одночасно існувати:

стара ціна, нова ціна зі знижкою, ціна для учасників програми лояльності або діапазон цін для різних варіантів товару.

Сучасна розмітка навіть дозволяє явно розрізняти активну ціну та перекреслену стару ціну.

Тому при імпорті не можна просто зібрати всі числа зі знаком ₴ або $ і вибрати перше.

Якщо сторінка нормально використовує Offer, я б насамперед довіряв активному offers.price разом із priceCurrency.

А валюту краще отримувати саме окремим полем на кшталт UAH, USD або EUR, а не намагатися визначити її тільки за символом. Навіть Schema.org рекомендує використовувати код валюти, щоб уникати неоднозначностей.

Далі — Open Graph

Якщо нормального Product немає, наступне корисне джерело — Open Graph.

Це ті самі метатеги, завдяки яким посилання красиво виглядає, коли ним діляться в соцмережі або месенджері.

Базовий Open Graph стандарт визначає, зокрема, og:title, og:image, og:url та og:type.

Для мене тут особливо цікаві назва та фотографія.

А от із ціною Open Graph значно менш надійний як універсальний стандарт для товарного імпорту. Тому я б не будував весь механізм навколо припущення, що кожен магазин покладе туди ціну.

І вже тут стає видно головний принцип: одного джерела недостатньо.

Коли закінчується красива частина

А далі починається трохи болю.

Деякі магазини віддають майже порожній HTML, а сам товар завантажують JavaScript'ом уже після відкриття сторінки в браузері.

Звичайний HTTP-запит у такому випадку може отримати HTML без ціни або навіть без самої інформації про товар.

Інша проблема — антибот-захист.

Cloudflare та подібні системи вміють визначати автоматизований трафік, блокувати його або показувати challenge замість сторінки. Cloudflare прямо має окремі механізми захисту від scraping та автоматичних ботів.

І тут для мене важлива межа.

Я не хочу перетворювати імпорт товару на боротьбу із захистом конкретного магазину.

Нормальний шлях — працювати з публічно доступними сторінками, враховувати правила сайту та robots.txt, обмежувати частоту запитів і не намагатися обходити CAPTCHA, авторизацію чи інші технічні обмеження.

robots.txt стандартизований у RFC 9309 як спосіб повідомити автоматичним клієнтам правила доступу до URL. При цьому сам стандарт окремо зазначає, що це не механізм авторизації доступу.

Тому я сприймаю його як частину нормальної поведінки crawler'а, а не як спосіб «перевірити, чи законно мені парсити цей сайт». У реальному продукті ще варто враховувати умови використання конкретного ресурсу та юрисдикцію.

Fallback важливіший, ніж здається

Можна нескінченно покращувати парсер, але я не думаю, що правильна ціль — пообіцяти користувачу магічні 100%.

Навпаки.

Якщо Bazhannya не вдалося отримати частину інформації, ми попереджаємо про це і дозволяємо людині доповнити все вручну.

Причому редагувати можна все: назву, фотографію, ціну, валюту та інші дані бажання.

Для мене це важлива частина архітектури самої функції.

Автоматичний імпорт повинен економити час, а не ставати єдиним способом створення бажання.

Якщо вдалося автоматично підтягнути назву й фотографію, але не вдалося визначити ціну — це все одно корисніше, ніж змусити людину починати з нуля.

А сам механізм можна поступово покращувати, знаходячи нові реальні кейси.

Чому я поки не оновлюю товар автоматично

Зараз після імпорту ми зберігаємо отримані дані в Bazhannya.

Вони не прив'язані назавжди до актуального стану сторінки магазину і не оновлюються в реальному часі.

Це свідомо залишає ще одну цікаву задачу на майбутнє.

Зараз я працюю над механізмом відстеження цін.

І ось тут імпорт товару перестає бути просто зручним способом заповнити форму.

Якщо система періодично перевірятиме актуальну ціну товару, Bazhannya зможе повідомити користувачу:

«Товар, який ти хотів, зараз продається зі знижкою».

І це вже значно цікавіше завдання.

Бо потрібно не просто один раз правильно прочитати сторінку, а ще й через тиждень зрозуміти, що перед тобою той самий товар, правильно визначити актуальну ціну і не сплутати знижку зі старою, акційною або клубною ціною.

Який алгоритм виходить у результаті

Якщо дуже спростити мій робочий підхід до цієї задачі, він виглядає так:

  1. Отримати публічний HTML сторінки.
  2. Пошукати структуровані дані Product у JSON-LD.
  3. Спробувати отримати назву, зображення та активний Offer із ціною і валютою.
  4. Якщо частини даних немає — перевірити Open Graph та інші доступні метадані сторінки.
  5. Не вгадувати ціну, якщо сторінка дає суперечливі дані.
  6. Не намагатися обходити захист сайту заради одного імпорту.
  7. Показати все, що вдалося отримати.
  8. Дозволити користувачу виправити або додати решту вручну.

Саме такий сценарій зараз лежить в основі імпорту бажань у Bazhannya, хоча сам механізм я продовжую покращувати.

А наступна велика задача для мене — нормальне відстеження зміни цін.

#wishlist#app#parse#gifts