Добрый вечер всем!

Возможно выбрал не лучшее время для охвата аудитории, но тем не менее главное чтоб продукт был хороший, а не статья о нем. Последние несколько недель я пишу приложение в рамках которого надо собирать огромное количество информации из сети(запросы к API/парсинг HTML кода) и под конец 4-ой интеграции я подумал что надо бы это максимально облегчить(не дело это пересобирать приложение под каждый чих интеграции), возможно это не лучшая преамбула, но хотя бы была реальная проблема решение к которой хотелось показать и заопенсорнуть.

Итак Fitter = сшиватель достаточно жаргонный перевод, но мне он кажется что лучше всего подходит. Я делал эту штуку исходя из следующих предположений:

  1. Данные могут меняться => нужен механизм обновления

  2. Данные могут быть в нескольких источниках => данные надо сшить(map/reduce)

  3. Нужна авторизация => API ключ/OAuth/Login+Pass

  4. Есть не только Server-side rendering =>  Данные могут появиться после загрузки клиентской части(я понимаю, что иногда можно сэмулировать запрос)

  5. Данные могут быть невалидны => поле может отсутствовать или разметка поменялась

  6. Мы не знаем где это будет развернуто => отсутствовать нужные абстракции

  7. Нужна конфигурация которую +- легко менять(не в текущем варианте)

  8. Нужно иметь возможность привести данные к одному виду

И так DEMO:

Немного расскажу что он умеет:

  1. Брать данные по HTTP запросам с авторизацией по Header

  2. Брать данные с бинарника Chromium

  3. Брать данные с Docker браузеров

  4. Брать данные с Playwright

  5. Парсить данные HTML/Json/XPath

  6. Пробрасывать данные/связывать с разных источников

Планы на будущее(Roadmap):

  1. Добавить сценарии: некоторые сайты для парсинга требуют авторизацию/принять cookie и тд тп, будет набор команд которые можно будет прогнать до парсинга и после

  2. Добавить способы отдачи информации: чтобы проект смог отправлять Webhook/Queue сообщения и тд тп

  3. Добавить способы сбора информации: сегодня пришла идея чтоб источником информации может быть любая штука например телеграм канал: и для этого нам нужен допустим бот с доступам к сообщениям

  4. Добавить способы запуска: тот же Webhook/Queue

  5. Валидация - отсеевать невалидные данные

  6. Редактор конфигурации - смотри ниже

Текущие точки боли(pain-points):

Пока что он один: конфигурация - для простых ситуаций это просто, однако если хочется связать несколько источников разобраться тяжело.

{
  "limits": {
    "playwright_instance": 3
  },
  "item": {
    "connector_config": {
      "response_type": "HTML",
      "connector_type": "server",
      "server_config": {
        "method": "GET",
        "url": "http://www.citymayors.com/gratis/uk_topcities.html"
      }
    },
    "model": {
      "type": "array",
      "array_config": {
        "root_path": "table table tr:not(:first-child)",
        "item_config": {
          "fields": {
            "name": {
              "base_field": {
                "path": "td:nth-of-type(1) font",
                "type": "string"
              }
            },
            "population": {
              "base_field": {
                "path": "td:nth-of-type(2) font",
                "type": "string"
              }
            },
            "temperature": {
              "base_field": {
                "path": "td:first-child font",
                "type": "string",
                "generated": {
                  "model": {
                    "type": "string",
                    "path": "temp.temp",
                    "model": {
                      "type": "object",
                      "object_config": {
                        "fields": {
                          "temp": {
                            "base_field": {
                              "type": "string",
                              "path": "//div[@id='forecast_list_ul']//td/b/a/@href",
                              "generated": {
                                "model": {
                                  "type": "string",
                                  "model": {
                                    "type": "object",
                                    "object_config": {
                                      "fields": {
                                        "temp": {
                                          "base_field": {
                                            "type": "string",
                                            "path": "div.current-temp span.heading"
                                          }
                                        }
                                      }
                                    }
                                  },
                                  "connector_config": {
                                    "response_type": "HTML",
                                    "connector_type": "browser",
                                    "attempts": 4,
                                    "browser_config": {
                                      "url": "https://openweathermap.org{PL}",
                                      "playwright": {
                                        "timeout": 30,
                                        "wait": 30,
                                        "install": false,
                                        "browser": "FireFox",
                                        "type_of_wait": "networkidle"
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "connector_config": {
                      "response_type": "xpath",
                      "connector_type": "browser",
                      "attempts": 3,
                      "browser_config": {
                        "url": "https://openweathermap.org/find?q={PL}",
                        "playwright": {
                          "timeout": 30,
                          "wait": 30,
                          "install": false,
                          "browser": "Chromium"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Как мы видим это больно, но давайте расскажу что тут происходит:

  1. Мы настраиваем лимиты параллельных запуска Playwright: 3 штуки

  2. Мы будем читать GET запросом HTML c сайта: http://www.citymayors.com/gratis/uk_topcities.html

  3. На выходе мы ожидаем массив следующего вида:

Array<City>

City = {
  name: string;
  population: string;
  temperature: string
}
  1. Если с полями name/population +- все понятно мы их берем из таблицы с сайта указанного выше, то temperature это сгенерированное поле

  2. Мы берем название города из таблицы и пробрасываем его на сайт: https://openweathermap.org/find?q={PL} - где {PL} - имя города , для этого мы используем Playwright так как там client-side rendering

  3. С результатов поиска мы берем relative ссылку на страницу города, пример: /city/2950159 и подставляем https://openweathermap.org{PL} - где {PL} ссылка, для этого мы используем Playwright так как там client side-rendering

  4. По ссылке выше выдергиваем температуру по селектору: `div.current-temp span.heading`

  5. И разворачиваем поле путем парсинга: `temp.temp` из сгенирированных данных

  6. Пример элемента массива

{
  "name": "Exeter",
  "population": "107,729",
  "temperature": "4°C"
},

Ну и принципе и выводим результат.

PS: я понимаю что для достижения результата можно было связать с API попроще, но мне хотелось показать сложный кейс.

PS2: я не выбрал я пиарюсь, потому как коммерческой ценности 0, проект будет открытыми бесплатным, но было бы хорошо собрать отзывы

Спасибо большое за внимание, очень хочется услышать критику ну и идеи на будущее!

Проект и примеры:

Примеры для поиграться(скачиваете Fitter_CLI из релизов и можете следовать Readme

Там есть разные примеры: Docker/Server side HTTP request/Chromium можно поиграться