Поиск открытого API сайта или Ускоряем парсинг в 10 раз


image


Цель статьи — описать алгоритм действий поиска открытого API сайта.
Целевая аудитория статьи — программисты, которым интересен парсинг и анализ уязвимостей сайтов.


В статье рассмотрим пример поиска API сайта edadeal.ru, познакомимся с протоколом google protobuf и сравним скорость различных подходов парсинга


1. Введение


Парсинг (в контексте статьи) — это автоматизированный процесс извлечение данных из Интернета.


Существует 2 подхода к извлечению данных со страниц сайта


  1. Извлекать данные из HTML-кода страницы сайта
    Плюсы — этот способ прост и работает всегда, так как код страницы всегда доступен пользователю
    Минусы — этот способ может работать долго (несколько секунд), если часть данных генерирует java script (например, данные появляются только после прокручивания страницы или нажатия кнопки)


  2. Использовать API сайта
    Плюсы — быстрее первого способа и не зависит от изменений структуры html-страницы
    Минус — не у всех сайтов есть открытое API



В статье рассмотрим пример поиска API сайта edadeal.ru, познакомимся с протоколом google protobuf и сравним скорость двух подходов парсинга


2. Постановка задачи


Задача — извлечь данные о продуктах с сайта Едадил (название продукта, цена, размер скидки, магазин, город и т.д)


3. Решение


1 Делаем запрос к странице, которую мы хотим парсить.
2 Перебираем все запросы, которые делает сайт. Для этого используем DevTools браузера
image


3 Анализируем запросы
Из названия запроса понимаем, что нам нужен запрос
https://squark.edadeal.ru/web/search/offers?count=30&locality=moskva&page=1&retailer=5ka


В ответ на запрос получаем файл (назовем его binary_file.bin). Как узнать кодировку этого файла?
Формат файла из пункта 3 нам подсказывает строка-хедер content-type: application/x-protobuf


4 Определим структуру данных (.proto файл)
с помощью утилиты protoc (http://google.github.io/proto-lens/installing-protoc.html) преобразуем закодированный файл в понятный человеку формат
protoc --decode_raw < binary_file.bin


Получаем список словарей:


1 {
  1: "e\341_\260\007\177W\202\222O\326\316\233\326\000A"
  2: "\320\242\321\203\320\260\320\273\320\265\321\202\320\275\320\260\321\217 \320\261\321\203\320\274\320\260\320\263\320\260 Familia Plus, 2 \321\201\320\273\320\276\321\217, 12 \321\200\321\203\320\273\320\276\320\275\320\276\320\262, 1 \321\203\320\277."
  3: "https://leonardo.edadeal.io/dyn/cr/catalyst/offers/u4nf6zbkjc3m5lss46ucvxjafm.jpg"
  4: 0x43ad7eb8
  5: 0x4347e666
  7: ";5\332^c\021\021\346\204\237RT\000\020\266\010"
  8: 0x41400000
  9: "\321\210\321\202"
  10: 0x422c0000
  11: "%"
  13: 43
  15: "2022-07-26T00:00:00Z"
  16: "2022-08-01T00:00:00Z"
  19: "A1\005L\332nPg\230\342q\375\031\335\014\336"
  20 {
    1: 0x3f800000
    2: 0x418547ae
    3: "\321\210\321\202"
    4: 1
  }
  21: "\224\331\203\202B\303\021\346\224\031RT\000\020\266\010"
  22: "K3\020\2537{O\271\273\374K\351\376\224\310*"
  22: "\300\336d(\224kL\025\224\300\355\256\247\327R\035"
  22: "\303O:\202\330\262A\326\246\023\307D\314F\303G"
  22: "\210\"\022?\250|L.\272\375\345{\335c,\026"
  22: "=3yP\026\004N\334\267\377\320\036F\326\331\\"
  22: "E\211\000\246e6EI\223\000)\242\3348\216M"
  22: "V#\263\022\367\324H\350\232r\013\010_KX\273"
  23: "\320\232\320\276\320\273\320\270\321\207\320\265\321\201\321\202\320\262\320\276"
  24: 1
}

5 Формируем .proto файл
Используем номера из предыдущего пункта, по контенту из предыдущего пункта нужно догадаться, какие поля, что означают (например 3 — это ссылка на изображение продукта)


Методом проб и ошибок получаем следующую структуру:


syntax = "proto2";

message Offers {
  repeated Offer offer = 1;
}

message Offer {
  optional string name = 2;
  optional string image_url = 3;
  optional float price_before = 4;
  optional float price_after = 5;
  optional float amount = 8;
  optional float discount = 10;
  optional string start_date = 15;
  optional string end_date = 16;
}

4 Переходим к написанию кода


Создаем питоновский файл с описанием структуры из .proto файла
protoc --proto_path=proto_files --python_out=proto_structs offers.proto


proto_files — имя директории с .proto файлами
proto_structs — в этой директории сохраняются результаты (_pb2.py файлы)


Код работает следующим образом:


  1. Делает запрос к API сайта
  2. Преобразует ответ сайта в json
  3. Выводит результат

import json
import requests

from google.protobuf.json_format import MessageToJson
from proto_structs import offers_pb2

def parse_page(city = "moskva", shop = "5ka", page_num = 1):
    """
    :param city: location of the shop
    :param shop: shop name
    :param page_num: parsed page number
    :return: None
    """
    url = f"https://squark.edadeal.ru/web/search/offers?count=30&locality={city}&page={page_num}&retailer={shop}"
    data = requests.get(url, allow_redirects=True)  # data.content is a protobuf message

    offers = offers_pb2.Offers()  # protobuf structure
    offers.ParseFromString(data.content)  # parse binary data
    products: str = MessageToJson(offers)  # convert protobuf message to json
    products = json.loads(products)
    print(json.dumps(products, indent=4, ensure_ascii=False,))

if __name__ == "__main__":
    parse_page()

Результат работы программы — список продуктов с описанием


{
    "offer": [
        {
            "name": "Наггетсы, куриные с ветчиной, Мираторг, 300 г",
            "imageUrl": "https://leonardo.edadeal.io/dyn/cr/catalyst/offers/necnmkv43splbm3hr5636snpry.jpg",
            "priceBefore": 218.99000549316406,
            "priceAfter": 109.48999786376953,
            "amount": 300.0,
            "discount": 51.0,
            "startDate": "2022-08-02T00:00:00Z",
            "endDate": "2022-08-08T00:00:00Z"
        },
        ...

5 Сравним результаты


Время выполнения кода из предыдущего пункта 0.3 — 0.4 секунды
Альтернативный вариант парсинга — загрузка всего html-кода страницы и извлечения нужной информации из этого кода


from selenium import webdriver

driver = webdriver.Chrome()
driver.get("https://edadeal.ru/moskva/retailers/5ka")
# извлечение данных из html кода

Время полной загрузки страницы 5 — 6 секунд.


6 Выводы


Лучше использовать API сайта для извлечения данных, если есть такая возможность
Использование API сайта позволяет не зависеть от изменений в html-коде страниы

Комментарии (10)


  1. Terranz
    07.08.2022 10:33
    -3

    Протобаф в вебе? Нет пути!


    1. rdo
      07.08.2022 10:51
      -3

      Это само по себе в 10 раз интересней статьи.

      Интересно, зачем они протобаф впихнули туда? Защита от скрипт-киддиз парсеров? Или какие-то непонятные вопросы производительности?


      1. mascai Автор
        07.08.2022 15:24
        -1

        Думаю, да
        Две цели
        1) Производительность
        2) Скрытие данных


        1. kin4stat
          07.08.2022 20:11

          или бекэнд на плюсах, в которых это единственный +- адекватный способ сериализации данных


  1. s_f1
    07.08.2022 11:07

    float price_before

    «priceBefore»: 218.99000549316406

    Точность – никогда не бывает лишней!


    1. mascai Автор
      07.08.2022 16:11

      Точно)


  1. GingerB
    07.08.2022 15:25
    +1

    from proto_structs import offers_pb2

    Это какой-то модуль, написанный Вами? Можно увидеть его содержимое? Спасибо!


    1. mascai Автор
      07.08.2022 15:29
      +1


      Команда

      protoc --proto_path=proto_files --python_out=proto_structs offers.proto

      создает файл offers_pb2.py в папке proto_structs/

      Этот файл содержит описание полей для протобафа. Пример:

      # Generated by the protocol buffer compiler.  DO NOT EDIT!
      # source: offers.proto
      ...
      
      _OFFER = _descriptor.Descriptor(
        name='Offer',
        full_name='Offer',
        filename=None,
        file=DESCRIPTOR,
        containing_type=None,
        fields=[
          _descriptor.FieldDescriptor(
            name='name', full_name='Offer.name', index=0,
            number=2, type=9, cpp_type=9, label=1,
            has_default_value=False, default_value=_b("").decode('utf-8'),
            message_type=None, enum_type=None, containing_type=None,
            is_extension=False, extension_scope=None,
            serialized_options=None, file=DESCRIPTOR),
        ...


  1. DonVietnam
    07.08.2022 17:09
    +1

    Я так и не понял, честно говоря, причем тут безопасность...


    1. mascai Автор
      07.08.2022 17:10

      пример того, как по бинарному файлу определить структуру сообщений, которую использует сайт

      и пример того, как декодировать бинарный файл (извлечь из него нужные данные)