Загрузка данных
import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
from bs4 import BeautifulSoup
import re
# ─────────────────────────────────────────────
# Вспомогательные функции
# ─────────────────────────────────────────────
def safe_click(driver, element):
"""Клик через JS — обходит перекрытия другими элементами."""
driver.execute_script("arguments[0].scrollIntoView({block:'center'});", element)
time.sleep(0.3)
driver.execute_script("arguments[0].click();", element)
def open_composition_tab(driver, wait):
"""
Открывает вкладку/аккордеон 'Состав' на странице товара.
Возвращает True, если успешно кликнули.
"""
# Стратегия 1: data-test-id (самый надёжный на ЗЯ)
try:
btn = wait.until(EC.presence_of_element_located((
By.XPATH,
"//*[@data-test-id='pdp-accordion-ingredients' "
"or @data-test-id='accordion-ingredients']"
)))
safe_click(driver, btn)
return True
except Exception:
pass
# Стратегия 2: любой кликабельный элемент с текстом «Состав»
try:
candidates = driver.find_elements(
By.XPATH,
"//*[self::button or self::div or self::span or self::p]"
"[contains(normalize-space(text()), 'Состав')]"
)
for el in candidates:
if el.is_displayed():
safe_click(driver, el)
time.sleep(0.5)
return True
except Exception:
pass
# Стратегия 3: поиск по aria-label / title
try:
btn = driver.find_element(
By.XPATH,
"//*[contains(@aria-label,'остав') or contains(@title,'остав')]"
)
safe_click(driver, btn)
return True
except Exception:
pass
return False
def extract_price(soup):
"""
Перебирает несколько вариантов разметки цены ЗЯ.
Возвращает строку с числом или '—'.
"""
# Актуальные селекторы (проверены на живом сайте, июнь 2025)
selectors = [
# data-test-id — самый стабильный
"[data-test-id='pdp-price-current']",
"[data-test-id='price-current']",
"[data-test-id='product-price']",
# классы
".wdp-price_type_current",
".pdp-price__current",
".price__current",
".price-value",
# fallback: первый элемент с ₽
]
for sel in selectors:
el = soup.select_one(sel)
if el:
raw = el.get_text(" ", strip=True)
# Оставляем только цифры и разделители
digits = re.sub(r"[^\d]", "", raw)
if digits:
return digits + " ₽"
# Последний шанс: ищем любой текст с ₽
for tag in soup.find_all(string=re.compile(r"\d[\d\s]*₽")):
digits = re.sub(r"[^\d]", "", tag)
if digits:
return digits + " ₽"
return "—"
def extract_volume(soup):
"""
Ищет объём/вес/количество на странице товара.
Возвращает строку или '—'.
"""
# Стратегия 1: data-test-id
for test_id in ("pdp-volume", "product-volume", "pdp-size", "product-size"):
el = soup.select_one(f"[data-test-id='{test_id}']")
if el:
return el.get_text(" ", strip=True)
# Стратегия 2: паттерн «число + единица измерения» в заголовке или описании
# Ищем в h1, подзаголовках, span-ах рядом с названием
volume_re = re.compile(
r"(\d+[\.,]?\d*)\s*(мл|л|г|кг|мг|oz|ml|fl\.oz|шт\.?|пары?)",
re.IGNORECASE
)
# Приоритет: специальные блоки описания
for tag in soup.select(
"[class*='volume'], [class*='size'], [class*='weight'], "
"[class*='amount'], [class*='capacity']"
):
m = volume_re.search(tag.get_text())
if m:
return m.group(0)
# В h1 объём часто пишут через запятую или в скобках
h1 = soup.find("h1")
if h1:
m = volume_re.search(h1.get_text())
if m:
return m.group(0)
return "—"
def extract_composition(soup):
"""
Ищет блок состава ПОСЛЕ того, как вкладка уже была открыта.
Возвращает строку или '—'.
"""
# Стратегия 1: data-test-id блока состава
for test_id in (
"pdp-ingredients-content",
"accordion-ingredients-content",
"pdp-accordion-ingredients-content",
"ingredients",
):
el = soup.select_one(f"[data-test-id='{test_id}']")
if el:
text = el.get_text(" ", strip=True)
if len(text) > 10:
return text
# Стратегия 2: ищем заголовок «Состав», потом смотрим соседей/детей
headers = soup.find_all(
string=re.compile(r"^\s*Состав\s*$", re.I)
)
for header in headers:
parent = header.find_parent()
if not parent:
continue
# Ищем следующий sibling с текстом
for sibling in parent.find_next_siblings():
text = sibling.get_text(" ", strip=True)
if len(text) > 20:
# Убираем сам заголовок из текста
text = re.sub(r"^Состав\s*", "", text, flags=re.I).strip()
return text
# Или внутри родительского контейнера
grandparent = parent.find_parent()
if grandparent:
text = grandparent.get_text(" ", strip=True)
text = re.sub(r"^.*?Состав\s*", "", text, flags=re.I | re.DOTALL).strip()
if len(text) > 20:
return text[:2000] # Ограничиваем длину
# Стратегия 3: поиск по классам
for sel in (
"[class*='ingredient']",
"[class*='composition']",
"[class*='consist']",
):
el = soup.select_one(sel)
if el:
text = el.get_text(" ", strip=True)
if len(text) > 20:
return text
return "—"
# ─────────────────────────────────────────────
# Основной скрипт
# ─────────────────────────────────────────────
def run_scraper():
options = uc.ChromeOptions()
options.add_argument("--incognito")
driver = uc.Chrome(options=options)
driver.maximize_window()
wait = WebDriverWait(driver, 15)
# ── 1. Открываем страницу каталога ────────────────────────────────────
driver.get(
"https://goldapple.ru/uhod/uhod-za-licom/osnovnoj-uhod"
"?calculatedprices=87-250&producttype=398"
)
print("Авторизуйся, прокрути список до конца. Нажми ENTER в консоли.")
input("Жду сигнала... ")
# ── 2. Сбор ссылок на товары ───────────────────────────────────────────
soup = BeautifulSoup(driver.page_source, "html.parser")
product_links = []
for a in soup.find_all("a", href=True):
href = a["href"]
# Ссылки вида /19000168838-nivea-creme (начинаются с длинного числа)
if re.match(r"^/\d{5,}-", href):
full_url = "https://goldapple.ru" + href
if full_url not in product_links:
product_links.append(full_url)
print(f"Найдено товаров для парсинга: {len(product_links)}")
if not product_links:
print("Ссылки не найдены. Проверь, прокрутил ли страницу до конца.")
driver.quit()
return
# ── 3. Парсинг каждого товара ──────────────────────────────────────────
with open("goldapple_products.txt", "w", encoding="utf-8") as f:
for i, url in enumerate(product_links, start=1):
try:
driver.get(url)
# Ждём загрузки h1 — признак того, что страница готова
try:
wait.until(EC.presence_of_element_located((By.TAG_NAME, "h1")))
except Exception:
time.sleep(4)
# Пробуем открыть вкладку «Состав»
opened = open_composition_tab(driver, wait)
if opened:
# Даём время на анимацию раскрытия аккордеона
time.sleep(1.5)
else:
print(f" [!] Вкладка 'Состав' не найдена: {url}")
ps = BeautifulSoup(driver.page_source, "html.parser")
# ── Название ──────────────────────────────────────────────
h1 = ps.find("h1")
name = h1.get_text(" ", strip=True) if h1 else "—"
# ── Цена ──────────────────────────────────────────────────
price = extract_price(ps)
# ── Объём ─────────────────────────────────────────────────
volume = extract_volume(ps)
# ── Состав ────────────────────────────────────────────────
composition = extract_composition(ps)
# ── Запись ────────────────────────────────────────────────
line = (
f"{name} | "
f"Цена: {price} | "
f"Объём: {volume} | "
f"Состав: {composition}"
)
f.write(line + "\n")
# Сбрасываем буфер сразу — данные не потеряются при сбое
f.flush()
print(f"[{i}/{len(product_links)}] {name} | {price} | {volume}")
except Exception as e:
print(f"Ошибка на {url}: {e}")
# Записываем URL с ошибкой, чтобы можно было перепарсить
f.write(f"ОШИБКА | {url}\n")
f.flush()
print("Готово! Файл goldapple_products.txt перезаписан.")
driver.quit()
if __name__ == "__main__":
run_scraper()