CoderCastrov logo
CoderCastrov
Парсер

Парсинг веб-страниц с использованием Elixir и Crawly. Извлечение данных из защищенных разделов.

Парсинг веб-страниц с использованием Elixir и Crawly. Извлечение данных из защищенных разделов.
просмотров
7 мин чтение
#Парсер
Entering a private area might be a challenge. Well, not for us.

В наших предыдущих статьях мы рассмотрели тему парсинга веб-страниц с использованием Elixir и показали, как Crawly может упростить эту работу, если вам нужно отображать динамический контент.

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

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

В этой статье мы покажем, как настроить веб-паука, который будет извлекать данные с http://quotes.toscrape.com/ (специального веб-сайта, созданного для экспериментов с парсингом веб-страниц), на этом сайте есть информация, которая доступна только при аутентификации запросов.

Изучение цели

Прежде всего, давайте посмотрим на http://quotes.toscrape.com/ веб-сайт, чтобы проанализировать цель и установить требования. Веб-сайт выглядит следующим образом:

pic1. A target website. Before login

Как вы можете видеть, http://quotes.toscrape.com - довольно простой веб-сайт, но он содержит ссылку для входа, которая перенаправляет вас на форму входа. Поскольку это тренировочный сайт, эта форма входа примет любую комбинацию логина/пароля.

Конечно, обычные формы входа будут требовать реальных учетных данных.

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

Pic2. The website after login. As you can see now we have a link to the Goodreads page

Задача: Итак, теперь, когда мы немного изучили цель, давайте извлечем следующие поля с веб-сайта: цитаты, авторы, теги и ссылку на Goodreads.

Настройка проекта

Теперь, когда у нас есть задача, выполните следующие шаги из руководства Quickstart:

mix new quotes -- sup
  1. Добавьте Crawly и Floki в файл mix:
defp deps() do
 [
   {:crawly, "~> 0.12.0"},
   {:floki, "~> 0.26.0"}
 ]
end
  1. Создайте папку config и файл config.exs
use Mix.Config

config :crawly**,  **closespider_timeout: **10,  **concurrent_requests_per_domain: **8,  **middlewares: [
    Crawly.Middlewares.DomainFilter**,    **Crawly.Middlewares.UniqueRequest**,    **Crawly.Middlewares.AutoCookiesManager**,    **{Crawly.Middlewares.UserAgent**, **user_agents: ["Crawly Bot"]}
  ]**,  **pipelines: [
    {Crawly.Pipelines.Validate**, **fields: [:quote**, **:author**, **:tags]}**,    **Crawly.Pipelines.JSONEncoder**,    **{Crawly.Pipelines.WriteToFile**, **extension: "json"**, **folder: "/tmp"}
  ]
  1. Определите файл паука: quotes_spider.ex
defmodule QuotesSpider do
 use Crawly.Spider

 alias Crawly.Utils

 @impl Crawly.Spider
 def base_url(), do: "http://quotes.toscrape.com/"

 @impl Crawly.Spider
 def init(), do: [start_urls: ["http://quotes.toscrape.com/"]]

 @impl Crawly.Spider
 def parse_item(response) do
   %{
     :requests => [],
     :items => []
   }
 end
end

На этом этапе мы закончили настройку проекта, поэтому пришло время добавить реальный код в функцию parse_item, чтобы получить извлеченные данные.

Извлечение данных

На этом этапе мы должны исследовать целевой веб-сайт, чтобы найти селекторы, которые позволят нам извлекать элементы, и узнать, как просматривать его. Процесс также описан в Crawly Tutorial, но мы решили следовать полному процессу здесь, чтобы этот учебник был самодостаточным, надеюсь, вы простите нас за эту избыточность. Давайте откроем эликсир-оболочку нашего проекта:

iex -S mix

И давайте получим страницу с помощью Crawly, чтобы мы могли экспериментировать с ней, чтобы найти подходящие селекторы:

response = Crawly.fetch("[http://quotes.toscrape.com/](http://quotes.toscrape.com/)")
{:ok**, **document} = Floki.parse_document(response.body)

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

Ссылки для перехода

Откройте веб-сайт с помощью инспектора Chrome, чтобы найти ссылки, по которым парсер будет переходить, чтобы найти все необходимые элементы:

Extracting the links to follow with the help of CSS selector

Как видите, на странице есть ссылка Next, которая имеет специальный CSS-класс (next). Мы можем использовать эту информацию, чтобы создать соответствующий селектор Floki. Попробуйте следующие строки в консоли:

(
  links =
    document
    |> Floki.find("li.next a")
    |> Floki.attribute("href")
)["/page/2/"]

Как видите, мы успешно преобразовали выделенную часть страницы в данные с помощью Floki.

Получение элементов

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

The HTML representation of the data

Как видите, все цитаты помечены одним и тем же CSS-стилем quote. Поэтому как только мы найдем извлекатели для всех полей в одной цитате, мы просто пройдемся по каждой цитате и получим все данные.

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

item_block = Floki.find(document, ".quote") |> List.first()
quote =  Floki.find(item_block**, **".text") |> Floki.text()****author = Floki.find(item_block**, **".author") |> Floki.text()****tags = Floki.find(item_block**, **".tags a.tag") |> Enum.map(&Floki.text/**1**)(
goodreads_link = Floki.find(item_block**, **"a:fl-contains('(Goodreads page)')")
|> Floki.attribute("href")
|> Floki.text()
)

Как видите, goodreads_link пока не может быть извлечен, так как мы еще не аутентифицировали наш запрос.

Выполнение входа в систему

Для аутентификации запроса необходимо получить goodreads_link. Технически это означает, что нам нужно получить сессионную cookie и добавить ее в запрос Crawly. После этого middleware Crawly.Middlewares.AutoCookiesManager автоматически добавит ее в каждый новый запрос. Но как получить сессионную cookie?

Это должно быть так просто, как отправка формы входа и получение cookie из response.

Давайте посмотрим на форму входа:

The login form

Как видно из скриншота выше, форма входа содержит 3 поля (csrf_token, username и password). И, наверное, сейчас каждый уже знает, что CSRF - это специальный метод, который предотвращает скриптовые отправки форм :(.


Как работать с формой, которая имеет поле CSRF?

Конечно, есть способы отправки форм с защитой от CSRF-атак. Давайте объясним, как работает эта проверка:

Это означает, что мы можем успешно отправить POST-запрос, если мы извлечем csrf_token и cookie со страницы входа. Это было переведено в следующий код:

def get_session_cookie(username, password) do
  action_url = "http://quotes.toscrape.com/login"
  response = Crawly.fetch(action_url)

  # Извлечение cookie из заголовков
  {{"Set-Cookie", cookie}, _headers} = List.keytake(response.headers, "Set-Cookie", 0)

  # Извлечение CSRF-токена из тела
  {:ok, document} = Floki.parse_document(response.body)

  csrf =
    document
    |> Floki.find("form input[name='csrf_token']")
    |> Floki.attribute("value")
    |> Floki.text()

  # Подготовка и отправка запроса. Данная форма входа принимает любую
  # комбинацию логина/пароля
  req_body =
    %{
      "username" => username,      "password" => password,      "csrf_token" => csrf
    }
    |> URI.encode_query()

  {:ok, login_response} =
    HTTPoison.post(action_url, req_body, %{
      "Content-Type" => "application/x-www-form-urlencoded",      "Cookie" => cookie
    })

  {{"Set-Cookie", session_cookie}, _headers} =
    List.keytake(login_response.headers, "Set-Cookie", 0)

  session_cookie
end

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

def init() do
  session_cookie = get_session_cookie("любое_имя_пользователя", "любой_пароль")

  [
    start_requests: [
      Crawly.Request.new("http://quotes.toscrape.com/", [{"Cookie", session_cookie}])
    ]
  ]
end

Наконец, полный код паука выглядит следующим образом:

defmodule QuotesSpider do
  use Crawly.Spider

  alias Crawly.Utils

  @impl Crawly.Spider
  def base_url(), do: "http://quotes.toscrape.com/"

  @impl Crawly.Spider
  def init() do
    session_cookie = get_session_cookie("любое_имя_пользователя", "любой_пароль")

    [
      start_requests: [
        Crawly.Request.new("http://quotes.toscrape.com/", [{"Cookie", session_cookie}])
      ]
    ]
  end

  @impl Crawly.Spider
  def parse_item(response) do
    {:ok, document} = Floki.parse_document(response.body)

    # Извлечение запроса из ссылок пагинации
    requests =
      document
      |> Floki.find("li.next a")
      |> Floki.attribute("href")
      |> Utils.build_absolute_urls(response.request_url)
      |> Utils.requests_from_urls()

    items =
      document
      |> Floki.find(".quote")
      |> Enum.map(&parse_quote_block/1)

    %{
      requests: requests,      items: items
    }
  end

  defp parse_quote_block(block) do
    %{
      quote: Floki.find(block, ".text") |> Floki.text(),      author: Floki.find(block, ".author") |> Floki.text(),      tags: Floki.find(block, ".tags a.tag") |> Enum.map(&Floki.text/1),      goodreads_link:
        Floki.find(block, "a:fl-contains('(Goodreads page)')")
        |> Floki.attribute("href")
        |> Floki.text()
    }
  end

  def get_session_cookie(username, password) do
    action_url = "http://quotes.toscrape.com/login"
    response = Crawly.fetch(action_url)

    # Извлечение cookie из заголовков
    {{"Set-Cookie", cookie}, _headers} = List.keytake(response.headers, "Set-Cookie", 0)

    # Извлечение CSRF-токена из тела
    {:ok, document} = Floki.parse_document(response.body)

    csrf =
      document
      |> Floki.find("form input[name='csrf_token']")
      |> Floki.attribute("value")
      |> Floki.text()

    # Подготовка и отправка запроса. Данная форма входа принимает любую
    # комбинацию логина/пароля
    req_body =
      %{
        "username" => username,        "password" => password,        "csrf_token" => csrf
      }
      |> URI.encode_query()

    {:ok, login_response} =
      HTTPoison.post(action_url, req_body, %{
        "Content-Type" => "application/x-www-form-urlencoded",        "Cookie" => cookie
      })

    {{"Set-Cookie", session_cookie}, _headers} =
      List.keytake(login_response.headers, "Set-Cookie", 0)

    session_cookie
  end
end

В качестве альтернативы вы можете ознакомиться с полным проектом здесь: https://github.com/oltarasenko/crawly-login-example

Заключение

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

Ищете больше информации?

Иногда парсинг может быть сложным. Мы считаем парсинг процессом, который включает разработку, контроль качества, мониторинг и другие действия. Поэтому мы создали инструмент с открытым исходным кодом, который позволяет вам разрабатывать, планировать и проверять ваши распределенные парсеры. Звучит интересно? Проверьте Crawly UI.