Скрейпимо публічні дані, або Як я робив мапу АЗС
24 лютого 2022 року росія почала повномасштабну війну проти України. Буквально в перші години на АЗС утворились кілометрові черги, які досі не зникли. Суттєво ситуація погіршилася в травні 2022 року після ракетного удару по нафтопереробній галузі. Як власник авто, я довго страждав, переглядаючи додатки різних АЗС в пошуках бензину і врешті-решт вирішив зробити свій ресурс, де буде видно загальну картину станом на зараз. Так з’явилась zapravka.info (далі — «заправка») — онлайн мапа працюючих АЗС з фільтрацією по типу пального та можливості купити за готівку чи банківську картку.
Спочатку я ділився посиланням з друзями і в себе в соцмережах, але сайт сподобався людям настільки, що зараз відвідуваність сягає більше 1000 унікальних користувачів на день, а мій коментар про «заправку» потрапив в найкращі в темі про пальне на DOU. Тому я вирішив поділитися зі спільнотою підкапотними процесами і розібратись, як робити корисний продукт на основі публічних даних. Стаття буде корисною людям, які роблять свої перші кроки в скрейпінгу, але вже мають уявлення, як клієнт і сервер спілкуються між собою та знайомі з мовою програмування (будь-якою).
Мене звати Вадим, я — Lead Software Engineer в Mate academy. Моя кар’єра в IT почалася з Frontend розробки, але бажання вивчити щось нове дозволило швидко познайомитися з Backend і навчитись закривати задачі end to end. Ще в доковідні часи на нетворкінгу спілкувалися з колегами про важливість pet-проєктів для розробника, і що основна задача девелопера, це не писати код, а вирішувати проблеми за допомогою коду. Крім того, pet project допомагає відчувати ownership та відповідальність за продукт, який ти робиш. На lemon.io є цікава стаття на цю тему. Я радий, що можу зробити щось корисне для суспільства за допомогою коду і, в цьому випадку, зекономити людям трохи часу хоча б в пошуках місця, де заправитись.
Як це працює
В основі роботи додатку лежить робота з публічними даними за допомогою скрейпінгу. Це коли ми кодом імітуємо візит на сайт і парсимо контент в потрібну нам структуру даних. Підхід дуже популярний в сферах продажу чи оренди житла / авто, e-commerce, порівняння цін на товари чи послуги, і т. д.
Найпопулярніші приклади українських компаній, побудованих на скрейпінгу:
- ЛУН, flatfy і їх дочірні продукти (продаж та оренда нерухомості).
- hotline.ua, price.ua (порівняння цін в інтернет магазинах).
- в Mate academy ми скрейпимо вакансії для студентів з різних ресурсів.
Одночасно з «заправкою» з’явились ще декілька телеграм-ботів, де теж можна дізнатись стан справ на АЗС. Вони також побудовані на даних, зібраних з публічних джерел:
- t.me/Kyiv_AZS (бензин) та t.me/Kyiv_Diesel (дизель) від Дмитра. Він також допоміг мені з ОККО, за що йому окреме дякую.
- t.me/petrol_alert_bot
Чи це законно
Працюючи з даними з інших ресурсів свідомо виникає питання, наскільки це легально. Якщо відповісти коротко, то вебскрейпінг легальний, якщо ми працюємо з публічними даними. Іншими словами, якщо можливо відкрити посилання в браузері і побачити контент, значить дані можна скрейпити. Існує навіть прецедент програної справи LinkedIn проти компаній, які парсили публічні профайли користувачів.
У статті я хотів би детальніше розповісти, які проблеми вирішував під час роботи з даними мереж АЗС. Базовий принцип роботи «заправки» — зібрати дані і розмістити маркери на мапі.
В якості мапи я обрав google maps, але є ще варіант — mapbox. UI частина зроблена на nextjs і деплоїться на vercel (так було найшвидше + я вже працюю з реактом). Бекенд — nestjs, який я деплою на сервер digitalocean (загалом тут підійде будь який хостинг, де можна купити віртуальну машину).
Так виглядав proof of concept, де був доступний тільки WOG:
Як працювати з даними
Перша і основна проблема, з якою я зіштовхнувся, це збір даних і їх конвертація в потрібний мені формат. Кожна мережа показує дані своїм способом. В одних можна просто зробити запит на їх API і отримати майже готову до «production» відповідь, а для інших потрібно відкривати автоматизований браузер і парсити HTML. Також я не додавав всі станції одразу, а робив це поступово, по мірі знаходження інформації про роботу тієї чи іншої мережі. Відповідно додавання нових мереж не мало б причиняти багато страждань і рефакторингу існуючого функціоналу (це той же Open-Closed principle з SOLID).
Для спрощення життя я використав патерн «адаптер». В його основі якраз можливість приводити різні об’єкти до одного формату. Якщо дуже поверхнево, то архітектура «заправки» виглядає приблизно так:
У цьому прикладі роль «скрейпера» — дістати дані тим чи іншим способом, а адаптера — перетворити відповідь у зручний для подальшого використання формат. Наприклад, OKKO віддає інформацію про стан пального в форматі HTML:
Так це виглядає в коді:
Після обробки адаптером інформація конвертується в більш зручний формат:
На що звертати увагу при скрейпінгу
Коли ми працюємо з публічними даними, потрібно звертати увагу на те, що дуже часто вони будуть форматовані в стилі «як зручніше для користувача». Приклад з ОККО і їхнім HTML описує ситуацію найкраще.
Основні місця, де можна шукати дані, це:
- HTML-сторінка.
- Відкрите API.
- Мобільний додаток.
Якщо вдалось знайти відкрите API, вважайте це перемогою (буде менше проблем з форматуванням). У випадку з АЗС я дивився, в першу чергу, на запити на сторінці мапи станцій через Chrome DevTools (більше про Chrome DevTools можна почитати в статті мого колеги).
Приклад: API + HTML контент (OKKO)
Для ОККО вдалося побачити запит на /fuel-map.
Проблема була в тому, що програмно сюди не виходило зробити запит (через fetch, request чи їх аналоги в інших мовах) в зв’язку з їх системою захисту від DDOS. Але URL коректно працюватиме, якщо його відкрити в браузері. На допомогу прийшла бібліотека Playwright. Вона корисна і використовується, в основному, для end to end тестування, але в той же час ніхто не забороняє використати їх headless браузер в своїх цілях. Код для отримання даних з ОККО виглядає приблизно так:
Я ще додатково передаю user-agent браузера Chrome, щоб мій запит виглядав менш підозріло.
В Firefox є дуже зручний спосіб побачити відформатований JSON (про інші особливості Firefox можна почитати в статті мого колеги). Дані, які нам потрібні, знаходяться в полі notification. Вже під час написання статті я побачив, що у відповіді з’явились поля {fuel_name}_tip_oplati. Це суттєво спрощує парсинг, але ще декілька тижнів тому цих полів не було, тому доводилось парсити HTML з notification).
Для обробки «сирого» HTML я використав бібліотеку node-html-parser (підійде будь-який аналог на вашій мові програмування). Бібліотека дозволяє перетворити HTML-рядок на об’єкт з інстансами HTML-нод (схожий до DOM в браузері) і використовувати більш декларативні методи і властивості, наприклад node.textContent. Основна особливість підходу — шукаємо контент за ключовими словами. У цьому випадку нам важливі наступні фрази:
- «Графік роботи:».
- «За готівку і банківські картки доступно».
- «З паливною карткою і талонами доступно».
Алгоритм наступний:
- Проходимось по всіх HTML-нодах.
- Якщо контент містить ключові слова, парсимо відповідний блок.
Після парсингу дані потрапляють в адаптер, на виході з якого ми отримуємо вже готовий до використання об’єкт, котрий і передасться на UI. Приклад того, як адаптер мапить дані:
Приклад: API (WOG)
Найпростіше було працювати з WOG. На сторінці з мапою одразу видно запит на api.wog.ua/fuel_stations
Єдиний нюанс — немає інформації про пальне. Її можна отримати, зробивши додатковий запит по id станції:
Як і в прикладі з OKKO, отримуємо дані про пальне, розпарсивши рядок workDescription, тільки тут простіше, бо працюємо зі звичайним рядком, а не з HTML-кодом. Ключові фрази в цьому випадку:
- «пальне відсутнє»;
- «тільки спецтранспорт»;
- «готівка»;
- «талони».
Приклад: Source code (UPG)
У випадку з UPG, сайт не шле запити на API, а дані на сторінку додаються ще на сервері. До того, як сторінка завантажиться і відобразиться в браузері. Тут корисно було почитати source code сторінки і знайти, як малюється сама мапа.
Знаходимо в devtools запит на сторінку:
Якщо трохи прогортати, знайдемо JS, що відповідає за ініціалізацію мапи і об’єкт objmap:
Це саме те, що нам потрібно. Подивимось на об’єкт в консолі:
Playwright дозволяє виконати JS код на сторінці за допомогою команди page.evaluate. Код для отримання даних виглядатиме наступним чином:
Пальне, де Price != 0, доступне для покупки за готівку чи банківську картку. Далі стандартна процедура з форматуванням даних і маємо +1 ресурс.
Приклад: мобільні додатки (БРСМ Нафта, АВІАС, Socar)
Зараз АВІАС і Socar показують більш-менш актуальні дані в вебверсії, але в момент додавання їх до «заправки», дані з мобільного додатку були точніші. АВІАС показував всі АЗС на сайті незалежно від того, є пальне, чи нема, а Socar в якийсь момент поламався і почав показувати застарілі дані (бензин по 30 грн, наприклад). Тому я вирішив перевірити їх мобільні додатки і знайшов там багато цікавого. Основне, що мені було потрібно, це інформація про API, яке там використовується. За допомогою додатку proxyman я перевірив запити, які надсилає мій телефон при використанні апки. Ось декілька скріншотів з БРСМ Нафта. Крім аналітики, тут з’явились запити на td4.brsm-nafta.com. Один з них був на /get_full_ffs. Те, що нам потрібно:
Повна адреса: td4.brsm-nafta.com/...pi/v2/Mobile/get_full_ffs
В браузері посилання теж без проблем відкривається і віддає необхідні дані. Якщо масив fuels не пустий, це означає, що на заправці є пальне у вільному продажу.
Зробивши додатковий запит на інформацію про заправку, отримаємо необхідні дані.
АВІАС віддає дані в .zip форматі, тому перед використанням їх потрібно додатково розархівувати. З Socar особливих проблем не було, стандартний запит на API і форматування відповіді. Як наслідок, я додав +3 ресурси теж без особливих зусиль.
Висновки
Використавши нестандартні підходи до отримання даних, на «заправці» вдалось зібрати близько 2000 АЗС з 6 найбільших мереж України. А правильний архітектурний підхід дозволяє додавати нові станції досить швидко. Останню мережу «БРСМ Нафта» я додав за годину, при цьому більше 30хв пішло на те, щоб намалювати маркер для мапи в figma.
До речі, про маркери. Мій внутрішній перфекціоніст не витримав і я таки намалював стандартні іконки для всіх мереж:
Зараз «заправка» виглядає набагато приємніше, ніж в першому варіанті:
Сподіваюсь, стаття була корисною і ви отримали більше натхнення і прикладів, як можна працювати з публічними даними і робити корисні речі. Буду радий, якщо хтось із вас зробить щось подібне і вирішить проблему, яка давно висить тягарем і забирає багато часу. Нижче додам декілька інсайтів та порад, які вважаю корисним виділити зі статті:
Поборов 100vh на телефонах
Хто верстав різні full-screen секції, той знає, що 100vh на телефоні рахуються без урахування панелі з URL, яка ховається при скролі. Якщо сторінку не скролити, то панель перекриває частину контенту, що дуже незручно. Є різні підходи, як це виправляти. Один з найкращих — значення -webkit-fill-available для Safari та Chrome, або -moz-available для Firefox. Автопрфіксер дозволяє написати max-height: stretch і на виході додає потрібні значення:
Google maps — досить дорогий продукт
Google maps дає $200 в місяць безкоштовно для розвитку продукту. Це ~28500 завантажень карти кожного місяця. Спочатку я думав, що цього буде достатньо, але на момент написання статті (21 червня 2022 р.) «заправка» вже використала $400 з початку місяця і це число продовжує рости. Орієнтуюсь на чек в $600 за червнеь. Not bad для волонтерського продукту, правда? :) Поки що просто замінюю акаунт, коли безкоштовні кредіти добігають кінця + в дружини ще діє $300 google cloud free trial. Це дозволяє хостити мапу умовно безкоштовно.
Це не прохання підтримати мене фінансово, просто ділюсь враженням. Якщо ж маєте бажання допомогти проєкту, задонатьте на потреби ЗСУ. Хлопці і дівчата роблять неймовірні речі і саме завдяки їм ми з вами маємо можливість в тому числі і ділитись інсайтами тут на форумі. Особисто я пересилаю гроші в наступні фонди:
- Повернись живим.
- Благодійний фонд Сергія Притули.
- KOLO, продукт від українських IT-фахівців. Тут можна навіть оформити помісячну підписку.
- Інші волонтерські організації: тут просто дивіться пости в соцмережах.
Update від 28.06.2022: Google заапрувив «заправку», як «critical responders» і надав $2500/місяць на використання карти до кінця літа.
Копати глибоко — корисно
Ще в минулій статті про анімації я рекомендував розбиратись, як працюють ті чи інші речі. Є декілька простих трюків, щоб дозволили відносно легко отримати доступ до потрібних даних. Яскравий приклад — глобальна змінна objmap в UPG. Я дізнався про неї, почитавши вихідний код сторінки.
Розуміння, як працює інтернет, точно не завадить
Клієнт-серверна комунікація — основа основ, яку варто знати хоча б на базовому рівні. Наприклад, розуміння, що клієнт не отримує дані магічним способом, а робить простий запит на API, дозволяє думати в потрібному напрямку. У тому числі це дозволило працювати з мережами, чиї дані доступні тільки в мобільних додатках. Почитати про це детальніше можна в статті від MDN.
Pet projects — важливі
До IT я допомагав батькам з сімейним бізнесом. В нас була (і є) невелика мережа продуктових магазинів. Там я познайомився з 1С і навчився автоматизовувати рутинні процеси, так і потрапив в девелопмент. Одна з найбільших проблем, яку ми мали, це автоматизація обліку розрахунків з постачальниками. Тому ще тоді, будучи Junior Frontend Developer’ом, я вже почав вивчати бекенд і робити власний додаток. Код там був дуже страшний (дуже!), але свою задачу та штука виконувала. А відчуття, що я своїми силами створив щось, що дозволило вирішити проблему бізнесу, було неймовірно крутим.
Зараз я продовжую виділяти декілька годин в місяць (в неробочий час, звісно ж) на власні проєкти і це дозволяє змінити контекст. Тут має місце жарт про водія вантажівки, який після тяжкого дня повертається додому і відпочиває, граючи в умовний Euro Truck Simulator.
Що це мені дає?
- Я маю можливість спробувати щось нове, з чим ще раніше не працював. Буде це нова технологія чи принципово новий підхід в розробці — не так важливо.
- Круто відчувати ownership за продукт і вирішувати, що саме там потрібно робити. Хоча і в компанії мені цього більш ніж достатньо, але корисно подивитись на інші домени.
- Приємно, коли людям подобається і приносить користь те, що я роблю. Додам пару скріншотів для натхнення.
А який функціонал ви б хотіли бачити на «заправці»? Маєте приклади власних петпроєктів? Є зауваження чи фідбек по роботі продукту?
Відповідь на ці питання та інші думки сміливо пишіть в коментарях :)
62 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів