Быстрый поиск по всем пользователям ВК

от автора

Решили на днях поучаствовать в одном Хакатоне, который мы, конечно же, не выиграем. И одной из задач было создание датасета из социальной сети Вконтакте. Долго не думая, я сказал товарищам по команде, что беру эту часть на себя. Но оказалось все не так просто. Хочу поделиться с Вами какие проблемы встретились и как я их по ходу решал.

Задача

Нужно пройтись по 650 000 000 пользователям ВК и вытащить только тех, кто живет в Москве. Затем отдельно обработать уже полученные айдишники.

Решение

Ну чтож задача понятна, нужно ее как-то решать. Писать код будем на языке Python и сразу подумал, что на своем компе это обрабатывать не стоит, а будем использовать мощности Google Colab. Ссылка на полный код проекта гугл колаб будет в конце.

Вк апи

Чтобы получить данные пользователя есть два путя:

  1. Парсить веб-страницу пользователя и вытаскивать нужную информацию

  2. Использовать vk api и обрабатывать json

Оба вариант имеют свои подводные камни. В первом варианте слишком много лишней информации, что замедляет в разы обработку. Во втором варианте проблема в токенах и ограничениях. Я все-таки решил пойти вторым путем.

Токены

Главная и первая проблема в ограничениях вк апи: 5 запросов в секунду для одного токена.

‘error’: {‘errorcode’: 6, ‘errormsg’: ‘Too many requests per second’}

Для того чтобы обойти это ограничение, нам понадобится много токенов. Есть три варианта как их получить:

  1. Ручками регистрировать новых пользователей и получать токен

  2. Купить токены

  3. Сгенерировать токены с помощью библиотеки vk (pip install vk)

В итоге я сгенерировал 1000 токенов, используя библиотеку. Не буду тут выкладывать код генерации токенов (это и так толстая подсказка), если все-таки не догадаетесь, то напишите в личку скину скрипт.

Сохраняем токены в txt файл, каждый токен с новой строчки.

Время говнокодить

Когда файл с токенами получен, можем приступать к коду. Загружаем в гугл колаб файл.

Считываем файл tokens.txt и добавляем токены в лист:

list_token=[] with open('tokens.txt', 'r') as f:     for line in f:         list_token.append(str(line).rstrip('\n')) len(list_token)

Сделать 650 000 000 запросов быстро без асинхронности мы никак не сможем. Я перепробовал много разных библиотек и максимальную скорость мне удалось выбить, используя библиотеку aiohttp.

Устанавливаем библиотеки для асинхронных запросов:

!pip install asyncio !pip install aiohttp !pip install nest_asyncio

А вот и сам сборщик:

import asyncio from aiohttp import ClientSession import json import nest_asyncio nest_asyncio.apply()  # Доступ к гугл диск from google.colab import drive drive.mount('/content/gdrive')   list_data=[]   async def bound_fetch_zero(sem,id,session):         async with sem:             await fetch_zero(id,session)     async def fetch_zero(id, session):     url = build_url(id)     try:         async with session.get(url) as response:                  # Считываем json                 resp=await response.text()                 js=json.loads(resp)                 list_users=[x for x in js['response'] if x != False]                  # Проверяем если город=1(Москва) тогда добавляем в лист                 for it in list_users:                     try:                         if it[0]['city']['id']==1:                                 list_data.append(it[0]['id'])                     except Exception:                         pass       except Exception as ex:         print(f'Error: {js}')   #  Генерация url к апи вк, 25 запросов в одном def build_url(id):     api = 'API.users.get({{\'user_ids\':{},\'fields\':\'city\'}})'.format(         id * 25 + 1)     for i in range(2, 26):         api += ',API.users.get({{\'user_ids\':{},\'fields\':\'city\'}})'.format(             id * 25 + i)     url = 'https://api.vk.com/method/execute?access_token={}&v=5.101&code=return%20[{}];'.format(         list_token[id%len(list_token)], api)     return url     async def run_zero(id):     tasks = []     sem = asyncio.Semaphore(1000)       async with ClientSession() as session:  				       	#  Значение 3200 зависит от вашего числа токенов          for id in range((id - 1) * 3200, id * 3200):             task = asyncio.ensure_future(bound_fetch_zero(sem,id, session))             tasks.append(task)           responses = asyncio.gather(*tasks)         await responses         del responses         await session.close()   # Запускаем  сборщик for i in range(0,17):   for id in range(i*500+1,(i+1)*500+1):       print(id)       loop = asyncio.new_event_loop()       asyncio.set_event_loop(loop)       loop.run_until_complete(run_zero(id))          # Сохраняем айдишники в файл на гугл диске и очищаем лист   with open(f'/content/gdrive/My Drive/data_main{i}.txt', 'w') as f:             for item in list_data:               f.write(f'{item}\n')    print(len(list_data))   list_data.clear()

Разберем по блокам, чтобы было понятнее что тут происходит.

Начнем с метода def build_url(id)

У вк апи есть фича execute, которая позволяет делать 25 запросов в одном

Execute — универсальный метод, который позволяет запускать последовательность других методов, сохраняя и фильтруя промежуточные результаты

#  Генерация url к апи вк, 25 запросов в одном def build_url(id):     api = 'API.users.get({{\'user_ids\':{},\'fields\':\'city\'}})'.format(         id * 25 + 1)     for i in range(2, 26):         api += ',API.users.get({{\'user_ids\':{},\'fields\':\'city\'}})'.format(             id * 25 + i)     url = 'https://api.vk.com/method/execute?access_token={}&v=5.101&code=return%20[{}];'.format(         list_token[id%len(list_token)], api)     return url

Вот так выглядит итоговый запрос:

https://api.vk.com/method/execute?access_token=6d62b2347f55e3591d99f7be9b78cf3ec2a4eda491cfe4aad0e59ffb9afa4e0378a48f04e156b88bdc1fd&v=5.101&code=return%20[API.users.get({‘user_ids’:1,’fields’:’city’}),API.users.get({‘user_ids’:2,’fields’:’city’}),API.users.get({‘user_ids’:3,’fields’:’city’}),API.users.get({‘user_ids’:4,’fields’:’city’}),API.users.get({‘user_ids’:5,’fields’:’city’}),API.users.get({‘user_ids’:6,’fields’:’city’}),API.users.get({‘user_ids’:7,’fields’:’city’}),API.users.get({‘user_ids’:8,’fields’:’city’}),API.users.get({‘user_ids’:9,’fields’:’city’}),API.users.get({‘user_ids’:10,’fields’:’city’}),API.users.get({‘user_ids’:11,’fields’:’city’}),API.users.get({‘user_ids’:12,’fields’:’city’}),API.users.get({‘user_ids’:13,’fields’:’city’}),API.users.get({‘user_ids’:14,’fields’:’city’}),API.users.get({‘user_ids’:15,’fields’:’city’}),API.users.get({‘user_ids’:16,’fields’:’city’}),API.users.get({‘user_ids’:17,’fields’:’city’}),API.users.get({‘user_ids’:18,’fields’:’city’}),API.users.get({‘user_ids’:19,’fields’:’city’}),API.users.get({‘user_ids’:20,’fields’:’city’}),API.users.get({‘user_ids’:21,’fields’:’city’}),API.users.get({‘user_ids’:22,’fields’:’city’}),API.users.get({‘user_ids’:23,’fields’:’city’}),API.users.get({‘user_ids’:24,’fields’:’city’}),API.users.get({‘user_ids’:25,’fields’:’city’})];

Если вам нужно вызывать другие методы вк апи, то просто замените ‘API.users.get({{\’user_ids\’:{},\’fields\’:\’city\’}})’ на нужный метод и данные.

Вот такой json мы получаем при вызове всего одно запроса:

{"response":[[{"first_name":"Павел","id":1,"last_name":"Дуров","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Александра","id":2,"last_name":"Владимирова","can_access_closed":false,"is_closed":true}],[{"first_name":"DELETED","id":3,"last_name":"","deactivated":"deleted"}],[{"first_name":"DELETED","id":4,"last_name":"","deactivated":"deleted"}],[{"first_name":"Илья","id":5,"last_name":"Перекопский","can_access_closed":true,"is_closed":false,"city":{"id":1,"title":"Москва"}}],[{"first_name":"Николай","id":6,"last_name":"Дуров","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Алексей","id":7,"last_name":"Кобылянский","can_access_closed":true,"is_closed":false,"city":{"id":295,"title":"London"}}],[{"first_name":"Аки","id":8,"last_name":"Сепиашвили","can_access_closed":true,"is_closed":false,"city":{"id":314,"title":"Киев"}}],[{"first_name":"Настя","id":9,"last_name":"Васильева","can_access_closed":false,"is_closed":true,"city":{"id":8162,"title":"Лобня"}}],[{"first_name":"Александр","id":10,"last_name":"Кузнецов","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Михаил","id":11,"last_name":"Петров","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"DELETED","id":12,"last_name":"","deactivated":"deleted"}],[{"first_name":"DELETED","id":13,"last_name":"","deactivated":"deleted"}],[{"first_name":"Андрей","id":14,"last_name":"Городецкий","can_access_closed":true,"is_closed":false,"city":{"id":295,"title":"London"}}],[{"first_name":"Сергей","id":15,"last_name":"Васильков","can_access_closed":false,"is_closed":true}],[{"first_name":"Виктория","id":16,"last_name":"Беспалова","can_access_closed":false,"is_closed":true,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Александр","id":17,"last_name":"Беспалов","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Симон","id":18,"last_name":"Кречмер","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Светочек","id":19,"last_name":"Аленький","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Илья","id":20,"last_name":"Турпиашвили","can_access_closed":true,"is_closed":false,"city":{"id":1,"title":"Москва"}}],[{"first_name":"Михаил","id":21,"last_name":"Равдоникас","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Семен","id":22,"last_name":"Воронин","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Андрей","id":23,"last_name":"Столбовский","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Рамми","id":24,"last_name":"Цицуашвили","can_access_closed":true,"is_closed":false,"city":{"id":2,"title":"Санкт-Петербург"}}],[{"first_name":"Анастасия","id":25,"last_name":"Ведущенко","can_access_closed":false,"is_closed":true,"city":{"id":2,"title":"Санкт-Петербург"}}]]}

Метод def fetch_zero(id, session)

В этом методе происходит сама обработка данных:

async def fetch_zero(id, session):     url = build_url(id)     try:         async with session.get(url) as response:                  # Считываем json                 resp=await response.text()                 js=json.loads(resp)                 list_users=[x for x in js['response'] if x != False]                  # Проверяем если город=1(Москва) тогда добавляем в лист                 for it in list_users:                     try:                         if it[0]['city']['id']==1:                                 list_data.append(it[0]['id'])                     except Exception:                         pass

Считываем json, проходим по всем пользователям из запроса и проверям поле город, можно заменить на любой другой город (1 — Москва, 2 — Питер и тд) и вытащить айдишники всех пользователей своего города. Тут https://vk.com/dev/database.getCities все айдишники городов.

Запускаем сборщик

# Запускаем  сборщик for i in range(0,17):   for id in range(i*500+1,(i+1)*500+1):       print(id)       loop = asyncio.new_event_loop()       asyncio.set_event_loop(loop)       loop.run_until_complete(run_zero(id))      # Сохраняем айдишники в файл на гугл диске и очищаем лист   with open(f'/content/gdrive/My Drive/data_main{i}.txt', 'w') as f:             for item in list_data:               f.write(f'{item}\n')    print(len(list_data))   list_data.clear()

Вот тут уже начинается математика)

Два цикла, первый с 0 по 16 включительно, второй 500 итераций + эти 3200:

for id in range((id - 1) * 3200, id * 3200):             task = asyncio.ensure_future(bound_fetch_zero(sem,id, session))             tasks.append(task)

Если у Вас не 1000 токенов, а 10 например, то значение 3200 нужно заменить на 40 максимум, этот цикл отвечает сколько сразу будет сделано асинхронных запросов, и если указать больше, то будет выскакивать то самое ограничение в 5 запросов в секунду.

В итоге 16 * 500 * 3200 * 25 (в 1 запросе 25 id)= 640 000 000 айдишников мы пройдем с id1 по конечный.

Зачем вы наверн подумаете столько циклов, а это нужно чтобы запустить обработку параллельно. Я запустил 5 сеансов в google colab с разным range(0,4), range(4,8) и тд в первом цикле. В итоге за полтора часа я смог обработать всех пользователей вк.

with open(f'/content/gdrive/My Drive/data_main{i}.txt', 'w') as f:             for item in list_data:               f.write(f'{item}\n')

И на каждой итерации мы сохраняем полученные айдишники в файл, всего получится 16 файлов, которые потом нужно объединить в один.

Итоги

За полтора часа работы сборщика на 5 сеансах гугл колаб можно вытащить любые открытые данные пользователей Вк (У vk api есть и другие ограничения, так что к некоторым методам нужно будет придумывать новые законные обходы). Вот ссылка на код проекта в Google Colab:

google-colab-project

И вот ссылка на датасет всех москвичей из всего вк, которые я вытащил, написав этого сборщика, можете себя найти там, если указывали москву в вк)

moscow-dataset-vk-ids

Из 650 млн. пользователей официальных москвичей 24 593 238.

Чтож надеюсь кому-то будет интересна данная статья и мои наработки будут полезны ^^

Меня в декабре забирают в армию, так что видимо это последняя статья. Хотел еще пару своих проектов описать на хабре, но видимо уже не успею. Всем позитива и удачи.

ссылка на оригинал статьи https://habr.com/ru/post/529696/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *