CoderCastrov logo
CoderCastrov
Эликсир

Парсинг веб-сайтов с помощью Elixir и Crawly. Отображение в браузере.

Парсинг веб-сайтов с помощью Elixir и Crawly. Отображение в браузере.
просмотров
5 мин чтение
#Эликсир

Введение

Поскольку количество веб-сайтов, использующих JavaScript для отображения контента, растет, растет и спрос на извлечение данных из них. Сама интерактивность добавляет некоторую сложность в процесс извлечения данных, так как невозможно получить полный контент с помощью обычного запроса, сделанного из командной строки HTTP-клиента.

Один из способов решения этой проблемы - симулировать асинхронный запрос с дополнительным POST-запросом. Хотя, на мой взгляд, такой подход добавляет еще больше сложности и хрупкости в ваш код.

Другой способ решения проблемы - использовать отображение браузера вместо командных HTTP-клиентов. Позвольте мне показать вам, как настроить паука Crawly, чтобы извлечь данные с autotrader.co.uk с помощью отображения браузера. Вы будете удивлены, насколько это просто!

Начало работы

Давайте создадим новый проект Elixir:

mix new autosites — sup

Теперь, когда проект создан, давайте добавим и загрузим самую последнюю версию Crawly:

# Запустите "mix help deps", чтобы узнать о зависимостях.
 defp deps do
 [
   {:crawly, "~> 0.8"},
   {:meeseeks, "~> 0.14.0"}
 ]

Хорошо, на этом этапе давайте немного изучим нашу цель. Давайте откроем одну из страниц, содержащих информацию о арендованном автомобиле: https://www.autotrader.co.uk/cars/leasing/product/201911194518336

Хорошо, пока все идет хорошо. Давайте попробуем получить эту страницу с помощью Crawly:

$ iex -S mix
iex(1)> response = Crawly.fetch("https://www.autotrader.co.uk/cars/leasing/product/201911194518336")
%HTTPoison.Response{
 body: "<!doctype html> …",
 headers: [
 {"Date", "Mon, 17 Feb 2020 13:49:42 GMT"},
 {"Content-Type", "text/html;charset=utf-8"},
 {"Transfer-Encoding", "chunked"},

ЗАМЕТКА: Autotrader.co.uk - это динамический веб-сайт с большим количеством автомобилей на продажу. Возможно (и, вероятно, так и будет), что к моменту, когда вы прочтете эту статью, данный автомобиль (id: 201911194518336) будет недоступен на их веб-сайте. Мы рекомендуем выбрать один из других автомобилей из раздела https://www.autotrader.co.uk/cars/leasing.

Хорошо, похоже, что автомобиль можно получить всего за 192,94 фунта в месяц... круто! Но давайте посмотрим, действительно ли мы можем получить эти данные в нашей оболочке. Давайте визуализируем наш загруженный HTML в браузере, чтобы увидеть, как он выглядит после загрузки:

iex(9)> File.write("/tmp/nissan.html", response.body)

Хорошо, теперь попробуем найти цену на данной странице:

Хорошо. Теперь видно, что в блоке цены нет цены. Как же мы будем с этим справляться? Один из возможных подходов - просмотреть исходный код страницы и найти следующий блок:

Однако, написание соответствующего регулярного выражения для извлечения этих данных из JavaScript - настоящая проблема. И, говоря серьезно, это добавит много ненужной сложности в код паука.

Давайте попробуем альтернативный подход.

Извлечение динамического контента

Новая версия Crawly 0.8.0 поставляется с поддержкой подключаемых загрузчиков, что позволяет нам переопределить способ, которым Crawly получает HTTP-ответы. В нашем случае нас интересует возможность направлять все запросы через браузер, чтобы получить отрендеренные страницы. Один из возможных вариантов - использовать легковесный браузер, который выполнит базовый JavaScript за нас. В качестве демонстрации мы возьмем рендерер Splash.

Splash - это легковесный и простой браузерный рендерер на основе Python и Qt. Хотя он может быть не подходящим для сложных целей, мы считаем его довольно полезным для более простых случаев.

Давайте запустим локальный сервис Splash:

docker run -it -p 8050:8050 scrapinghub/splash — max-timeout 300

Теперь Splash работает и может принимать запросы.

Настройка Crawly

Давайте зададим некоторые основные конфигурации. Предполагая, что мы хотим извлечь: id, заголовок, URL и цену, конфигурация может выглядеть следующим образом.

Создайте файл config/config.exs со следующим содержимым:

# Этот файл отвечает за настройку вашего приложения
# и его зависимостей с помощью модуля Mix.Config.
use Mix.Configconfig :crawly,
 fetcher: {Crawly.Fetchers.Splash, [base_url: "http://localhost:8050/render.html"]},
 # Определяет, как повторять запросыretry:
 [
 retry_codes: [400, 500],
 max_retries: 5,
 ignored_middlewares: [Crawly.Middlewares.UniqueRequest]
 ],closespider_timeout: 5,
 concurrent_requests_per_domain: 20,
 closespider_itemcount: 1000,
 middlewares: [
 Crawly.Middlewares.DomainFilter,
 Crawly.Middlewares.UniqueRequest,
 Crawly.Middlewares.UserAgent
 ],
 user_agents: [
 "Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41"
 ],
 pipelines: [
 {Crawly.Pipelines.Validate, fields: [:id, :title, :url, :price]},
 {Crawly.Pipelines.DuplicatesFilter, item_id: :id},
 {Crawly.Pipelines.JSONEncoder, []},
 {Crawly.Pipelines.WriteToFile, extension: "jl", folder: "/tmp"}
 ]

Важная часть здесь:

fetcher: {Crawly.Fetchers.Splash, [base_url:"http://localhost:8050/render.html"]},

Так как это определяет, что Crawly будет использовать загрузчик Splash для получения страниц из целевого веб-сайта.

Получение страницы автомобиля еще раз

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

iex(3)> response = Crawly.fetch("https://www.autotrader.co.uk/cars/leasing/product/201911194518336")
iex(3)> File.write("/tmp/nissan_splash.html", response.body)

Теперь давайте посмотрим, как выглядит отрендеренная страница:

Ну :(. Это немного пусто. Это произошло потому, что мы не дали Splash достаточно времени для отображения страницы. Давайте немного изменим конфигурацию и попросим Splash подождать 3 секунды после загрузки страницы (чтобы у него было достаточно времени для отображения всех элементов).

Обновленная конфигурация выглядит следующим образом:

fetcher: {Crawly.Fetchers.Splash, [base_url: "http://localhost:8050/render.html", wait: 3]}

Теперь, после повторной загрузки страницы (просто повторите предыдущие команды), мы получим следующие результаты:

Паук

Наконец, давайте обернем все в паука, чтобы мы могли извлечь информацию о всех доступных автомобилях на Autotrader. Процесс написания паука описан в нашей предыдущей статье и также в руководстве по началу работы с Crawly, поэтому мы не будем повторять это здесь. Но для полноты картины давайте посмотрим, как может выглядеть код паука (вам нужно добавить {:meeseeks, "~> 0.14.0"} в зависимости mix.exs):

defmodule AutotraderCoUK do
  [@behaviour](http://twitter.com/behaviour) Crawly.Spider  require Logger  import Meeseeks.CSS  [@impl](http://twitter.com/impl) Crawly.Spider
  def base_url(), do: "https://www.autotrader.co.uk/"  [@impl](http://twitter.com/impl) Crawly.Spider
  def init() do
    [
      start_urls: [
        "https://www.autotrader.co.uk/cars/leasing/search",
        "https://www.autotrader.co.uk/cars/leasing/product/201911194514187"
      ]
    ]
  end[@impl](http://twitter.com/impl) Crawly.Spider
  def parse_item(response) do
    case String.contains?(response.request_url, "cars/leasing/search") do
      false ->
        parse_product(response)true ->
        parse_search_results(response)
    end
  enddefp parse_search_results(response) do
    # Разбор страницы только один раз
    parsed_body = Meeseeks.parse(response.body, :html)
    # Извлечение элементов href
    hrefs =
      parsed_body
      |> Meeseeks.all(css("ul.grid-results__list a"))
      |> Enum.map(fn a -> Meeseeks.attr(a, "href") end)
      |> Crawly.Utils.build_absolute_urls(base_url())# Получение пагинации
    pagination_hrefs =
      parsed_body
      |> Meeseeks.all(css(".pagination a"))
      |> Enum.map(fn a ->
        number = Meeseeks.own_text(a)
        "/cars/leasing/search?pageNumber=" <> number
      end)all_hrefs = hrefs ++ pagination_hrefsrequests =
      Crawly.Utils.build_absolute_urls(all_hrefs, base_url())
      |> Crawly.Utils.requests_from_urls()%Crawly.ParsedItem{requests: requests, items: []}
  enddefp parse_product(response) do
    # Разбор страницы только один раз
    parsed_body = Meeseeks.parse(response.body, :html)title =
      parsed_body
      |> Meeseeks.one(css("h1.vehicle-title"))
      |> Meeseeks.own_text()price =
      parsed_body
      |> Meeseeks.one(css(".card-monthly-price__cost span"))
      |> Meeseeks.own_text()thumbnails =
      parsed_body
      |> Meeseeks.all(css("picture img"))
      |> Enum.map(fn elem -> Meeseeks.attr(elem, "src") end)url = response.request_urlid =
      response.request_url
      |> URI.parse()
      |> Map.get(:path)
      |> String.split("/product/")
      |> List.last()item = %{
      id: id,
      url: url,
      thumbnails: thumbnails,
      price: price,
      title: title
    }%Crawly.ParsedItem{items: [item], requests: []}
  end
end

Наконец, давайте запустим паука:

iex(1)> Crawly.Engine.start_spider(AutotraderCoUK)

Вот и все. Теперь у вас будут извлечены контент!

Я считаю, что такой подход довольно интересен, поскольку он позволяет переложить сложность извлечения данных на внешний сервис. Таким образом, сам код не должен иметь дело с этим и может оставаться простым.

Спасибо за чтение!