В данной статье речь пойдёт о небольшой программке, которая репостит твиты в статус во вконтакте.
Задача довольно простая и совершенно неоригинальная. Началось всё с того, что я прочитал статью на Хабре о том, как это решается на python'е и аналогичную статью про php. В интернетах вроде бы даже какие-то онлайн сервисы есть специально для этой задачи. Но тут весь цимус в том, чтобы решить эту несложную задачу самому, используя свои любимые инструменты. Собственно решение на php появилось позже и с такой же целью.
Ну и на чём же писал я? На haskell, natürlich!
Дальше подробно расскажу о том, как я всё сделал и как это повторить. Никаких особых знаний для понимания, пожалуй, не требуется.
Вступление
В реализации решения мне помогли те две статьи и статья про репостинг из rss в livejournal на хаскелле.
Сначала я хотел по-честному сделать работу с твиттером через twitter-api: потыкал соответствующую библиотечку из hackage, но она сходу не заработала и я её оставил — мне хотелось побыстрее получить результат и было лень копаться и разбираться, что я не так делаю. А поскольку твиттер транслируется по rss и чтение rss на haskell' е — уже решённая задача, я пошёл этим путём.
Тем более, что это более универсальное решение. Можно транслировать любой rss-канал во вконтакт. Можно даже сказать, что это не twitter2vkontakte, а rss2vkontakte.
Кроме того, я пользовался vkontakte-api, а не парсил страницу в поисках статуса, как мои предшественники. Думаю, это плюс.
Остальное — это literate-haskell-код. То есть не код с комментариями, а подробные комментарии с кусочками кода, которые являются обычными исходниками на haskell'е. Этот пост можно просто сохранить целиком в файл с расширением .lhs и скормить интерпритатору/компилятору. Всё должно нормально работать.
Весь рабочий код здесь выделен вот такими символами:
Необходимые приготовления
Предполагается, что компилятор Haskell и основной набор библиотек у Вас уже есть. Если нет, то это легко исправить — надо установить Haskell Platform. Это очень просто.
Теперь, чтобы установить дополнительные библиотеки, достаточно набрать в консоли:
Дальше небольшой список импортов с краткими пояснениями.
Пару раз я использовал регулярные выражения:
Один раз разрезал и склеивал список:
Для всех интернет-запросов пользовался библиотекой curl:
Читал и парсил rss-фиды:
И даже разок кодировал строку в юникод:
Дальше будет более содержательный код с более содержательными и, возможно, местами излишне подробными разъяснениями…
Twitter через rss
Первое, что нам понадобится — адрес нашей rss-ленты твитов. Его можно взять у себя на страничке в твиттере. Заведём для него отдельную константу:
Как забирать rss-фиды и парсить, я подсмотрел в статье про rss2lj. Но пользоваться этой библиотечкой я не стал. Там всё конечно хорошо сделано, но мне нужна одна простая функция, которая будет скачивать rss-фид, брать первый элемент и извлекать его содержание. И вот как я её сделал:
Поясню, что в ней происходит. Функция
Следующую строку надо читать справа налево: извлекаем элементы фида (
А теперь поподробнее об этих функциях, в таком же порядке. Каждая из них написана в point-free-style, то есть без указания аргумента, просто как композиция (точка .) Других функций.
Композицию тоже можно читать справа налево, по точкам (: то есть в порядке применения фукнций: в
После работы
Эта функция имеет схожую с
Форматирование (
Вот и всё с rss. Я, возможно, описал всё излишне подробно, но думаю, для любопытствующих, незнакомых с haskell'ем, это описание было содержательным.
Vkontakte api
Первым делом заведём несколько констант для работы с вконтактом:
Это данные, соответствующие вашей регистрации во вконтакте.
Все операции осуществляются GET запросами на сервер (всё та же функция
базовый адрес (например userapi.com/data?) плюс список параметров в форме ключ=значение, разделённых амперсандами &.
Чтобы формировать такие адреса, напишем пару вспомогательных функций:
Эта функция просто берёт пару (ключ, значение) и делает из неё строку нужного формата.
Формируем url нужного формата из базового адреса
Содержательная часть находится в скобках:
Для разных задач набор опций отличается, но во всех случаях нужно указывать идентификатор пользователя (
Чтобы как-то работать с вконтактом, нужно сначала авторизоваться. Тогда сервер даст нам печеньки (cookies) и идентификатор сессии (sid = session id). Печеньками я пользоваться не стал, а вот sid нужен практически для любой операции с получением/изменением данных пользователя.
Адрес для аутентификации имеет кучу опций, назначение которых я не понял, но взял из документации и без них ничего не работает. Формируем этот адрес спомощью только что написанной функции
Что в ней происходит: посылается curl-запрос, по адресу
На регулярных выражениях в haskell'е я останавливаться не буду — это отдельная тема. Можно считать, что это просто поиск подстроки нужного вида.
Замечательно! sid мы получили, теперь перед нами открыты все возможности api вконтакта. Для нашей задачи нужна только одна — изменение статуса.
В принципе любое взаимодествие с вконтактом будет свобиться к следующей команде:
где
Обратите внимание на то, что третий параметр функции
Ещё одна мелочь: в тексте твита будут пробелы, а это недопустимо для url-запроса. Поэтому мы сделаем простенькую функцию, заменяющую все пробелы, на %20:
Она разбивает строку на список слов, вставляет между соседними элементами этого списка строку "%20", а потом склеивает всё снова в одну строку (последние два действия делает функция
Теперь мы можем собрать из уже обсуждённых частей, функцию изменения статуса:
Можно было бы написать эту функцию и проще, в одну строку:
Но первый вариант нагляднее, там делается проверка ответа сервера — если ответ содержит
Всё! Теперь у нас есть все части мозаики и собрать её очень просто.
main
То, ради чего было были написаны все эти функции:
Выглядит до крайности просто, не правда ли? Тут уж комментарии излишни.
Думаю, что и все остальные функции выглядят достаточно понятно с моими пояснениями.
Статистики ради: ~40 LinesOfCode.
Заключение
Чтобы запустить этот код, надо как уже говорилось, просто сохранить весь пост в файл с расширением .lhs и набрать в консоли:
Вот и всё.
Не знаю, нужно ли продолжение, рассказывающее о том, как автоматизировать этот запуск.
Лично для себя я (как пользователь Mac OS X) решил это созданием «Службы» в Automator и назначением горячей клавиши, для её быстрого вызова — это только автоматизация запуска, но для меня этого достаточно.
Надеюсь, это было кому-нибудь интересно читать. Жду Ваши вопросы/предложения/возражения (:
upd: переместил в тематический блог.
Задача довольно простая и совершенно неоригинальная. Началось всё с того, что я прочитал статью на Хабре о том, как это решается на python'е и аналогичную статью про php. В интернетах вроде бы даже какие-то онлайн сервисы есть специально для этой задачи. Но тут весь цимус в том, чтобы решить эту несложную задачу самому, используя свои любимые инструменты. Собственно решение на php появилось позже и с такой же целью.
Ну и на чём же писал я? На haskell, natürlich!
Дальше подробно расскажу о том, как я всё сделал и как это повторить. Никаких особых знаний для понимания, пожалуй, не требуется.
Вступление
В реализации решения мне помогли те две статьи и статья про репостинг из rss в livejournal на хаскелле.
Сначала я хотел по-честному сделать работу с твиттером через twitter-api: потыкал соответствующую библиотечку из hackage, но она сходу не заработала и я её оставил — мне хотелось побыстрее получить результат и было лень копаться и разбираться, что я не так делаю. А поскольку твиттер транслируется по rss и чтение rss на haskell' е — уже решённая задача, я пошёл этим путём.
Тем более, что это более универсальное решение. Можно транслировать любой rss-канал во вконтакт. Можно даже сказать, что это не twitter2vkontakte, а rss2vkontakte.
Кроме того, я пользовался vkontakte-api, а не парсил страницу в поисках статуса, как мои предшественники. Думаю, это плюс.
Остальное — это literate-haskell-код. То есть не код с комментариями, а подробные комментарии с кусочками кода, которые являются обычными исходниками на haskell'е. Этот пост можно просто сохранить целиком в файл с расширением .lhs и скормить интерпритатору/компилятору. Всё должно нормально работать.
Весь рабочий код здесь выделен вот такими символами:
>
Необходимые приготовления
Предполагается, что компилятор Haskell и основной набор библиотек у Вас уже есть. Если нет, то это легко исправить — надо установить Haskell Platform. Это очень просто.
Теперь, чтобы установить дополнительные библиотеки, достаточно набрать в консоли:
cabal update
cabal install regex-tdfa curl feed utf8-string
Дальше небольшой список импортов с краткими пояснениями.
Пару раз я использовал регулярные выражения:
> import Text.Regex.TDFA ((=~))
Один раз разрезал и склеивал список:
> import Data.List (intercalate)
Для всех интернет-запросов пользовался библиотекой curl:
> import Network.Curl (curlGetString)
> import Network.Curl.Opts
Читал и парсил rss-фиды:
> import Text.Feed.Import (parseFeedString)
> import Text.Feed.Query (getFeedItems, getItemSummary)
И даже разок кодировал строку в юникод:
> import Codec.Binary.UTF8.String (encodeString)
Дальше будет более содержательный код с более содержательными и, возможно, местами излишне подробными разъяснениями…
Twitter через rss
Первое, что нам понадобится — адрес нашей rss-ленты твитов. Его можно взять у себя на страничке в твиттере. Заведём для него отдельную константу:
> feedUrl = "https://twitter.com/statuses/user_timeline/22251772.rss"
Как забирать rss-фиды и парсить, я подсмотрел в статье про rss2lj. Но пользоваться этой библиотечкой я не стал. Там всё конечно хорошо сделано, но мне нужна одна простая функция, которая будет скачивать rss-фид, брать первый элемент и извлекать его содержание. И вот как я её сделал:
> getTweet :: IO String
> getTweet = do
> (_,feed) <- curlGetString feedUrl []
> return $ getMsg $ head $ getItems feed
> where
> getItems = maybe (error "rss parsing failed!") getFeedItems . parseFeedString
> getMsg = maybe (error "rss-item parsing failed!") format . getItemSummary
> format = unwords . ("twitter:":) . tail . words . encodeString
Поясню, что в ней происходит. Функция
curlGetString :: URLString -> [CurlOption] -> IO (CurlCode, String)
берёт url-адрес, список опций, и выдаёт код операции (CurlOk
, если всё прошло успешно) и ответ сервера. В данном случае, в качестве адреса мы указываем нашу twitter-rss ленту, и не даём никаких опций. На код завершения не обращаем внимания. А вот содержательную часть ответа обзываем feed.Следующую строку надо читать справа налево: извлекаем элементы фида (
getItems feed
), получаем список, берём из него первый элемент (head
), извлекаем из него собственно сообщение (getMsg
) и возвращаем на выход.А теперь поподробнее об этих функциях, в таком же порядке. Каждая из них написана в point-free-style, то есть без указания аргумента, просто как композиция (точка .) Других функций.
Композицию тоже можно читать справа налево, по точкам (: то есть в порядке применения фукнций: в
getItems
сначала применяется функция parseFeedString
(из библиотеки Feed), она имеет тип (String -> Maybe Feed
), то есть на вход получает строку со всякой кашей из rss-тегов, а выдаёт абстрактный тип фида, с которым уже можно что-то делать. Поскольку возвращается значение Maybe Feed
(«Может быть фид»), может статься, что парсер подавится, и вернёт Nothing
— тогда мы выдаём ошибку с текстом «rss parsing failed!». Если же парсинг пройдёт удачно, мы получим значение (Just фид
), и тогда применим к нему функцию getFeedItems
, которая извлекает из фида элементы в виде списка. Это ветвление (Nothing
или Just ...
) реализуется стандартной функцией maybe
.После работы
getItems
мы получим список элементов фида: [Item]
. Нам нужен только первый из них (то есть последний по дате). Берём его функцией head
. И теперь хотим выковырять из него текст сообщения: getMsg
.Эта функция имеет схожую с
getItems
структуру: сначала применяется getItemSummary
, которая возвращяет Maybe String
. Если извлечь содержание не удалось, выдаём соответствующую ошибку. Иначе, форматируем полученное сообщение.Форматирование (
format
) производится вкратце следующим образом (опять справа налево): кодируем строку в unicode, разбиваем на слова (по пробелам), выбрасываем первое слово, вставляем вместо него «twitter:» (по желанию), склеиваем обратно все слова в одну строку. Первое слово в rss-твитах — это всегда ваш ник. Поэтому мы его выкидываем.Вот и всё с rss. Я, возможно, описал всё излишне подробно, но думаю, для любопытствующих, незнакомых с haskell'ем, это описание было содержательным.
Vkontakte api
Первым делом заведём несколько констант для работы с вконтактом:
> email = "Ваш e-mail"
> uid = "Ваш user-id вконтакте"
> pass = "Ваш пароль"
Это данные, соответствующие вашей регистрации во вконтакте.
Все операции осуществляются GET запросами на сервер (всё та же функция
curlGetString
), с соответствующими хитрыми адресами. Строятся они следующим образом:базовый адрес (например userapi.com/data?) плюс список параметров в форме ключ=значение, разделённых амперсандами &.
Чтобы формировать такие адреса, напишем пару вспомогательных функций:
> param :: (String, String) -> String
> param (key, value) = key ++ "=" ++ value ++ "&"
Эта функция просто берёт пару (ключ, значение) и делает из неё строку нужного формата.
> formUrl :: String -> [(String, String)] -> String -> String
> formUrl base opts sid = base ++ ( concatMap param (opts++[("id",uid)]) ) ++ sid
Формируем url нужного формата из базового адреса
base
, списка опций opts
(ввиде пар), и идентификатора сессии sid
(о нём позже).Содержательная часть находится в скобках:
map
берёт функцию и список, и применяет функцию к каждому элементу списка. То есть из списка пар (ключ, значение)
, делает список строк "ключ=значение&"
. А concat
просто склеивает все эти строки в одну (concatMap = concat . map
).Для разных задач набор опций отличается, но во всех случаях нужно указывать идентификатор пользователя (
uid
), поэтому, чтобы не писать эту опцию каждый раз, мы добавляем её в определении этой функции.Чтобы как-то работать с вконтактом, нужно сначала авторизоваться. Тогда сервер даст нам печеньки (cookies) и идентификатор сессии (sid = session id). Печеньками я пользоваться не стал, а вот sid нужен практически для любой операции с получением/изменением данных пользователя.
> login :: IO String
> login = do
> (_,headers) <- curlGetString authUrl [CurlHeader True]
> return ( headers =~ "sid=[a-z0-9]*" :: String )
> where
> authUrl = formUrl "http://login.userapi.com/auth?"
> [("site","2"), ("fccode","0"),
> ("fcsid","0"), ("login","force"),
> ("email",email), ("pass",pass)] ""
Адрес для аутентификации имеет кучу опций, назначение которых я не понял, но взял из документации и без них ничего не работает. Формируем этот адрес спомощью только что написанной функции
formUrl
, при этом в последние две опции вставляются наш email и пароль. А параметр sid остётся пустым — у нас его пока нет, и собственно ради него мы и написали функцию login
.Что в ней происходит: посылается curl-запрос, по адресу
authUrl
, который возвращает заголовки headers
(для этого выставляется опция CurlHeader
). В них собственно печеньки, адрес перенаправления и что-то ещё. Вот в адресе, куда нас посылает сервер, и спрятано то, что мы ищем. С помощью секретной техники регулярных выражений, из headers
выдирается заветный session id, вида «sid=35dfe55b09b599c9fx622fcx8cd83a37».На регулярных выражениях в haskell'е я останавливаться не буду — это отдельная тема. Можно считать, что это просто поиск подстроки нужного вида.
Замечательно! sid мы получили, теперь перед нами открыты все возможности api вконтакта. Для нашей задачи нужна только одна — изменение статуса.
В принципе любое взаимодествие с вконтактом будет свобиться к следующей команде:
(_,answer) <- curlGetString someUrl []
где
someUrl
— соотвествующий запрос (смотреть в документации), а answer
— ответ сервера. Вот как выглядит запрос на изменение статуса: > setActivityUrl :: String -> String -> String
> setActivityUrl text = formUrl "http://userapi.com/data?" [("act", "set_activity"), ("text", text)]
Обратите внимание на то, что третий параметр функции
formUrl
— sid
, не указан. Это частичное применение — у функции 3 параметра, а мы дали только 2, значит получилась функция от оставшегося одного параметра. То есть setActivityUrl
— функция не только от параметра text
(собственно новый статус), но и от второго параметра sid
, который как бы дописывается справа.Ещё одна мелочь: в тексте твита будут пробелы, а это недопустимо для url-запроса. Поэтому мы сделаем простенькую функцию, заменяющую все пробелы, на %20:
> escSpaces = intercalate "%20" . words
Она разбивает строку на список слов, вставляет между соседними элементами этого списка строку "%20", а потом склеивает всё снова в одну строку (последние два действия делает функция
intercalate
).Теперь мы можем собрать из уже обсуждённых частей, функцию изменения статуса:
> setStatus :: String -> String -> IO ()
> setStatus text sid = do
> (_,answer) <- curlGetString url []
> if answer =~ "\"ok\":1" :: Bool
> then putStrLn text
> else error "something is bad with vkontakte-api..."
> where
> url = setActivityUrl (escSpaces text) sid
Можно было бы написать эту функцию и проще, в одну строку:
setStatus text sid = curlGetString (setActivityUrl (escSpaces text) sid) []
Но первый вариант нагляднее, там делается проверка ответа сервера — если ответ содержит
"ok":1
, то всё хорошо — статус сменился, о чём мы и сообщаем пользователю (себе то есть).Всё! Теперь у нас есть все части мозаики и собрать её очень просто.
main
То, ради чего было были написаны все эти функции:
> main = do
> tweet <- getTweet
> sid <- login
> setStatus tweet sid
Выглядит до крайности просто, не правда ли? Тут уж комментарии излишни.
Думаю, что и все остальные функции выглядят достаточно понятно с моими пояснениями.
Статистики ради: ~40 LinesOfCode.
Заключение
Чтобы запустить этот код, надо как уже говорилось, просто сохранить весь пост в файл с расширением .lhs и набрать в консоли:
runhaskell имя_файла.lhs
Вот и всё.
Не знаю, нужно ли продолжение, рассказывающее о том, как автоматизировать этот запуск.
Лично для себя я (как пользователь Mac OS X) решил это созданием «Службы» в Automator и назначением горячей клавиши, для её быстрого вызова — это только автоматизация запуска, но для меня этого достаточно.
Надеюсь, это было кому-нибудь интересно читать. Жду Ваши вопросы/предложения/возражения (:
upd: переместил в тематический блог.