Парсер статей Хабра по количеству комментариев и рейтинга
У меня есть небольшое увлечение — искать статьи на Хабре с отрицательным рейтингом и большим количеством комментариев. Открываю их и читаю нередко провокационный текст, дискуссии по 40 комментариев... Но была проблема — приходилось искать такие статьи вручную, так как инструменты Хабра не позволяют фильтровать статьи по отрицательному рейтингу и количеству комментариев. Публичного апи для таких нужд у Хабра тоже нет. Пришла идея — в качестве пет‑проекта написать парсер таких статей.
Парсер был написан на Elixir + Crawly + Floki. Стак был выбран не из каких‑то рациональный побуждений, а из цели получше разобраться в эликсире. Исходный код. Так как это статья, то опишу принцип работы парсера.
Релиз опубликован на гитхабе, запускается через habr_parser.bat start. Все спарсенные ссылки хранятся в виде json строк в папке temp.
@impl Crawly.Spider
def base_url(), do: "https://habr.com/"
@impl Crawly.Spider
def init() do
[
start_urls: gen_urls(base_url(), 1..50//5)
]
endЗдесь настраиваем базовый url + выбираем количество страниц для парсинга. На хабре можно просматривать только 50 страниц, поэтому выбрано такое ограничение. 1..50//5 - диапазон с 1 до 50 с шагом по 5. Это нужно для того, чтобы парсер не искал с 1 до 50 однопоточно, а запустил 10 отдельных парсеров, каждый из которых считает по 5 страниц.
defp gen_urls(base_url, range) do
range
|> Enum.map(fn i ->
Crawly.Utils.build_absolute_url("/ru/articles/page#{i}/", base_url)
end)
endФункция для генерации url для поиска. Здесь объяснять, думаю, нечего, обычный for each цикл.
document
|> Floki.find("article")
|> Stream.map(fn x ->
%{
title: Floki.find(x, ".tm-title__link") |> Floki.text(),
url:
Floki.find(x, ".tm-title__link")
|> Floki.attribute("href")
|> Enum.map(fn url -> Crawly.Utils.build_absolute_url(url, response.request.url) end)
|> List.first(),
votes: Floki.find(x, ".tm-votes-meter__value") |> Floki.text(),
comments:
Floki.find(x, ".article-comments-counter-link")
|> Floki.find("span")
|> Floki.text(),
time:
Floki.find(x, ".tm-article-datetime-published")
|> Floki.find("time")
|> Floki.attribute("datetime")
|> List.first()
}
end)Здесь происходит маппинг спарсенной html страницы, ищем по атрибуту .tm-votes-meter__value - это количество рейтинга, .tm-article-datetime-published - дата (полезно в будущем для фильтрации по ней), .article-comments-counter-link - собственно, количество комментариев. С помощью pattern matching эликсира и возможностей либы для парсинга эти атрибуты складываются в одну мапу, по которой в дальнейшем идёт фильтрация.
|> Enum.filter(fn %{votes: votes, comments: comments} ->
Integer.parse(votes)
|> then(fn
{int, _} -> int < 0
_ -> false
end) and
comments
|> Integer.parse()
|> elem(0) > 10
end)Здесь можно настроить фильтр под свои нужды. В данном случае голоса — меньше 0, комментариев — больше 10. Можно добавить фильтрацию по дате.
urls =
document
|> Floki.find("#pagination-next-page")
|> Floki.attribute("href")
|> Enum.map(fn url ->
Crawly.Utils.build_absolute_url(url, response.request.url)
|> Crawly.Utils.request_from_url()
end)Очень важная штука для Crawly, которая позволяет ему находить ссылки для рекурсивного парсинга. Проблемы повторного парсинга уже отпарсенных страниц (так как парсер работает в несколько потоков, парсер, начинающий с 1, однажды дойдёт до 6 страницы, с которой начинает второй поток) нет, так как Crawly держит в уме уже спарсенные страницы и отбрасывает их.
Собственно, весь код. Моя первая статья на хабре, не претендую на красивый стиль написания, как и на идеально написанный код, это всё же пет‑проект, как раз написанный с целью изучения языка. Но критика приветствуется.