Пользователь
0,0
рейтинг
1 декабря 2014 в 12:58

Разработка → Делаем дамп фотографий из диалога vk.com из песочницы

Всем привет!

Вчера мне понадобилось скачать все фотографии из диалога с одним человеком в vk.com. Фотографий было больше 1000 штук. Понятное дело, что ручками это все делать было бы утомительно и… Стыдно. Не для того программированием занимаюсь, чтобы такую грязную работу делать не автоматизированно. Поэтому было решено написать скрипт.

В качестве языка был выбран Python. Его удобно использовать для консоли, он довольно быстрый, есть модуль urllib, позволяющий «одним движением» скачивать картинки по ссылке. Но главная причина — это то, что я начал изучать его недавно. Решил дополнительно попрактиковаться.

Сам скрипт получился небольшой, но было бы интересно описать процесс создания. Буду стараться писать побольше комментариев в коде, чтобы те, кто не знает python, тоже смогли понять процесс. А от знатоков очень приветствуются советы и указания. Итак, приступим.

«Вконтакте» не предоставляет API конкретно для скачивания материалов из беседы, поэтому самое долгое время заняло изучение того, как устроена система подгрузки картинок из диалога в vk.com. Все картинки лежат у них, понятное дело, на сервере, и доступ к ним имеет любой, у кого есть ссылка на эту картинку. Таким образом, чтобы скачать все фотографии из диалога, нам надо получить все ссылки на картинки. Тыкаясь туда-сюда, было выяснено, что при нажатии на «Действия -> показать материалы из беседы» отправляется POST запрос на vk.com/wkview.php. Запрос содержит параметры:

  • act:show
  • al:1
  • loc:im
  • w:history<dialog_id>_photo

В этом запросе dialog_id — это значение параметра «sel» в адресной строке, когда мы заходим в диалог.
Выполнив такой запрос, мы получим в ответ что-то вроде вот этого:

16515<!>wkview.js,wkview.css,page.js,page.css,page_help.css<!>0<!>6590<!>0<!><!bool><!><div id="wk_history_wrap">
  <div class="wk_history_title tb_title" id="wk_history_title">Фотографии в переписке с ЮЗЕР_НЭЙМ</div>
  <div class="wk_history_tabs tb_tabs_wrap">
    <div class="tb_tabs clear_fix" id="wk_history_tabs"><div class="progress tb_prg fl_r" id="wk_history_tabs_prg"></div><div class="fl_l summary_tab_sel">
  <a class="summary_tab2"  onclick="showWiki({w: 'history<dialog_id>_photo'})" >
    <div class="summary_tab3">
      <nobr>Фотографии</nobr>
    </div>
  </a>
</div><div class="fl_l summary_tab">
  <a class="summary_tab2"  onclick="showWiki({w: 'history<dialog_id>_video'})" >
    <div class="summary_tab3">
      <nobr>Видеозаписи</nobr>
    </div>
  </a>
</div><div class="fl_l summary_tab">
  <a class="summary_tab2"  onclick="showWiki({w: 'history<dialog_id>_audio'})" >
    <div class="summary_tab3">
      <nobr>Аудиозаписи</nobr>
    </div>
  </a>
</div><div class="fl_l summary_tab">
  <a class="summary_tab2"  onclick="showWiki({w: 'history<dialog_id>_doc'})" >
    <div class="summary_tab3">
      <nobr>Документы</nobr>
    </div>
  </a>
</div></div>
    <div class="tb_tabs_sh" id="wk_history_tabs_sh"></div>
  </div>
  <div class="wall_module wide_wall_module" id="wk_history_wall">
    <div class="post_media" id="wk_history_rows"><div class="page_post_sized_thumbs clear_fix" style="width: 597px; height: 1722px;"><a  onclick="return showPhoto('...', 'mail...', {"temp":{"base":"/","x_":["",500,331]},queue: 1}, event);" style="width: 193px; height: 127px;" class="page_post_thumb_wrap  fl_l"><img src="" width="193" height="128" style="margin-top: 0px;"  class="page_post_thumb_sized_photo" /></a> ... (и еще много ссылок с картинками)</div></div>
  </div>
  <div id="wk_history_empty" style="">Список пуст.</div>
  <div id="wk_history_more" class="">
    <div id="wk_history_more_link" onclick="return WkView.historyShowMore();" style="">Показать еще</div>
    <div id="wk_history_more_progress" class="progress"></div>
  </div>
</div><!><!json>{"count":"23318","offset":3330,"type":"history","commonClass":"wk_history_content wk_history_photo_content","wkRaw":"history<dialog_id>_photo","canEdit":false,"lang":[]}<!>WkView.historyInit();<!><!pageview_candidate>

Здесь я заменил ссылки на <некая ссылка>, так как уже говорил, что картинки vk лежат в открытом доступе и получить их может любой, кто знает ссылку.

Из всего этого нам интересны только ссылки, которые находятся внутри <img src=" ">, а так же json на конце. Я был не до конца честен, говоря, что POST запрос принимает 4 параметра. Точнее, он принимает, но если его выполнить нам выдадутся только первые несколько фотографий. Так как vk.com имеет подгрузку контента по мере прокручивания страницы, то существует параметр offset, который отвечает за то, какую часть из всего множества фотографий нам подгрузить. В итоге параметры запроса выглядят вот так:

  • act:show
  • al:1
  • loc:im
  • w:history<dialog_id>_photo
  • offset: offset
  • part: 1

Из всех параметров меняться будет меняться только offset. Его мы вытаскивает из того самого json'a на конце ответа. Каждый раз при выполнении запроса offset внутри json'а будет увеличиваться, показывая, какое «смещение» надо сделать в следующий раз. Таким образом, нам надо будет делать запросы до тех пор, пока у нас offset будет меньше count.

Кстати, а что насчет выполнения запросов? Как нам получить доступ к своей странице? Было выяснено, что доступ к странице может получить тот, у кого есть cookie под названием remixsid. Таким образом нам надо подставить эту куку в функцию, которая выполняет запрос и все получится. Безопасно? Не совсем, швыряться куками — это не есть хорошо, но я не нашел другого варианта. Если кто-то знает, напишите пожалуйста.

Общий алгоритм вроде понятен: сделать запрос, вытащить ссылки, записать их в файл, проверить-
новый offset>count?-, если нет, то присвоить offset новое значение и выполнить запрос с ним, если да, то выйти из цикла. Затем пройтись по всем ссылкам в файле и скачать картинки лежащие по их адресу. Начинаем писать код.

# coding=utf-8
import requests # для выполнения запросов 
import re # для парсинга по регулярным выражениям
import sys # для обработки аргументов командной строки
import os # для создания папок с фотографиями
import urllib # для скачивания картинок
import json # для обработки json

# argv[1] = remixsid_cookie
# argv[2] = dialog_id
# argv[3] = person_name

Аргументы у нас будут передаваться через терминал (remixsid, dialog_id и название папки):

remixsid_cookie = sys.argv[1]
# Словарь запроса
RequestData = {
    "act": "show",
    "al": 1,
    "loc":"im",
    "w": "history" + sys.argv[2] + "_photo",
    "offset" : 0,
    "part" : 1
}

request_href = "http://vk.com/wkview.php"
# Установим первоначальные offset и count. Count изменится при первом запросе
bound = {"count" : 10000, "offset" : 0}

Создадим отдельную папку для фотографий:

try:
    os.mkdir("drop_" + sys.argv[3]) # Пытаемся создать папку
except OSError:
    print "Проблемы с созданием папки 'drop_" + sys.argv[3] + "'"
if( os.path.exists("drop_" + sys.argv[3]) ):
    os.chdir("drop_" + sys.argv[3]) # Переходим в эту папку
else:
    print "Не удалось создать папку\n"
    exit()

Отлично, начинаем выполнение запросов:

test = open("links", "w") 
while( bound['offset'] < bound['count'] ):

    RequestData['offset'] = bound['offset']

    content = requests.post(request_href, cookies={"remixsid": remixsid_cookie}, params=RequestData).text
    # Этой командой мы выполняем post запрос с параметрами params и передавая куки. .text возвращает ответ запроса в виде текста. Все просто.

Теперь начинаем парсинг ответа. Извлекаем все через регулярные выражения. Сначала извлекаем json и устанавливаем следующий offset:

    #ищем первое совпадение по регулярному выражению
    json_data_offset = re.compile('\{"count":.+?,"offset":.+?\}').search(content) 
    # .search возвращает специальный объект. У него есть метод span(), который возвращает кортеж с индексами начала и конца найденной подстроки
    bound = json.loads(content[json_data_offset.span()[0]:json_data_offset.span()[1]]) # декодируем json
    bound['count'] = int(bound['count']) #count отдается в виде строки
    bound['offset'] = int(bound['offset']) # на случай, если в будущем тоже будет отдаваться в виде строки. В принципе это написано ради "на всякий случай"

Теперь надо извлечь все ссылки из тегов src. Действуем тем же способом, но используем метод findall, который возвращает массив всех строк, которые совпали с регуляркой:

    links = re.compile('src="http://.+?"').findall(content)

Теперь запишем все в файл:

    for st in links:
        test.write(st[5:len(st)-1] + '\n') # пишем то, что внутри src="..."
test.close()

С этим все. Осталось только пройтись по файлу и скачать все по ссылкам. Это делается с помощью модуля urllib, вот так:

urllib.urlretrieve(ссылка, имя файла)

А для нашего случая:

test = open("links", "r")
file_num = 0
for href in test: # берем строку из файла которая является ссылкой, и так до конца файла
    urllib.urlretrieve(href, str(file_num)) # в качестве имени файла просто используем его порядковый номер
    file_num += 1
    print "Скачано " + str(file_num) + " файлов\n"
test.close()

Готово! Но, так как использовать это мы будем из командной строки, давайте еще напишем небольшую документацию (--help), а так же вывод об ошибке, если аргументов командной строки меньше, чем нужно. Добавим в начало:

if( sys.argv[1] == '--help' ):
    print """
    Usage: python main.py <remixsid_cookie> <dialog_id> <name_of_folder>
    <dialog_id> is a string parameter "sel" in address line which you see when open a dialog
    """
    exit()
else:
    if( len(sys.argv) < 4 ):
        print """
        Invalid number of arguments. Use parameter --help to know more
        """
        exit()

Вот и все, вроде. Конечно, можно еще многое добавить: проверку на выполнен запрос или нет, проверку на корректность входящих данных, автоматическое вытаскивание <dialog_id> (например, первых 10), но мне просто хотелось описать основные моменты. В итоге те самые 1000 фотографий, которые мне были нужны, были скачаны. Заняло это где-то 2 минуты. Никаких ограничений на запросы, как так понял, vk.com не ставит, хотя могу предположить, что на такой маленький для него трафик он даже не реагирует.

Весь рабочий код целиком лежит на Гитхабе.

Всем спасибо.
@shrikimaru
карма
6,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • +6
    А почему всё-таки не через API? Там есть получение сообщений из диалога, ну а в объекте сообщения к которому приложена картинка есть ссылка на неё.
    • +4
      Дык это нужно читать документацию дальше первого пункта! Это не всем дано :)
      • +1
        Ничего сложного в документации нет, можно было сделать все быстрее и консольно, и не нужно было городить велосипед.
  • 0
    Мне кажется вы немного всё усложнили.
    После открытия материалов диалога, скопировал ноду содержащую картинки
    Дальше, ползуясь sublime, произвел две замены:
    jpg на jpg\n
    и
    src на \nsrc
    после чего удалил все строки начинающиеся не с src.
    A затем удалил и сам src, получив список ссылок на картинки.

    Дальше ничего интересного:
    cat imgs | while read line
    do
    wget line
    done

    Кoнечно, все замены можно было бы сделать одной командой sed — но вспоминание особенностей синтаксиса заняло бы больше времени, чем эти 3 замены.

    Для знакомства с языком — почему бы и нет. В качестве единоразовой задачи — моё решение заняло 3 минуты. У вас исходники читаются дольшe.
    • +1
      Проблема вашего решения заключается в том, что для каждого диалога нужно ручками скроллить вниз список фотографий, а потом вставлять все в sublime, вручную запускать rex exp и.т.д. Тогда как решение shrikimaru при должных оптимизациях позволит скопировать все изображения из всех диалогов. К тому же для изучения питона (и я так понимаю еще и получение инвайта) статья очень и очень годная. Песочница-жи
      • 0
        Это не столько проблема, сколько критерий выбора данного решения.

        Если вам нужно каждый день доставать фотографии из 20 диалогов — конечно, лучше автоматизировать процесс полностью. Если же это задача выполняется хотя бы 1 раз в месяц — время, затраченное на эти «должные оптимизации» окупиться совсем не скоро.

        Конечно, обучения годятся любые задачи, тем более хорошо если они приносят пользу.
  • –1
    Мне кажется это будет дольше перебирать все сообщения (коих может быть овер 50000) и проверять у каждого есть ли там приложенный объект и извлекать в случае если есть. Гораздо быстрее обратиться «напрямую».
    • 0
      Я так понял, у вас тоже не за один раз достаются все изображения из диалога? Через api за один запрос к серверу (с помощью execute) можно получить 200*25=5000 сообщений, запросов можно делать до трёх в секунду — получается в идеале 15к сообщений в секунду. Время локальной обработки полученных сообщений можно вообще не учитывать :)

      Ещё конечно плюс будет в том, что так можно достать не только картинки, а любую информацию.
      • 0
        Да, не за один раз, но у меня не происходит лишних запросов, как например в случае, если вытаскивать ссылки через api (будет много лишнего мусора, в котором не факт что содержится ссылка на картинку), а все идет только по делу. Но я с вами соглашусь, что с такой скоростью отдачи сообщений через api этот вариант тоже пригоден.
        Что касается извлечения любой информации — да, вы правы. Но в данном случае мне нужны были только фотографии.
  • –1
    Ага-ага, сами себе попоболь придумали, сами ей и гордимся.
    Та-да!

    «Вконтакте» не предоставляет API конкретно для скачивания материалов из беседы
    Зато там есть вариант скачивания самой беседы, а уж получить ссылки на картинки оттуда даже самый рукожоп-пхпшник сможет.
  • 0
    Похоже, нужно учить Питон, а то тут вон какие интересности :).

    off-top
    Кстати. Слово «Всем» не нужно отбивать запятой, потому что это не обращение. «Всем» вообще не может быть обращением, потому что падеж неправильный — любое обращение стоит только в именительном, обращением может быть только «все». Обычно, кстати, не будет: «Все на борьбу с Деникиным» — без запятой, потому что это «Все [должны идти] на борьбу с Деникиным». «До свидания, все» — с запятой, потому что это обращение к группе «все». Общий признак — обращение можно выбросить из предложения, и предложение полностью сохранит смысл — исчезнет только адресация к конкретному человеку или группе.

    Так что проверка простая. Падеж именительный? Если да — можно ли выбросить? «Андрей, смотри бодрей» — можно. «Андрей опаздывает» — уже нельзя. Первое — обращение, второе — подлежащее :).

    А то очень режет глаз выделение запятыми любых местоимений :).
    on-top
    • 0
      Спасибо, поправил!

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