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

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

Всё заработало!
В данный момент я подгрузил все субтитры для сериала “Друзья” и “Теория большого взрыва”. Степень марковской цепи пока что выбрал равной двум(при увеличении базы субтитров степень будет увеличиваться). Как это работает можно посмотреть в Твиттере [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/
Нажмите здесь для печати.