Пишем twitter-бота на python

python_twitter.png Захотелось мне видеть в своей твитер-ленте анонсы погоды, фильмов в своем городе. Очень удобно в одном месте наблюдать за твитами людей, на которых подписан, и разные мелкие анонсы. Так и родилась идея написать твитер-бота информирующего о погоде в Виннице (Украина). Потом решил написать еще одного бота, а потом еще одного, на данный момент в армии твитер-ботов числится три курсанта :).

При написании использовал: python 2.6.4, python-twitter, elementtree, BeautifulSoup, python-bitly.

И так приступим-с.

UPDATE

01.09.2010 поменялся способ авторизации для приложений, теперь используется oauth вместо basic auth. В связи с этим все приложения которые авторизовались через basic auth перестали работать.

Подробнее про спецификацию oauth можно почитать тут.

Список используемых компонентов немного видоизменился, вместо python-twitter будем использовать oauth-python-twitter2, причем берем последнюю версию из svn, версия что отдается архивом - не работает в связи с небольшими изменениями в еще одном нужном нам компоненте - python-oauth2.

Вытягиваем последнюю версию oauth-python-twitter2:

svn checkout http://oauth-python-twitter2.googlecode.com/svn/trunk/ oauth-python-twitter2

и делаем файл oauthtwitter.py видимым через PYTHONPATH.

Ставим oauth2

pip install oauth2

Для работы с Twitter API нам понадобится четыре параметра, которые будут отвечать за oauth авторизацию: CONSUMER_KEY, CONSUMER_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET.

Значения двух первых нам даст twitter после того, как мы зарегистрируем наше приложение, для этого:

  1. Заходим сюда и выбираем Register a new application
  2. Заполняем все поля на свое усмотрение, для Application Type выбираем Client
  3. Для Default Access type выбираем Read & Write, Use Twitter for login я оставил не выбранным
  4. Вводим капчу и сабмитим

После этого twitter сгенерит CONSUMER_KEY, CONSUMER_SECRET.

Следующие два параметра нам вернет такой скрипт

from oauthtwitter import OAuthApi
import pprint

CONSUMER_KEY = "*****"
CONSUMER_SECRET = "*****"

twitter = OAuthApi(consumer_key, consumer_secret)
temp_credentials = twitter.getRequestToken()
print(twitter.getAuthorizationURL(temp_credentials))
oauth_verifier = raw_input('What is the PIN? ')
access_token = twitter.getAccessToken(temp_credentials, oauth_verifier)

print("oauth_token: " + access_token['oauth_token'])
print("oauth_token_secret: " + access_token['oauth_token_secret'])

В CONSUMER_KEY, CONSUMER_SECRET вводим значения полученные на предыдущем этапе, запускаем. Скрипт сгенерит url, переходим по нему и копируем PIN который нам вежливо предлагает twitter. Вставляем PIN в скрипт, он как раз его ждет.

На выходе мы получим значения для двух последних параметров OAUTH_TOKEN и OAUTH_TOKEN_SECRET.

Все, теперь у нас есть все что надо для oauth авторизации, осталось немного подправить скрипты.

Twitter-бот показывающий погоду в г. Винница

Sunny_bigger.png Реализация весьма прямолинейна:

  1. Создаем объект который будет работать с Twitter API;
  2. Получаем xml с погодой на сутки и распарсиваем с помощью ElementTree. Большинство данных в xml о погоде хранится в виде чисел, их перевожу в удобный вид для беглого просмотра;
  3. Подготовленные данные постим в тивтер;

Есть одна особенность при твите: иногда Twitter-сервер отклоняет твиты по причине своей перегрузки, потому блок с отправкой твита на сервер заключен в try ... catch, иначе скрипт валится. Ветку catch поленился реализовывать, если сервер перегружен и твит был отвергнут ... такова его судьба. По моим наблюдениям за все время существования бота, около 1,5 месяца, такое случалось только один раз. После каждого твита я задал задержку на 3 секунды, на случай если Twitter-сервер перегружен.

Больше особо описывать нечего, остальные подсказки есть в комментариях по коду.

# -*- coding: utf-8 -*-

# подключаем нужные библиотеки 
import urllib, datetime, time
from elementtree.ElementTree import parse
from oauthtwitter import OAuthApi

# инициализируем переменные

CONSUMER_KEY = '*****'
CONSUMER_SECRET = '*****'
OAUTH_TOKEN = '*****'
OAUTH_TOKEN_SECRET = '*****'

url = 'http://informer.gismeteo.ru/xml/33562_1.xml'
current_day = datetime.datetime.now()
forecasts = []

twitter = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET)

# загружаем xml с погодой
rss = parse(urllib.urlopen(URL)).getroot()

# парсим xml
for element in rss.findall('REPORT/TOWN/FORECAST'):

    # усредняем скорость ветра
    wind = (int(element.find('WIND').get('max')) + int(element.find('WIND').get('min')))/2

    # формируем массив, в котором лежат данные о погоде
    forecasts.append({
        'day': int(element.get('day')),
        'tod': int(element.get('tod')),
        'cloudiness': int(element.find('PHENOMENA').get('cloudiness')),
        'precipitation': int(element.find('PHENOMENA').get('precipitation')),
        'temp_min': element.find('TEMPERATURE').get('min'),
        'temp_max': element.find('TEMPERATURE').get('max'),
        'wind': wind,
        'pressure': "%s - %s" % (element.find('PRESSURE').get('min'), element.find('PRESSURE').get('max')),
    })

# приводим числовые показатели в удобочитаемый вид и постим в твитер

for forecast in forecasts[::-1]:
    day = u'сегодня' if forecast['day'] == current_day.day else u'завтра'

    if forecast['tod'] == 0:
        tod = u'ночью'
    elif forecast['tod'] == 1:
        tod = u'утром'
    elif forecast['tod'] == 2:
        tod = u'днем'
    elif forecast['tod'] == 3:
        tod = u'вечером'
    else:
        tod = '-'


    if forecast['cloudiness'] == 0:
        cloudiness = u'ясно'
    elif forecast['cloudiness'] == 1:
        cloudiness = u'малооблачно'
    elif forecast['cloudiness'] == 2:
        cloudiness = u'облачно'
    elif forecast['cloudiness'] == 3:
        cloudiness = u'пасмурно'
    else:
        cloudiness = forecast['cloudiness']


    if forecast['precipitation'] == 4:
        precipitation = u'дождь'
    elif forecast['precipitation'] == 5:
        precipitation = u'ливень'
    elif forecast['precipitation'] == 6 or forecast['precipitation'] == 7:
        precipitation = u'снег'
    elif forecast['precipitation'] == 8:
        precipitation = u'гроза'
    elif forecast['precipitation'] == 9:
        precipitation = u'нет данных'
    elif forecast['precipitation'] == 10:
        precipitation = u'без осадков'
    else:
        precipitation = forecast['precipitation']

    # формируем строку для твита в кодировке utf-8

    twit = u"%s %s: темп. %s˚С (мин), %s˚C (макс); облачность - %s; осадки - %s; ветер %s м/с; 
атм. давление %s мм.рт.ст." % (day, tod, forecast['temp_min'], forecast['temp_max'], cloudiness,
precipitation, forecast['wind'], forecast['pressure'])
    twit = twit.encode('utf-8')

    try:
        twitter.UpdateStatus(twit)
    except ValueError:
        pass

    # делаем трех-секундный перерыв между твитами

    time.sleep(3)

Twitter-бот показывающий анонсы фильмов в кинотеатрах г. Винница

film_bigger Код получился меньше чем у предыдущего бота, все почти так же само: инициализируем объект работающий с Twitter API; создаем функцию которая принимает url с страницей анонсов фильмов и название кинотеатра, для серфинга по html-странице использую BeautifulSoup. После того как все распарсено - твитем.

# -*- coding: utf-8 -*-

# подключаем нужные библиотеки 
from BeautifulSoup import BeautifulSoup
from oauthtwitter import OAuthApi
import urllib2

# инициализируем переменные

CONSUMER_KEY = '*****'
CONSUMER_SECRET = '*****'
OAUTH_TOKEN = '*****'
OAUTH_TOKEN_SECRET = '*****'

twitter = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET)

def movies(url, cinema_name):
    """ Функция принимает url с анонсами и имя кинотатра, парсит html-код, постит анонс в твитер """
    response = urllib2.urlopen(url)
    html = response.read().decode('cp1251').encode('utf-8')
    soup = BeautifulSoup(html)

    for tag_td in soup.find("div", id="af").findAll('td')[2:]:
        tag_a = tag_td.find('a')
        if tag_a:
            film_name = tag_a.string
        else:
            txt = u'%s: %s, время сеансов: %s' % (cinema_name, film_name.strip(), tag_td.string)
            txt = txt.encode('utf-8')
            try:
                twitter.UpdateStatus(txt)
            except ValueError:
                pass

url = 'http://kino.ukr.net/cinema/vinnica/kotsyubinskogo/'
movies(url, u'Коцюбинского')

url = 'http://kino.ukr.net/cinema/vinnica/mir-/'
movies(url, u'Мир')

url = 'http://kino.ukr.net/cinema/vinnica/rodina/'
movies(url, u'Родина')

Twitter-бот показывающий праздники и события на сегодня/завтра

cookie_bigger.png Операции с формированием твитов очень похожи на предыдущие: запрашиваем xml-файл, парсим, твитем. Но есть одно исключение: в xml есть ссылки на страницу с подробным описанием праздника/события, а так как у нас есть ограничение в 140 символов то укорачиваем ссылки с помощью bit.ly. API key берем тут

# -*- coding: utf-8 -*-

# подключаем нужные библиотеки 
import urllib, datetime, time
from BeautifulSoup import BeautifulSoup
from oauthtwitter import OAuthApi
import bitly

# инициализируем переменные

CONSUMER_KEY = '*****'
CONSUMER_SECRET = '*****'
OAUTH_TOKEN = '*****'
OAUTH_TOKEN_SECRET = '*****'

url = 'http://www.calend.ru/img/export/today-holidays.rss'
twitter = OAuthApi(CONSUMER_KEY, CONSUMER_SECRET, OAUTH_TOKEN, OAUTH_TOKEN_SECRET)
bitly_api = bitly.Api(login='proft', apikey='*********') 
current_day = datetime.datetime.now()
events = []

# запрашиваем xml-файл
rss = BeautifulSoup(urllib.urlopen(url))

# перебираем все item
for item in rss.findAll('item')[::-1]:
    title_raw = item.title.string
    date_end = title_raw.find(' - ')

    if date_end != -1:
        item_day = int(title_raw[0:title_raw.find(' ')])
        title = title_raw.string[date_end+3:]
        day_word = u'сегодня' if item_day == current_day.day else u'завтра' 

        link_raw = item.guid.string
        # укорачиваем url с помощью bitly
        link = bitly_api.shorten(link_raw[0:link_raw.find('?')])

        twit = "%s %s %s" % (day_word, title, link)
        twit = twit.encode('utf-8')

        # твитем
        try:
            twitter.UpdateStatus(twit)
        except ValueError:
            pass

Дополнительное чтиво:

blog comments powered by Disqus