Пользователь
0,0
рейтинг
16 мая 2012 в 17:49

Разработка → Пишем модуль для авторизации в VK API из песочницы

На днях возникла необходимость сохранить все фотографии из своего фотоальбома ВКонтакте на жесткий диск. Естественно, вариант, с сохранением фотографий по одной, меня не устроил. Тут вспомнилось, что у ВКонтакте есть 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.
@dzhioev
карма
7,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (24)

  • 0
    Для этого есть аддоны для браузеров, например.
    Я не говорю, что зря писали скрипт, на «велосипедах» и учат программирование, по началу :)
    • +2
      И зачем мне на фряхе браузер? ;)
    • 0
      Инструмент, который автор предоставил, является общим для любых манипуляций с VK API. Найдите-ка аддоны для проверки леммы о 6 рукопожатиях, или для таймера обратного отсчета прямо у вас в статусе :)
      • 0
        Про таймер не понял.
        Про 6 рукопожатий — я писал скрипт и публиковал в паблике (правда не для вконтакте).
        • 0
          Про таймер: я, например, когда-то публиковал статусы вроде «25 дней до конце сессии» или «400 дней до начала Евро2012», но само собой рано или поздно забывал обновить. Написать простенький скрипт для такого достаточно легко, и инструмент для авторизации в этом поможет.

          Я вообще к тому, что ценность скрипта не только в скачивании фотографий, а и (возможно, даже больше) в самом процессе авторизации.
          • 0
            Скрипт для скачивания фотографий практически никакой ценности не несет, я его добавил только для демонстрации работы с модулем авторизации.
  • +1
    Несколько месяцев назад я модифицировал немного чужой класс для работы с API, тогда он вполне работал github.com/inlanger/pyvk, может кому пригодится. Подход немного отличается от предложенного автором этой статьи.
  • 0
    Интересно. Вчера велосипеда ради написал скриптик, что скачивает все треки музыкального альбома из VK по его названию (т.е. ищет по названию альбом на last.fm, получает список треков, и выкачивает их из VK). Но вот авторизация проходится вручную. Теперь возможно реализую ваш вариант.
  • +4
    Спасибо, что на Python!
  • 0
    > «Единственная проблема – не существует нормального способа, для получения доступа к API.»

    Это почему?
    На страничке vk.com/developers.php есть запрятанная кнопка «создать приложение» в верхнем правом углу. Сразу в глаза не бросается. Располагается чуть ниже кнопок меню «музыка» и «помощь».

    Прямая ссылка:
    vk.com/editapp?act=create

    Присутствует довольно внятная документация по созданию standalone приложений. Хотя, конечно, у твиттера документацию по oauth 2.0 читать приятнее.
    • 0
      Это само собой разумеется: надо создать приложение и использовать его client_id. Я просто на этом не акцентировал внимание. Но чтобы использовать API из standalone приложения, нужно авторизироваться и получить token. Единственный способ авторизироваться, описаный тут, как раз начинается с «Открытия окна браузера...».
      • 0
        Есть еще вот такой метод — Авторизация сервера приложения. С виду похоже на то, что нужно.
        • 0
          Хотя нет, в этом случае доступен только ограниченный функционал.

          В качестве [standalone] клиента может выступать любое Desktop/мобильное приложение, имеющее доступ к управлению Web-браузером (например, компонент UIWebView при создании приложения для iOS).

          Похоже что действительно без браузера не обойтись. Печально. В твиттере, например, такая возможность предусмотрена.
    • 0
      Документация-то есть, но ещё год назад без своего доменного имени приложение нельзя было делать, насколько помню.
  • 0
    находил на github'е навороченную, написанную на python

    З.Ы. Не спорю, свое милее.
    • 0
      А скиньте ссылку, если не сложно. Я не смог найти. Если бы нашел, то не стал велосипед изобретать.
  • 0
    А чем вам Mechanize не понравился? По редиректам ходит, куки сохраняет.
  • 0
    В вашем скрипте вы 2 раза импортируйте json?
    • 0
      Да, ошибка.
  • 0
    На гитхабе в исходнике ошибка (83 строка), ТС вместо parser.method ссылается на params.method. Эта ошибка в обработке ошибки, поэтому не срабатывала, поэтому ее не заметили. Будьте внимательны, кто намерен пользоваться. Но либа получилась годная, код хороший.
    • 0
      Спасибо, поправлю. В свое оправдание скажу, что эта ошибка не возникала никогда, и вряд ли когда-нибудь возникет, так что ничего страшного.
      • 0
        Ну я так и подумал, поэтому и решил обратить внимание. А то неприятно было бы словить редкий эксцепшн в теле эксцепшна))
      • 0
        Столкнулся с получением на выходе набора абстрактных картинок, решил заменой атрибута в последней строке функции save_photos на
        open(filename, "wb").write(urllib2.urlopen(url).read())
        

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.