Олег Тарасенко

Вступление

Бизнесы инвестируют в данные. Ожидается, что рынок аналитики больших данных вырастет до 103 миллиардов долларов в течение следующих пяти лет. Легко понять, почему, поскольку каждый из нас в среднем генерирует 1,7 мегабайта данных в секунду.

По мере роста объема данных, которые мы создаем, растет и наша способность присваивать, интерпретировать и понимать их. Сбор огромных наборов данных и получение очень конкретных результатов ведет к фантастическому прогрессу во всех областях человеческих знаний, включая науку, маркетинг и машинное обучение.

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

Что такое парсинг?

Википедия определяет парсинг веб-страниц как:

Очистка веб-страниц, сбор веб-данных или извлечение веб-данных - это сбор данных, используемый для извлечения данных с веб-сайтов. [1] Хотя веб-скрапинг может выполняться пользователем программного обеспечения вручную, этот термин обычно относится к автоматизированным процессам, реализованным с помощью бота или веб-краулера. Это форма копирования, при которой конкретные данные собираются и копируются из Интернета, обычно в центральную локальную базу данных или электронную таблицу, для последующего поиска или анализа. (См .: https://en.wikipedia.org/wiki/Web_scraping).

Веб-парсинг с помощью Elixir

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

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

Начиная

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

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

mix new esl_blog --sup

Теперь, когда проект создан, измените deps` function файла mix.exs, чтобы он выглядел так:

#Run "mix help deps" to learn about dependencies. defp deps do [ {:crawly, "~> 0.1"}, ] end

Получите зависимости с помощью: mix deps.get, и мы готовы к работе! Итак, давайте определим наши правила сканирования с помощью ... пауков.

Паук.

Пауки - это модели поведения, которые вы создаете в качестве реализации и которые Crawly использует для извлечения информации с определенного веб-сайта. Паук должен реализовывать поведение паука (необходимо реализовать обратные вызовы parse_item/1, init/0`, `base_url/0). Это код нашего первого паука. Сохраните его в файл с именем esl.ex в каталоге lib/esl_blog/spiders вашего проекта.

defmodule Esl do @behaviour Crawly.Spider @impl Crawly.Spider def base_url() do "https://www.erlang-solutions.com" end @impl Crawly.Spider def init() do [ start_urls: ["https://www.erlang-solutions.com/blog.html"] ] end @impl Crawly.Spider def parse_item(_response) do %Crawly.ParsedItem{:items => [], :requests => []} end end

Вот более подробный анализ приведенного выше кода:

base_url (): функция, которая возвращает base_urls для данного Spider, используемая для фильтрации всех нерелевантных запросов. В нашем случае мы не хотим, чтобы наш сканер переходил по ссылкам, ведущим на сайты социальных сетей и другие партнерские сайты (которые не связаны с просканированной целью).

init (): должен возвращать список KW, содержащий список start_urls, с которого сканер начнет сканирование. Последующие запросы будут генерироваться из этих исходных URL.

parse_item (): функция, которая будет вызываться для обработки ответа, загруженного Crawly. Он должен возвращать структуру Crawly.ParsedItem.

Сканирование веб-сайта

Пришло время запустить нашего первого паука: запустите интерактивную консоль эликсира в текущем проекте и выполните следующую команду:

Crawly.Engine.start_spider(Esl)

Вы получите следующий (или аналогичный) результат:

22: 37: 15.064 [info] Запуск менеджера Elixir.Esl

22: 37: 15.073 [отладка] Запуск рабочего хранилища запросов для Elixir.Esl…

22: 37: 15.454 [отладка] Запущены 4 воркера для Elixir.Esl: ok iex (2) ›22: 38: 15.455 [info] Текущая скорость сканирования: 0 элементов / мин.

22: 38: 15.455 [info] Остановка Esl, истекло время ожидания счетчика элементов

Так что же только что произошло под капотом?

Сканирование запланировало объект Requests, возвращаемый функцией init / 0 Spider. Получив ответ, Crawly использовал функцию обратного вызова (parse_item / 1) для обработки данного ответа. В нашем случае мы не определили какие-либо данные, которые должны быть возвращены обратным вызовом parse_item / 1, поэтому рабочие процессы Crawly (отвечающие за загрузку запросов) не имели ничего общего, и в результате паук закрывается по истечении тайм-аута. достиг. Пришло время собрать реальные данные!

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

Пришло время реализовать часть реального извлечения данных. Ожидается, что наш обратный вызов parse_item / 1 вернет запросы и элементы. Начнем с извлечения запросов:

Откройте консоль Elixir и выполните следующее:

`{:ok, response} = Crawly.fetch("https://www.erlang-solutions.com/blog.html")` you will see something like: {:ok, %HTTPoison.Response{ body: "<!DOCTYPE html>\n\n<!--[if IE 9 ]>" <> ..., headers: [ {"Date", "Sat, 08 Jun 2019 20:42:05 GMT"}, {"Content-Type", "text/html"}, {"Content-Length", "288809"}, {"Last-Modified", "Fri, 07 Jun 2019 09:24:07 GMT"}, {"Accept-Ranges", "bytes"} ], request: %HTTPoison.Request{ body: "", headers: [], method: :get, options: [], params: %{}, url: "https://www.erlang-solutions.com/blog.html" }, request_url: "https://www.erlang-solutions.com/blog.html", status_code: 200 }}

Это наш запрос, полученный из Интернета. Теперь давайте воспользуемся Floki, чтобы извлечь данные из запроса.

Прежде всего, нас интересуют ссылки «Читать дальше», поскольку они позволяют нам переходить на фактические страницы блога. Чтобы понять «сейчас», давайте воспользуемся Floki для извлечения URL-адресов с данной страницы. В нашем случае мы извлечем Read more ссылок.

С помощью firebug или любого другого расширения веб-разработчика найдите соответствующие селекторы CSS. В моем случае я получил следующие записи: iex (6) ›response.body |› Floki.find («a.more») | ›Floki.attribute (« href ») [« / blog / a-guide -to-tracing-in-elixir.html »,« /blog/blockchain-no-brainer-ownership-in-the-digital-era.html »,« /blog/introduction-telemetry.html »,« / blog / памятуя-джо-четверть-века-вдохновения-и-дружбы.html »,

Далее нам нужно преобразовать ссылки в запросы. Crawly-запросы - это способ предоставить пауку некоторую гибкость. Например, иногда вам нужно изменить HTTP-параметры запроса перед их отправкой на целевой веб-сайт. Crawly полностью асинхронный. После того, как запросы составлены по расписанию, они обрабатываются отдельными рабочими процессами и выполняются параллельно. Это также означает, что другие запросы могут продолжать выполняться, даже если какой-то запрос не удался или произошла ошибка при его обработке. В нашем случае мы не хотим настраивать заголовки HTTP, но можно использовать функцию быстрого доступа: Crawly.Utils.request_from_url / 1 Crawly ожидает в запросах абсолютных URL-адресов. Подготовить правильные URL - это ответственность паука. Теперь мы знаем, как извлекать ссылки на фактические сообщения в блогах, давайте также извлечем данные со страниц блога. Давайте возьмем одну из страниц, используя ту же команду, что и раньше (но используя URL-адрес сообщения в блоге): {: ok, response} = Crawly.fetch (« https://www.erlang-solutions.com/blog /a-guide-to-tracing-in-elixir.html ')

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

Floki.find(response.body, "h1:first-child") |> Floki.text Extract the author with: author = response.body |> Floki.find("article.blog_post p.subheading") |> Floki.text(deep: false, sep: "") |> String.trim_leading() |> String.trim_trailing()

Извлеките текст с помощью:

Floki.find(response.body, "article.blog_post") |> Floki.text

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

defmodule Esl do @behaviour Crawly.Spider @impl Crawly.Spider def base_url() do "https://www.erlang-solutions.com" end @impl Crawly.Spider def init() do [ start_urls: ["https://www.erlang-solutions.com/blog.html"] ] end @impl Crawly.Spider def parse_item(response) do # Getting new urls to follow urls = response.body |> Floki.find("a.more") |> Floki.attribute("href") # Convert URLs into requests requests = Enum.map(urls, fn url -> url |> build_absolute_url(response.request_url) |> Crawly.Utils.request_from_url() end) # Extract item from a page, e.g. # https://www.erlang-solutions.com/blog/introducing-telemetry.html title = response.body |> Floki.find("article.blog_post h1:first-child") |> Floki.text() author = response.body |> Floki.find("article.blog_post p.subheading") |> Floki.text(deep: false, sep: "") |> String.trim_leading() |> String.trim_trailing() text = Floki.find(response.body, "article.blog_post") |> Floki.text() %Crawly.ParsedItem{ :requests => requests, :items => [ %{title: title, author: author, text: text, url: response.request_url} ] } end def build_absolute_url(url, request_url) do URI.merge(request_url, url) |> to_string() end end

Снова запускаем паука

Если вы запустите паука, он выведет извлеченные данные с журналом:

22:47:06.744 [debug] Scraped "%{author: \"by Lukas Larsson\", text: \"Erlang 19.0 Garbage Collector2016-04-07" <> ...

Это означает, что данные были успешно извлечены со всех страниц блога.

Что еще?

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

  • Гибкая подмена запросов (например, ротация пользовательских агентов)
  • Проверка элемента с использованием конвейерного подхода
  • Фильтрация уже увиденных запросов и предметов
  • Фильтрация всех запросов, поступающих с других доменов
  • Применение robots.txt
  • Контроль параллелизма
  • HTTP API для управления сканерами
  • Интерактивная консоль, позволяющая создавать и отлаживать пауков.

Учить больше

Хотите узнать, как инструменты, использованные в этом блоге, можно использовать для создания программы машинного обучения в Elixir? Не пропустите вторую часть этой серии блогов: Подпишитесь на нашу рассылку новостей, и мы отправим следующий блог прямо на ваш почтовый ящик.

Вернуться в блог

Первоначально опубликовано на https://www.erlang-solutions.com 20 сентября 2019 г.