Популярность тегов Хабра: какие тренды в постах?

в 19:47, , рубрики: python, ненормальное программирование, теги, теги никто не читает, хабрахабр, метки: , ,

Доброго времени суток!

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

1. Описание

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

Сразу оговорюсь, что далее по тексту встречается слово «тег» — это тоже самое что и тэг, таг, метка. Писать будем под python 3.3.2, так как в нём у меня не возникало проблем с юникодом.

2. Структура и подготовка

Для этой задачи нам необходима база данных. Она будет содержать 2 таблицы post_tags и tags. Поля первой pid — ид поста, tid — ид тега, date — дата поста с тегом. Поля второй rowid — ид тега, tag_title — заголовок тега. Тут всё предельно просто, поэтому создадим класс для работы с базой.

import sqlite3

class Base:
    def __init__(self,dbname):
        self.con=sqlite3.connect(dbname)

    def __del__(self):
        self.con.close( )

    def maketables(self):
        """
        Для создания таблицы
        """
        self.con.execute('create table post_tags(pid,tid,date)')
        self.con.execute('create table tags(tag_title)')
        self.con.commit( )

Так как всё сохранено в файл tags.py, попробуем выполнить:

import tags
extend = tags.Base('tags.db')
extend.maketables()

Теперь у нас есть пустая база, пойдёмте заполним её.

3. Парсинг тегов

Необходимо собрать все теги и записать совместно с датой публикации поста. Для этого используем beautifulsoup и urllib.request.

Добавим в созданный класс функции:

  • get_tag — для получения тега из базы
  • add_tag — для добавления тега
  • add_post — для просмотра и добавления поста

Код функций для парсинга

    def get_tag(self, name, added = True):
        """
        Поиск тега, если есть вернуть tid, иначе создать новый
        cur - текущий тег
        res - будет получен если тег уже в базе
        added - Флаг/ если установлен то будет дописывать, если нет то возвращать false
        """
        cur=self.con.execute("select rowid from %s where %s='%s'" % ('tags','tag_title',name))
        res=cur.fetchone( )
        if res==None:
            if added:
                cur=self.con.execute("insert into %s (%s) values ('%s')" % ('tags','tag_title',name))
                self.con.commit( )
                return cur.lastrowid
            else:
                return False
        else:
            return res[0]

    def add_tag(self, pid, name, date):
        """
        Добавление нового тега
        pid - ид поста
        name - сам тег
        date - дата поста
        """
        rowid = self.get_tag(name);
        print(pid,rowid ,name, date)
        self.con.execute("insert into %s (%s,%s,%s) values (%d,%d,'%s')" % ('post_tags','pid','tid','date',pid,rowid,date))
        self.con.commit( )

    def add_post(self, pid):
        """
        Просмотр поста (выборка даты и тегов)
        """
        if (self.get_post(pid)):
            return

        print('-'*10,'http://habrahabr.ru/post/'+str(pid),'-'*10)
        cur=self.con.execute("select pid from %s where %s=%d" % ('post_tags','pid',pid))
        res=cur.fetchone( )
        if res==None:
            try:
                soup=BeautifulSoup(urllib.request.urlopen('http://habrahabr.ru/post/'+str(pid)).read( ))
            except (urllib.request.HTTPError):
                self.add_tag(pid,"parse_error_404","")
                print("error 404")
            else:
                published = soup.find("div", { "class" : "published" })
                tags = soup.find("ul", { "class" : "tags" })
                if tags:
                    for tag in tags.findAll("a"):
                        self.add_tag(pid, tag.string, get_date(published.string))
                else:
                    self.add_tag(pid,"parse_access_denied","")
                    print("access denied")
        else:
            print("post has already")

Чтобы наполнить базу достаточно запустить цикл по всем постам. Ограничимся 196000 он попадает на 1 октября 2013.

for pid in range(196000):
    extend.add_post(pid+1)

Знаю, что способ не идеальный и с мегабитным интернетом работал бы около 170 часов.

Для ускорения процесса было решено добавить ещё одну таблицу post, в которой будет храниться отдельный ид поста, не связанный с логической частью, и использовать его как флаг. Так большую часть времени программа «висит». В ожидании загрузки страницы можно запустить ещё пару таких программ и натравив на одну базу заполнять её параллельно. Возникновение коллизий конечно возможно и в конце концов потеря или частичная запись составила 3% постов, что не так много. После таких действий выяснилось, что параллельно не может работать больше 8 программ, т.к. при запуске 9 Хабр отдает 503 ошибку, что вполне логично. Поэтому, запустив 6 экземпляров (именно такое количество не вызывало ошибок и не конфликтовало между собой), была получена база(17 Mb).

4. Обработка данных

Собственно, теперь основная часть — обработать полученные данные.
Добавим в созданный класс функции:

  • get_count_byid — количество по ид тега
  • get_graph — получить список кортежей (дата, количество)
  • get_image — показать изображение
  • get_all_tags_sorted — сортировать по убыванию
  • get_all_tag_count — получить список кортежей (ид, тег, количество)

Код функций для обработки данных

    def get_count_byname(self, name, date = ''):
        """
        Найти name по tid и получить количество по имени из get_count_byid
        name - имя тега
        date - (*)дата Формат mm_yyyy
        """
        tid = self.get_tag(name, False)
        return self.get_count_byid(tid, date)

    def get_count_byid(self, tid, date = ''):
        """
        Вернуть количество за весь период или за указанную дату
        """
        if tid:
            if date:
                count=self.con.execute("select count(pid) from %s where %s=%d and %s='%s'" % ('post_tags','tid',tid,'date',date))
            else:
                count=self.con.execute("select count(pid) from %s where %s=%d" % ('post_tags','tid',tid))
            res=count.fetchone( )
            return res[0]
        else:
            return False

    def get_graph(self, name):
        """
        Формирование списка дата - количество
        """
        month = ('01','января'),('02','февраля'),('03','марта'),('04','апреля'),('05','мая'),('06','июня'),('07','июля'),('08','августа'),('09','сентября'),('10','октября'),('11','ноября'),('12','декабря')
        years = [2006,2007,2008,2009,2010,2011,2012,2013]

        graph = []
        for Y in years:
            for M,M_str in month:
                date = str(M)+'_'+str(Y)
                graph.append((date, self.get_count_byname(name, date)))
        return graph

    def get_image(self, name):
        """
        Построение графика
        m_x - масштаб по X
        m_y - масштаб по Y
        img_x - ширина рисунка
        img_y - высота рисунка
        """
        img_x = 960
        img_y = 600

        img=Image.new('RGB',(img_x,img_y),(255,255,255))
        draw=ImageDraw.Draw(img)

        graph = self.get_graph(name)
        max_y = max(graph,key=lambda item:item[1])[1]
        if max_y == 0:
            print('tag not found')
            return False
        m_x, m_y = int(img_x/(len(graph))), int(img_y/max_y)

        draw.text((10, 10), str(max_y), (0,0,0))
        draw.text((10, 20), name, (0,0,0))

        x,prev_y = 0,-1
        for x_str, y in graph:
            x += 1
            if (x%12 == 1): draw.text((x*m_x, img_y - 30), str(x_str[3:]),(0,0,0))
            if prev_y >= 0: draw.line(((x-1)*m_x, img_y-prev_y*m_y-1, x*m_x, img_y-y*m_y-1), fill=(255,0,0))
            prev_y = y

        img.save('graph.png','PNG')
        Image.open('graph.png').show()

    def get_all_tags_sorted(self, tags):
        """
        По убыванию
        """
        return sorted(tags, key=lambda tag:tag[2], reverse=True)

    def get_all_tag_count(self):
        count=self.con.execute("select count(rowid) from %s" % ('tags'))
        res=count.fetchone( )
        alltag_count = res[0]
        tags = []
        for tag_id in range(alltag_count-1):
            tags.append((tag_id+1,self.get_tag_name(tag_id+1),self.get_count_byid(tag_id+1)))
            print (tag_id+1,self.get_tag_name(tag_id+1),self.get_count_byid(tag_id+1))
        return tags

5. Итог (для любителей статистики)

Top 25 «тегов лидеров»:

  1. google — 2611
  2. android — 2188
  3. Google — 2024
  4. linux — 1978
  5. php — 1947
  6. javascript — 1877
  7. microsoft — 1801
  8. apple — 1668
  9. социальные сети — 1509
  10. стартапы — 1484
  11. стартап — 1317
  12. программирование — 1229
  13. Microsoft — 1220
  14. java — 1180
  15. игры — 1141
  16. Apple — 1135
  17. iphone — 1122
  18. дизайн — 1110
  19. python — 1108
  20. юмор — 1061
  21. интернет — 1040
  22. хабрахабр — 983
  23. видео — 970
  24. реклама — 968
  25. Android — 876

Полный список можно посмотреть тут (формат id, title_tag, количество упоминаний).

Теперь, собственно, возвращаемся к мысли о трендах. Ниже приведены пару примеров (кликабельно):

python javascript android
Популярность тегов Хабра: какие тренды в постах? Популярность тегов Хабра: какие тренды в постах? Популярность тегов Хабра: какие тренды в постах?

Получить такие изображения можно из функции get_image(tag_title).

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

Полный код выложен на github.

Всем спасибо за внимание, буду рад критике.

Автор: hashspark

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js