Мне нужно отфильтровать все веб-страницы с 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
Два подхода, которые я выясняю:
Попробуйте заблокировать файлы cookie и всплывающие окна для входа в систему. Однако я не уверен, возможно ли это без сохранения какого-либо профиля пользователя, после чего я беспокоюсь, что Grailed заблокирует меня.
Запускайте несколько экземпляров одновременно, желательно между 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}")
Кажется, что элементы загружаются даже без нажатия на модальное окно cookie. Кроме того, похоже, существует динамическая нагрузка - поэтому вам понадобится бесконечная прокрутка, прежде чем подсчитывать количество элементов. Похоже, он действительно затрагивает конечную точку API, которую вы можете имитировать с помощью запросов для повышения скорости. Наконец, распараллельте и используйте резидентные прокси.
@inspectorG4dget, не могли бы вы предоставить более подробную информацию о том, что нужно сделать OP, чтобы создать запрос API к нужной конечной точке с правильными данными?
@ryyyn: Посмотрите на вкладку «Сеть» в консоли разработчика вашего браузера. Посмотрите на вкладку XHR и посмотрите, какие вызовы API выполняются. Скопируйте запрос на связывание как запрос на завивку и реплицируйте его с помощью модуля Python requests.
Кажется, это часть более серьезной проблемы. Уточните, пожалуйста, как вы собираетесь использовать FilteredCategoryLink и где вы взяли CSV-файл.
если вам нужно просмотреть 500 тысяч URL-адресов, что вы на самом деле делаете? Почему вы очищаете эти URL-адреса (т. е. какую задачу вам нужно выполнить, которая, по вашему мнению, требует очистки такого количества URL-адресов)?
Найдите профилировщик и проверьте, где код проводит свое время. Вероятно, в сетевом вводе-выводе, но, возможно, вы найдете что-то еще, что легко оптимизировать. Если это сетевой ввод-вывод, изучите асинхронное программирование. Если вам нужно только чтение/запись CSV, Pandas может быть излишним, а стандартный CSV Python может быть более легким.






Как предложено в комментариях, лучше получать данные через API, используя библиотеку Python requests.
На веб-сайте в настоящее время около 12 тысяч дизайнеров и 128 подкатегорий, что дает до 1,5 миллиона точек данных. Вот 3 шага, которые помогут значительно ускорить процесс:
requests на запрос требуется всего ~0,3 секунды, что является дополнительным улучшением в 43 раза по сравнению с 13 секундами.Объединение этих вещей вместе привело к увеличению скорости примерно в 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}}
...
]
Используйте определенный профиль Chrome в Selenium (в который вы входите заранее и принимаете файлы cookie), чтобы всплывающие окна не беспокоили вас. Держите браузер (селен) открытым, прямо сейчас вы повторно открываете браузер для каждой ссылки в
BrandCategoryLinks. Не загружайте ненужные данные (изображения,...)