Парсинг веб-страниц с использованием Elixir и Crawly. Извлечение данных из защищенных разделов.
В наших предыдущих статьях мы рассмотрели тему парсинга веб-страниц с использованием Elixir и показали, как Crawly может упростить эту работу, если вам нужно отображать динамический контент.
Я продемонстрирую, как Crawly может извлекать данные, которые находятся за авторизацией (это не тривиальная задача, которую можно решить без правильных инструментов).
Это может потребоваться в нескольких случаях. Например, когда вы хотите извлечь данные профиля из другой социальной сети. Или, например, когда вы хотите извлечь и проанализировать специальные предложения, доступные только для участников определенного веб-сайта.
В этой статье мы покажем, как настроить веб-паука, который будет извлекать данные с http://quotes.toscrape.com/ (специального веб-сайта, созданного для экспериментов с парсингом веб-страниц), на этом сайте есть информация, которая доступна только при аутентификации запросов.
Изучение цели
Прежде всего, давайте посмотрим на http://quotes.toscrape.com/ веб-сайт, чтобы проанализировать цель и установить требования. Веб-сайт выглядит следующим образом:
Как вы можете видеть, http://quotes.toscrape.com - довольно простой веб-сайт, но он содержит ссылку для входа, которая перенаправляет вас на форму входа. Поскольку это тренировочный сайт, эта форма входа примет любую комбинацию логина/пароля.
Конечно, обычные формы входа будут требовать реальных учетных данных.
Теперь давайте посмотрим, как выглядит веб-сайт после авторизации:
Задача: Итак, теперь, когда мы немного изучили цель, давайте извлечем следующие поля с веб-сайта: цитаты, авторы, теги и ссылку на Goodreads.
Настройка проекта
Теперь, когда у нас есть задача, выполните следующие шаги из руководства Quickstart:
mix new quotes -- sup
- Добавьте Crawly и Floki в файл mix:
defp deps() do
[
{:crawly, "~> 0.12.0"},
{:floki, "~> 0.26.0"}
]
end
- Создайте папку 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"}
]
- Определите файл паука: 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, чтобы найти ссылки, по которым парсер будет переходить, чтобы найти все необходимые элементы:
Как видите, на странице есть ссылка Next, которая имеет специальный CSS-класс (next). Мы можем использовать эту информацию, чтобы создать соответствующий селектор Floki. Попробуйте следующие строки в консоли:
(
links =
document
|> Floki.find("li.next a")
|> Floki.attribute("href")
)["/page/2/"]
Как видите, мы успешно преобразовали выделенную часть страницы в данные с помощью Floki.
Получение элементов
Теперь давайте воспользуемся инспектором Chrome, чтобы увидеть, как элементы представлены на той же странице:
Как видите, все цитаты помечены одним и тем же 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.
Давайте посмотрим на форму входа:
Как видно из скриншота выше, форма входа содержит 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.