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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Задача

Нужно пройтись по 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/


Интересные статьи

Интересные статьи

Компания OpenAI, которую несколько лет назад основал Илон Маск, в июне выпустила новый алгоритм нейросети GPT-3. На сегодня это самая совершенная система, которая умеет работать с...
Работа над вакцинами и лекарствами против коронавируса идет полным ходом, однако ученым приходится решать множество проблем. По данным CNN, на этапе разработки находится более 20 лекарственных ср...
Всемирная паутина — это океан данных. Здесь можно посмотреть практически любую интересующую Вас информацию. Однако, "вытащить" эту информацию из интернета уже сложнее. Есть несколько способов п...
Правительства многих стран так или иначе ограничивают доступ граждан к информации и сервисам в интернете. Борьба с подобной цензурой – важная и непростая задача. Обычно простые решения не мог...
Вступительное слово Я выступил с этим докладом на английском языке на конференции GopherCon Russia 2019 в Москве и на русском — на митапе в Нижнем Новгороде. Речь в нём идёт о bitmap-индексе...