harden price normalization
This commit is contained in:
@@ -62,9 +62,21 @@ class CarMapper:
|
|||||||
parser_id = f"iaai:{origin_id}"
|
parser_id = f"iaai:{origin_id}"
|
||||||
brand = self._as_str(first_non_empty([core.get("make"), vehicle_summary.get("make")])) or "UNKNOWN"
|
brand = self._as_str(first_non_empty([core.get("make"), vehicle_summary.get("make")])) or "UNKNOWN"
|
||||||
model = self._as_str(first_non_empty([core.get("model"), vehicle_summary.get("model")])) or "UNKNOWN"
|
model = self._as_str(first_non_empty([core.get("model"), vehicle_summary.get("model")])) or "UNKNOWN"
|
||||||
year = self._to_int(first_non_empty([core.get("year"), vehicle_summary.get("year")]))
|
year = self._to_year(first_non_empty([core.get("year"), vehicle_summary.get("year")]))
|
||||||
price = self._to_int(first_non_empty([pricing.get("buy_now"), pricing.get("current_bid"), vehicle_summary.get("buy_now"), vehicle_summary.get("current_bid")]))
|
price = self._first_parsed_int(
|
||||||
mileage = self._to_int(first_non_empty([core.get("odometer"), vehicle_summary.get("odometer"), 0])) or 0
|
[
|
||||||
|
pricing.get("buy_now"),
|
||||||
|
pricing.get("current_bid"),
|
||||||
|
vehicle_summary.get("buy_now"),
|
||||||
|
vehicle_summary.get("current_bid"),
|
||||||
|
pricing.get("actual_cash_value"),
|
||||||
|
],
|
||||||
|
self._to_money_int,
|
||||||
|
)
|
||||||
|
mileage = self._first_parsed_int(
|
||||||
|
[core.get("odometer"), vehicle_summary.get("odometer"), 0],
|
||||||
|
self._to_int,
|
||||||
|
) or 0
|
||||||
color = self._normalize_color(first_non_empty([core.get("color"), vehicle_summary.get("color"), "other"]))
|
color = self._normalize_color(first_non_empty([core.get("color"), vehicle_summary.get("color"), "other"]))
|
||||||
drive = self._normalize_drive(first_non_empty([core.get("drive"), vehicle_summary.get("drive")]))
|
drive = self._normalize_drive(first_non_empty([core.get("drive"), vehicle_summary.get("drive")]))
|
||||||
gearbox = self._normalize_gearbox(first_non_empty([core.get("gearbox"), vehicle_summary.get("gearbox")]))
|
gearbox = self._normalize_gearbox(first_non_empty([core.get("gearbox"), vehicle_summary.get("gearbox")]))
|
||||||
@@ -83,7 +95,19 @@ class CarMapper:
|
|||||||
repair_history = self._boolish(first_non_empty([core.get("repair_history"), vehicle_summary.get("repair_history"), False]))
|
repair_history = self._boolish(first_non_empty([core.get("repair_history"), vehicle_summary.get("repair_history"), False]))
|
||||||
non_smoking = self._boolish(first_non_empty([core.get("non_smoking"), vehicle_summary.get("non_smoking"), True]))
|
non_smoking = self._boolish(first_non_empty([core.get("non_smoking"), vehicle_summary.get("non_smoking"), True]))
|
||||||
evaluation = self._as_str(first_non_empty([core.get("grade"), core.get("evaluation"), vehicle_summary.get("evaluation")])) or None
|
evaluation = self._as_str(first_non_empty([core.get("grade"), core.get("evaluation"), vehicle_summary.get("evaluation")])) or None
|
||||||
currency = self._normalize_currency(first_non_empty([pricing.get("currency"), vehicle_summary.get("currency"), "USD"]))
|
currency = self._normalize_currency(
|
||||||
|
first_non_empty(
|
||||||
|
[
|
||||||
|
pricing.get("currency"),
|
||||||
|
vehicle_summary.get("currency"),
|
||||||
|
pricing.get("buy_now"),
|
||||||
|
pricing.get("current_bid"),
|
||||||
|
vehicle_summary.get("buy_now"),
|
||||||
|
vehicle_summary.get("current_bid"),
|
||||||
|
"USD",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
slug = self._slugify(" ".join(filter(None, [str(year or ""), brand, model, origin_id])))
|
slug = self._slugify(" ".join(filter(None, [str(year or ""), brand, model, origin_id])))
|
||||||
images_records = self._build_images(images.get("urls") or vehicle_summary.get("image_urls") or [])
|
images_records = self._build_images(images.get("urls") or vehicle_summary.get("image_urls") or [])
|
||||||
origin = "IAAI" if "IAAI" in ORIGIN_ENUM_VALUES else "NA"
|
origin = "IAAI" if "IAAI" in ORIGIN_ENUM_VALUES else "NA"
|
||||||
@@ -162,6 +186,76 @@ class CarMapper:
|
|||||||
digits = re.sub(r"[^\d]", "", str(value))
|
digits = re.sub(r"[^\d]", "", str(value))
|
||||||
return int(digits) if digits else None
|
return int(digits) if digits else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _to_money_int(cls, value: Any) -> int | None:
|
||||||
|
# Нормализация «грязной» стоимости: "$4,500", "4 500 USD", "4.500,00 €", "USD 4,500 - 5,200".
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return None
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return int(value)
|
||||||
|
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lowered = text.lower()
|
||||||
|
if any(token in lowered for token in ["n/a", "na", "tbd", "unknown", "call", "contact"]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
numbers = re.findall(r"\d[\d\s.,]*", text)
|
||||||
|
if not numbers:
|
||||||
|
return None
|
||||||
|
|
||||||
|
best: int | None = None
|
||||||
|
for number in numbers:
|
||||||
|
clean = number.replace(" ", "")
|
||||||
|
if "," in clean and "." in clean:
|
||||||
|
# Поддержка и 1,234.56, и 1.234,56.
|
||||||
|
if clean.rfind(",") > clean.rfind("."):
|
||||||
|
clean = clean.replace(".", "").replace(",", ".")
|
||||||
|
else:
|
||||||
|
clean = clean.replace(",", "")
|
||||||
|
elif "," in clean:
|
||||||
|
parts = clean.split(",")
|
||||||
|
# Десятичный формат 123,45 -> 123.45 иначе считаем разделителем тысяч.
|
||||||
|
if len(parts[-1]) in {1, 2} and len(parts) == 2:
|
||||||
|
clean = clean.replace(",", ".")
|
||||||
|
else:
|
||||||
|
clean = clean.replace(",", "")
|
||||||
|
elif "." in clean:
|
||||||
|
parts = clean.split(".")
|
||||||
|
if not (len(parts[-1]) in {1, 2} and len(parts) == 2):
|
||||||
|
clean = clean.replace(".", "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = int(float(clean))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if parsed > 0 and (best is None or parsed > best):
|
||||||
|
best = parsed
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _first_parsed_int(values: list[Any], parser) -> int | None:
|
||||||
|
for value in values:
|
||||||
|
parsed = parser(value)
|
||||||
|
if parsed is not None:
|
||||||
|
return parsed
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _to_year(value: Any) -> int | None:
|
||||||
|
parsed = CarMapper._to_int(value)
|
||||||
|
if parsed is None:
|
||||||
|
return None
|
||||||
|
if 1900 <= parsed <= 2100:
|
||||||
|
return parsed
|
||||||
|
return None
|
||||||
|
|
||||||
def _to_engine_cc(self, value: Any) -> int | None:
|
def _to_engine_cc(self, value: Any) -> int | None:
|
||||||
# Поддерживаем и литры, и уже готовые cc.
|
# Поддерживаем и литры, и уже готовые cc.
|
||||||
text = str(value).lower().strip() if value is not None else ""
|
text = str(value).lower().strip() if value is not None else ""
|
||||||
@@ -174,7 +268,24 @@ class CarMapper:
|
|||||||
return int(text) if re.match(r"^\d+$", text) else None
|
return int(text) if re.match(r"^\d+$", text) else None
|
||||||
|
|
||||||
def _normalize_currency(self, value: Any) -> str:
|
def _normalize_currency(self, value: Any) -> str:
|
||||||
text = self._as_str(value).upper() or "USD"
|
text = self._as_str(value)
|
||||||
|
upper = text.upper() or "USD"
|
||||||
|
if any(token in text for token in ["€", "EUR"]):
|
||||||
|
return "EUR"
|
||||||
|
if any(token in text for token in ["¥", "JPY"]):
|
||||||
|
return "JPY"
|
||||||
|
if any(token in text for token in ["₩", "KRW"]):
|
||||||
|
return "KRW"
|
||||||
|
if any(token in text for token in ["£", "GBP"]):
|
||||||
|
return "GBP"
|
||||||
|
if any(token in text for token in ["₽", "RUB"]):
|
||||||
|
return "RUB"
|
||||||
|
if any(token in text for token in ["AED", "د.إ"]):
|
||||||
|
return "AED"
|
||||||
|
if any(token in text for token in ["CA$", "CAD"]):
|
||||||
|
return "CAD"
|
||||||
|
|
||||||
|
text = upper
|
||||||
if text in CURRENCY_ENUM_VALUES:
|
if text in CURRENCY_ENUM_VALUES:
|
||||||
return text
|
return text
|
||||||
return "USD" if "$" in str(value) else "USD"
|
return "USD" if "$" in str(value) else "USD"
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ STEERING_WHEEL_ENUM_VALUES = ("LEFT", "RIGHT", "left", "right", "NA")
|
|||||||
BODY_TYPE_ENUM_VALUES = ("COUPE", "SUV", "HATCHBACK", "MINIVAN", "SEDAN", "NA", "Station Wagon", "Pickup", "Truck", "Open", "RV", "Other", "STATION_WAGON", "PICKUP", "TRUCK", "OPEN", "OTHER")
|
BODY_TYPE_ENUM_VALUES = ("COUPE", "SUV", "HATCHBACK", "MINIVAN", "SEDAN", "NA", "Station Wagon", "Pickup", "Truck", "Open", "RV", "Other", "STATION_WAGON", "PICKUP", "TRUCK", "OPEN", "OTHER")
|
||||||
COUNTRY_ENUM_VALUES = ("JP", "KR", "US", "CA", "NA")
|
COUNTRY_ENUM_VALUES = ("JP", "KR", "US", "CA", "NA")
|
||||||
ORIGIN_ENUM_VALUES = ("TAU", "CARSENSOR", "HANAMARU", "ENCAR", "KURUMA_TRADER", "carsensor", "encar", "kuruma_trader", "asnet", "kababa", "ACV", "COPART", "copart", "NA", "ASNET", "KABABA", "IAAI")
|
ORIGIN_ENUM_VALUES = ("TAU", "CARSENSOR", "HANAMARU", "ENCAR", "KURUMA_TRADER", "carsensor", "encar", "kuruma_trader", "asnet", "kababa", "ACV", "COPART", "copart", "NA", "ASNET", "KABABA", "IAAI")
|
||||||
SELLING_TYPE_ENUM_VALUES = ("STOCK", "AUCTION", "TENDER", "stock", "auction", "tender", "NA")
|
SELLING_TYPE_ENUM_VALUES = ("STOCK", "AUCTION", "TENDER", "stock", "auction", "tender", "NA")
|
||||||
|
|||||||
Reference in New Issue
Block a user