- PVSM.RU - https://www.pvsm.ru -

Twitter-бот на основе цепей Маркова и фраз из сериалов

Twitter-бот на основе цепей Маркова и фраз из сериалов - 1

Просматривал форумы в поисках вопросов, которые задают python-программистам на собеседованиях и наткнулся на один очень замечательный. Вольно его процитирую: ”Попросили написать генератор бреда на основе марковской цепи n-го порядка”. “А ведь у меня ещё нет такого генератора!” — прокричал мой внутренний голос — “Скорей открывай sublime и пиши!” — продолжал он настойчиво. Что же, пришлось подчиниться.

А здесь я расскажу, как я его сделал.

Сразу было решено, что генератор будет все свои мысли излагать в Твиттер и свой сайт. В качестве основных технологий я выбрал Flask и PostgreSQL. Связываться друг с другом они будут через SQLAlchemy.

Структура.

И так. Следующим образом выглядят модели:

class Srt(db.Model): 
    id = db.Column(db.Integer, primary_key = True) 
    set_of_words = db.Column(db.Text()) 
    list_of_words = db.Column(db.Text()) 

class UpperWords(db.Model): 
    word = db.Column(db.String(40), index = True, primary_key = True, unique = True) 
    def __repr__(self): 
        return self.word 

class Phrases(db.Model): 
    id = db.Column(db.Integer, primary_key = True) 
    created = db.Column(db.DateTime, default=datetime.datetime.now) 
    phrase = db.Column(db.String(140), index = True) 
    def __repr__(self): 
        return str(self.phrase)

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

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

Ну и класс Phrases нужен для хранения уже сгенерированных твитов.
Структура отчаянно простая.

Парсер субтитров формата .srt выведен в отдельный модуль add_srt.py. Там нет ничего экстраординарного, но если кому интересно, все исходники есть на GitHub [1].

Генератор.

Для начала нужно выбрать первое слово для твита. Как говорилось раньше, это будет любое слово из модели UpperWords. Его выбор реализован в функции:

def add_word(word_list, n): 
    if not word_list: 
        word = db.session.query(models.UpperWords).order_by(func.random()).first().word #postgre 
    elif len(word_list) <= n: 
        word = get_word(word_list, len(word_list)) 
    else: 
        word = get_word(word_list, n) 
    if word: 
        word_list.append(word) 
        return True 
    else: 
        return False

Выбор этого слова реализуется непосредственно строкой:

word = db.session.query(models.UpperWords).order_by(func.random()).first().word

Если Вы используете MySQL, то нужно использовать func.rand() вместо func.random(). Это единственное отличие в данной реализации, всё остальное будет работать полностью идентично.

Если первое слово уже есть, функция смотрит на длину цепи, и в зависимости от этого выбирает с каким количеством слов в тексте нужно сравнить наш список(цепь n-го порядка) и получить следующее слово.

А следующее слово мы получаем в функции get_word:

def get_word(word_list, n): 
    queries = models.Srt.query.all() 
    query_list = list() 
    for query in queries: 
        if set(word_list) <= set(query.set_of_words.split()): 
            query_list.append(query.list_of_words.split()) 
    if query_list: 
        text = list() 
        for lst in query_list: 
            text.extend(lst) 
        indexies = [i+n for i, j in enumerate(text[:-n]) if text[i:i+n] == word_list[len(word_list)-n:]] 
        word = text[random.choice(indexies)] 
        return word 
    else: 
        return False

Первым делом скрипт пробегает по всем загруженным субтитрам и проверяет, входит ли наше множество слов в множество слов конкретных субтитров. Затем тексты отсеянных субтитров складываются в один список и в нём ищутся совпадения фраз целиком и возвращаются позиции слов, следующими за этими фразами. Всё заканчивается слепым выбором(random) слова. Всё как в жизни.
Так добавляются слова в список. Сам же твит собирается в функции:

def get_twit(): 
    word_list = list() 
    n = N 
    while len(' '.join(word_list))<140: 
        if not add_word(word_list, n): 
            break 
        if len(' '.join(word_list))>140: 
            word_list.pop() 
            break 
    while word_list[-1][-1] not in '.?!': 
        word_list.pop() 
    return ' '.join(word_list)

Всё очень просто – необходимо, чтобы твит не превышал 140 символов и заканчивался завершающим предложение знаком препинания. Всё. Генератор выполнил свою работу.

Отображение на сайте.

Отображением на сайте занимается модуль views.py.

@app.route('/') 
def index(): 
    return render_template("main/index.html")

Просто отображает шаблон. Все твиты будут подтягиваться из него при помощи js.

@app.route('/page') 
def page(): 
    page = int(request.args.get('page')) 
    diff = int(request.args.get('difference')) 
    limit = 20 
    phrases = models.Phrases.query.order_by(-models.Phrases.id).all() 
    pages = math.ceil(len(phrases)/float(limit)) 
    count = len(phrases) 
    phrases = phrases[page*limit+diff:(page+1)*limit+diff] 
    return json.dumps({'phrases':phrases, 'pages':pages, 'count':count}, cls=controllers.AlchemyEncoder)

Возвращает твиты определённой страницы. Это нужно для бесконечного скрола. Всё довольно обыденно. diff – количество твитов, добавленных после загрузки сайта при апдейте. На это количество нужно смещать выборку твитов для страницы.

И непосредственно сам апдейт:

@app.route('/update') 
def update(): 
    last_count = int(request.args.get('count')) 
    phrases = models.Phrases.query.order_by(-models.Phrases.id).all() 
    count = len(phrases) 
    if count > last_count: 
        phrases = phrases[:count-last_count] 
        return json.dumps({'phrases':phrases, 'count':count}, cls=controllers.AlchemyEncoder) 
    else: 
        return json.dumps({'count':count})

На клиентской стороне он вызывается каждые n секунд и догружает в реальном времени вновь добавленные твиты. Так работает отображение нашего твита. (Если кому-то интересно, то можно посмотреть класс AlchemyEncoder в controllers.py, с его помощью производится сериализация твитов, полученных от SQLAlchemy)

Добавление твитов в базу и постинг в Твиттер.

Для постинга в Твиттер я использовал tweepy. Очень удобная батарейка, заводится сразу.

Как это выглядит:

def twit(): 
    phrase = get_twit() 
    twited = models.Phrases(phrase=phrase) 
    db.session.add(twited) 
    db.session.commit() 

    auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) 
    auth.set_access_token(ACCESS_TOKEN, ACCESS_TOKEN_SECRET) 

    api = tweepy.API(auth) 
    api.update_status(status=phrase)

Вызов этой функции я вынес в cron.py в корне проекта, и, как можно догадаться, оно запускается по крону. Каждые полчаса добавляется новый твит в базу и Твиттер.
Twitter-бот на основе цепей Маркова и фраз из сериалов - 2
Всё заработало!

В заключение.

В данный момент я подгрузил все субтитры для сериала “Друзья” и “Теория большого взрыва”. Степень марковской цепи пока что выбрал равной двум(при увеличении базы субтитров степень будет увеличиваться). Как это работает можно посмотреть в Твиттере [2], а все исходники доступны и лежат на гитхабе [3]. Намеренно не выкладываю ссылку на сам сайт. Если она нужна кому-то, он её обязательно добудет.

Всем большое спасибо за внимание. До новых встреч!

Автор: tenoclock

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/81570

Ссылки в тексте:

[1] GitHub: https://github.com/tenoclock/series_nerd/

[2] Твиттере: https://twitter.com/series_nerd

[3] гитхабе: https://github.com/tenoclock/series_nerd

[4] Источник: http://habrahabr.ru/post/249637/