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

в 16:40, , рубрики: flask, python, python3, sqlalchemy, 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.

Генератор.

Для начала нужно выбрать первое слово для твита. Как говорилось раньше, это будет любое слово из модели 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
Всё заработало!

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

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

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

Автор: tenoclock

Источник

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


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