Files
service-finder/backend/app/workers/vehicle/R2_generation_scout.py
2026-03-22 11:02:05 +00:00

214 lines
8.5 KiB
Python

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).")