import asyncio import logging import random import re from playwright.async_api import async_playwright from sqlalchemy import text from app.database import AsyncSessionLocal # --- NAPLÓZÁS KONFIGURÁCIÓ --- logging.basicConfig( level=logging.INFO, format='%(asctime)s [R2-AUTOS-ONLY] %(message)s', handlers=[logging.StreamHandler()] ) logger = logging.getLogger("R2") async def get_page_safe(page, url): """ Gondolatmenet: Az anti-bot védelem (Cloudflare) kijátszása érdekében véletlenszerű várakozást és valós User-Agent viselkedést szimulálunk. """ delay = random.uniform(4, 7) await asyncio.sleep(delay) try: # A domcontentloaded gyorsabb, mint a networkidle, de elég a linkgyűjtéshez await page.goto(url, wait_until="domcontentloaded", timeout=60000) # Ellenőrizzük, hogy nem kaptunk-e blokkoló oldalt title = await page.title() if "Just a moment" in title or "Cloudflare" in title: raise Exception(f"Bot védelem észlelve az URL-en: {url}") return page except Exception as e: logger.error(f"Hiba az oldal betöltésekor: {url} -> {e}") raise async def extract_scoped_links(page, p_id, current_url): """ Gondolatmenet: A 'Scope-Lock' technika lényege, hogy az URL-kből kinyert márkanév horgony (anchor) segítségével megakadályozzuk, hogy a robot kilépjen a jelenlegi autócsalád környezetéből. Javítás: Beépített nyelvi szűrő és 'Language Shield' a nem kívánt (görög, spanyol, bolgár stb.) változatok elkerülésére. Minden talált új linket 'car' kategóriával mentünk el. """ # Kinyerjük a márka/típus nevét az URL-ből (pl. 'alfa-romeo') url_parts = current_url.split('/')[-1].split('-') brand_anchor = "-".join(url_parts[:2]) # Csak azokat a linkeket gyűjtjük, amik valódi navigációt jelentenek hrefs = await page.eval_on_selector_all( "a", "nodes => nodes.map(n => ({ 'name': n.innerText.trim(), 'url': n.href }))" ) found_count = 0 async with AsyncSessionLocal() as db: for link in hrefs: url = link['url'] name = link['name'].replace('\n', ' ').strip() # --- 1. ALAPVETŐ ÉRVÉNYESSÉG --- if not name or len(name) < 2: continue # --- 2. LANGUAGE SHIELD (ÚJ VÉDELEM) --- # Karakterkészlet ellenőrzés: Ha görög, cirill vagy egyéb nem latin karakter van benne, eldobjuk. if re.search(r'[^\x00-\x7F]+', name): continue # Szigorított angol-kényszerítés az URL-ben if '/en/' not in url: continue # Szövegalapú zajszűrés (Meta-linkek kizárása) junk_keywords = [ 'privacy', 'configuracion', 'ρυθμίσεις', 'cookie', 'settings', 'contact', 'about us', 'terms', 'advertising', 'login', 'registration', 'pribatutasun', 'configuració', 'naslovnica', 'stisni', 'personvern', 'prywatnosci', 'ustawienia', 'endre', 'zmień' ] if any(junk in name.lower() for junk in junk_keywords): continue # --- 3. EREDETI NYELVI SZŰRŐ (Language Lock) --- # Megtartva az eredeti logikát: domain.com/bg/..., domain.com/se/... path_segments = url.split('/') if len(path_segments) > 3: lang_segment = path_segments[3] if len(lang_segment) == 2 and lang_segment != 'en': continue # --- 4. SCOPE SZŰRÉS --- # Csak az adott márkához tartozó linkeket engedjük át if brand_anchor not in url: continue # --- 5. NAVIGÁCIÓS SZŰRÉS --- # Ne lépjen vissza a listákhoz, és zárjuk ki az idegen nyelvű könyvtárakat (teljes lista) excluded_patterns = [ '-brand-', 'allbrands', 'en/brands', '/bg/', '/ru/', '/de/', '/it/', '/fr/', '/es/', '/tr/', '/ro/', '/fi/', '/se/', '/no/', '/pl/', '/gr/', '/hr/', '/cz/', '/sk/', '/ua/' ] if any(x in url for x in excluded_patterns): continue # --- 6. ÖNHIVATKOZÁS SZŰRÉS --- if url.strip('/') == current_url.strip('/'): continue # --- 7. SZINT MEGHATÁROZÁSA MINTÁZAT ALAPJÁN --- if '-generation-' in url: target_level = 'generation' elif re.search(r'-\d+$', url) and '-model-' not in url: target_level = 'engine' else: continue # --- 8. MENTÉS AZ ADATBÁZISBA --- await db.execute(text(""" INSERT INTO vehicle.auto_data_crawler_queue (url, level, parent_id, name, status, category) VALUES (:url, :level, :p_id, :name, 'pending', 'car') ON CONFLICT (url) DO NOTHING """), {"url": url, "level": target_level, "p_id": p_id, "name": name}) found_count += 1 await db.commit() return found_count async def process_target(context, t_id, t_url, t_name, t_level): """ Gondolatmenet: Egy adott feladat (URL) teljes körű feldolgozása. A volume mapping miatt a módosítás azonnal látszik a konténerben is. """ page = await context.new_page() try: logger.info(f"🚀 Autós felderítés indítása [{t_level}]: {t_name}") await get_page_safe(page, t_url) # Linkek kinyerése és mentése found = await extract_scoped_links(page, t_id, t_url) async with AsyncSessionLocal() as db: new_status = 'completed' if found > 0 else 'completed_leaf' await db.execute(text(""" UPDATE vehicle.auto_data_crawler_queue SET status = :s, error_msg = NULL, updated_at = NOW() WHERE id = :id """), {"s": new_status, "id": t_id}) await db.commit() logger.info(f"✅ Befejezve: {t_name} -> {found} új link.") except Exception as e: logger.error(f"❌ Kritikus hiba feldolgozás közben ({t_name}): {e}") async with AsyncSessionLocal() as db: await db.execute(text(""" UPDATE vehicle.auto_data_crawler_queue SET status = 'error', error_msg = :msg, updated_at = NOW() WHERE id = :id """), {"msg": str(e), "id": t_id}) await db.commit() finally: await page.close() async def main(): """ Gondolatmenet: A fő vezérlő hurok. STRATÉGIA: Csak a 'car' kategóriájú feladatokat vesszük fel (category='car'). """ async with async_playwright() as p: browser = await p.chromium.launch(headless=True) context = await browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", viewport={'width': 1920, 'height': 1080} ) logger.info("🤖 R2 Autós Felderítő Robot aktív. (Filter: category='car')") while True: async with AsyncSessionLocal() as db: # Csak 'car' kategóriájú, pending feladatok lekérése res = await db.execute(text(""" UPDATE vehicle.auto_data_crawler_queue SET status = 'processing' WHERE id = ( SELECT id FROM vehicle.auto_data_crawler_queue WHERE status = 'pending' AND level IN ('model', 'generation') AND category = 'car' ORDER BY level ASC, id ASC LIMIT 1 FOR UPDATE SKIP LOCKED ) RETURNING id, url, name, level """)) target = res.fetchone() await db.commit() if not target: logger.info("🏁 Nincs több autós feladat (car). Alvás 60mp...") await asyncio.sleep(60) continue await process_target(context, target[0], target[1], target[2], target[3]) await browser.close() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("🛑 Felhasználói leállítás (Ctrl+C).")