#!/usr/bin/env python3 # Собирает ссылки на товары из списка категорий IKEA (leaf_categories.txt) import re, sys, time, pathlib, requests, pandas as pd, html from urllib.parse import urljoin from bs4 import BeautifulSoup # нужен только для поиска "Pokaż więcej" from math import ceil import json, re, time, requests # ── константы ─────────────────────────────────────────────────────── PRODUCT_RE = re.compile( r'https?://www\.ikea\.com/pl/pl/p/[a-z0-9-]+-\d+/', re.I) BASE_DIR = pathlib.Path(__file__).resolve().parent BASE_DOMAIN = "https://www.ikea.com" LOCALE_PREFIX = "/pl/pl/" # ← смените при другой локали HEADERS = {"User-Agent": "Mozilla/5.0"} CAT_FILE = BASE_DIR / "leaf_categories.txt" OUT_TXT = BASE_DIR / "product_links.txt" OUT_XLSX = BASE_DIR / "product_links.xlsx" LOAD_PAUSE = 10 # задержка между страницами категории WAIT_BEFORE_PARSE = 5 SEARCH_URL = "https://sik.search.blue.cdtapps.com/pl/pl/search?c=listaf&v=20250507" # ──────────────────────────────────────────────────────────────────── def fetch_html(url, as_json=False): r = requests.get(url, headers=HEADERS, timeout=10) r.raise_for_status() return r.json() if as_json else r.text def extract_product_links(html_text: str) -> list[str]: """ Возвращает ВСЕ (может с повторами) абсолютные ссылки, начинающиеся с https://www.ikea.com/pl/pl/p/ """ return re.findall(r'https://www\.ikea\.com/pl/pl/p/[a-z0-9-]+-\d+/', html_text, flags=re.I) # NEW ──────────────────────────────────────────────────────────────── # — замените старую функцию этим вариантом — def product_links_from_category(cat_url: str) -> list[str]: """ Через POST-Search API вытаскивает все pipUrl категории. """ # --- достаём id категории из URL ---------------------------- m = re.search(r'-([0-9]+)/?$', cat_url.rstrip("/")) if not m: print(" ⚠️ не могу получить categoryId из URL") return [] cat_id = m.group(1) print (cat_id) all_links = [] offset = 0 size = 24 # сайт именно так запрашивает headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", "Content-Type": "application/json", "session-id":"99c25720-5826-4c57-87b2-d6e29397b584", "Accept":"*/*", } while True: # --- формируем payload точно как фронт ------------------ payload = { "searchParameters": {"input": cat_id, "type": "CATEGORY"}, "zip": "05-090", "store": "188", "isUserLoggedIn": False, "optimizely": { "listing_3547_filter_hnf_sticky": None, "listing_3332_collapsed_filter_bar": None, "discount_percentage": None, "listing_3790_simplify_rating_stars": None }, "optimizelyAttributes": { "market": "pl", "device": "desktop", "deviceVendor": "Apple", "deviceType": "desktop", "isLoggedIn": False, "environment": "prod", "browser": "Chrome", "os": "Mac OS", "language": "pl", "feedMarket": "pl-PL", "locale": "pl-PL", "customerType": "guest", "isEntranceVisit": False, "pip_to_pip_src": "" }, "components": [ { "component": "PRIMARY_AREA", "columns": 4, "types": { "main": "PRODUCT", "breakouts": ["PLANNER", "LOGIN_REMINDER", "MATTRESS_WARRANTY"] }, "filterConfig": {"max-num-filters": 6}, "window": {"size": size, "offset": offset}, "forceFilterCalculation": True } ] } print(payload) print(f" POST offset={offset}") r = requests.post(SEARCH_URL, headers=headers, data=json.dumps(payload), timeout=15) print(f" status: {r.status_code}") # --- выводим статус --- print(f" status: {r.status_code}") # --- печатаем полученный JSON --- try: data = r.json() print(" JSON:", json.dumps(data, ensure_ascii=False, indent=2)) except ValueError: print(" ⚠️ ответ не JSON") break if r.status_code != 200: break # --- вытаскиваем все pipUrl из блока products ------------ products = data.get("product", []) for prod in products: purl = prod.get("pipUrl") if purl: all_links.append(urljoin(BASE_DOMAIN, purl)) print(f" +{len(products)} товаров") # если меньше, чем size → это была последняя порция if len(products) < size: break offset += size #time.sleep(PAUSE_SEC) # пауза между API-вызовами return all_links # ---------- остальные страницы ---------- for page in range(2, pages_total + 1): url = f"{cat_url}?page={page}" print(f" fetch: {url}") html = fetch_html(url) time.sleep(WAIT_BEFORE_PARSE) # ★ пауза 5 с page_links = extract_product_links(html) print(f" page {page}: +{len(page_links)}") all_links.extend(page_links) #time.sleep(PAUSE_SEC) # старая пауза между запросами return all_links # ──────────────────────────────────────────────────────────────────── def main(): if not CAT_FILE.exists(): sys.exit("✖ leaf_categories.txt не найден — запустите сборщик категорий") categories = [l.strip() for l in CAT_FILE.read_text(encoding="utf-8").splitlines() if l.strip()] all_links = set() for idx, cat in enumerate(categories, 1): print(f"[{idx}/{len(categories)}] {cat}") try: # OLD: html = fetch_html(cat) # OLD: links = extract_product_links(html) links = product_links_from_category(cat) # NEW print(f" Σ {len(links)} товаров") # NEW # all_links.update(links) # ← удаляет дубликаты # стало, чтобы сохранять повторы: all_links = [] all_links.extend(links) except Exception as e: print(" ⚠️", e) time.sleep(10) # пауза между категориями (оставляем как было) print("\nВсего уникальных товаров:", len(all_links)) # ---------- сохраняем ---------- OUT_TXT.write_text("\n".join(sorted(all_links)), encoding="utf-8") print("🔗 product_links.txt создан") try: pd.DataFrame(sorted(all_links), columns=["url"]).to_excel( OUT_XLSX, index=False) print("📊 product_links.xlsx создан") except Exception as e: print("⚠️ Excel не записан:", e) if __name__ == "__main__": main()