Мой скрипт занимает около 13 секунд на каждую веб-страницу. Что я могу сделать, чтобы ускорить это?

Мне нужно отфильтровать все веб-страницы с 0 списками на Grailed. Мне нужно просмотреть более 500 тысяч URL-адресов. Я использую Python и Selenium. Моя проблема заключается в том, что для каждой новой веб-страницы сценарию необходимо щелкнуть всплывающее окно с файлом cookie и входом пользователя, чтобы получить доступ к количеству списков. В результате обработка каждой веб-страницы занимает около 13 секунд. Для 500 тысяч URL-адресов это займет 75 дней.

Ссылка на пример: https://www.grailed.com/designers/acne-studios/casual-pants

Все 500 тысяч ссылок: https://www.grailed.com/designers/designer-name/category-name

Два подхода, которые я выясняю:

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

  2. Запускайте несколько экземпляров одновременно, желательно между 13 (~2 недели) и 130 (~14 часами). Однако я не уверен, будет ли это дорого и как избежать блокировки. нужно ли мне использовать для этого прокси?

Пожалуйста, скажите мне, если я что-то упускаю. Мой код:

import pandas as pd
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException, ElementClickInterceptedException
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.action_chains import ActionChains
import os
import time

# Update the PATH environment variable
os.environ['PATH'] += r";C:\Users\rafme\Desktop\Selenium Drivers"

# Read the CSV file
BrandCategoryLinks = pd.read_csv('C:/Users/rafme/Downloads/Test Brands & Categories.csv')

FilteredCategoryLink = []

# Loop through each link in the DataFrame
for index, link in BrandCategoryLinks.iterrows():
    driver = None
    try:
        base_url = link['Links']
        chrome_options = webdriver.ChromeOptions()
        chrome_options.add_argument("--disable-gpu")  # Disable GPU usage
        chrome_options.add_argument("--no-sandbox")  # Disable sandboxing
        chrome_options.add_argument("--disable-dev-shm-usage")  # Disable shared memory usage
        chrome_options.add_argument("--window-size=1920x1080")  # Set the window size
        chrome_options.add_argument("--headless")
        chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36")
        service = Service(r"C:\Users\rafme\Desktop\Selenium Drivers\chromedriver.exe")
        driver = webdriver.Chrome(service=service, options=chrome_options)

        driver.get(base_url)

        timeout = 60  # Increase timeout

        try:
            WebDriverWait(driver, timeout).until(EC.presence_of_element_located((By.ID, "onetrust-reject-all-handler")))
            reject_button = driver.find_element(By.ID, "onetrust-reject-all-handler")

            # Scroll the element into view using JavaScript
            driver.execute_script("arguments[0].scrollIntoView(true);", reject_button)
            time.sleep(2)  # Wait for the scrolling to complete

            # Click the element
            reject_button.click()
            time.sleep(1)
            reject_button.click()
            time.sleep(1)
        except (NoSuchElementException, ElementClickInterceptedException):
            pass
        except Exception as e:
            print(f"Error occurred: {e}")
            continue

        # Close the user login modal if it exists
        try:
            elem = driver.find_element(By.XPATH, "//div[@class='Modal-Content']")
            ac = ActionChains(driver)
            ac.move_to_element(elem).move_by_offset(250, 0).click().perform()  # clicking away from login window
        except NoSuchElementException:
            pass
        except Exception as e:
            print(f"Error clicking 'User Authentication' button: {e}")
            continue

        # Check listing count
        try:
            listing_count = driver.find_elements(By.XPATH,
                                                 "//div[@class='FiltersInstantSearch']//div[@class='feed-item']")
            if len(listing_count) > 1:
                print(f"Found {len(listing_count)} listings on {base_url}")
                FilteredCategoryLink.append(base_url)
            else:
                print(f"Found {len(listing_count)} listings on {base_url}, not enough to keep.")
        except Exception as e:
            print(f"Error finding listings: {e}")
            continue

    except Exception as e:
        print(f"Error processing link {link}: {e}")
    finally:
        if driver:
            driver.quit()

# Save the filtered categories to CSV
filtered_categories = pd.DataFrame(FilteredCategoryLink, columns=['Link'])
filtered_categories.to_csv('filtered_categories.csv', index=False)

Обновлять:

Большое спасибо InspectorG4adget и xoxouser.

Я отредактировал код xoxouser так, чтобы 1. отфильтровать все подкатегории с количеством списков менее 25 и 2. распараллелить с 10 потоками.

Менее чем через 10 минут я собрал CSV-файл с примерно 20 тысячами имен дизайнеров, подкатегориями и количеством списков. Именно то, что мне было нужно, но в стиле xoxouser: в 10800 раз быстрее ;)

Обновленный код:

import requests
import json
import csv
from urllib.parse import quote
from concurrent.futures import ThreadPoolExecutor, as_completed

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36',
    'X-Algolia-Api-Key': 'bc9ee1c014521ccf312525a4ef324a16',
    'X-Algolia-Application-Id': 'MNRWEFSS2Q'
}

url_designers = 'https://www.grailed.com/api/designers'
req_designers = requests.get(url_designers, headers=headers)
designers = json.loads(req_designers.text)['data']

url_api = 'https://mnrwefss2q-dsn.algolia.net/1/indexes/*/queries'
data = []

def fetch_designer_data(des):
    facetFilters = quote(f'[["designers.name:{des["name"]}"]]')
    facets = quote('["category_path"]')
    payload = '{"requests":[{"indexName": "Listing_by_low_price_production", "params": "maxValuesPerFacet=200&hitsPerPage=0&facetFilters=%s&facets=%s"}]}' % (facetFilters, facets)
    req = requests.post(url_api, headers=headers, data=payload)
    listings = json.loads(req.text)['results'][0]['facets']
    designer_data = []
    if 'category_path' in listings:
        for category_path, nr_listings in listings['category_path'].items():
            if nr_listings >= 25:
                designer_data.append({
                    'designer_name': des['name'],
                    'category_path': category_path,
                    'nr_listings': nr_listings
                })
    return designer_data

with ThreadPoolExecutor(max_workers=10) as executor:
    futures = {executor.submit(fetch_designer_data, des): des for des in designers}
    for i, future in enumerate(as_completed(futures)):
        designer_data = future.result()
        data.extend(designer_data)
        if (i + 1) % 10 == 0:
            print(f"Processed {i + 1} designers")

# Define the CSV file path
csv_file_path = 'designers_listings_all.csv'

# Write data to CSV
with open(csv_file_path, 'w', newline='', encoding='utf-8') as csvfile:
    fieldnames = ['designer_name', 'category_path', 'nr_listings']
    writer = csv.DictWriter(csvfile, fieldnames=fieldnames)

    writer.writeheader()
    for row in data:
        writer.writerow(row)

print(f"Data successfully written to {csv_file_path}")

Используйте определенный профиль Chrome в Selenium (в который вы входите заранее и принимаете файлы cookie), чтобы всплывающие окна не беспокоили вас. Держите браузер (селен) открытым, прямо сейчас вы повторно открываете браузер для каждой ссылки в BrandCategoryLinks. Не загружайте ненужные данные (изображения,...)

MafMal 18.06.2024 16:04

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

inspectorG4dget 18.06.2024 16:05

@inspectorG4dget, не могли бы вы предоставить более подробную информацию о том, что нужно сделать OP, чтобы создать запрос API к нужной конечной точке с правильными данными?

ryyyn 18.06.2024 17:47

@ryyyn: Посмотрите на вкладку «Сеть» в консоли разработчика вашего браузера. Посмотрите на вкладку XHR и посмотрите, какие вызовы API выполняются. Скопируйте запрос на связывание как запрос на завивку и реплицируйте его с помощью модуля Python requests.

inspectorG4dget 18.06.2024 18:24

Кажется, это часть более серьезной проблемы. Уточните, пожалуйста, как вы собираетесь использовать FilteredCategoryLink и где вы взяли CSV-файл.

GTK 18.06.2024 23:47

если вам нужно просмотреть 500 тысяч URL-адресов, что вы на самом деле делаете? Почему вы очищаете эти URL-адреса (т. е. какую задачу вам нужно выполнить, которая, по вашему мнению, требует очистки такого количества URL-адресов)?

Mike 'Pomax' Kamermans 20.06.2024 01:54

Найдите профилировщик и проверьте, где код проводит свое время. Вероятно, в сетевом вводе-выводе, но, возможно, вы найдете что-то еще, что легко оптимизировать. Если это сетевой ввод-вывод, изучите асинхронное программирование. Если вам нужно только чтение/запись CSV, Pandas может быть излишним, а стандартный CSV Python может быть более легким.

Robert 20.06.2024 02:24
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
7
116
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как предложено в комментариях, лучше получать данные через API, используя библиотеку Python requests.

На веб-сайте в настоящее время около 12 тысяч дизайнеров и 128 подкатегорий, что дает до 1,5 миллиона точек данных. Вот 3 шага, которые помогут значительно ускорить процесс:

  1. Изучив вызовы API, я нашел способ создать один запрос, который будет возвращать несколько списков на каждую подкатегорию. Таким образом, от первоначальных 500 тысяч URL-адресов мы сократили количество запросов до 12 тысяч (общее количество дизайнеров). Увеличение пропускной способности в 41 раз.
  2. Более того, при использовании requests на запрос требуется всего ~0,3 секунды, что является дополнительным улучшением в 43 раза по сравнению с 13 секундами.
  3. Наконец, вы можете запустить приведенный ниже код в нескольких потоках. Я попытался запустить 100 параллельных потоков без каких-либо проблем с сервером. Теоретически это может привести к 100-кратному улучшению. Осторожно, вы рискуете занести свой IP в черный список.

Объединение этих вещей вместе привело к увеличению скорости примерно в 180 000 раз по сравнению с исходной реализацией. Другими словами, получение всех данных занимает чуть больше минуты.

А если этого все еще недостаточно, вы можете добавить прокси на шаге №4. Надеюсь, это даст некоторую полезную информацию.

import requests
import json
from urllib.parse import quote

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36',
           'X-Algolia-Api-Key': 'bc9ee1c014521ccf312525a4ef324a16',
           'X-Algolia-Application-Id': 'MNRWEFSS2Q'}

url_designers = 'https://www.grailed.com/api/designers'
req_designers = requests.get(url_designers, headers=headers)
designers = json.loads(req_designers.text)['data']

url_api = 'https://mnrwefss2q-dsn.algolia.net/1/indexes/*/queries'
data = []

for des in designers:
    facetFilters = quote(f'[["designers.name:{des['name']}"]]')
    facets = quote('["category_path"]')
    payload = '{"requests":[{"indexName": "Listing_by_low_price_production", "params": "maxValuesPerFacet=200&hitsPerPage=0&facetFilters=%s&facets=%s"}]}' % (facetFilters, facets)
    req = requests.post(url_api, headers=headers, data=payload)
    listings = json.loads(req.text)['results'][0]['facets']
    if 'category_path' in listings:
        data.append({des['name']: listings['category_path']})
    else:
        data.append({des['name']: {}})

data вывод выглядит так:

[
...
 {'Acne Studios': {'bottoms.denim': 4644, 'tops.sweaters_knitwear': 2266, 'tops.sweatshirts_hoodies': 1658, 'womens_bottoms.jeans': 1122, 'tops.short_sleeve_shirts': 1087, 'bottoms.casual_pants': 1078, 'tops.button_ups': 960, 'outerwear.light_jackets': 591, 'womens_tops.sweaters': 557, 'footwear.lowtop_sneakers': 331, 'tops.long_sleeve_shirts': 295, 'outerwear.heavy_coats': 289, 'bottoms.shorts': 288, 'outerwear.denim_jackets': 279, 'outerwear.bombers': 257, 'womens_bottoms.pants': 211, 'outerwear.leather_jackets': 207, 'accessories.hats': 188, 'womens_tops.sweatshirts': 160, 'womens_tops.short_sleeve_shirts': 159, 'tailoring.blazers': 142, 'womens_footwear.boots': 140, 'outerwear.parkas': 122, 'womens_dresses.midi': 116, 'bottoms.sweatpants_joggers': 108, 'tops.polos': 107, 'accessories.gloves_scarves': 102, 'footwear.hitop_sneakers': 94, 'womens_outerwear.jackets': 91, 'womens_tops.blouses': 90, 'womens_outerwear.coats': 86, 'footwear.boots': 83, 'womens_tops.button_ups': 80, 'bottoms.cropped_pants': 76, 'tops.sleeveless': 74, 'womens_dresses.mini': 65, 'womens_footwear.lowtop_sneakers': 65, 'accessories.bags_luggage': 64, 'womens_tops.long_sleeve_shirts': 64, 'womens_outerwear.denim_jackets': 60, 'accessories.sunglasses': 57, 'womens_outerwear.blazers': 55, 'footwear.leather': 53, 'womens_accessories.scarves': 53, 'womens_outerwear.leather_jackets': 52, 'womens_bottoms.mini_skirts': 47, 'womens_bottoms.midi_skirts': 44, 'tailoring.suits': 41, 'womens_dresses.maxi': 41, 'womens_accessories.hats': 40, 'womens_tops.hoodies': 40, 'womens_tops.tank_tops': 38, 'womens_bottoms.shorts': 37, 'outerwear.vests': 35, 'womens_outerwear.bombers': 31, 'footwear.formal_shoes': 29, 'womens_footwear.heels': 29, 'accessories.jewelry_watches': 25, 'tailoring.formal_trousers': 24, 'womens_tops.crop_tops': 22, 'womens_tops.polos': 22, 'outerwear.raincoats': 19, 'womens_outerwear.down_jackets': 18, 'outerwear.cloaks_capes': 17, 'womens_accessories.miscellaneous': 17, 'womens_bags_luggage.shoulder_bags': 17, 'accessories.misc': 16, 'accessories.wallets': 16, 'footwear.slip_ons': 15, 'womens_footwear.sandals': 14, 'womens_accessories.sunglasses': 13, 'womens_bags_luggage.tote_bags': 12, 'womens_bottoms.joggers': 12, 'accessories.belts': 11, 'accessories.glasses': 11, 'womens_footwear.flats': 11, 'footwear.sandals': 10, 'tops.jerseys': 10, 'womens_footwear.hitop_sneakers': 10, 'womens_footwear.platforms': 9, 'womens_bottoms.leggings': 8, 'womens_bottoms.maxi_skirts': 8, 'accessories.socks_underwear': 7, 'bottoms.swimwear': 7, 'womens_accessories.belts': 7, 'womens_outerwear.vests': 7, 'bottoms.jumpsuits': 6, 'womens_footwear.slip_ons': 6, 'womens_bags_luggage.crossbody_bags': 5, 'womens_bottoms.sweatpants': 5, 'tailoring.vests': 4, 'womens_accessories.socks_intimates': 4, 'womens_accessories.wallets': 4, 'womens_bags_luggage.handle_bags': 4, 'womens_dresses.gowns': 4, 'accessories.periodicals': 3, 'accessories.ties_pocketsquares': 3, 'bottoms.leggings': 3, 'tailoring.formal_shirting': 3, 'womens_bags_luggage.clutches': 3, 'womens_bags_luggage.mini_bags': 3, 'womens_bags_luggage.other': 3, 'womens_jewelry.necklaces': 3, 'womens_outerwear.fur_faux_fur': 3, 'bottoms': 2, 'womens_bags_luggage.backpacks': 2, 'womens_bags_luggage.bucket_bags': 2, 'womens_bottoms.jumpsuits': 2, 'womens_footwear.mules': 2, 'womens_jewelry.bracelets': 2, 'womens_jewelry.earrings': 2, 'tailoring.tuxedos': 1, 'womens_accessories.glasses': 1, 'womens_accessories.hair_accessories': 1, 'womens_jewelry.body_jewelry': 1, 'womens_jewelry.rings': 1, 'womens_outerwear.rain_jackets': 1, 'womens_tops.bodysuits': 1}}, 
 {'A.Coba.Lt': {'footwear.boots': 1, 'tops.sweatshirts_hoodies': 1}},
 {'A Cold Wall': {'tops.short_sleeve_shirts': 293, 'tops.sweatshirts_hoodies': 280, 'footwear.lowtop_sneakers': 187, 'bottoms.sweatpants_joggers': 183, 'outerwear.light_jackets': 148, 'tops.long_sleeve_shirts': 133, 'accessories.bags_luggage': 129, 'bottoms.casual_pants': 108, 'tops.sweaters_knitwear': 71, 'accessories.hats': 61, 'outerwear.vests': 61, 'footwear.boots': 56, 'bottoms.shorts': 54, 'footwear.hitop_sneakers': 52, 'outerwear.heavy_coats': 48, 'tops.button_ups': 47, 'outerwear.raincoats': 30, 'accessories.belts': 24, 'bottoms.denim': 21, 'accessories.misc': 18, 'outerwear.denim_jackets': 18, 'outerwear.parkas': 17, 'outerwear.bombers': 14, 'accessories.gloves_scarves': 13, 'footwear.leather': 11, 'tops.polos': 11, 'footwear.slip_ons': 10, 'womens_bottoms.midi_skirts': 10, 'accessories.jewelry_watches': 8, 'accessories.sunglasses': 7, 'accessories.socks_underwear': 6, 'accessories.wallets': 6, 'tops.sleeveless': 6, 'bottoms.cropped_pants': 5, 'footwear.sandals': 5, 'bottoms.leggings': 4, 'outerwear.cloaks_capes': 4, 'tops.jerseys': 4, 'tailoring.blazers': 3, 'womens_bottoms.jeans': 3, 'womens_footwear.boots': 3, 'womens_footwear.hitop_sneakers': 3, 'accessories.periodicals': 2, 'footwear.formal_shoes': 2, 'womens_bottoms.shorts': 2, 'womens_outerwear.rain_jackets': 2, 'womens_tops.sweaters': 2, 'accessories.glasses': 1, 'bottoms.jumpsuits': 1, 'bottoms.swimwear': 1, 'tailoring.suits': 1, 'womens_bottoms.leggings': 1, 'womens_outerwear.vests': 1, 'womens_tops.button_ups': 1}}
...
]

Другие вопросы по теме