Update model translations

This commit is contained in:
qananasikq
2026-08-03 21:04:19 +03:00
parent 6bdbbe396c
commit 72e78de1d7
3 changed files with 32 additions and 56 deletions

View File

@@ -362,7 +362,7 @@ class EncarMapper:
normalized = self._as_str(text)
if not normalized:
return normalized
# На оригинальном сайте Encar скобок нет — убираем скобки вместе с содержимым.
normalized = re.sub(r"[\(\[(【][^)\])】]*[\)\])】]?", " ", normalized)
normalized = normalized.replace("·", " ").replace("/", " ")
normalized = re.sub(r"(?<=[A-Za-z])-클래스", " 클래스", normalized)
@@ -370,52 +370,54 @@ class EncarMapper:
normalized = re.sub(r"\s+", " ", normalized).strip()
return normalized
def _translate_model(self, text: str) -> str:
"""Безопасно переводит модель Encar: точные совпадения + корейские фрагменты.
def _clean_base_model_name(self, text: str) -> str:
model = self._as_str(text)
if not model:
return model
marketing_prefix = r"(?:The\s+All\s+New|All\s+New|The\s+New|Very\s+New|New)"
previous = None
while previous != model:
previous = model
model = re.sub(rf"^\s*{marketing_prefix}\s+", "", model, flags=re.IGNORECASE)
model = re.sub(r"\b\d+(?:st|nd|rd|th)\s+gen\b", " ", model, flags=re.IGNORECASE)
model = re.sub(r"\bgen\b", " ", model, flags=re.IGNORECASE)
model = re.sub(r"\s+", " ", model).strip(" -")
return model
def _translate_model(self, text: str) -> str:
Старый общий replace ломал смешанные названия. Здесь английские строки
возвращаются как есть, а короткие корейские токены заменяются только
когда они реально присутствуют как отдельный смысловой фрагмент.
"""
original = self._normalize_model_text(text)
if not original or not self._has_korean(original):
return original
return self._clean_base_model_name(original)
exact = MODEL_EXACT_TRANSLATIONS.get(original)
if exact:
return exact
return self._clean_base_model_name(exact)
translated = original
# Сначала длинные точные модели и фразы — чтобы "렉с턴 스포츠 칸"
# не превратился в "Rexton Sport 칸".
combined = {**MODEL_EXACT_TRANSLATIONS, **MODEL_PHRASE_TRANSLATIONS, **MODEL_TRANSLATIONS}
for source, target in sorted(combined.items(), key=lambda item: len(item[0]), reverse=True):
if source in translated:
# Заменяем только когда корейский ключ стоит как отдельный
# смысловой фрагмент (границы — не корейские символы). Иначе
# короткий ключ "레이" (Ray) резал бы "슈팅브레이크" → "슈팅브Ray크".
pattern = rf"(?<![가-힣]){re.escape(source)}(?![가-힣])"
translated = re.sub(pattern, target, translated)
# Потом безопасные короткие токены.
for source, target in sorted(MODEL_TOKEN_TRANSLATIONS.items(), key=lambda item: len(item[0]), reverse=True):
translated = re.sub(rf"(?<![A-Za-z0-9가-힣]){re.escape(source)}(?![A-Za-z0-9가-힣])", target, translated)
# Типовые слитные варианты Encar: 5시리즈, S-클래스.
translated = re.sub(r"(?<=\d)시리즈", " Series", translated)
translated = re.sub(r"(?<=[A-Za-z])-?클래스", "-Class", translated)
translated = re.sub(r"\b([A-Z]{1,4}) Class\b", r"\1-Class", translated)
# Оставшиеся непереведённые корейские фрагменты отбрасываем — как на
# оригинальном сайте (напр. "슈팅브레이크" исчезает, не портя слово).
translated = re.sub(r"[가-힣]+", " ", translated)
# Чистим осиротевшую пунктуацию/скобки после удаления корейского.
translated = re.sub(r"\(\s*\)", " ", translated)
translated = re.sub(r"\s*-\s*(?=$|\))", "", translated)
translated = re.sub(r"\s+([),])", r"\1", translated)
translated = re.sub(r"\s+", " ", translated).strip(" -")
return translated
return self._clean_base_model_name(translated)
def _translate_color(self, color_raw: str) -> str:
value = self._as_str(color_raw)
@@ -1499,7 +1501,6 @@ class EncarScraper:
chunks.append(tasks[i:i + BATCH_VEHICLES_CHUNK_SIZE])
def _fetch_chunk(chunk: list[tuple[int, str]]) -> list[tuple[int, list[ImageRecord]]]:
"""Запрашивает фото для чанка машин одним batch-запросом."""
ids_str = ",".join(vid for _, vid in chunk)
path = f"/v1/readside/vehicles?vehicleIds={ids_str}&include=PHOTOS"
for attempt in range(2):

View File

@@ -1,11 +1,5 @@
# Словари переводов корейского текста русский / английский.
# Используется в encar.py для маппинга данных Encar API.
#
# Эталон переводов брендов и моделей — словарь партнёра
# (`partner_dictionary.py`). Он подмешивается ниже как база: значения оттуда
# имеют приоритет для брендов и моделей, а специфичные для проекта записи
# (корейские модели Hyundai/Kia/Genesis и т.п., которых нет у партнёра)
# сохраняются как дополнение.
import re
@@ -186,9 +180,6 @@ BRAND_TRANSLATIONS = {
"기타 수입차": "Other Imported",
}
# Подмешиваем бренды из эталонного словаря партнёра как базу. Существующие
# записи проекта имеют приоритет (не перезаписываются), новые из словаря
# партнёра добавляются.
for _ko, _en in _PARTNER_TABLES["makers"].items():
BRAND_TRANSLATIONS.setdefault(_ko, _en)
@@ -293,10 +284,7 @@ def expand_allowed_brands(brands: set[str] | None) -> set[str] | None:
# Модели / комплектации: корейский → английский
# Точные переводы базовых названий моделей Encar. Этот словарь используется
# перед частичным переводом и не должен содержать общие слова вроде "뉴".
MODEL_EXACT_TRANSLATIONS = {
# BMW / Mercedes-Benz
"1시리즈": "1 Series",
"2시리즈": "2 Series",
"3시리즈": "3 Series",
@@ -318,7 +306,6 @@ MODEL_EXACT_TRANSLATIONS = {
"GLE-클래스": "GLE-Class",
"GLS-클래스": "GLS-Class",
"V-클래스": "V-Class",
# Hyundai / Kia / Genesis
"팰리세이드": "Palisade",
"싼타페": "Santa Fe",
"투싼": "Tucson",
@@ -362,7 +349,6 @@ MODEL_EXACT_TRANSLATIONS = {
"스토닉": "Stonic",
"카렌스": "Carens",
"제네시스": "Genesis",
# Chevrolet / Renault Korea / KG Mobility
"스파크": "Spark",
"말리부": "Malibu",
"트랙스": "Trax",
@@ -389,7 +375,6 @@ MODEL_EXACT_TRANSLATIONS = {
"코란도": "Korando",
"체어맨": "Chairman",
"오피러스": "Opirus",
# Import models written in Korean
"익스플로러": "Explorer",
"머스탱": "Mustang",
"카이엔": "Cayenne",
@@ -471,16 +456,10 @@ MODEL_EXACT_TRANSLATIONS = {
"AMG GT 4도어 쿠페": "AMG GT 4-Door Coupe",
}
# Подмешиваем модели из эталонного словаря партнёра как базу. Существующие
# записи проекта имеют приоритет. Дополнительно добавляем нормализованные
# варианты ключей партнёра (напр. "5 시리즈" -> "5시리즈", "S 클래스" -> "S-클래스"),
# т.к. в сырых данных Encar формат отличается пробелами/дефисами.
def _register_partner_models() -> None:
for _ko, _en in _PARTNER_TABLES["models"].items():
MODEL_EXACT_TRANSLATIONS.setdefault(_ko, _en)
# "5 시리즈" -> "5시리즈"
_compact = re.sub(r"(?<=\d)\s+시리즈", "시리즈", _ko)
# "S 클래스" -> "S-클래스"
_compact = re.sub(r"(?<=[A-Za-z])\s+클래스", "-클래스", _compact)
if _compact != _ko:
MODEL_EXACT_TRANSLATIONS.setdefault(_compact, _en)
@@ -488,7 +467,6 @@ def _register_partner_models() -> None:
_register_partner_models()
# Безопасные составные фразы. Применяются только к строкам, где есть корейский текст.
MODEL_PHRASE_TRANSLATIONS = {
"올 뉴": "All New",
"더 뉴": "New",
@@ -517,7 +495,6 @@ MODEL_PHRASE_TRANSLATIONS = {
"에어": "Air",
}
# Короткие токены переводятся только как отдельные токены, не внутри других слов.
MODEL_TOKEN_TRANSLATIONS = {
"시리즈": "Series",
"클래스": "Class",
@@ -801,7 +778,6 @@ MODEL_TRANSLATIONS = {
"그란투리스모": "GranTurismo",
"반퀴시": "Vanquish",
"밴티지": "Vantage",
# --- Дополнительные модели, встреченные в данных Encar ---
"시에라": "Sierra",
"시빅": "Civic",
"레인저": "Ranger",
@@ -835,8 +811,6 @@ MODEL_TRANSLATIONS = {
}
# Цвета: корейский → английский
COLOR_TRANSLATIONS = {
"흰색": "white",
"검정색": "black",

View File

@@ -27,7 +27,6 @@ class TestEncarMapper(unittest.TestCase):
self.assertEqual(record.origin_id, "encar:41421262")
self.assertEqual(record.parser_id, "encar:41421262")
self.assertEqual(record.brand, "Hyundai")
# Скобки удаляются — на оригинальном сайте Encar их нет.
self.assertEqual(record.model, "G90")
self.assertEqual(record.year, 2023)
self.assertEqual(record.price, 85900000) # 8590 만원 × 10000
@@ -87,23 +86,26 @@ class TestEncarMapper(unittest.TestCase):
def test_translate_model_keeps_english_text(self):
mapper = EncarMapper()
self.assertEqual(mapper._translate_model("All New Carnival"), "All New Carnival")
self.assertEqual(mapper._translate_model("New Kia Ray"), "New Kia Ray")
self.assertEqual(mapper._translate_model("K5 Hybrid 3rd gen"), "K5 Hybrid 3rd gen")
self.assertEqual(mapper._translate_model("All New Carnival"), "Carnival")
self.assertEqual(mapper._translate_model("New Kia Ray"), "Kia Ray")
self.assertEqual(mapper._translate_model("K5 Hybrid 3rd gen"), "K5 Hybrid")
self.assertEqual(mapper._translate_model("New K5 3rd gen"), "K5")
self.assertEqual(mapper._translate_model("New Tiguan"), "Tiguan")
self.assertEqual(mapper._translate_model("New QM6"), "QM6")
self.assertEqual(mapper._translate_model("Discovery Sport 2nd gen"), "Discovery Sport")
def test_translate_model_handles_korean_fragments(self):
mapper = EncarMapper()
cases = {
# Скобки и их содержимое отбрасываются как на сайте Encar.
"5시리즈 (G30)": "5 Series",
"S-클래스 W223": "S-Class W223",
"Sonata 디 엣지(DN8)": "Sonata The Edge",
"렉스턴 Sport 칸": "Rexton Sport Khan",
"티볼리 Air": "Tivoli Air",
"코란도 투리스모": "Korando Turismo",
"Kia Carnival 4세대": "Kia Carnival 4th gen",
"Cadillac 에스컬레이드 5세대": "Cadillac Escalade 5th gen",
"Kia Carnival 4세대": "Kia Carnival",
"Cadillac 에스컬레이드 5세대": "Cadillac Escalade",
"K7 프리미어": "K7 Premier",
}
@@ -115,9 +117,8 @@ class TestEncarMapper(unittest.TestCase):
mapper = EncarMapper()
cases = {
# "레이" (Ray) не должен резать слово "슈팅브레이크" изнутри.
"슈팅브레이크": "",
"The New G70 슈팅브레이크": "The New G70",
"The New G70 슈팅브레이크": "G70",
"G70 (슈팅브레이크)": "G70",
}