improve scraper runtime
This commit is contained in:
@@ -15,6 +15,7 @@ VEHICLE_HREF_RE = re.compile(r"/VehicleDetail/\d+(?:~[A-Z]{2})?", re.IGNORECASE)
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ListingVehicleLink:
|
||||
# Ссылка на одну карточку из листинга.
|
||||
href: str
|
||||
title: str = ""
|
||||
lot_number: str | None = None
|
||||
@@ -22,6 +23,7 @@ class ListingVehicleLink:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ListingPageResult:
|
||||
# Результат сбора ссылок с одной страницы листинга.
|
||||
source_url: str
|
||||
page_number: int
|
||||
vehicle_links: list[ListingVehicleLink] = field(default_factory=list)
|
||||
@@ -31,10 +33,12 @@ class ListingPageResult:
|
||||
|
||||
class ListingCollector:
|
||||
def __init__(self, settings: Settings, pacer: HumanPacer) -> None:
|
||||
# Сборщик ссылок из публичного листинга IAAI.
|
||||
self.settings = settings
|
||||
self.pacer = pacer
|
||||
|
||||
def open_cars_listing(self, page: Page) -> None:
|
||||
# Открываем листинг и ждём появления ссылок на карточки.
|
||||
logger.info("Opening cars listing page: %s", self.settings.listing.cars_url)
|
||||
page.goto(self.settings.listing.cars_url, wait_until="domcontentloaded")
|
||||
try:
|
||||
@@ -48,6 +52,7 @@ class ListingCollector:
|
||||
self.pacer.after_listing_open()
|
||||
|
||||
def apply_filters(self, page: Page, make: str | None = None, model: str | None = None) -> dict[str, str | None]:
|
||||
# Пытаемся применить make/model фильтры через UI.
|
||||
applied = {"make": None, "model": None}
|
||||
if make and self._try_fill_filter_input(page, ["input[placeholder*='Make']", "input[aria-label*='Make']"], make):
|
||||
applied["make"] = make
|
||||
@@ -58,6 +63,7 @@ class ListingCollector:
|
||||
return applied
|
||||
|
||||
def collect_current_page(self, page: Page, page_number: int = 1) -> ListingPageResult:
|
||||
# Собираем ссылки только с текущей страницы.
|
||||
anchors = page.locator("a[href*='/VehicleDetail/']")
|
||||
total = min(anchors.count(), self.settings.listing.page_link_limit)
|
||||
links: list[ListingVehicleLink] = []
|
||||
@@ -80,6 +86,7 @@ class ListingCollector:
|
||||
return ListingPageResult(source_url=page.url, page_number=page_number, vehicle_links=links, pagination_available=next_page_detected, next_page_detected=next_page_detected)
|
||||
|
||||
def go_to_next_page(self, page: Page) -> bool:
|
||||
# Переход на следующую страницу, если кнопка доступна.
|
||||
selectors = ["a[aria-label*='Next']", "button[aria-label*='Next']", "a.pagination-next", "button.pagination-next", "a:has-text('Next')", "button:has-text('Next')"]
|
||||
for selector in selectors:
|
||||
locator = page.locator(selector).first
|
||||
@@ -101,6 +108,7 @@ class ListingCollector:
|
||||
return False
|
||||
|
||||
def collect_listing_links(self, page: Page, *, make: str | None = None, model: str | None = None) -> dict[str, object]:
|
||||
# Последовательно собираем ссылки с учётом лимитов и пагинации.
|
||||
self.open_cars_listing(page)
|
||||
applied_filters = self.apply_filters(page, make=make, model=model)
|
||||
pages: list[dict[str, object]] = []
|
||||
@@ -121,6 +129,7 @@ class ListingCollector:
|
||||
|
||||
@staticmethod
|
||||
def _try_fill_filter_input(page: Page, selectors: list[str], value: str) -> bool:
|
||||
# Пробуем несколько селекторов для одного и того же фильтра.
|
||||
for selector in selectors:
|
||||
locator = page.locator(selector).first
|
||||
if locator.count() == 0:
|
||||
@@ -138,6 +147,7 @@ class ListingCollector:
|
||||
|
||||
@staticmethod
|
||||
def _has_next_page(page: Page) -> bool:
|
||||
# Грубая проверка наличия кнопки next.
|
||||
for selector in ["a[aria-label*='Next']", "button[aria-label*='Next']", "a.pagination-next", "button.pagination-next", "a:has-text('Next')", "button:has-text('Next')"]:
|
||||
if page.locator(selector).count() > 0:
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user