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 в корені проекту, і, як можна здогадатися, воно запускається за крону. Кожні півгодини додається новий твіт в базу і Твіттер.

Все запрацювало!

висновок.
В даний момент я подгрузил всі субтитри для серіалу «Друзі» і «Теорія великого вибуху». Ступінь марківського ланцюга поки що вибрав дорівнює двом(при збільшенні бази субтитрів ступінь буде збільшуватися). Як це працює можна подивитися в Twitter, а всі вихідні коди доступні і лежать на гітхабі. Навмисно не викладаю посилання на сам сайт, не хочеться рекламуватися.

Всім велике спасибі за увагу. До нових зустрічей!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.