CoderCastrov logo
CoderCastrov
Питон

Чтение вашей сети друзей в Facebook с помощью парсинга на Python

Чтение вашей сети друзей в Facebook с помощью парсинга на Python
просмотров
19 мин чтение
#Питон

Примечание: вы можете получить весь код из этой статьи в Kaggle notebook.

В математике графы - это математические структуры, используемые для моделирования попарных отношений между объектами (согласно Википедии).

Простыми словами, это абстракция для представления группы элементов и связей между ними. Она может использоваться, например, для представления того, как города связаны железнодорожными путями. В своей простейшей форме граф может быть визуально представлен группой кругов, представляющих элементы (или узлы в терминологии графов), и линий (или ребер), соединяющих эти круги, если мы хотим представить связь.

Вот пример графа:

Элемент/узел номер 6 связан с элементом/узлом номер 4, и тот связан с элементами 5 и 3, и так далее.

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

Именно это мы собираемся сделать. Но с какой социальной сетью мы собираемся это сделать? Хорошо, мы собираемся использовать вашу собственную сеть Facebook.

Facebook - это огромная социальная сеть. Вы не можете знать структуру всей сети Facebook. Там есть тысячи миллионов пользователей. У вас нет к ней доступа и нет вычислительных ресурсов для обработки всей этой информации. Однако вы можете получить доступ к подмножеству этой сети. Как?

У вас нет доступа ко всей информации, но у вас есть доступ к некоторой. Прежде всего, вы находитесь в Facebook (я предполагаю, иначе этот статья не будет иметь особого смысла для вас). Так что вы знаете, что вы являетесь узлом сети.

Что еще вы знаете? Вы знаете своих друзей. Они также являются узлами графа/сети. Вы знаете, что вы друзья с ними, поэтому между вами и ними есть ребра/связи.

Что еще вы знаете? Вы знаете своих друзей. Они также являются узлами графа/сети. Вы знаете, что вы друзья с ними, поэтому между вами и ними есть ребра/связи.

Таким образом, вы можете узнать свою сеть Facebook! Это не вся сеть, но это подмножество всей сети. Это действительно подмножество, которое вас, вероятно, больше всего интересует.

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

И это сеть, которая нас интересует.

В этой статье я покажу вам, как вы можете получить необходимую информацию для построения этого графа. Эта информация - это, с одной стороны, ваш список друзей, а с другой - связи между ними. И мы собираемся сделать это, парся веб-страницу Facebook, то есть получая информацию из html-кода, который мы загружаем в наш браузер каждый раз, когда входим в систему.

Основным инструментом, который мы собираемся использовать, является Selenium, мощная библиотека Python для подобных проектов.

Когда мы получим всю эту информацию, я научу вас, как визуализировать этот граф в следующей статье: Организуйте свою свадьбу с помощью анализа социальной сети на Python.

Если вы просто хотите это сделать, но вам не интересно учиться, вы также можете скачать веб-приложение, которое я разработал с использованием Django, которое делает все это. Скачайте его из репозитория GitHub repository и следуйте инструкциям в файле Readme.md.

Начнем!!!

Важное примечание

Facebook часто меняет свой веб-дизайн. Это означает, что решение, которое я представляю здесь, может не работать в момент, когда вы читаете эту статью.

Но не расстраивайтесь. Процесс, который я представляю здесь, на самом деле полезен, но конкретные кнопки или элементы <div>, которые нужно найти, могут быть другими, поэтому вы должны адаптировать используемые мной CSS-селекторы для обновленного дизайна Facebook.

Установка Selenium

Selenium - это программное обеспечение, которое позволяет автоматизировать использование веб-браузеров. Простыми словами, мы можем выполнять действия, которые обычный пользователь веб-сайта может выполнить с помощью мыши и клавиатуры, но мы можем определить их, написав код, и, таким образом, автоматизировать их, планировать, выполнять повторно очень быстро и т. д. Использование зависит от вас.

Зачем это полезно? Допустим, например, что ваш друг каждую неделю просит вас поставить лайк на все его новые фотографии в Instagram. Это утомительная задача. Вам не очень нравится Instagram, и вам даже не нравятся его фотографии, он использует так много фильтров, что становится неразличимым. Вы даже не знаете, почему вы все еще друзья... но это не мое дело, давайте сосредоточимся, пожалуйста.

Вместо того, чтобы каждую неделю нажимать кнопку "лайк" на всех его фотографиях, вы можете использовать Selenium для автоматизации этой задачи. Вам просто нужно написать некоторый код, и Selenium подключится, например, к Google Chrome, чтобы поставить лайк на все фотографии вашего друга.

Он может использоваться во многих языках программирования и, конечно же, имеет API для Python.

Установка Selenium обычно не вызывает слишком много проблем, но иногда может быть сложной. Пожалуйста, обратитесь к предыдущей ссылке для получения инструкций.

Вам просто нужно сделать несколько вещей. Во-первых, начните с установки пакета Python:

pip install selenium

Затем вам нужно выбрать, какой браузер вы собираетесь разрешить Selenium использовать. Я выбрал Google Chrome, потому что я всегда его использую.

Чтобы разрешить связь между Selenium и Chrome, вам нужно предоставить Selenium веб-драйвер. Если вы хотите использовать Chrome, как и я, вы можете скачать веб-драйвер, соответствующий вашей версии Chrome, по этой ссылке. Загрузите файл .zip в соответствии с вашей операционной системой, извлеките файл и поместите его в ту же папку, где вы собираетесь выполнять свой код.

Наконец, импортируйте библиотеки в Python и создайте экземпляр веб-драйвера:

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome()

Если вы поместили его в другую папку, вы также можете указать путь:

driver = webdriver.Chrome("путь/к/chromedriver.exe")

Однако для меня самым простым вариантом является использование утилиты под названием webdriver_manager, которая обрабатывает загрузку драйвера за вас и сохраняет его в кэше для будущего использования.

from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(ChromeDriverManager().install())

Когда мы выполняем любой из трех упомянутых выше вариантов (я лично предпочитаю последний), появится окно Chrome. Возможно, у вас будет немного другой вид, и, вероятно, текст, который вы увидите, будет на вашем языке. Я испанец, поэтому вижу текст на испанском языке.

Если все прошло хорошо, мы можем приступить к веселью.

Вход в Facebook

Первый шаг для получения списка друзей на Facebook - это войти в свою учетную запись. Сначала мы должны посетить страницу. Это так просто, как:

driver.get("https://www.facebook.com/")

Мы попадем на экран, похожий на:

Facebook Login Page

На этой странице нас просят принять файлы cookie, потому что мы не находимся в нашем обычном экземпляре Chrome, где мы уже вошли в систему и имеем доступ ко всем файлам cookie. Это новая сессия, и для Facebook мы - неизвестный пользователь.

Чтобы принять файлы cookie, нам нужно найти кнопку "Принять все" и нажать на нее, но не с помощью мыши, а с помощью кода.

Но так как страница не загружается мгновенно, мы должны указать Selenium подождать несколько секунд, пока кнопка не будет загружена, то есть пока она не появится в html-коде.

Мы будем использовать CSS-селектор, чтобы найти кнопку, и такой же подход будет использоваться во всей остальной статье. Чтобы узнать, какой CSS-селектор найдет каждую кнопку или любой другой элемент, вы можете использовать некоторое расширение Chrome или просто открыть инструменты разработчика Chrome (Ctrl + Shift + I) и изучить html-код страницы.

from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

accept_cookies_button = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable(
        (By.CSS_SELECTOR, "button[data-testid='cookie-policy-dialog-accept-button']")
    )
)

accept_cookies_button.click()

Отлично! Вы нажали на свою первую кнопку! Поздравляю. Продолжим двигаться дальше.

Теперь мы можем заполнить поля входа своим именем пользователя и паролем.

user_css_selector = "input[name='email']"
password_css_selector = "input[name='pass']"

username_input = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, user_css_selector))
)
password_input = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, password_css_selector))
)

username_input.clear()
username_input.send_keys("myuser@blablabla.com")
password_input.clear()
password_input.send_keys("myPassword1232345345")

Или, если у вас есть имя пользователя и пароль, сохраненные в файле .env, что я рекомендую, вы можете использовать dotenv, чтобы загрузить их:

from dotenv import dotenv_values

config = dotenv_values(".env")

username_input.clear()
username_input.send_keys(config["user"])
password_input.clear()
password_input.send_keys(config["password"])

Теперь просто нажмите кнопку входа. Можете ли вы догадаться, как это сделать по приведенному выше примеру?

WebDriverWait(driver, 2).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, "button[type='submit']"))
).click()

Мы вошли в систему!!! Не чувствуете ли себя могущественным.

Но не будем так рано праздновать. Давайте продолжим. Что мы хотим сделать сейчас? Мы хотим получить список наших друзей. Затем мы должны перейти на соответствующую страницу. Но сначала подождите несколько секунд, чтобы страница Facebook могла правильно загрузиться.

import time

time.sleep(2)
driver.get("https://www.facebook.com/friends/list")

Чтение списка друзей

Теперь мы погрузимся в бурные воды. Мы хотим получить список всех наших друзей. Здесь есть два подхода. Первый - прокрутить вручную все панели друзей. Второй - использовать GraphQL API, чтобы сделать это более эффективно и элегантно. Я выбрал второй способ, потому что нахожу его более интересным.

Идея заключается в следующем. Когда вы прокручиваете панель друзей, ваш браузер отправляет HTTP-запросы на сервер, запрашивая информацию о новой группе друзей (следующие 10 друзей, которые будут видны на экране) в формате JSON.

Таким образом, вместо прокрутки мы просто эмулируем те же самые HTTP-запросы, которые используют GraphQL. Вам не нужно знать, что такое GraphQL, не волнуйтесь. Я тоже не очень много знаю об этом. Если вы не хотите тратить время на исследование этого, просто думайте о нем как о наборе правил для обмена информацией между серверной и клиентской частями. Это своего рода улучшенный способ проектирования веб-API.

Библиотека, которую мы собираемся использовать для отправки HTTP-запросов, - requests. Все, что я сделал для создания запросов, - это использовал инструменты разработчика Chrome (вкладка Network), чтобы изучить, какие запросы были обменены, и посмотреть, какие заголовки и тело использовались в отправленных на конечную точку graphql/ запросах:

Я не собираюсь подробно описывать заголовки и тело запросов и ответов, главным образом потому, что они могут измениться со временем, вероятно, раньше, чем позже.

Это своего рода плохая новость для вас, уважаемый читатель, потому что это означает, что вам нужно адаптировать код, который я собираюсь показать вам. В любом случае, процесс, который вы должны следовать, чтобы вывести правильные заголовки и тело, такой же, какой я следовал.

То, что я обнаружил, это следующее. Чтобы сервер предоставил вам запрашиваемую информацию, он должен быть уверен, что вы - тот, за кого себя выдаете. И он делает это двумя способами. Во-первых, вы должны установить куки для сеанса такие же, какие использует экземпляр Chrome. И во-вторых, вы должны отправить в теле HTTP-запроса поле с именем fb_dtsg. Я не веб-разработчик, поэтому не буду притворяться, что понимаю, как работает GraphQL, и каковы соглашения или почему Facebook нужно это поле fb_dtsg, в дополнение к кукам, чтобы идентифицировать пользователя. В любом случае, эти два значения - то, что мне нужно было правильно установить, чтобы получить нужную мне информацию.

Проблема с куками легко решается. Вы можете легко получить доступ к полям кук с помощью драйвера Selenium и присвоить их сеансу запросов:

import requests

session = requests.session()
session.cookies.update({
    cookie["name"]: cookie["value"]
    for cookie in driver.get_cookies()
})

Токен fb_dtsg можно найти, найдя шаблон в исходном коде страницы со списком друзей:

import json
import re

pattern = r'\["DTSGInitData",\[\],{"token":"\S+","async_get_token":"\S+?"},\d+\]'
match = re.search(pattern, driver.page_source)
fb_dtsg_token = json.loads(match.group())[2]["token"]

Теперь мы готовы запросить информацию о нашей первой группе друзей:

import urllib

url = 'https://www.facebook.com/api/graphql/'

headers = {
    "accept": "*/*",
    "accept-language": "es-ES,es;q=0.9",
    "content-type": "application/x-www-form-urlencoded",
    "sec-ch-ua": "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"",
    "sec-ch-ua-mobile": "?0",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-fb-friendly-name": "FriendingCometFriendsListPaginationQuery",
    "referrer": "https://www.facebook.com/friends/list",
    "referrerPolicy": "strict-origin-when-cross-origin",
}

response = session.post(
    url,
    headers=headers,
    data=urllib.parse.urlencode(
        {
            "fb_dtsg": fb_dtsg_token,
            "fb_api_req_friendly_name": "FriendingCometFriendsListPaginationQuery",
            "variables": json.dumps(
                {
                    "count": 30,
                    "cursor": None,
                    "scale": 1,
                }
            ).replace(" ", ""),
            "doc_id": 4268740419836267,
        }
    )
)

Две вещи, о которых стоит упомянуть. Во-первых, не беспокойтесь о полях, значения которых вы не знаете. Просто установите их так, как я делаю. Одно из них, на которое вы должны обратить внимание, - это doc_id. Это число, присутствующее в данных запроса, когда вы его проверяете с помощью инструментов разработчика Chrome. Как и раньше, я не буду притворяться, что понимаю, для чего оно используется или почему оно необходимо. Просто включите его. В противном случае это не сработает.

Я думаю, что это число идентифицирует тип данных, который вы запрашиваете. Я заметил, что это число меняется очень часто, но, если вы использовали кого-то, кого вы наблюдали, оно будет работать. По крайней мере, для меня это работает. Это означает, что, вероятно, существуют несколько допустимых doc_id, что является хорошей новостью. Приведенное выше - то, что работает для меня. Я не знаю, являются ли они универсальными или должны быть настроены для каждого пользователя.

Теперь давайте продолжим. Мы получили HTTP-ответ с данными наших друзей. Разберем его на словарь Python:

response_dict = json.loads(response.content)
friend_objects = response_dict["data"]["viewer"]["all_friends"]["edges"]

friends = [
    dict(
        user_id=str(friend["node"]["id"]),
        name=friend['node']['name'],
        link=friend['node']['url'],
        gender=friend['node']['gender']
    )
    for friend in friend_objects
    if friend["node"]["__typename"] == "User"
]

print(f"Количество друзей: {len(friends)}")# Output:
Количество друзей: 30

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

Вторая вещь, на которую стоит обратить внимание, - это то, что есть всего 30 друзей. Как так? Я думал, что я немного популярен 😭.

На самом деле у меня больше 300 друзей. Почему в списке только 30? Хорошо, мы сами сказали ему так делать. Если вы обратите внимание на поле count в данных запроса, мы запросили 30 друзей, и это то, что мы получили. А почему мы не запросили 300 друзей? Я пробовал, это не сработало.

Причина в том, что бэкэнд отправляет браузеру только информацию о видимых друзьях на панели. Когда вы прокручиваете вниз, браузер последовательно запрашивает следующую группу друзей, пока не достигнет конца списка.

В первом запросе мы получили только 30 друзей, потому что это единственная актуальная информация на данный момент. Представьте себе, насколько неэффективно было бы загружать весь список друзей каждый раз, когда вы хотите поговорить с лучшим другом (который, вероятно, появится в верхней части списка). Это было бы пустой тратой пропускной способности. Поскольку это ожидаемое поведение для обычного пользователя, они, вероятно, установили ограничение в 30 друзей на запрос.

Но как мы просим следующую группу друзей? Вот "секрет". Когда мы получаем ответ, есть поле, которое говорит нам, есть ли еще информация. И, в случае, если она есть, оно также говорит нам, какой cursor следующего друга.

cursor? Что это? Кажется, что при запросе списка ваших друзей каждому другу присваивается алфавитно-цифровой длинный код (курсор). Таким образом, в полученном ответе нам говорят: "эй, есть еще друзья, и следующий друг в списке - тот, у которого этот курсор".

Как получить эту информацию? Вот так:

page_info = response_dict["data"]["viewer"]["all_friends"]["page_info"]

Поле page_info - это объект с двумя полями: has_next_page и end_cursor. has_next_page - это логическое значение, которое говорит, есть ли еще друзья для запроса или это конец списка. В случае, если их больше, end_cursor - это курсор этого следующего друга. Например:

{
    'has_next_page': True,
    'end_cursor': 'VeryLongRandomAlphanumericSequence-x9QpwrV7w2xv2QS6DlDOOKQIIfWJAQropRaZvQUz'
}

И что мы делаем с этим? При отправке нового запроса, если он идентичен предыдущему, бэкэнд отправит нам ту же информацию. Мы должны сказать ему, что мы хотим, чтобы следующая последовательность друзей начиналась с пользователя с курсором, равным значению end_cursor, которое мы получили.

response = session.post(
    url,
    headers=headers,
    data=urllib.parse.urlencode(
        {
            "fb_dtsg": fb_dtsg_token,
            "fb_api_req_friendly_name": "FriendingCometFriendsListPaginationQuery",
            "variables": json.dumps(
                {
                    "count": 30,
                    "cursor": page_info["end_cursor"],
                    "scale": 1,
                }
            ).replace(" ", ""),
            "doc_id": 4268740419836267,
        }
    )
)

response_dict = json.loads(response.content)
friend_objects = response_dict["data"]["viewer"]["all_friends"]["edges"]

friends += [
    dict(
        user_id=str(friend["node"]["id"]),
        name=friend['node']['name'],
        link=friend['node']['url'],
        gender=friend['node']['gender']
    )
    for friend in friend_objects
    if friend["node"]["__typename"] == "User"
]

print(f"Количество друзей: {len(friends)}")# Output:
Количество друзей: 60

Как вы можете видеть, теперь у нас есть еще 30 друзей. И вы можете продолжать делать это, пока не останется больше друзей:

page_info = dict(has_next_page=True, end_cursor=None)

url = 'https://www.facebook.com/api/graphql/'

headers = {
    "accept": "*/*",
    "accept-language": "es-ES,es;q=0.9",
    "content-type": "application/x-www-form-urlencoded",
    "sec-ch-ua": "\" Not;A Brand\";v=\"99\", \"Google Chrome\";v=\"91\", \"Chromium\";v=\"91\"",
    "sec-ch-ua-mobile": "?0",
    "sec-fetch-dest": "empty",
    "sec-fetch-mode": "cors",
    "sec-fetch-site": "same-origin",
    "x-fb-friendly-name": "FriendingCometFriendsListPaginationQuery",
    "referrer": "https://www.facebook.com/friends/list",
    "referrerPolicy": "strict-origin-when-cross-origin",
}

friends = []

while page_info["has_next_page"]:
    response = session.post(
        url,
        headers=headers,
        data=urllib.parse.urlencode(
            {
                "fb_dtsg": fb_dtsg_token,
                "fb_api_req_friendly_name": "FriendingCometFriendsListPaginationQuery",
                "variables": json.dumps(
                    {
                        "count": 30,
                        "cursor": page_info["end_cursor"],
                        "scale": 1,
                    }
                ).replace(" ", ""),
                "doc_id": 4268740419836267,
            }
        )
    )
    
    response_dict = json.loads(response.content)
    friend_objects = response_dict["data"]["viewer"]["all_friends"]["edges"]

    friends += [
        dict(
            user_id=str(friend["node"]["id"]),
            name=friend['node']['name'],
            link=friend['node']['url'],
            gender=friend['node']['gender']
        )
        for friend in friend_objects
        if friend["node"]["__typename"] == "User"
    ]
    
    page_info = response_dict["data"]["viewer"]["all_friends"]["page_info"]
    
print(f"Количество друзей: {len(friends)}")

Мы получили полный список наших друзей!!! Поздравляю, приятель, ты на полпути! Отдохни немного и возвращайся, когда будешь готов к новым заданиям.

Получение общих друзей всех ваших друзей

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

Мы собираемся получить связи в сети (линии, соединяющие круги). И мы сделаем это самым простым и примитивным способом, который я могу придумать. Мы просто посетим, одного за другим, профили каждого из ваших друзей, перейдем в раздел общих друзей, прокрутим вниз, пока они все не появятся, и, наконец, прочитаем и сохраним их.

Почему бы не использовать снова GraphQL API?

Это справедливый вопрос. Ответ в том, что я попробовал, и мне удалось (всю информацию я загрузил всего за несколько секунд), но Facebook узнает, что у вас подозрительное поведение и заблокирует некоторые функции вашей учетной записи как минимум на несколько часов, что очень неудобно. Но не жалуйтесь. Эта политика имеет много смысла, поскольку Facebook не знает, проводите ли вы практику по сбору данных с помощью Python, или злонамеренное программное обеспечение, использующее ваш браузер без вашего согласия. Так что это на самом деле хорошая новость, что Facebook защищает наши данные таким образом.

В любом случае, обход, который я наконец использовал, действительно притворяется, что мы обычный пользователь, посещающий профили своих друзей. До сих пор у меня не было заблокировано моей учетной записи.

Давайте начнем с простых шагов, а затем объединим все вместе. Что для этого нужно сделать? Перейти на профиль нашего друга. Для этого нам нужен URL этой страницы. Это сложно, потому что есть два типа URL-адресов для профилей пользователей.

Некоторые из них, которые, я полагаю, являются старыми, имеют следующую форму (id - это пример, которого не существует):

[https://www.facebook.com/profile.php?id=123456789014389713490753713](https://www.facebook.com/profile.php?id=123456789014389713490753713)

Но большинство из них имеют следующую форму:

[https://www.facebook.com/barack.obama.or.something](https://www.facebook.com/barack.obama.or.something)

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

friend = friends[0]

profile_link = friend["link"]

url_parsed = urllib.parse.urlparse(profile_link)

if url_parsed.path == "/profile.php":
    mutual_friends_link =  f"**{**profile_link**}**&sk=friends_mutual"
else:
    mutual_friends_link = f"**{**profile_link**}**/friends_mutual"
    
driver.get(mutual_friends_link)
time.sleep(1.5)

Бум! Хорошая работа.

Теперь мы на странице. Но, даже если мы подождали некоторое время, чтобы она загрузилась, очень вероятно, что она все еще не полностью загружена и никогда не будет загружена, как бы долго вы ни ждали.

Помните: Facebook не загружает всю информацию, если пользователь не прокручивает вниз, в противном случае это было бы пустой тратой ресурсов. Так что, если у вас и у вашего друга много общих друзей, они не будут показаны на экране, пока вы не прокрутите вниз.

Как мы узнаем, что все загружено? Мы должны найти элемент <div>, который указывает на то, что еще несколько друзей загружаются:

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

from selenium.webdriver.common.keys import Keys

loading_mutual_friends_panel_class = "lzcic4wl j83agx80 btwxx1t3 lhclo0ds i1fnvgqd"
loading_mutual_friends_panel_selector = f"div[class='**{**loading_mutual_friends_panel_class**}**'][data-visualcompletion='loading-state']"

loading_element = driver.find_elements_by_css_selector(
    loading_mutual_friends_panel_selector
)

while len(loading_element) > 0:
    driver.find_element_by_xpath('//body').send_keys(Keys.END)
    time.sleep(0.5)
    loading_element = driver.find_elements_by_css_selector(
        loading_mutual_friends_panel_selector
    )

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

mutual_friends_panel_selector = "div[data-pagelet='ProfileAppSection_0']"

mutual_friends_pannel = driver.find_element_by_css_selector(
    mutual_friends_panel_selector
)

Каждый из общих друзей появится в своей собственной "карточке", и его или ее имя будет <a> элементом:

mutual_friend_link_class = (
    "oajrlxb2 g5ia77u1 qu0x051f esr5mh6w e9989ue4 r7d6kgcz rq0escxv"
    " nhd2j8a9 nc684nl6 p7hjln8o kvgmc6g5 cxmmr5t8 oygrvhab hcukyx3x"
    " jb3vyjys rz4wbd8a qt6c0cv9 a8nywdso i1ao9s8h esuyzwwr f1sip0of lzcic4wl gmql0nx0 gpro0wi8"
)

mutual_friend_link_selector = f"a[class='**{**mutual_friend_link_class**}**']"

mutual_friend_links = mutual_friends_pannel.find_elements_by_css_selector(
    mutual_friend_link_selector
)

print(f"Количество общих друзей с **{**friend['name']**}**: **{**len(mutual_friend_links)**}**")

У каждой ссылки есть две полезные информации. С одной стороны есть текст, который является именем общего друга. С другой стороны, есть URL, на который он перенаправляет, который является ссылкой на его профиль.

mutual_friends = []
for link **in** mutual_friend_links:
    name = link.text
    link = link.get_attribute("href")
    mutual_friends.append(
        dict(
            name=name,
            link=link
        )
    )

Отлично! Мы почти закончили.

У нас есть проблема. Хорошо, это просто личное предпочтение. Я хочу использовать user_id в качестве уникального идентификатора каждого человека. Я мог бы работать со ссылкой на профиль link, которая, вероятно, является уникальной, но я предпочитаю user_id.

Он не предоставляется напрямую в ссылке, но мы можем получить его из нашего списка друзей. Поэтому мы собираемся изменить предыдущий цикл следующим образом:

def get_friend_by_link(link):
    for friend **in** friends:
        if friend["link"] == link:
            return friend
        
    return None


mutual_friends = []
for link **in** mutual_friend_links:
    name = link.text
    link = link.get_attribute("href")
    
    friend = get_friend_by_link(link)
    user_id = friend["user_id"]
    gender = friend["gender"]
    
    mutual_friends.append(
        dict(
            user_id = user_id,
            name=name,
            link=link,
            gender = gender,
        )
    )

Что, если мы объединим все это вместе и сделаем эту операцию для всех ваших друзей?

loading_mutual_friends_panel_class = "lzcic4wl j83agx80 btwxx1t3 lhclo0ds i1fnvgqd"loading_mutual_friends_panel_selector = f"div[class='**{**loading_mutual_friends_panel_class**}**'][data-visualcompletion='loading-state']"mutual_friends_panel_selector = "div[data-pagelet='ProfileAppSection_0']"mutual_friend_link_class = (
    "oajrlxb2 g5ia77u1 qu0x051f esr5mh6w e9989ue4 r7d6kgcz rq0escxv"
    " nhd2j8a9 nc684nl6 p7hjln8o kvgmc6g5 cxmmr5t8 oygrvhab hcukyx3x"
    " jb3vyjys rz4wbd8a qt6c0cv9 a8nywdso i1ao9s8h esuyzwwr f1sip0of lzcic4wl gmql0nx0 gpro0wi8"
)mutual_friend_link_selector = f"a[class='**{**mutual_friend_link_class**}**']"


def visit_mutual_friends_page(friend):
    profile_link = friend["link"]
    url_parsed = urllib.parse.urlparse(profile_link)

    if url_parsed.path == "/profile.php":
        mutual_friends_link =  f"**{**profile_link**}**&sk=friends_mutual"
    else:
        mutual_friends_link = f"**{**profile_link**}**/friends_mutual"

    driver.get(mutual_friends_link)
    time.sleep(1.5)
    
    
def wait_for_every_friend_to_load():
    loading_element = driver.find_elements_by_css_selector(
        loading_mutual_friends_panel_selector
    )

    while len(loading_element) > 0:
        driver.find_element_by_xpath('//body').send_keys(Keys.END)
        time.sleep(0.5)
        loading_element = driver.find_elements_by_css_selector(
            loading_mutual_friends_panel_selector
        )

def get_friend_by_link(link):
    for friend **in** friends:
        if friend["link"] == link:
            return friend
        
    return None


mutual_friends_all = {}
num_friends = len(friends)
for i, friend **in** enumerate(friends, start=1):
    print(f"Чтение общих друзей с **{**friend['name']**}**. (**{**i**}** из **{**num_friends**}**)")
    
    visit_mutual_friends_page(friend)

    wait_for_every_friend_to_load()
    
    mutual_friends_pannel = driver.find_element_by_css_selector(
        mutual_friends_panel_selector
    )
    
    mutual_friend_links = mutual_friends_pannel.find_elements_by_css_selector(
        mutual_friend_link_selector
    )
    
    mutual_friends = []
    for link **in** mutual_friend_links:
        link = link.get_attribute("href")

        friend = get_friend_by_link(link)

        mutual_friends.append(
            dict(
                user_id=friend["user_id"],
                name=friend["name"],
                link=link,
                gender=friend["gender"],
            )
        )
    
    mutual_friends_all[friend["user_id"]] = mutual_friends

Это было не просто, но мы сделали это. Сохраните его, пока ваш ноутбук не разрядится!

with open("my_friends_network", "w") as outfile:
    json.dump(
        dict(
            friend_list=friends,
            mutual_friends=mutual_friends_all,
        ),
        outfile
    )

Мы сделали это!!!!! 🥳🥳🥳

Теперь мы можем использовать его для визуализации сети разными способами, даже находить разные кластеры пользователей, как на этой картинке:

Я научу вас, как сделать это в следующей статье: Организуйте свою свадьбу с помощью анализа социальных сетей в Python.


Опубликовано на https://www.listeningtothedata.com.