Python
VK API
May 2012 16

Пишем модуль для авторизации в VK API

From Sandbox
На днях возникла необходимость сохранить все фотографии из своего фотоальбома ВКонтакте на жесткий диск. Естественно, вариант, с сохранением фотографий по одной, меня не устроил. Тут вспомнилось, что у ВКонтакте есть API. Пять минут чтения мануалов, и все нужные функции найдены. Единственная проблема – не существует нормального способа, для получения доступа к API. В документации сказано следующее:
Процесс авторизации приложения состоит из 3-х шагов:
  1. Открытие окна браузера для аутентификации пользователя на сайте ВКонтакте.
  2. Разрешение пользователем доступа к своим данным.
  3. Передача в приложение ключа access_token для доступа к API.


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

Для достижения наших целей, будем использовать только стандартные модули Python:
  1. urllib2
  2. cookielib
  3. HTMLParser

Создаем opener

Для того, чтобы прикинутся настоящим браузером, просто загружать нужные страницы недостаточно. Как минимум, нужно корректно обрабатывать cookies и редиректы. Для этого создаем opener, который будет делать всю работу за нас:
opener = urllib2.build_opener(
        urllib2.HTTPCookieProcessor(cookielib.CookieJar()),
        urllib2.HTTPRedirectHandler())

Обращаемся к странице авторизации

response = opener.open(
        "http://oauth.vk.com/oauth/authorize?" + \
        "redirect_uri=http://oauth.vk.com/blank.html&response_type=token&" + \
        "client_id=%s&scope=%s&display=wap" % (client_id, ",".join(scope))
        )

Подробно про параметры обращения можно прочитать в документации. Здесь следует отметить, что мы будем использовать параметр display со значением wap, т.к. в этом варианте странички практически отсутствует javascript. scope представляет собой список названий прав, к которым мы хотим получить доступ.

Парсим ответ

Посмотрим на код странички авторизации. Больше всего нас будет интересовать следующий участок:
<form method="POST" action="https://login.vk.com/?act=login&soft=1&utf8=1">
<input type="hidden" name="q" value="1">
<input type="hidden" name="from_host" value="oauth.vk.com">
<input type="hidden" name="from_protocol" value="http">
<input type="hidden" name="ip_h" value="df5a3639f3cb32ecc1" />
<input type="hidden" name="to" value="aHR0cDovL29hdXRoLnZrLmNvbS9vYXV0aC9hdXRob3JpemU/Y2xpZW50X2lkPTI5NTE4NTcmcmVkaXJlY3RfdXJpPWJsYW5rLmh0bWwmcmVzcG9uc2VfdHlwZT10b2tlbiZzY29wZT00JnN0YXRlPSZkaXNwbGF5PXdhcA--">
<span class="label">Телефон или e-mail:</span><br />
<input type="text" name="email"><br />
<span class="label">Пароль:</span><br />
<input type="password" name="pass">
<div style="padding: 8px 0px 5px 0px">
<div class="button_yes">
  <input type="submit" value="Войти" />
</div>
<a class="button_no" href="https://oauth.vk.com/grant_access?hash=95a8fc64a19d011436&client_id=2951857&settings=4&redirect_uri=blank.html&cancel=1&state=&token_type=0">
  <div>
  Отмена
  </div>
</a>
</form>

Для последующей отправки формы необходимо распарсить все input-ы (ы том числе и hidden), а также url, на который форма сабмитится. Пишем простенький парсер на основе HTMLParser:
class FormParser(HTMLParser):
    def __init__(self):
        HTMLParser.__init__(self)
        self.url = None
        self.params = {}
        self.in_form = False
        self.form_parsed = False
        self.method = "GET"

    def handle_starttag(self, tag, attrs):
        tag = tag.lower()
        if tag == "form":
            if self.form_parsed:
                raise RuntimeError("Second form on page")
            if self.in_form:
                raise RuntimeError("Already in form")
            self.in_form = True 
        if not self.in_form:
            return
        attrs = dict((name.lower(), value) for name, value in attrs)
        if tag == "form":
            self.url = attrs["action"] 
            if "method" in attrs:
                self.method = attrs["method"]
        elif tag == "input" and "type" in attrs and "name" in attrs:
            if attrs["type"] in ["hidden", "text", "password"]:
                self.params[attrs["name"]] = attrs["value"] if "value" in attrs else ""

    def handle_endtag(self, tag):
        tag = tag.lower()
        if tag == "form":
            if not self.in_form:
                raise RuntimeError("Unexpected end of <form>")
            self.in_form = False
            self.form_parsed = True

Авторизируемся

Подставляем в параметры запроса email и пароль пользователя и отправляем форму:
parser.params["email"] = email
parser.params["pass"] = password
response = opener.open(parser.url, urllib.urlencode(parser.params))

Разрешаем доступ

Следующим этапом, если пользователь этого еще не делал, нам надо дать приложению те права, которые мы запрашивали в параметре scope. Для этого нам будет предложена страничка с формой, на которой будут кнопки Allow и Deny. Парсим и отправляем форму методом, описаным выше.

Получаем token и user_id

Если мы все сделали правильно, то нас в итоге перекинет на страничку с url вида
http://oauth.vk.com/blank.html#access_token= 533bacf01e11f55b536a565b57531ad114461ae8736d6506a3&expires_in=86400&user_id=8492

откуда несложными манипуляциями мы можем достать нужные нам access_token и user_id.

Пример использования модуля

Собственно, ради чего все и затевалось – скрипт для загрузки альбома:
import vk_auth
import json
import urllib2
from urllib import urlencode
import json
import os
import os.path
import getpass
import sys

def call_api(method, params, token):
    if isinstance(params, list):
        params_list = [kv for kv in params]
    elif isinstance(params, dict):
        params_list = params.items()
    else:
        params_list = [params]
    params_list.append(("access_token", token))
    url = "https://api.vk.com/method/%s?%s" % (method, urlencode(params_list)) 
    return json.loads(urllib2.urlopen(url).read())["response"]

def get_albums(user_id, token):
    return call_api("photos.getAlbums", ("uid", user_id), token)

def get_photos_urls(user_id, album_id, token):
    photos_list = call_api("photos.get", [("uid", user_id), ("aid", album_id)], token)
    result = []
    for photo in photos_list:
        #Choose photo with largest resolution
        if "src_xxbig" in photo:
            url = photo["src_xxbig"]
        elif "src_xbig" in photo:
            url = photo["src_xbig"]
        else:
            url = photo["src_big"]
        result.append(url)
    return result

def save_photos(urls, directory):
    if not os.path.exists(directory):
        os.mkdir(directory)
    names_pattern = "%%0%dd.jpg" % len(str(len(urls)))
    for num, url in enumerate(urls):
        filename = os.path.join(directory, names_pattern % (num + 1))
        print "Downloading %s" % filename
        open(filename, "w").write(urllib2.urlopen(url).read())

if len(sys.argv) != 2:
   print "Usage: %s destination" % sys.argv[0]
   sys.exit(1)

directory = sys.argv[1]
email = raw_input("Email: ")
password = getpass.getpass()
token, user_id = vk_auth.auth(email, password, "2951857", "photos")
albums = get_albums(user_id, token)
print "\n".join("%d. %s" % (num + 1, album["title"]) for num, album in enumerate(albums))
choise = -1
while choise not in xrange(len(albums)):
    choise = int(raw_input("Choose album number: ")) - 1
photos_urls = get_photos_urls(user_id, albums[choise]["aid"], token)
save_photos(photos_urls, directory)

и как оно выглядит в работе:


Заключение

Конечно этот модуль не подходит для использования в серьезных проектах, но для личных целей – вполне.
Ссылка на GitHub.
+26
86.1k 323
Comments 24
Top of the day