...

суббота, 8 февраля 2014 г.

Рейтинг хабов по постам и подписчикам

На данный момент на хабре около 350 хабов. Функционал сайта позволяет сортировать их по имени и по индексу. А по другим параметрам — например по количеству постов — нет, а хотелось бы.

Меня вдохновила статья рейтинг постов хаба, и я решил сделать похожую, но составить уже рейтинг самих хабов.


В первой половине статьи я представлю вам рейтинги хабов по постам и подписчикам, а также небольшой их анализ. А во второй — подробно распишу, как я на Java с помощью библиотеки JSoup парсил HTML страницы хабры, с какими интересными явлениями и проблемами столкнулся. И в конце статьи выложу полный исходный код программы.






По количеству постов

По количеству подписчиков



Когда я отсортировал хабы, обнаружились интересные вещи. Например я не знал, что существуют хабы с нулевым количеством постов. А их оказалось целых 4 штуки! Причём на каждый из них подписано более 500 человек. Наверно это очень перспективные направления — не имея постов на них уже набежало столько народу. У хаба MySpace нету ни одного поста/события, зато более 55К подписчиков — это о чём-то говорит!

Тройка хабов — Чулан, Я пиарюсь и Веб-разработка — лидируют как по количеству постов, так и по количеству читателей. И в этом нет ничего удивительного — первые два хаба весьма общие и свободные, а уж веб-разработка интересна многим. Далее идёт Информационная безопасность, которая пользуется на хабре бешеной популярностью. За ней — Google, которую можно считать компанией №1 в области IT — по ней больше всего постов, по сравнению с другими. Отчасти благодаря её ОС Android, которой выпало 4 место по количеству подписчиков.


К сожалению, я так и не понял, почему хаб Хабрахабр — оффтопик. По количеству постов он будет на 13 месте, да и подписчиков у него >80К. Получается, что писать на сайте об этом же сайте — отход от темы?


Огорчило, что хаб Java находится не так высоко, как хотелось бы.


Очень долго я пытался разобраться в Хабрахабр Api. Как выяснилось, он закрыт и пока что в процессе разработки. Однако в переписке с support@habrahabr.ru мне сказали, что они не имеют ничего против парсинга их страниц. Собственно, именно так и работают хабраклиенты для Android (на данный момент).

Когда речь идёт о проектах «для себя», я выбираю любимую Джаву. Она и на этот раз меня не подвела — библиотека JSoup позволила в несколько строчек получить необходимые данные с HTML страницы. Но давайте сперва обсудим, как устроены хабы.


Страницы с хабами расположены по адресам http://ift.tt/1eHJyfS, где N — номер от 1 и далее. Посему, если мы хотим получить полный список из всех хабов — нам нужно загружать и анализировать эти страницы, пока они не закончатся. На каждой странице присутствует список из хабов. Формат элемента списка довольно простой и легко анализируется. Выглядит он так:



<div class="hub " id="hub_50">
<div class="habraindex">1 280,58</div>
<div class="info">
<div class="title">
<a href="http://ift.tt/1kOHAit">Информационная безопасность</a>
<span class="profiled_hub" title="Профильный хаб"></span>

</div>
<div class="buttons">
<input type="button" class="mini blue subscribeHub" value="Подписаться" data-id="50">
<input type="button" class="mini hidden unsubscribeHub" value="Подписан" data-id="50" "="">
</div>
<div class="clear"></div>
<div class="stat"><a href="http://ift.tt/1lcYUdX" class="members_count">91741 подписчик</a>, <a href="http://ift.tt/1lcYUdZ">3385 постов</a><a></a></div><a>
</a></div><a>
</a></div>


Давайте напишем метод, который возвращает нам список из всех хабов на сайте:



static List<Hub> getAllHubs() {
ArrayList<Hub> fullHubsList = new ArrayList<>();
String urlHubsIncomplete = "http://ift.tt/1eHJAEv";
int pageNum = 1;

do {
String urlHubs = urlHubsIncomplete + pageNum;

try {
Document doc = Jsoup.connect(urlHubs).get();
Elements hubs = doc.select(".hub");
if (hubs.size() == 0) {
break;
}
for (Element hubElem : hubs) {
Hub hub = new Hub(hubElem);
fullHubsList.add(hub);
}

pageNum++;
} catch (Exception e) {
e.printStackTrace();
break;
}

} while (true);

return fullHubsList;
}




Мы крутим бесконечный цикл while, формируя с каждой итерацией новый URL. Затем, с помощью Jsoup.connect(urlHubs).get() получаем непосредственно HTML-документ со списком хабов и их параметрами. Как несложно заметить — div с информацией о хабе имеет класс hub — и, вызвав doc.select(".hub"), мы получаем список из этих элементов. Если его размер равен нулю — значит мы прошли последнюю страницу и уже проанализировали все хабы — тогда мы выходим из цикла.

Далее — проходим по всем хабам-элементам и для каждого создаём объект типа Hub, передав в конструктор наш org.jsoup.nodes.Element. В нём располагается HTML-код такого же формата, как указан выше. Теперь давайте абстрагируемся от всего. Для этого и существует ООП. Перед нами есть только тот кусочек HTML, представленный выше, и класс, в который его нужно запихнуть. Напишем каркас для нашего класса:



import org.jsoup.nodes.Element;

public class Hub {
String title;
int posts;
boolean profiled;
int membersCount;
float habraindex;
String url;

public Hub(Element hubElem) {

}
}




Напишем конструктор. Для начала сделаем самое простое — получим данные из заголовочного тега. Для этого мы сначала извлекаем сам div вида

<div class="title">
<a href="http://ift.tt/1kOHAit">Информационная безопасность</a>
<span class="profiled_hub" title="Профильный хаб"></span>
</div>




Парсим через

Element titleDiv = hubElem.select(".title").get(0);
Element tagA = titleDiv.getElementsByTag("a").get(0);
title = tagA.text();
url = tagA.attr("href");
profiled = (hubElem.select(".profiled_hub").size() != 0);




Далее, мы хотим пропарсить количество подписчиков и постов — собственно те параметры, по которым мы и будем сортировать. Но сразу же сталкиваемся с первой проблемой — тег содержит строку «91741 подписчик», которую мы не можем просто так взять и преобразовать в Integer — она содержит буквы! Тут нам на помощь приходят регулярные выражения. Быстренько пишем ловкий метод, который получает строку и вырезает из неё всё, кроме цифр, да ещё и преобразует результат в int. \D — это НЕ цифра, а + — «встречается 1 или более раз». Т.е. мы в данном случае заменяем буквы на пустоту.

private int getNumbers(String str) {
String numbers = str.replaceAll("\\D+", "");
return Integer.valueOf(numbers);
}




Вот теперь мы уже можем со спокойной душой получить наши значения:

String membersCountFullStr = hubElem.select(".members_count").get(0).text();
membersCount = getNumbers(membersCountFullStr);

String statFullStr = hubElem.select(".stat").get(0).getAllElements().get(2).text();
posts = getNumbers(statFullStr);




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

Для начала, следует заменить запятую на точку и убрать лишние пробелы. Но этого не достаточно! Парсер всё равно выдаёт ошибку, если скопировать и вставить хабраиндекс в код — Double.valueOf("–1.11"). А если ввести вручную то же самое число — всё окей. Причём визуально в моей IDEA они абсолютно идентично выглядят!


Оказывается, дизайнеры хабры просто использовали dash вместо minus — c иным кодом символа, и парсер его, понятное дело, не ест. Возьмите на заметку. Суть проблемы в следующем:



System.out.println((int)'-');//45
System.out.println((int)'–');//8211




Когда-то в своей статье Хитрые задачи по Java я рассмотрел подвох, когда L маленькую можно не отличить от 1. Собственно, сейчас я напоролся на аналогичную проблему.

Посему, код для извлечения хабраиндекса будет чуть сложнее:



String rawHabraIndex = hubElem.select(".habraindex").get(0).text();//1 265,92
char minus = 45;//'-'
char dash = 8211;//'–'
String niceHabraIndex = rawHabraIndex.replaceAll(" ", "").replace(",", ".").replace(dash,minus);//1266.72
habraindex = Float.valueOf(niceHabraIndex);




Далее, пишем компаратор по постам как вложенный статический класс для Hub

public static class ComparePosts implements Comparator<Hub> {
@Override
public int compare(Hub o1, Hub o2) {
return o2.posts - o1.posts;
}
}




И сортируем по нему где-нибудь в main

List<Hub> hubs = getAllHubs();
Collections.sort(hubs, new Hub.ComparePosts());




Всё, задача выполнена! С количеством подписчиков аналогично. Далее я написал код, который выводит в консоль два списка в таком виде, чтобы их сразу можно было вставить в статью — и сделал это вначале.

На получение всех хабов уходит примерно 10 секунд. Исходный код можно скачать здесь. Собираем и запускаем вот так, не забыв установить Jsoup и заменить путь на ваш:



javac -cp .;"C:\prog\lib\jsoup-1.7.3.jar" com/kciray/habrahubs/Main.java
java -cp .;"C:\prog\lib\jsoup-1.7.3.jar" com.kciray.habrahubs.Main


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий