Python-разработчик
0,0
рейтинг
3 февраля 2015 в 19:40

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



Просматривал форумы в поисках вопросов, которые задают 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 в корне проекта, и, как можно догадаться, оно запускается по крону. Каждые полчаса добавляется новый твит в базу и Твиттер.

Всё заработало!

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


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

Всем большое спасибо за внимание. До новых встреч!
Роман Королев @tenoclock
карма
30,0
рейтинг 0,0
Python-разработчик
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (14)

  • 0
    наткнулся на один очень замечательный. Вольно его процитирую: ”Попросили написать генератор бреда на основе марковской цепи n-го порядка”

    А по-моему, он как раз вполне себе классический, поскольку практически в нужном виде присутствует в виде задания в тех самых гуглелекциях по питону, по которым добрая половина программистов-самоучек начинают знакомиться с этим языком.
    • 0
      А не подскажете, о каких лекциях идет речь?
  • +2
    Как-то на удивление осмысленные фразы получаются для сети второго порядка.
    PS: но обращаетесь вы с SQL, имхо, как-то очень «ненормализовано» и «фуллсканно».
    • 0
      Больше смысла достигается за счёт того, что знаки препинания входят в слово. Т.е. «Where» и «Where?» в данной реализации — это разные слова. Но ветвлений тем не менее достаточно, чтоб алгоритм не просто брал неизменённую цитату.
      А с SQL на самом деле здесь я никак не обращаюсь, за меня это делает ORM. Но если Вы имеете ввиду саму структуру, то мне очень бы хотелось посмотреть ваш альтернативный вариант.
      • +2
        1. Все слова держим в «таблице слов»
        2. Таблица «Цепей» — по сути два столбца left-id и right-id высотою в километр.
        * это нормализация *
        3. ORDER BY RAND — фулскан. Лучше создать где-то таблицу word-id,randomNumber(index) и брать из нее
        4. Query-all — это тоже не очень хорошо, лучше пару JOINов и немного магии.
        Пункт 2 можно изменить на большее колличество столбцов и/или переформатировать в деревья (или в циклические графы)
        Свои «генераторы рефератов», к счастью, я пару лет назад перенес в чулан, где они и сгнили. Но диван остался.
        • 0
          Я согласен с вами по всем пунктам, но такая структура необходима для действительно большой базы. В этом конкретном случае непосредственное заполнение базы с предложенной Вами структурой будет занимать значительно большее время, а выигрыш на выходе с маленькой базой текста будет минимален. Ну и волшебство с sql лучше делать мимо orm.
          Я хочу сказать, что временная сложность алгоритма, конечно, проигрывает вашему. Но на небольшом количестве данных проигрыш оправдан малой затратой времени на саму реализацию.
  • 0
    Думаю, что максимально забивать все 140 (с точностью до слова) символов твита не стоит — т.к. длинные твиты по большей части TL;DR; — посоветовал бы заменить 140 на случайное число из интервала [80;140].

    PS. Проверил на github предположение, что вы пишете без комментариев код — как так можно?
    • 0
      По поводу 140 символов — совсем короткие фразы чаще всего не будут отличаться от оригинальных. Но я допишу статистику, чтоб посмотреть, какой процент изменений происходит на скольких символах.
      А комментарии — это не промышленный проект, а задачка на вечер. Если вдруг он окажется интересным общественности, будет переписана структура наподобие предложенной выше, добавлены лексические паттерны и веб-мордочка для добавления субтитров со стороны(с премодерацией, разумеется:) ). Ну и не только комментарии, а и полноценная документация.
  • 0
    на всякий случай: в твиттере есть довольно большое количество ботов, работающих на этой платформе github.com/mispy/twitter_ebooks github.com/mispy/ebooks_example Поведением напомниают MegaHAL ( megahal.alioth.debian.org/ )
  • +2
    в Джаббере когда-то подобное было в моде, вот лишь пара примеров автосгенерированных фраз тех лет

    — участники форума скачаны
    — Продается памятник В.И.Ленину, материал-алюминий, размеры 4м*1м*1м, масса 1,2т тел.8-950-6470-*** Цена 10 секунд доступа Dial-Up услуги VoIP
    — Я самое удачное цветовое решение…
    — Я чуть более новый массив…
    — Я очень вроде ас по тюнингу запорожца
    — Сижу читаю документацию по идее может задосить один перерыв. Проект JK выпустили в файл всю музыка
    — что лишний трафик и тебе будет работать в Анаево 9 июля Любители скверных чисел — “Teo & Аригато?
    — Понг от психического состояния пациента.
    — Сплик, поющая в двойных кавычках ::))
    — * sheep знает секрета D, а не нужна как художник, хочет заняться сексом на stderr!
    — Все пойдет, как по руби
    — only one of Indonesia! We need a guy or streaming, friend?
    — SWIG на связи
    — а ассоциативные массивы упорядочиваются по ст. 58-7, 58-8, 58-11 УК РСФСР приговорён к нормальному
    — понг от молекулярных облаков до 1.0 включительно. Три точки доступа
    — я придумал одно число в чашке
    — понг от php-fpm.org открывает перспективы для освоения «фигур высшего профессионального образования, ведущее свою работу.
    — От msn-транспорта приходит отрицательное количество непрочтённых сообщений на id1, попингуй google
    — Аж интересно, трафик составляет 197953.78 руб. 11. Лесоповал — на форуме джсмарт были проведены экспериментальные работы
  • +8
    В 2004 году писал генератор рассказов. Разумеется, «Войны и мир» не получилось, но когда эксперимента ради в качестве первичной базы для анализа связей слов загрузил кучу порнорассказов — подивился результату. Фразочки вида «я ласкала его вкусные ухи» сильно радовали.
  • +1
    Теперь я наконец-то знаю, для чего нужен твиттер! *ушёл_писать_своего_бота*
  • +2
    Спасибо, как раз хотел поиграть с таким генератором — вчера RSS-лента вынесла ссылку на похожего бота, который делает твиты из
    • писем от спецов по найму программистов
    • отчетов о галлюциногенных трипах

    Получается местами очень весело
    Kyle, My name is Jen Burns and I wanted to just follow the birds

    Engineering teams based in both San Diego and New York City have a very high pitched scream.

    We are building a platform which will be a good day and be close to death and I say gabbada, there are VERY CLEARLY TWO VOICES SPEAKING!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.