diff --git a/encar_scraper/encar.py b/encar_scraper/encar.py index 523dbe1..9d5f5f5 100644 --- a/encar_scraper/encar.py +++ b/encar_scraper/encar.py @@ -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"(? 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): diff --git a/encar_scraper/translations.py b/encar_scraper/translations.py index 08091ac..a74e5bd 100644 --- a/encar_scraper/translations.py +++ b/encar_scraper/translations.py @@ -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", diff --git a/tests/test_encar.py b/tests/test_encar.py index 442bce4..4178547 100644 --- a/tests/test_encar.py +++ b/tests/test_encar.py @@ -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", }