fix encar duplicates
This commit is contained in:
@@ -2,6 +2,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
from collections import Counter
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from dataclasses import dataclass, field, replace
|
from dataclasses import dataclass, field, replace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -41,6 +42,7 @@ BATCH_VEHICLES_CHUNK_SIZE = 20
|
|||||||
ENCAR_DETAIL_URL_TEMPLATE = "https://www.encar.com/dc/dc_cardetailview.do?carid={vehicle_id}"
|
ENCAR_DETAIL_URL_TEMPLATE = "https://www.encar.com/dc/dc_cardetailview.do?carid={vehicle_id}"
|
||||||
ENCAR_IMAGE_BASE = "https://ci.encar.com"
|
ENCAR_IMAGE_BASE = "https://ci.encar.com"
|
||||||
VEHICLE_ID_RE = re.compile(r"(?:carid|vehicleId)=?(\d+)")
|
VEHICLE_ID_RE = re.compile(r"(?:carid|vehicleId)=?(\d+)")
|
||||||
|
PHOTO_VEHICLE_ID_RE = re.compile(r"/(\d+)_\d+\.(?:jpg|jpeg|png|webp)$", re.IGNORECASE)
|
||||||
|
|
||||||
# Максимальный номер фото для пробинга и допустимые промахи подряд
|
# Максимальный номер фото для пробинга и допустимые промахи подряд
|
||||||
PHOTO_PROBE_MAX = 50
|
PHOTO_PROBE_MAX = 50
|
||||||
@@ -613,6 +615,42 @@ class EncarScraper:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _canonical_vehicle_id_from_images(self, record: CarRecord) -> str | None:
|
||||||
|
current_vehicle_id = self._extract_vehicle_id(record.origin_url or "")
|
||||||
|
if not current_vehicle_id:
|
||||||
|
current_vehicle_id = record.origin_id.split(":", 1)[-1] if ":" in record.origin_id else record.origin_id
|
||||||
|
|
||||||
|
image_vehicle_ids: list[str] = []
|
||||||
|
for image in record.images:
|
||||||
|
url = image.fullres_image or ""
|
||||||
|
match = PHOTO_VEHICLE_ID_RE.search(url)
|
||||||
|
if match:
|
||||||
|
image_vehicle_ids.append(match.group(1))
|
||||||
|
|
||||||
|
if not image_vehicle_ids:
|
||||||
|
return None
|
||||||
|
|
||||||
|
canonical_vehicle_id, occurrences = Counter(image_vehicle_ids).most_common(1)[0]
|
||||||
|
if canonical_vehicle_id == current_vehicle_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Канонизируем только если mismatch подтверждается минимум двумя фото.
|
||||||
|
if occurrences < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return canonical_vehicle_id
|
||||||
|
|
||||||
|
def _apply_canonical_vehicle_id(self, record: CarRecord) -> bool:
|
||||||
|
canonical_vehicle_id = self._canonical_vehicle_id_from_images(record)
|
||||||
|
if not canonical_vehicle_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
record.parser_id = f"encar:{canonical_vehicle_id}"
|
||||||
|
record.origin_id = f"encar:{canonical_vehicle_id}"
|
||||||
|
record.origin_url = ENCAR_DETAIL_URL_TEMPLATE.format(vehicle_id=canonical_vehicle_id)
|
||||||
|
record.slug = self.car_mapper._build_slug(record.brand, record.model, canonical_vehicle_id)
|
||||||
|
return True
|
||||||
|
|
||||||
def collect_listing(
|
def collect_listing(
|
||||||
self,
|
self,
|
||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
@@ -1130,6 +1168,7 @@ class EncarScraper:
|
|||||||
failed_ids: list[str] = []
|
failed_ids: list[str] = []
|
||||||
skipped_brands = 0
|
skipped_brands = 0
|
||||||
skipped_runtime = 0
|
skipped_runtime = 0
|
||||||
|
canonicalized_records = 0
|
||||||
|
|
||||||
# Фаза 1: маппинг без пробинга + фильтр брендов
|
# Фаза 1: маппинг без пробинга + фильтр брендов
|
||||||
for item in items:
|
for item in items:
|
||||||
@@ -1151,7 +1190,6 @@ class EncarScraper:
|
|||||||
|
|
||||||
records.append(record)
|
records.append(record)
|
||||||
probe_tasks.append((len(records) - 1, item))
|
probe_tasks.append((len(records) - 1, item))
|
||||||
all_origin_ids.add(record.origin_id)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("Failed to map vehicle %s: %s", vehicle_id, exc)
|
logger.warning("Failed to map vehicle %s: %s", vehicle_id, exc)
|
||||||
failed += 1
|
failed += 1
|
||||||
@@ -1171,6 +1209,30 @@ class EncarScraper:
|
|||||||
# Без пробинга: берём только фото из API (как reference-проект)
|
# Без пробинга: берём только фото из API (как reference-проект)
|
||||||
self._apply_api_photos(records, probe_tasks)
|
self._apply_api_photos(records, probe_tasks)
|
||||||
|
|
||||||
|
if records:
|
||||||
|
for record in records:
|
||||||
|
if self._apply_canonical_vehicle_id(record):
|
||||||
|
canonicalized_records += 1
|
||||||
|
|
||||||
|
deduped_records: list[CarRecord] = []
|
||||||
|
seen_origin_ids: set[str] = set()
|
||||||
|
collapsed_duplicates = 0
|
||||||
|
for record in records:
|
||||||
|
if record.origin_id in seen_origin_ids:
|
||||||
|
collapsed_duplicates += 1
|
||||||
|
continue
|
||||||
|
seen_origin_ids.add(record.origin_id)
|
||||||
|
deduped_records.append(record)
|
||||||
|
all_origin_ids.add(record.origin_id)
|
||||||
|
records = deduped_records
|
||||||
|
|
||||||
|
if canonicalized_records or collapsed_duplicates:
|
||||||
|
logger.info(
|
||||||
|
"Canonicalized %d Encar records by photo vehicle id and collapsed %d duplicates in batch",
|
||||||
|
canonicalized_records,
|
||||||
|
collapsed_duplicates,
|
||||||
|
)
|
||||||
|
|
||||||
# Фаза 3: upsert в БД
|
# Фаза 3: upsert в БД
|
||||||
if records:
|
if records:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user