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

Возможно выбрал не лучшее время для охвата аудитории, но тем не менее главное чтоб продукт был хороший, а не статья о нем. Последние несколько недель я пишу приложение в рамках которого надо собирать огромное количество информации из сети(запросы к 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 можно поиграться

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


  1. MagisterAlexandr
    00.00.0000 00:00

    Возможно выбрал не лучшее время для охвата аудитории

    Почему?


    1. PYXRU Автор
      00.00.0000 00:00
      +1

      По статистикe рабочии блоги читают в середину дня в среду :)


  1. Hungryee
    00.00.0000 00:00
    +2

    Выглядит как воплощение моей мечты в сфере скрейпинга - максимально абстрактная конфигурация для сбора информации. К сожалению в реальности это не будет работать для большинства сайтов, потому что как показывает практика, это большинство сайтов сделаны идиотами на отвали (включая разную структуру html для страниц, которые должны быть по логике одинаковыми и тд)

    Именно поэтому много скрейперов - это код на питоне (вставьте другой язык), где множество ветвлений/условий/проверок


    1. PYXRU Автор
      00.00.0000 00:00

      Спасибо большое за идею, я совершенно про такое забыл, я добавлю в поля optional параметр который будет лестинца с приоритетом(первый который не пустой будет выбран)
      Понимаю не все это решит в коде все таки легче условия описать, но чуток упростит


      1. PYXRU Автор
        00.00.0000 00:00

        Добавил такую возможность в инструмент


  1. nin-jin
    00.00.0000 00:00

    Я немного ужал ваш конфиг в 4 раза:

    limits playwright_instance 3
    out Server
    	method \GET
    	url \http://www.citymayors.com/gratis/uk_topcities.html
    	out Array
    		in xpath \//table//table//tr[preceding-sibling::*]
    		out Object
    			name String xpath \td[1]//font
    			population String xpath \td[2]/font
    			temperature Chromium
    				in String xpath \td[1]//font
    				url \https://openweathermap.org/find?q={in}
    				attempts 3
    				timeout 30
    				wait 30
    				install false
    				out FireFox
    					in String xpath \//div[@id='forecast_list_ul']//td/b/a/@href
    					url \https://openweathermap.org{in}
    					attempts 4
    					timeout 30
    					wait 30
    					install false
    					type_of_wait \networkidle
    					out String css \//div.current-temp span.heading
    


    1. PYXRU Автор
      00.00.0000 00:00

      Спасибо большое, это принципе очень похоже на YAML который уже поддерживается(я больше говорил про сложность понимания вариантов, чем про размер)

      Ну и у вас опущена, ну и они не совместимы в вашем случае это подошло бы для запуска бинарника Firefox, а в нашем случае Firefox как часть playwright

      Но все таки я имел ввиду понимание его неделе размер(согласен чем меньше размер легче понять, попозже перепишу на yaml)


      1. nin-jin
        00.00.0000 00:00

        Это совсем не ямл. Тому, кто будет писать эти конфиги, ему не нужно знать ни про какой playwrite, playleft или какую ещё утилиту для управления браузером вы захотите использовать. Chromium и Firefox - это абстрактные адаптеры. Можно даже добавить адаптер Browser для случая, когда не важно каким браузером загружать данные. Например, он может брать первый освободившийся.


        1. PYXRU Автор
          00.00.0000 00:00

          Ну тоесть таких абстракций много, я больше пока склоняюсь к jsonnet, для более юзабельной конфигурации