• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Сбор данных с сайта знакомств с помощью Selenium, BeautifulSoup и requests используя скрипт на Python

В данной статье я предлагаю вам немного отвлечься от слишком серьезных задач и попрактиковаться в парсинге. А, чтобы не было скучно, будем мы парсить достаточно известный сайт знакомств. А именно, забирать оттуда фото пользователей. Для наших целей мы будем использовать Selenium, BeautifulSoup, requests. А работать это будет в скрипте Python.

000.jpg

Дисклеймер: Все данные, предоставленные в данной статье, взяты из открытых источников, не призывают к действию и являются только лишь данными для ознакомления, и изучения механизмов используемых технологий.
Сначала немного (совсем чуть-чуть) предыстории. Уж не помню точно, что я искал в сети, но наткнулся на один забавный форум. Ничего необычного. Решил походить по разделам и наткнулся на забавный инструмент, сделанный на BAS. BAS – это BrowserAutomationStudio. Не знаю, насколько он популярен сейчас, однако пару-тройку лет назад, а примерно тогда и была написана статья об этом инструменте, был достаточно популярен.

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

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

Давайте же начинать.

Что потребуется?

Для начала – регистрация на данном сайте, то есть: . К сожалению, без регистрации попасть дальше анкеты пользователя и его альбома с фото не получиться. В самом альбоме прямых ссылок на фото нет. Поэтому придется открывать каждую фотографию, чтобы ссылку эту получить. А без регистрации открыть фото нельзя. Для регистрации идем на любой сервис временной почты, получаем e-mail и регистрируемся хоть папой Римским. Это совершенно не важно, в данном конкретном случае. Я даже накидал небольшую схемку доступа для данного сайта.

Путь к ссылке на фото.png

Теперь, что касается будущего скрипта. Для его работы нам понадобиться несколько сторонних библиотек. А именно: Selenium – для автоматизации браузера; BeautifulSoup – для парсинга страниц; lxml – парсер; requests – для скачивания фото. Поэтому, для их установки пишем в терминале:

pip install selenium bs4 lxml requests

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

pip install selenium

После этого, так как мы будем использовать selenium, необходимо скачать webdriver для конкретного браузера. Я использую Google Chrome, потому буду качать драйвер именно под него. Для того, чтобы узнать, какая версия драйвера нужна именно в вашем случае, запускаем браузер, жмем на три точки в правом верхнем углу, идем к пункту меню «Справка» и жмем на пункт «О браузере Google Chrome».

02.png

Откроется страница, где и будет указана версия. На момент написания статьи версия браузера была: 112.0.5615.138.

03.png

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

04.png

После того, как драйвер будет загружен, необходимо положить его куда-то, в директории вашего скрипта, ну или просто прописать путь к нему в переменных окружения. Я пошел по простому пути, поэтому положил драйвер в папку… driver.

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

Python:
login = 'ххххх@ххххххххх.com'  # ваш логин
password = 'хххххххххххххххх'  # ваш пароль

А после импортировать в скрипт данные переменные. Впрочем, данные тут не такие уж секретные, а потому, на ваше усмотрение.
Что же, с подготовкой, вроде бы закончили. Давайте приступать к написанию кода.


Импорт библиотек в скрипт

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

Python:
import os
import sys
import time
from pathlib import Path
from platform import system

import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By

from set_love import login, password

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

Python:
options = Options()
# options.add_argument("--headless")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--disable-notifications")

Режим безголовости в данном случае не нужен. Позже поясню почему.
Указываем путь к загруженному нами ранее webriver и создаем переменную, в которую будем складывать адреса просмотренных страниц.

Python:
exec_path = os.path.join(os.getcwd(), 'driver', 'chromedriver.exe') if system() == "Windows" else \
    os.path.join(os.getcwd(), 'driver', 'chromedriver')

all_link = set()

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


Запрос параметров поиска

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

05.png

И получаем вот такую ссылку:

https://loveplanet.ru/a-search/d-1/pol-1/spol-2/tage-35/bage-18/purp-0/proof-1/country-0/region-0/city-0

Рассмотрим несколько параметров из нее, которые будут нам необходимы. pol-1 – это пол того, кто ищет. В данном случае 1 – соответствует пункту меню: Парень. spol-2 – пол того, кого ищут. В данном случае 2 – пункт меню: Девушку. Здесь будьте внимательны и аккуратны. Так как при неверном указании параметров можете найти не совсем то, что вам хотелось, вроде парней считающих себя девушками и девушек, считающих себя парнями ))

tage-35 – ограничение поиска по возрасту: До… bage-18 – ограничение поиска по возрасту: От…proof-1 – соответствует выставлению галочки – Проверенные фото. country – страна. В нашем случае 0, то есть все страны, равно как и у параметров: region и city.

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

https://loveplanet.ru/a-search/d-1/pol-1/spol-2/tage-35/bage-18/purp-0/proof-1/country-0/region-0/city-0/p-1/

p-1 – номер страницы.

06.png

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

Создаем функцию main() -> None. Определяем глобальные параметры: all_link, и глобальный параметр внутри функции: driver. Если вы побываете на сайте, то достаточно быстро поймете, что страниц с анкетами пользователей там достаточно много. Поэтому, скорее всего, выкачать все фото за один раз у вас не получиться. Хотя, кто знает, тут уже только ваше упорство. Тем не менее, при закрытии скрипта реализуем небольшой код, который будет сохранять содержимое all_link, а именно здесь будут храниться просмотренные адреса страниц анкет в текстовый файл. Проверяем наличие такого файла. Если он есть, загружаем его в множество all_link. Затем просим пользователя ввести пол для поиска. Тут еще можно добавить два пункта (Не такие мужчины, Не такие женщины), но, я этого делать не буду. В этом случае ничего в логике скрипта не измениться, будут только лишь меняться параметры в поисковом запросе. Проверяем корректность ввода. Просим ввести возраст До. Затем возраст От. Выполняем небольшие проверки на корректность ввода.

Python:
def main() -> None:
    global all_link
    driver = None
    try:
        if Path('links_people.txt').exists():
            with open('links_people.txt', 'r', encoding='utf-8') as file:
                for ln in file.readlines():
                    all_link.add(ln.strip())
        spol = input('Выберите пол для поиска фото:\n   [1] Мужчины\n   [2] Женщины\n   >>> ')
        if spol not in ["1", "2"]:
            print('Введите корректный пол!')
            sys.exit(0)
        tage = input("Введите возраст до (max=100): ")
        if not tage.isdigit() or int(tage) > 100:
            print("Вы ввели не число. Перезапустите программу!")
            sys.exit(0)
        bage = input("Введите возраст от (min=18): ")
        if not bage.isdigit() or int(bage) < 18:
            print("Вы ввели слишком юный возраст! Перезапустите программу!")
            sys.exit(0)

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

Python:
        driver = webdriver.Chrome(options=options, service=Service(log_path=os.devnull, executable_path=exec_path))

        driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            'source': '''
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
              '''
        })

Запускаем функцию авторизации, в которую передаем созданные объект драйвера. После авторизации проверяем, что ввел пользователь и, в зависимости от этого, передаем параметры в функцию поиска find_people. Также, оборачиваем весь код в блок try – except и обрабатываем прерывание с клавиатуры. Закрываем браузер и сохраняем данные из множества в текстовый документ. Ну и также прописываем закрытие браузера после окончания работы скрипта.

Python:
        logging(driver)
        if spol == "1":
            find_people(int(tage), int(bage), 1, 2, driver)
        elif spol == "2":
            find_people(int(tage), int(bage), 2, 1, driver)
    except KeyboardInterrupt:
        driver.close()
        driver.quit()
        with open('links_girl.txt', 'w', encoding='utf-8') as file:
            for lin in all_link:
                file.write(f'{lin}\n')
    driver.close()
    driver.quit()

Python:
def main() -> None:
    """
    Запрос параметров поиска у пользователя. Проверка полученных параметров.
    Загрузка просмотренных ссылок и добавление их в множество для будущих проверок.
    Запуск функции авторизации и перехода по страницам.
    Если закрыт браузер или выполнено прерывание по "Ctrl + C" сохраняется информация
    о просмотренных страницах.
    """
    global all_link
    driver = None
    try:
        if Path('links_people.txt').exists():
            with open('links_people.txt', 'r', encoding='utf-8') as file:
                for ln in file.readlines():
                    all_link.add(ln.strip())
        spol = input('Выберите пол для поиска фото:\n   [1] Мужчины\n   [2] Женщины\n   >>> ')
        if spol not in ["1", "2"]:
            print('Введите корректный пол!')
            sys.exit(0)
        tage = input("Введите возраст до (max=100): ")
        if not tage.isdigit() or int(tage) > 100:
            print("Вы ввели не число. Перезапустите программу!")
            sys.exit(0)
        bage = input("Введите возраст от (min=18): ")
        if not bage.isdigit() or int(bage) < 18:
            print("Вы ввели слишком юный возраст! Перезапустите программу!")
            sys.exit(0)

        driver = webdriver.Chrome(options=options, service=Service(log_path=os.devnull, executable_path=exec_path))

        driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            'source': '''
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
              '''
        })

        logging(driver)
        if spol == "1":
            find_people(int(tage), int(bage), 1, 2, driver)
        elif spol == "2":
            find_people(int(tage), int(bage), 2, 1, driver)
    except KeyboardInterrupt:
        driver.close()
        driver.quit()
        save_data()
    driver.close()
    driver.quit()


Сохранение данных в файл

Создадим небольшую функцию save_data(), которая будет необходима для сохранения просмотренных страниц анкет в файл. Так как данная функция дублируется в нескольких местах.

Python:
def save_data():
    global all_link
    with open('links_people.txt', 'w', encoding='utf-8') as file:
        for lin in all_link:
            file.write(f'{lin}\n')


Авторизация на сайте

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

Создадим функцию logging(driver) -> None. В нее передается объект драйвера. Переходим на страницу авторизации. Ждем полсекунды для того, чтобы все прогрузилось. Ищем поле ввода логина по CSS-селектору. Отправляем в него логин. Также ищем поле ввода пароля и помещаем в него пароль. После чего еще чуть ожидания, ищем кнопку для отправки данных и жмем на нее.

Тут есть небольшой нюанс. Если вы первый раз заходите на сайт, да и вообще делаете это не слишком часто, то все будет хорошо и подойдет вышеописанный алгоритм. Однако, если вы мучаете сайт постоянными входами и выходами, вам будет предложено ввести капчу. Поэтому, обрабатываем исключение, которое возникнет, когда не будет найдена кнопка отправки данных. Дело в том, что ее селектор сместиться на 1. Так как добавиться еще поле ввода для капчи. Потому, ищем это поле ввода, помещаем в него курсор и ручками вбиваем капчу. На это действие у вас будет 5 секунд. Я успевал. За редким исключением, когда капча уж совсем дикая. Ну, а далее жмем кнопку отправки данных. Не нажимайте ее ручками. Скрипт все сделает за вас. От вас только требуется ввести капчу.

Python:
def logging(driver) -> None:
    """
    Авторизация на сайте. Если часто авторизоваться выскакивает капча. В этом случае
    предоставляется время - 5 секунд, чтобы ее ввести.
    """

    """Поиск полей. Заполнение паролем и логином."""
    driver.get('https://loveplanet.ru/a-logon')
    time.sleep(0.5)
    field_login = driver.find_element(By.CSS_SELECTOR, '#dlg_login_log')
    field_login.send_keys(login)
    time.sleep(0.2)
    field_pass = driver.find_element(By.CSS_SELECTOR, '#dlg_login_pas')
    field_pass.send_keys(password)
    try:
        """Если нет капчи, просто нажатие на кнопку вход"""
        button = driver.find_element(By.CSS_SELECTOR, '#dlg_login > ul > li:nth-child(4) > button')
        button.click()
        time.sleep(1)
    except NoSuchElementException:
        """Если есть капча, пауза, затем нажатие на вход"""
        field_capt = driver.find_element(By.CSS_SELECTOR, '#dlg_pict_val')
        field_capt.click()
        time.sleep(5)
        button = driver.find_element(By.CSS_SELECTOR, '#dlg_login > ul > li:nth-child(5) > button')
        button.click()
        time.sleep(1)


Постраничный переход в бесконечном цикле

Создадим функцию find_people(tage: int, bage: int, spol: int, pol: int, driver: webdriver) -> None, которая принимает возраст До, возраст От, пол который искать, пол ищущего, и вебдрайвер.

Для поиска ссылок нам потребуется перейти на страницу, получить ее код и передать в функцию поиска. Что мы и сделаем. Неудобство данного сайта в том, что непонятно, какое количество страниц с анкетами пользователя доступно для просмотра. Потому и бесконечный цикл. Однако, если, для примера, ввести страницу 1000, то на данной странице отобразиться только одна анкета. И так будет, какой бы номер мы не вводили далее: 1001, 1002 … Поэтому, будем проверять количество найденных анкет. И если количество равно 1, то увеличивать счетчик. Далее проверять, если счетчик равен или больше 2, то выходим из цикла с сохранением данных.

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

Python:
def find_people(tage: int, bage: int, spol: int, pol: int, driver: webdriver) -> None:
    """
    Переход по страницам сайта. Запуск функции поиска страниц.
    """
    num = 0
    link_count = 0
    while True:
        try:
            url = f'https://loveplanet.ru/a-search/d-1/pol-{pol}/spol-{spol}/tage-{tage}/bage-{bage}/purp-0/proof-1/country-0/region-0/city-0/p-{num}'
            driver.get(url)
            time.sleep(1)
            count = find_pages(driver.page_source, driver, num)
            if count == 1:
                link_count += 1
            if link_count >= 2:
                save_data()
                break
            num += 1
        except Exception as ex:
            print(ex)
            save_data()
            break
    print("All pages down")


Поиск ссылок на анкеты пользователей

Создадим функцию find_pages(source: str, driver: webdriver, num: int) -> int. На входе она получает код страницы со ссылками на анкеты, вебдрайвер, номер страницы. А возвращает количество найденных ссылок на анкеты.

Для начала создадим объект BeautifulSoup, в который передадим код страницы и укажем парсер, с помощью которого будем парсить код. Выполняем поиск ссылок, которые содержаться в «div» с классом «to_els fbold». Вот этот тег для наглядности на изображении:

07.png

Проверяем, есть ли что-то в найденных ссылках. Создаем два списка, в которые будем помещать имя владельца анкеты и ссылку на страницу анкеты. Затем в цикле перебираем найденные ссылки. Находим тег «a», из которого забираем текст содержащий имя, а также ссылку на анкету. Проверяем, нет ли найденной ссылки в множестве. Так как, данная анкета уже может быть обработана ранее. Если есть, пропускаем. Если нет, добавляем ссылку и имя в соответствующие списки.

Python:
def find_pages(source: str, driver: webdriver, num: int) -> int:
    global all_link
    soup = BeautifulSoup(source, 'lxml')
    if links := soup.find_all('div', class_='to_els fbold'):
        names = []
        pages = []
        for link in links:
            name = link.find('a').text.strip()
            page = f"https://loveplanet.ru{link.find('a')['href']}"
            if page in all_link:
                print("Просмотрено")
                continue
            else:
                pages.append(page)
                names.append(name)

После того, как будет выполнен поиск, запустим функции для поиска данных и загрузки фото. В цикле перебираем полученные ссылки. Забираем из списка с именем по индексу имя пользователя анкеты. Из него мы будем формировать уникальное имя для папки, куда будем складывать данные по анкете. Так как, для примера Оксан на данном сайте, сами понимаете, может быть очень большое количество. Выводим в терминал данные об обрабатываемой странице. Переходим по ссылке в браузере. Затем передаем в функцию find_data код страницы анкеты, а также имя и номер который добавляем к нему и ссылку на анкету. Это будет необходимо для общих данных, которые мы соберем со страницы анкеты. Так как я подумал, что получение только фото будет достаточно скучным.

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

Python:
        print(f'Обработка страниц:')
        """Переход на страницу пользователя. Сбор информации.
        запуск функции поиска фото"""
        for n, pg in enumerate(pages):
            print(f'[{n + 1}].{names[n]} | {pg}')
            driver.get(pg)
            name_age = find_data(driver.page_source, f"{names[n]}_{num}", pg)
            time.sleep(1)
            find_photo(driver.page_source, name_age, driver)
            all_link.add(pg)
        return len(pages)

Python:
def find_pages(source: str, driver: webdriver, num: int) -> int:
    """
    Поиск ссылок на страницы пользователей.
    """
    global all_link
    """Поиск ссылок. Проверка на наличие в уже проверенных. Если есть - пропуск"""
    soup = BeautifulSoup(source, 'lxml')
    if links := soup.find_all('div', class_='to_els fbold'):
        names = []
        pages = []
        for link in links:
            name = link.find('a').text.strip()
            page = f"https://loveplanet.ru{link.find('a')['href']}"
            if page in all_link:
                print("Просмотрено")
                continue
            else:
                pages.append(page)
                names.append(name)
        print(f'Обработка страниц:')
        """Переход на страницу пользователя. Сбор информации.
        запуск функции поиска фото"""
        for n, pg in enumerate(pages):
            print(f'[{n + 1}].{names[n]} | {pg}')
            driver.get(pg)
            name_age = find_data(driver.page_source, f"{names[n]}_{num}", pg)
            time.sleep(1)
            find_photo(driver.page_source, name_age, driver)
            all_link.add(pg)
        return len(pages)


Получение данных со страницы анкеты

Создадим функцию find_data(source: str, name: str, url: str) -> str. Возвращает она сформированное на основании полученных данных имя директории для сохранения фото. А на входе получает код страницы с анкетой, имя, которое формируется в предыдущей функции из имени пользователя, плюс номер страницы с анкетами и ссылку на страницу анкеты.

Создаем список, куда будем складывать все полученные данные для последующего сохранения. Создаем объект BeautifulSoup, куда передаем код страницы и парсер. Далее выполняем поиск имени пользователя. На странице анкеты имя пользователя отображается вместе с его возрастом. Для примера: «Надежда, 25». Не буду подробно описывать теги. Их можно посмотреть в коде. Укажу только изображение с данными тегами на странице.

08.png

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

09.png

Здесь ищем и забираем данные из блока: «Я ищу».

10.png

Все полученные данные добавляем в список data.

Python:
def find_data(source: str, name: str, url: str) -> str:
    data = []
    soup = BeautifulSoup(source, 'lxml')
    name_age = soup.find('div', class_='flex ai-center mb24').find('span', class_='fbold fsize20').text.strip()
    data.append(name_age)
    location = soup.find('div', class_='flex ai-center jc-between').find('div', class_='blue_14').find('span').text. \
        strip()
    data.append(f'{location}\n')
    su = soup.find('div', class_='fbold fsize15 mb6').text.strip()
    ifind = soup.find('ul', class_='reset fsize15 mt7').find('li').text.strip()
    data.append(f'{su}: {ifind}')

Также на странице (у большинства) есть блок с Личной информацией. Ее нам тоже нужно найти и забрать. Так как информации на странице может не оказаться, обрабатываем исключение как при переборе тегов «li», так и при получении информации о самом блоке.

11.png

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

Python:
    try:
        data.append(f"\n{soup.find('div', class_='fbold fsize15 mb7').text.strip()}:")
        info = soup.find('ul', class_='reset list_info show li_mt10').find_all('li')
        for inf in info:
            try:
                lb = f"{inf.find('label').text.strip()} {inf.find('div').text.strip()}"
                data.append(lb)
            except Exception:
                continue
    except Exception:
        pass
    data.append(f'\n{url}')
    print(data)

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

Python:
    if data:
        path = Path.cwd() / 'photo'
        path.mkdir(exist_ok=True)
        path = path / f'{name_age}_{name}'
        path.mkdir(exist_ok=True)
        with open(path / f'{name}.txt', 'w', encoding='utf-8') as file:
            for dat in data:
                file.write(f'{dat}\n')
    return f'{name_age}_{name}'

Python:
def find_data(source: str, name: str, url: str) -> str:
    """
    Поиск и сбор информации со страницы пользователя. Возвращает имя и возраст.
    """

    """Поиск имени и информации "Я ищу..."""""
    data = []
    soup = BeautifulSoup(source, 'lxml')
    name_age = soup.find('div', class_='flex ai-center mb24').find('span', class_='fbold fsize20').text.strip()
    data.append(name_age)
    location = soup.find('div', class_='flex ai-center jc-between').find('div', class_='blue_14').find('span').text. \
        strip()
    data.append(f'{location}\n')
    su = soup.find('div', class_='fbold fsize15 mb6').text.strip()
    ifind = soup.find('ul', class_='reset fsize15 mt7').find('li').text.strip()
    data.append(f'{su}: {ifind}')
    """Поиск доступной личной информации"""
    try:
        data.append(f"\n{soup.find('div', class_='fbold fsize15 mb7').text.strip()}:")
        info = soup.find('ul', class_='reset list_info show li_mt10').find_all('li')
        for inf in info:
            try:
                lb = f"{inf.find('label').text.strip()} {inf.find('div').text.strip()}"
                data.append(lb)
            except Exception:
                continue
    except Exception:
        pass
    data.append(f'\n{url}')
    print(data)
    """Проверка, если список с данными не пуст, сохраняем их в текстовый документ"""
    if data:
        path = Path.cwd() / 'photo'
        path.mkdir(exist_ok=True)
        path = path / f'{name_age}_{name}'
        path.mkdir(exist_ok=True)
        with open(path / f'{name}.txt', 'w', encoding='utf-8') as file:
            for dat in data:
                file.write(f'{dat}\n')
    return f'{name_age}_{name}'


Получение ссылок на фото, загрузка и сохранение

Создадим функцию find_photo(source: str, name: str, driver: webdriver) -> None. На входе она получает код страницы анкеты, имя директории в которую будут сохранятся фото и вебдрайвер.

Так как мы будем сохранять фото с помощью библиотеки requests, нам необходимо определить заголовки для запроса. Поэтому добавим User-Agent, Accept и Host. Кстати, из этого также следует, что если вам удалось получить ссылку на фото, то она будет работать и без авторизации.

Python:
def find_photo(source: str, name: str, driver: webdriver) -> None:
    headers = {
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 '
                      'Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
                  'application/signed-exchange;v=b3;q=0.7',
        'Host': 'loveplanet.ru'
    }

Создаем объект BeautifulSoup, в который передаем код страницы анкеты и указываем парсер. После этого необходимо получить ссылку на альбом. Так как под основной фотографией пользователя располагается блок, в котором находятся превью фото, логично было бы парсить ссылки оттуда. Но, у некоторых пользователей нет этого блока, у кого-то там одна, основная фотография, а у кого-то так много фото, что дополнительные скрыты под спойлером. Обрабатывать каждый случай не имеет смысла. Поэтому, будем забирать ссылку из основной фотографии пользователя, которая также подтягивается из альбома. И вот уже здесь обработаем три случая с помощью блоков try – except. Дело в том, что есть два типа аккаунтов. Обычные и «элитные». Ну, или те, которые продвигаются системой. Они на странице поиска выделены желтой рамкой. В этом случае теги на странице будут слегка отличаться. Поэтому, для начала ищем обычное фото.

12.png

Затем, если тегов не найдено, обрабатываем исключение и пытаемся найти «элитный».

13.png

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

После этого обрабатываем ссылку на фото, а это будет именно она, удалив последнюю часть после «/». Добавляем домен и переходим по сформированной ссылке на страницу альбома. После чего ждем полсекунды.

Python:
    soup = BeautifulSoup(source, 'lxml')
    try:
        user_info_left = soup.find('div', class_='user-info_left').find('div', class_='prof-photo p_rel mb15'). \
            find('a', class_='block')['href']
    except (AttributeError, TypeError):
        try:
            user_info_left = soup.find('div', class_='user-info_left'). \
                find('div', class_='frame-elite prof-photo p_rel mb15').find('a', class_='block')['href']
        except (AttributeError, TypeError):
            return
    user_info_left = f'https://loveplanet.ru{"/".join(user_info_left.split("/")[:-1])}'
    driver.get(user_info_left)
    time.sleep(0.5)

Дальше, после того, как мы перейдем на страницу альбома, собираем ссылки на все фото в нем. Также создаем объект BeautifulSoup, передаем в него код страницы альбома и парсер. Ищем все теги со ссылками на фото. После в цикле пробегаемся по ним, забираем ссылки из тегов и добавляем в список для последующей обработки.

Python:
    sp = BeautifulSoup(driver.page_source, 'lxml')
    ul = sp.find('ul', class_='reset album_photo_list lifl ovh').find_all('li', class_='p_rel')
    photo_links = []
    for li in ul:
        ph = li.find('div', class_='img_clr tdbox').find('a')['href']
        photo_links.append(f'https://loveplanet.ru{ph}')

Однако, полученные ссылки, это еще не совсем ссылки на фото. Это скорее ссылки на тулбокс, куда и подгружаются ссылки на фото. Поэтому, браузеру необходимо перейти по каждой найденной ссылке. Запускаем цикл, итерируемся по полученным ссылкам на тулбоксы. Переходим по ссылке на страницу. Забираем ее код и передаем в BeautifulSoup. Затем ищем тег, который содержит ссылку на фото. Ну, а далее выполняем запрос по найденной ссылке и загружаем ее на диск в указанную директорию, перед этим, на всякий случай пишем код, который создает эту директорию. В функции Path хорошо то, что она самостоятельно обрабатывает исключения, если директория существует, при указании соответствующего параметра: exist_ok=True. Ну и, собственно, обрабатываем статус-коды и возникшие исключения.

Python:
    for n, photo in enumerate(photo_links):
        driver.get(photo)
        sp_ph = BeautifulSoup(driver.page_source, 'lxml')
        link = sp_ph.find('div', class_='gnl_photo_show tdbox').find('img')['src']
        try:
            res = requests.get(url=link, headers=headers)
            if res.status_code == 200:
                path = Path.cwd() / 'photo'
                path.mkdir(exist_ok=True)
                path = path / name
                path.mkdir(exist_ok=True)
                with open(path / f'photo_{n+1}.jpg', 'wb') as pht:
                    pht.write(res.content)
            else:
                continue
        except Exception:
            continue

Python:
def find_photo(source: str, name: str, driver: webdriver) -> None:
    """
    Поиск фото. Сохранение на диск.
    """
    headers = {
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 '
                      'Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
                  'application/signed-exchange;v=b3;q=0.7',
        'Host': 'loveplanet.ru'
    }
    soup = BeautifulSoup(source, 'lxml')
    """Поиск ссылок на альбом пользователя."""
    try:
        user_info_left = soup.find('div', class_='user-info_left').find('div', class_='prof-photo p_rel mb15'). \
            find('a', class_='block')['href']
    except (AttributeError, TypeError):
        try:
            user_info_left = soup.find('div', class_='user-info_left'). \
                find('div', class_='frame-elite prof-photo p_rel mb15').find('a', class_='block')['href']
        except (AttributeError, TypeError):
            return
    """Переход по найденным ссылкам на альбомы. Поиск ссылок на фото."""
    user_info_left = f'https://loveplanet.ru{"/".join(user_info_left.split("/")[:-1])}'
    driver.get(user_info_left)
    time.sleep(0.5)
    sp = BeautifulSoup(driver.page_source, 'lxml')
    ul = sp.find('ul', class_='reset album_photo_list lifl ovh').find_all('li', class_='p_rel')
    photo_links = []
    for li in ul:
        ph = li.find('div', class_='img_clr tdbox').find('a')['href']
        photo_links.append(f'https://loveplanet.ru{ph}')
    """Загрузка ссылок на диск."""
    for n, photo in enumerate(photo_links):
        driver.get(photo)
        sp_ph = BeautifulSoup(driver.page_source, 'lxml')
        link = sp_ph.find('div', class_='gnl_photo_show tdbox').find('img')['src']
        try:
            res = requests.get(url=link, headers=headers)
            if res.status_code == 200:
                path = Path.cwd() / 'photo'
                path.mkdir(exist_ok=True)
                path = path / name
                path.mkdir(exist_ok=True)
                with open(path / f'photo_{n+1}.jpg', 'wb') as pht:
                    pht.write(res.content)
            else:
                continue
        except Exception:
            continue


Вот в принципе и все. После того, как написан код, запустим получившийся скрипт и посмотрим, что он нам насобирает.

16.png

Конечно же, это далеко не все, что можно загрузить. Я собрал несколько анкет и прервал работу скрипта. В результате такого прерывания был создан файл с уже просмотренными анкетами. Таким образом, в следующий раз данные анкеты обрабатываться не будут.

17.png

К чему этот код? Просто, для того, чтобы попрактиковаться в парсинге данных. Ну и это просто весело.

Python:
# pip install selenium bs4 lxml requests

import os
import sys
import time
from pathlib import Path
from platform import system

import requests
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By

from set_love import login, password

options = Options()
# options.add_argument("--headless")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--disable-notifications")

exec_path = os.path.join(os.getcwd(), 'driver', 'chromedriver.exe') if system() == "Windows" else \
    os.path.join(os.getcwd(), 'driver', 'chromedriver')

all_link = set()


def logging(driver) -> None:
    """
    Авторизация на сайте. Если часто авторизоваться выскакивает капча. В этом случае
    предоставляется время - 5 секунд, чтобы ее ввести.
    """

    """Поиск полей. Заполнение паролем и логином."""
    driver.get('https://loveplanet.ru/a-logon')
    time.sleep(0.5)
    field_login = driver.find_element(By.CSS_SELECTOR, '#dlg_login_log')
    field_login.send_keys(login)
    time.sleep(0.2)
    field_pass = driver.find_element(By.CSS_SELECTOR, '#dlg_login_pas')
    field_pass.send_keys(password)
    try:
        """Если нет капчи, просто нажатие на кнопку вход"""
        button = driver.find_element(By.CSS_SELECTOR, '#dlg_login > ul > li:nth-child(4) > button')
        button.click()
        time.sleep(1)
    except NoSuchElementException:
        """Если есть капча, пауза, затем нажатие на вход"""
        field_capt = driver.find_element(By.CSS_SELECTOR, '#dlg_pict_val')
        field_capt.click()
        time.sleep(5)
        button = driver.find_element(By.CSS_SELECTOR, '#dlg_login > ul > li:nth-child(5) > button')
        button.click()
        time.sleep(1)


def find_pages(source: str, driver: webdriver, num: int) -> int:
    """
    Поиск ссылок на страницы пользователей.
    """
    global all_link
    """Поиск ссылок. Проверка на наличие в уже проверенных. Если есть - пропуск"""
    soup = BeautifulSoup(source, 'lxml')
    if links := soup.find_all('div', class_='to_els fbold'):
        names = []
        pages = []
        for link in links:
            name = link.find('a').text.strip()
            page = f"https://loveplanet.ru{link.find('a')['href']}"
            if page in all_link:
                print("Просмотрено")
                continue
            else:
                pages.append(page)
                names.append(name)
        print(f'Обработка страниц:')
        """Переход на страницу пользователя. Сбор информации.
        запуск функции поиска фото"""
        for n, pg in enumerate(pages):
            print(f'[{n + 1}].{names[n]} | {pg}')
            driver.get(pg)
            name_age = find_data(driver.page_source, f"{names[n]}_{num}", pg)
            time.sleep(1)
            find_photo(driver.page_source, name_age, driver)
            all_link.add(pg)
        return len(pages)


def find_data(source: str, name: str, url: str) -> str:
    """
    Поиск и сбор информации со страницы пользователя. Возвращает имя и возраст.
    """

    """Поиск имени и информации "Я ищу..."""""
    data = []
    soup = BeautifulSoup(source, 'lxml')
    name_age = soup.find('div', class_='flex ai-center mb24').find('span', class_='fbold fsize20').text.strip()
    data.append(name_age)
    location = soup.find('div', class_='flex ai-center jc-between').find('div', class_='blue_14').find('span').text. \
        strip()
    data.append(f'{location}\n')
    su = soup.find('div', class_='fbold fsize15 mb6').text.strip()
    ifind = soup.find('ul', class_='reset fsize15 mt7').find('li').text.strip()
    data.append(f'{su}: {ifind}')
    """Поиск доступной личной информации"""
    try:
        data.append(f"\n{soup.find('div', class_='fbold fsize15 mb7').text.strip()}:")
        info = soup.find('ul', class_='reset list_info show li_mt10').find_all('li')
        for inf in info:
            try:
                lb = f"{inf.find('label').text.strip()} {inf.find('div').text.strip()}"
                data.append(lb)
            except Exception:
                continue
    except Exception:
        pass
    data.append(f'\n{url}')
    print(data)
    """Проверка, если список с данными не пуст, сохраняем их в текстовый документ"""
    if data:
        path = Path.cwd() / 'photo'
        path.mkdir(exist_ok=True)
        path = path / f'{name_age}_{name}'
        path.mkdir(exist_ok=True)
        with open(path / f'{name}.txt', 'w', encoding='utf-8') as file:
            for dat in data:
                file.write(f'{dat}\n')
    return f'{name_age}_{name}'


def find_photo(source: str, name: str, driver: webdriver) -> None:
    """
    Поиск фото. Сохранение на диск.
    """
    headers = {
        'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 '
                      'Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,'
                  'application/signed-exchange;v=b3;q=0.7',
        'Host': 'loveplanet.ru'
    }
    soup = BeautifulSoup(source, 'lxml')
    """Поиск ссылок на альбом пользователя."""
    try:
        user_info_left = soup.find('div', class_='user-info_left').find('div', class_='prof-photo p_rel mb15'). \
            find('a', class_='block')['href']
    except (AttributeError, TypeError):
        try:
            user_info_left = soup.find('div', class_='user-info_left'). \
                find('div', class_='frame-elite prof-photo p_rel mb15').find('a', class_='block')['href']
        except (AttributeError, TypeError):
            return
    """Переход по найденным ссылкам на альбомы. Поиск ссылок на фото."""
    user_info_left = f'https://loveplanet.ru{"/".join(user_info_left.split("/")[:-1])}'
    driver.get(user_info_left)
    time.sleep(0.5)
    sp = BeautifulSoup(driver.page_source, 'lxml')
    ul = sp.find('ul', class_='reset album_photo_list lifl ovh').find_all('li', class_='p_rel')
    photo_links = []
    for li in ul:
        ph = li.find('div', class_='img_clr tdbox').find('a')['href']
        photo_links.append(f'https://loveplanet.ru{ph}')
    """Загрузка ссылок на диск."""
    for n, photo in enumerate(photo_links):
        driver.get(photo)
        sp_ph = BeautifulSoup(driver.page_source, 'lxml')
        link = sp_ph.find('div', class_='gnl_photo_show tdbox').find('img')['src']
        try:
            res = requests.get(url=link, headers=headers)
            if res.status_code == 200:
                path = Path.cwd() / 'photo'
                path.mkdir(exist_ok=True)
                path = path / name
                path.mkdir(exist_ok=True)
                with open(path / f'photo_{n+1}.jpg', 'wb') as pht:
                    pht.write(res.content)
            else:
                continue
        except Exception:
            continue


def save_data():
    global all_link
    with open('links_people.txt', 'w', encoding='utf-8') as file:
        for lin in all_link:
            file.write(f'{lin}\n')


def find_people(tage: int, bage: int, spol: int, pol: int, driver: webdriver) -> None:
    """
    Переход по страницам сайта. Запуск функции поиска страниц.
    """
    num = 0
    link_count = 0
    while True:
        try:
            url = f'https://loveplanet.ru/a-search/d-1/pol-{pol}/spol-{spol}/tage-{tage}/bage-{bage}/purp-0/proof-1/country-0/region-0/city-0/p-{num}'
            driver.get(url)
            time.sleep(1)
            count = find_pages(driver.page_source, driver, num)
            if count == 1:
                link_count += 1
            if link_count >= 2:
                save_data()
                break
            num += 1
        except Exception as ex:
            print(ex)
            save_data()
            break
    print("All pages down")


def main() -> None:
    """
    Запрос параметров поиска у пользователя. Проверка полученных параметров.
    Загрузка просмотренных ссылок и добавление их в множество для будущих проверок.
    Запуск функции авторизации и перехода по страницам.
    Если закрыт браузер или выполнено прерывание по "Ctrl + C" сохраняется информация
    о просмотренных страницах.
    """
    global all_link
    driver = None
    try:
        if Path('links_people.txt').exists():
            with open('links_people.txt', 'r', encoding='utf-8') as file:
                for ln in file.readlines():
                    all_link.add(ln.strip())
        spol = input('Выберите пол для поиска фото:\n   [1] Мужчины\n   [2] Женщины\n   >>> ')
        if spol not in ["1", "2"]:
            print('Введите корректный пол!')
            sys.exit(0)
        tage = input("Введите возраст до (max=100): ")
        if not tage.isdigit() or int(tage) > 100:
            print("Вы ввели не число. Перезапустите программу!")
            sys.exit(0)
        bage = input("Введите возраст от (min=18): ")
        if not bage.isdigit() or int(bage) < 18:
            print("Вы ввели слишком юный возраст! Перезапустите программу!")
            sys.exit(0)

        driver = webdriver.Chrome(options=options, service=Service(log_path=os.devnull, executable_path=exec_path))

        driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
            'source': '''
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
                    delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
              '''
        })

        logging(driver)
        if spol == "1":
            find_people(int(tage), int(bage), 1, 2, driver)
        elif spol == "2":
            find_people(int(tage), int(bage), 2, 1, driver)
    except KeyboardInterrupt:
        driver.close()
        driver.quit()
        save_data()
    driver.close()
    driver.quit()


if __name__ == "__main__":
    main()

А на этом, пожалуй, все.

Спасибо за внимание. Надеюсь, данная информация будет вам полезна
 

Вложения

  • script.zip
    4,4 КБ · Просмотры: 138
Последнее редактирование модератором:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!