214 lines
8.5 KiB
Python
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).") |