persist raw attributes and tighten sync assertions

This commit is contained in:
qananasikq
2026-04-08 14:30:57 +03:00
parent b2d3902bcc
commit 447a88feed
7 changed files with 112 additions and 13 deletions

View File

@@ -22,10 +22,10 @@ class NetworkCapture:
_seen_resp: set[str] = field(default_factory=set)
_origin: str | None = None
def attach(self, page: Page) -> None:
def attach(self, page: Page, origin_url: str | None = None) -> None:
# Подписываемся на request/response события страницы.
try:
self._origin = urlparse(page.url).netloc.lower() or None
self._origin = urlparse(origin_url or page.url).netloc.lower() or None
except Exception:
self._origin = None
page.on("request", self._on_request)
@@ -47,7 +47,7 @@ class NetworkCapture:
if len(self.requests) >= self.settings.gentle.max_requests:
return
key = f"{request.method}:{request.url}:{request.post_data or ''}"
# Дедупликация одинаковых запросов.
# Дедупликация одинаковых запросов.
if key in self._seen_req:
return
self._seen_req.add(key)
@@ -95,7 +95,7 @@ class NetworkCapture:
@staticmethod
def _categorize(url: str) -> str:
# Простая эвристика для разбивки ответов по смыслу.
# Простая эвристика для разбивки ответов по смыслу.
low = url.lower()
mapping = {
"images": ["image", "media", "photos", "gallery"],

View File

@@ -130,7 +130,10 @@ class CarMapper:
"brand": brand, "model": model, "year": year, "price": price, "mileage": mileage,
"color": color, "drive": drive, "gearbox": gearbox, "body_type": body_type,
"engine_volume": engine_volume, "is_damaged": is_damaged, "is_sold": is_sold,
"image_count": len(images_records),
"country": country, "selling_type": "AUCTION", "one_owner": one_owner,
"new_car": new_car, "evaluation": evaluation, "non_smoking": non_smoking,
"rental": rental, "repair_history": repair_history,
"images": [image.fullres_image for image in images_records],
}, sort_keys=True, default=str).encode()).hexdigest()
return CarRecord(

View File

@@ -1,3 +1,4 @@
import json
import logging
from contextlib import contextmanager
from datetime import datetime, timezone
@@ -13,6 +14,41 @@ from .schemas import CarRecord
logger = logging.getLogger("iaai_scraper.db")
CAR_DB_FIELDS = {
"parser_id",
"brand",
"model",
"year",
"price",
"currency",
"mileage",
"country",
"is_sold",
"color",
"drive",
"gearbox",
"steering_wheel",
"body_type",
"engine_volume",
"selling_type",
"one_owner",
"new_car",
"is_hidden",
"origin",
"origin_url",
"origin_id",
"is_damaged",
"evaluation",
"non_smoking",
"rental",
"repair_history",
"slug",
"last_seen_at",
"content_hash",
"raw_attributes",
}
class PersistenceService:
def __init__(self, settings: Settings) -> None:
@@ -64,15 +100,21 @@ class PersistenceService:
for image_payload in images:
session.add(Image(fullres_image=str(image_payload["fullres_image"]), preview_image=str(image_payload["preview_image"]), order_index=int(image_payload.get("order_index", 0)), car_id=car_id))
@staticmethod
def _car_payload(record: CarRecord) -> dict[str, object]:
payload = record.model_dump(mode="python")
result = {key: value for key, value in payload.items() if key in CAR_DB_FIELDS}
# Serialize raw_attributes dict to JSON string for Text column.
if "raw_attributes" in result and isinstance(result["raw_attributes"], dict):
result["raw_attributes"] = json.dumps(result["raw_attributes"], ensure_ascii=False, default=str)
return result
def upsert_car(self, record: CarRecord):
"""Insert/update/skip по content_hash."""
# Готовим payload отдельно от вложенных изображений и служебных полей.
payload = record.model_dump(mode="python")
images = payload.pop("images", [])
payload.pop("raw_attributes", None)
payload.pop("mapping_notes", None)
content_hash = payload.pop("content_hash", "")
payload["content_hash"] = content_hash
# В БД отправляем только поля, реально существующие в финальной схеме cars.
payload = self._car_payload(record)
images = [image.model_dump(mode="python") for image in record.images]
content_hash = str(payload.get("content_hash") or "")
with self.session_scope() as session:
# поиск по origin_id
car = session.execute(select(Car).where(Car.origin_id == record.origin_id)).scalar_one_or_none()

View File

@@ -54,6 +54,7 @@ class Car(Base):
slug: Mapped[str] = mapped_column(String(), nullable=False)
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, default=func.now())
content_hash: Mapped[str] = mapped_column(String(64), nullable=False, default="", index=True)
raw_attributes: Mapped[str | None] = mapped_column(Text, nullable=True)
images: Mapped[list["Image"]] = relationship("Image", back_populates="car", cascade="all, delete-orphan")