Update model translations

This commit is contained in:
qananasikq
2026-07-01 14:42:35 +03:00
parent 1c5c30e31e
commit efbbf4a067
7 changed files with 805 additions and 2 deletions

100
cars_100.txt Normal file
View File

@@ -0,0 +1,100 @@
1. Chevrolet | Spark | 2012 | id=42291468
2. Mercedes-Benz | G-Class W465 | 2025 | id=42290455
3. Honda | Civic | 2017 | id=41122992
4. Honda | Civic | 2017 | id=41122190
5. Hyundai | Grandeur IG | 2019 | id=42291487
6. Mercedes-Benz | S-Class W223 | 2021 | id=42289505
7. Kia | K5 | 2011 | id=42291450
8. KG Mobility | Very New Tivoli | 2022 | id=41711722
9. Mercedes-Benz | CLS-Class C257 | 2019 | id=42260678
10. Kia | Seltos 2nd gen | 2026 | id=42291463
11. BMW | X7 | 2023 | id=42291443
12. Hyundai | Sonata | 2022 | id=42291455
13. Mercedes-Benz | E-Class W213 | 2020 | id=42291456
14. Hyundai | Grandeur Hybrid | 2025 | id=42291436
15. Mercedes-Benz | C-Class W205 | 2019 | id=42291451
16. Hyundai | Santa Fe TM | 2019 | id=42291444
17. Mercedes-Benz | M-Class W164 | 2007 | id=42257783
18. Land Rover | Discovery 4 | 2015 | id=42261349
19. BMW | 3 Series | 2020 | id=42224479
20. Land Rover | Range Rover Velar | 2019 | id=42273669
21. Porsche | Cayenne | 2023 | id=42291413
22. KG Mobility | Very New Tivoli | 2020 | id=42291412
23. Hyundai | Avante MD | 2011 | id=42291395
24. Chevrolet | All New Camaro | 2017 | id=42284504
25. Renault Korea | SM6 | 2017 | id=42278601
26. Kia | Mohave | 2012 | id=41576139
27. Kia | Carnival 4th gen | 2023 | id=42291411
28. Kia | All New K7 | 2016 | id=41951116
29. Kia | Carnival 4th gen | 2023 | id=42283927
30. BMW | 3 Series | 2006 | id=42291399
31. Hyundai | Ioniq 6 | 2023 | id=42291352
32. Hyundai | New Grand Starex | 2020 | id=41953927
33. KG Mobility | Korando Sport | 2016 | id=41949895
34. Renault Korea | New QM6 | 2020 | id=42291394
35. Mercedes-Benz | E-Class W214 | 2024 | id=42291297
36. Volvo | S60 | 2016 | id=42291398
37. BMW | 3 Series | 2017 | id=42291386
38. Kia | EV6 | 2022 | id=42291336
39. KG Mobility | 2nd gen | 2026 | id=42291391
40. Volkswagen | Jetta 7th gen | 2024 | id=42277008
41. KG Mobility | 2nd gen | 2026 | id=42279398
42. Chevrolet | Spark | 2012 | id=42291385
43. Kia | New K9 2nd gen | 2022 | id=42291378
44. Kia | Carnival 4th gen | 2021 | id=42291381
45. Audi | A7 | 2015 | id=42291370
46. Kia | Carnival 4th gen | 2021 | id=42283942
47. Hyundai | Grandeur HG | 2016 | id=42291377
48. Kia | Carnival 4th gen | 2021 | id=41916671
49. Hyundai | New Palisade | 2023 | id=42291375
50. Porsche | Panamera | 2024 | id=42290634
51. Genesis | EQ900 | 2018 | id=42291315
52. Hyundai | | 2015 | id=42291074
53. Hyundai | Sonata | 2020 | id=42279668
54. Hyundai | New Grandeur IG Hybrid | 2023 | id=42291312
55. Kia | New Kia Ray | 2026 | id=42291083
56. Mercedes-Benz | GLC-Class X253 | 2019 | id=42291373
57. Hyundai | New Tucson | 2025 | id=42286946
58. Hyundai | New Palisade | 2023 | id=42291369
59. BMW | 3 Series | 2017 | id=42291367
60. Mercedes-Benz | E-Class W213 | 2021 | id=42281942
61. Kia | K8 | 2023 | id=42291332
62. KG Mobility | Tivoli Air | 2017 | id=42291259
63. Renault Korea | SM6 | 2017 | id=42291349
64. Chevrolet | Cruze | 2014 | id=42291366
65. Mercedes-Benz | E-Class W212 | 2015 | id=42291345
66. Kia | K8 Hybrid | 2023 | id=42291363
67. Renault Korea | NewQM3 | 2019 | id=42291364
68. Mercedes-Benz | S-Class W222 | 2018 | id=42291350
69. Hyundai | Palisade | 2021 | id=42291359
70. Hyundai | Grand Starex | 2018 | id=42291358
71. Mercedes-Benz | GLS-Class X167 | 2022 | id=42291347
72. Hyundai | Palisade | 2021 | id=42278631
73. Peugeot | 2008 | 2016 | id=42289865
74. Mercedes-Benz | E-Class W213 | 2019 | id=42291344
75. Hyundai | Grand Starex | 2017 | id=42273594
76. Kia | All New Sorento | 2015 | id=42273586
77. Kia | New Morning | 2016 | id=42254096
78. Hyundai | Santa Fe DM | 2013 | id=42291313
79. Mercedes-Benz | E-Class W213 | 2019 | id=42287351
80. Kia | All New K7 | 2017 | id=42291339
81. Kia | Niro | 2017 | id=42291337
82. Infiniti | Q30 | 2019 | id=42291333
83. Kia | Niro | 2017 | id=42289711
84. Mercedes-Benz | E-Class W213 | 2020 | id=42291331
85. Ford | Explorer 6th gen | 2023 | id=42291323
86. Infiniti | Q30 | 2019 | id=42284562
87. BMW | 3 Series | 2024 | id=42291329
88. Genesis | GV70 | 2025 | id=42279323
89. Hyundai | Grandeur HG | 2012 | id=42291328
90. Mercedes-Benz | E-Class W213 | 2020 | id=42263463
91. BMW | 3 Series | 2024 | id=42289258
92. Genesis | GV70 | 2025 | id=42279316
93. Hyundai | Grandeur HG | 2012 | id=42288803
94. Kia | Ray | 2014 | id=42291325
95. Porsche | Cayenne | 2021 | id=42289156
96. KG Mobility | Tivoli Air | 2018 | id=41570718
97. Toyota | Crown Crossover | 2024 | id=42291308
98. Kia | New Sportage R | 2014 | id=42252147
99. Genesis | GV80 | 2024 | id=42291320
100. Kia | Ray | 2014 | id=42279563

View File

@@ -26,6 +26,9 @@ from .translations import (
FUEL_TYPE_MAP, FUEL_TYPE_MAP,
GEARBOX_MAP_KO, GEARBOX_MAP_KO,
MODEL_BODY_TYPE_MAP, MODEL_BODY_TYPE_MAP,
MODEL_EXACT_TRANSLATIONS,
MODEL_PHRASE_TRANSLATIONS,
MODEL_TOKEN_TRANSLATIONS,
MODEL_TRANSLATIONS, MODEL_TRANSLATIONS,
SELL_TYPE_MAP, SELL_TYPE_MAP,
expand_allowed_brands, expand_allowed_brands,
@@ -173,7 +176,7 @@ class EncarMapper:
model_raw = self._as_str(payload.get("Model") or "UNKNOWN") model_raw = self._as_str(payload.get("Model") or "UNKNOWN")
badge = self._as_str(payload.get("Badge") or "") badge = self._as_str(payload.get("Badge") or "")
badge_detail = self._as_str(payload.get("BadgeDetail") or "") badge_detail = self._as_str(payload.get("BadgeDetail") or "")
model = self._translate_text(model_raw) model = self._translate_model(model_raw)
year = self._to_year(payload.get("FormYear") or payload.get("Year")) year = self._to_year(payload.get("FormYear") or payload.get("Year"))
# Price в 만원 (10 000 KRW) → переводим в KRW # Price в 만원 (10 000 KRW) → переводим в KRW
@@ -352,6 +355,68 @@ class EncarMapper:
translated = re.sub(r"\s+", " ", translated).strip() translated = re.sub(r"\s+", " ", translated).strip()
return translated return translated
def _has_korean(self, text: str) -> bool:
return bool(re.search(r"[가-힣]", text))
def _normalize_model_text(self, text: str) -> str:
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)
normalized = re.sub(r"(?<=[0-9])시리즈", "시리즈", normalized)
normalized = re.sub(r"\s+", " ", normalized).strip()
return normalized
def _translate_model(self, text: str) -> str:
"""Безопасно переводит модель Encar: точные совпадения + корейские фрагменты.
Старый общий replace ломал смешанные названия. Здесь английские строки
возвращаются как есть, а короткие корейские токены заменяются только
когда они реально присутствуют как отдельный смысловой фрагмент.
"""
original = self._normalize_model_text(text)
if not original or not self._has_korean(original):
return original
exact = MODEL_EXACT_TRANSLATIONS.get(original)
if exact:
return 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
def _translate_color(self, color_raw: str) -> str: def _translate_color(self, color_raw: str) -> str:
value = self._as_str(color_raw) value = self._as_str(color_raw)
if not value: if not value:

View File

@@ -0,0 +1,326 @@
"""Korean -> English translation tables for EnCar (partner-provided basis).
Источник переводов от партнёра. Этот файл — эталон: бренды и модели берём
отсюда в первую очередь. Держим секционные словари плюс один плоский для
простых поисков.
"""
from __future__ import annotations
ENCAR_KO_EN_TABLES: dict[str, dict[str, str]] = {
"makers": {
"포르쉐": "Porsche",
"메르세데스-벤츠": "Mercedes-Benz",
"메르세데스 벤츠": "Mercedes-Benz",
"벤츠": "Mercedes-Benz",
"메르세데스-AMG": "Mercedes-AMG",
"메르세데스 AMG": "Mercedes-AMG",
"AMG": "AMG",
"BMW": "BMW",
"아우디": "Audi",
"폭스바겐": "Volkswagen",
"렉서스": "Lexus",
"토요타": "Toyota",
"닛산": "Nissan",
"인피니티": "Infiniti",
"혼다": "Honda",
"마쯔다": "Mazda",
"스바루": "Subaru",
"미쓰비시": "Mitsubishi",
"랜드로버": "Land Rover",
"재규어": "Jaguar",
"지프": "Jeep",
"볼보": "Volvo",
"푸조": "Peugeot",
"르노": "Renault",
"미니": "MINI",
"로터스": "Lotus",
"벤틀리": "Bentley",
"롤스로이스": "Rolls-Royce",
"람보르기니": "Lamborghini",
"페라리": "Ferrari",
"맥라렌": "McLaren",
"애스턴마틴": "Aston Martin",
"애스톤마틴": "Aston Martin",
"마세라티": "Maserati",
"코닉세그": "Koenigsegg",
"파가니": "Pagani",
"부가티": "Bugatti",
"캐딜락": "Cadillac",
"쉐보레": "Chevrolet",
"포드": "Ford",
"링컨": "Lincoln",
"크라이슬러": "Chrysler",
"닷지": "Dodge",
"": "Ram",
"테슬라": "Tesla",
"제네시스": "Genesis",
"현대": "Hyundai",
"기아": "Kia",
"KG모빌리티": "KG Mobility",
"쌍용": "SsangYong",
"알파로메오": "Alfa Romeo",
"피아트": "Fiat",
"시트로엥": "Citroen",
"스마트": "Smart",
"폴스타": "Polestar",
"리비안": "Rivian",
"루시드": "Lucid",
"BYD": "BYD",
"지리": "Geely",
"홍치": "Hongqi",
"이스즈": "Isuzu",
},
"models": {
"718 박스터": "718 Boxster",
"718 카이맨": "718 Cayman",
"911": "911",
"911 카브리올레": "911 Cabriolet",
"카이엔": "Cayenne",
"카이엔 쿠페": "Cayenne Coupe",
"파나메라": "Panamera",
"파나메라 스포츠 투리스모": "Panamera Sport Turismo",
"마칸": "Macan",
"타이칸": "Taycan",
"타이칸 크로스 투리스모": "Taycan Cross Turismo",
"918 스파이더": "918 Spyder",
"카레라 GT": "Carrera GT",
"718 스파이더": "718 Spyder",
"박스터": "Boxster",
"카이맨": "Cayman",
"4.0 스파이더": "4.0 Spyder",
"C 클래스": "C-Class",
"E 클래스": "E-Class",
"S 클래스": "S-Class",
"A 클래스": "A-Class",
"B 클래스": "B-Class",
"CLA": "CLA",
"CLS": "CLS",
"SL": "SL",
"SLK": "SLK",
"G 클래스": "G-Class",
"GLA": "GLA",
"GLB": "GLB",
"GLC": "GLC",
"GLE": "GLE",
"GLS": "GLS",
"AMG GT": "AMG GT",
"AMG GT 4도어 쿠페": "AMG GT 4-Door Coupe",
"V 클래스": "V-Class",
"1 시리즈": "1 Series",
"2 시리즈": "2 Series",
"3 시리즈": "3 Series",
"4 시리즈": "4 Series",
"5 시리즈": "5 Series",
"6 시리즈": "6 Series",
"7 시리즈": "7 Series",
"8 시리즈": "8 Series",
"M2": "M2",
"M3": "M3",
"M4": "M4",
"M5": "M5",
"M8": "M8",
"X1": "X1",
"X2": "X2",
"X3": "X3",
"X4": "X4",
"X5": "X5",
"X6": "X6",
"X7": "X7",
"Z4": "Z4",
"A3": "A3",
"A4": "A4",
"A5": "A5",
"A6": "A6",
"A7": "A7",
"A8": "A8",
"Q3": "Q3",
"Q5": "Q5",
"Q7": "Q7",
"Q8": "Q8",
"R8": "R8",
"TT": "TT",
"고스트": "Ghost",
"팬텀": "Phantom",
"레이스": "Wraith",
"": "Dawn",
"컬리넌": "Cullinan",
"스펙터": "Spectre",
"벤테이가": "Bentayga",
"컨티넨탈 GT": "Continental GT",
"플라잉스퍼": "Flying Spur",
"우라칸": "Huracan",
"아벤타도르": "Aventador",
"우루스": "Urus",
"레부엘토": "Revuelto",
"로마": "Roma",
"포르토피노": "Portofino",
"SF90 스트라달레": "SF90 Stradale",
"F8 트리뷰토": "F8 Tributo",
"콰트로포르테": "Quattroporte",
"기블리": "Ghibli",
"르반떼": "Levante",
"그란투리스모": "GranTurismo",
"반퀴시": "Vanquish",
"밴티지": "Vantage",
"DB11": "DB11",
"DB12": "DB12",
"DBS": "DBS",
},
"body_types": {
"세단": "Sedan",
"쿠페": "Coupe",
"컨버터블": "Convertible",
"카브리올레": "Cabriolet",
"오픈카": "Open Top",
"해치백": "Hatchback",
"왜건": "Wagon",
"스테이션왜건": "Station Wagon",
"SUV": "SUV",
"크로스오버": "Crossover",
"픽업트럭": "Pickup Truck",
"": "Van",
"미니밴": "Minivan",
"트럭": "Truck",
"쿠페형 SUV": "Coupe SUV",
"기타": "Other",
},
"fuel_types": {
"가솔린": "Gasoline",
"디젤": "Diesel",
"경유": "Diesel",
"하이브리드": "Hybrid",
"가솔린 하이브리드": "Gasoline Hybrid",
"플러그인 하이브리드": "Plug-in Hybrid",
"전기": "Electric",
"전기차": "Electric",
"수소": "Hydrogen",
"LPG": "LPG",
"LNG": "LNG",
"가솔린+전기": "Gasoline + Electric",
"가솔린 + 전기": "Gasoline + Electric",
},
"transmissions": {
"수동": "Manual",
"오토": "Automatic",
"자동": "Automatic",
"자동변속": "Automatic",
"세미오토": "Semi-automatic",
"CVT": "CVT",
"듀얼클러치": "Dual-clutch",
},
"sell_types": {
"일반": "General",
"리스": "Lease",
"렌트": "Rent",
"할부": "Installment",
},
"lease_types": {
"운용리스": "Operating Lease",
"금융리스": "Financial Lease",
},
"regions": {
"서울": "Seoul",
"부산": "Busan",
"인천": "Incheon",
"대구": "Daegu",
"대전": "Daejeon",
"광주": "Gwangju",
"울산": "Ulsan",
"세종": "Sejong",
"경기": "Gyeonggi",
"강원": "Gangwon",
"충북": "Chungbuk",
"충남": "Chungnam",
"전북": "Jeonbuk",
"전남": "Jeonnam",
"경북": "Gyeongbuk",
"경남": "Gyeongnam",
"제주": "Jeju",
},
"dealer_companies": {
"(주)기억": "Gieok Co., Ltd.",
"(주)네바퀴": "Four Wheels Co., Ltd.",
"(주)에이스타모터스": "A-Star Motors Co., Ltd.",
"(주)우진 오토모빌": "Woojin Automobile Co., Ltd.",
"(주)지온컴퍼니": "Zion Company Co., Ltd.",
"(주)플로시모터스": "Flossy Motors Co., Ltd.",
"㈜오토핸즈": "Auto Hands Co., Ltd.",
"GD카": "GD Car",
"MK모터스": "MK Motors",
"꿈차": "Dream Car",
"마이카4": "My Car 4",
"블루오토": "Blue Auto",
"세영모빌리티(주)": "Seyoung Mobility Co., Ltd.",
"스마일자동차": "Smile Motors",
"오토링크": "Auto Link",
"오토핸즈": "Auto Hands",
"우리들모터스": "Woorideul Motors",
"주식회사 모터바겐": "Motorwagen Co., Ltd.",
"주식회사 아이오토스": "iAutos Co., Ltd.",
"주식회사이들컴퍼니": "Ideul Company Co., Ltd.",
"카메이트": "Car Mate",
"카톡모터스": "Katok Motors",
"카피디": "Car PD",
"포르쉐 인증 중고차 센터 성수": "Porsche Certified Used Car Center Seongsu",
"포르쉐인증중고차센터성수": "Porsche Certified Used Car Center Seongsu",
},
"dealer_names": {
"고준용": "Go Jun-yong",
"공민중": "Gong Min-jung",
"김경진": "Kim Gyeong-jin",
"김명진": "Kim Myeong-jin",
"김민수": "Kim Min-su",
"김성언": "Kim Seong-eon",
"김성진": "Kim Seong-jin",
"김영균": "Kim Yeong-gyun",
"김완섭": "Kim Wan-seop",
"김현진": "Kim Hyeon-jin",
"김형진": "Kim Hyeong-jin",
"김호영": "Kim Ho-yeong",
"문명기": "Moon Myeong-gi",
"박원석": "Park Won-seok",
"박재현": "Park Jae-hyeon",
"방지윤": "Bang Ji-yun",
"송슬기": "Song Seul-gi",
"신영철": "Shin Yeong-cheol",
"신호진": "Shin Ho-jin",
"양시언": "Yang Si-eon",
"양태규": "Yang Tae-gyu",
"윤지완": "Yoon Ji-wan",
"이경민": "Lee Gyeong-min",
"이재욱": "Lee Jae-uk",
"임종현": "Lim Jong-hyeon",
"임형욱": "Lim Hyeong-uk",
"전태준": "Jeon Tae-jun",
"정상필": "Jeong Sang-pil",
"조경식": "Jo Gyeong-sik",
"조승재": "Jo Seung-jae",
"조환성": "Jo Hwan-seong",
"차상훈": "Cha Sang-hun",
"최경욱": "Choi Gyeong-uk",
"하상열": "Ha Sang-yeol",
"허지훈": "Heo Ji-hun",
},
}
def _build_flat_map() -> dict[str, str]:
result: dict[str, str] = {}
for section in ENCAR_KO_EN_TABLES.values():
result.update(section)
return result
ENCAR_KO_EN_TRANSLATIONS: dict[str, str] = _build_flat_map()
def translate_encar_ko_to_en(value: str, section: str | None = None) -> str:
"""Перевод корейского термина Encar на английский.
Если задан `section`, используется только словарь этой секции. Иначе —
плоский объединённый словарь.
"""
if section is not None:
return ENCAR_KO_EN_TABLES.get(section, {}).get(value, value)
return ENCAR_KO_EN_TRANSLATIONS.get(value, value)

View File

@@ -1,5 +1,15 @@
# Словари переводов корейского текста русский / английский. # Словари переводов корейского текста русский / английский.
# Используется в encar.py для маппинга данных Encar API. # Используется в encar.py для маппинга данных Encar API.
#
# Эталон переводов брендов и моделей — словарь партнёра
# (`partner_dictionary.py`). Он подмешивается ниже как база: значения оттуда
# имеют приоритет для брендов и моделей, а специфичные для проекта записи
# (корейские модели Hyundai/Kia/Genesis и т.п., которых нет у партнёра)
# сохраняются как дополнение.
import re
from .partner_dictionary import ENCAR_KO_EN_TABLES as _PARTNER_TABLES
# Топливо # Топливо
@@ -176,6 +186,12 @@ BRAND_TRANSLATIONS = {
"기타 수입차": "Other Imported", "기타 수입차": "Other Imported",
} }
# Подмешиваем бренды из эталонного словаря партнёра как базу. Существующие
# записи проекта имеют приоритет (не перезаписываются), новые из словаря
# партнёра добавляются.
for _ko, _en in _PARTNER_TABLES["makers"].items():
BRAND_TRANSLATIONS.setdefault(_ko, _en)
# Бренды: английский → алиасы (корейский + русский) # Бренды: английский → алиасы (корейский + русский)
@@ -277,6 +293,240 @@ 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",
"4시리즈": "4 Series",
"5시리즈": "5 Series",
"6시리즈": "6 Series",
"7시리즈": "7 Series",
"8시리즈": "8 Series",
"A-클래스": "A-Class",
"B-클래스": "B-Class",
"C-클래스": "C-Class",
"E-클래스": "E-Class",
"S-클래스": "S-Class",
"SL-클래스": "SL-Class",
"SLK-클래스": "SLK-Class",
"GLA-클래스": "GLA-Class",
"GLB-클래스": "GLB-Class",
"GLC-클래스": "GLC-Class",
"GLE-클래스": "GLE-Class",
"GLS-클래스": "GLS-Class",
"V-클래스": "V-Class",
# Hyundai / Kia / Genesis
"팰리세이드": "Palisade",
"싼타페": "Santa Fe",
"투싼": "Tucson",
"아반떼": "Avante",
"쏘나타": "Sonata",
"쏘나타 디 엣지": "Sonata The Edge",
"그랜저": "Grandeur",
"아이오닉5": "Ioniq 5",
"아이오닉6": "Ioniq 6",
"아이오닉": "Ioniq",
"넥쏘": "Nexo",
"벨로스터": "Veloster",
"코나": "Kona",
"캐스퍼": "Casper",
"베뉴": "Venue",
"스타렉스": "Starex",
"스타리아": "Staria",
"아슬란": "Aslan",
"베라크루즈": "Veracruz",
"카니발": "Carnival",
"쏘렌토": "Sorento",
"스포티지": "Sportage",
"에스컬레이드": "Escalade",
"프리미어": "Premier",
"1세대": "1st gen",
"2세대": "2nd gen",
"3세대": "3rd gen",
"4세대": "4th gen",
"5세대": "5th gen",
"6세대": "6th gen",
"7세대": "7th gen",
"8세대": "8th gen",
"모닝": "Morning",
"레이": "Ray",
"셀토스": "Seltos",
"니로": "Niro",
"스팅어": "Stinger",
"쏘울": "Soul",
"모하비": "Mohave",
"프라이드": "Pride",
"스토닉": "Stonic",
"카렌스": "Carens",
"제네시스": "Genesis",
# Chevrolet / Renault Korea / KG Mobility
"스파크": "Spark",
"말리부": "Malibu",
"트랙스": "Trax",
"크루즈": "Cruze",
"올란도": "Orlando",
"트레일블레이저": "Trailblazer",
"콜로라도": "Colorado",
"트래버스": "Traverse",
"캡티바": "Captiva",
"이쿼녹스": "Equinox",
"아베오": "Aveo",
"볼트": "Volt",
"임팔라": "Impala",
"맥스크루즈": "Max Cruz",
"렉스턴 스포츠 칸": "Rexton Sport Khan",
"렉스턴 스포츠": "Rexton Sport",
"렉스턴": "Rexton",
"토레스": "Torres",
"티볼리 에어": "Tivoli Air",
"티볼리 아머": "Tivoli Armor",
"티볼리": "Tivoli",
"코란도 투리스모": "Korando Turismo",
"코란도 스포츠": "Korando Sport",
"코란도": "Korando",
"체어맨": "Chairman",
"오피러스": "Opirus",
# Import models written in Korean
"익스플로러": "Explorer",
"머스탱": "Mustang",
"카이엔": "Cayenne",
"카이엔 쿠페": "Cayenne Coupe",
"파나메라": "Panamera",
"파나메라 스포츠 투리스모": "Panamera Sport Turismo",
"마칸": "Macan",
"타이칸": "Taycan",
"타이칸 크로스 투리스모": "Taycan Cross Turismo",
"918 스파이더": "918 Spyder",
"718 스파이더": "718 Spyder",
"4.0 스파이더": "4.0 Spyder",
"박스터": "Boxster",
"카이맨": "Cayman",
"911 카브리올레": "911 Cabriolet",
"카레라": "Carrera",
"티구안": "Tiguan",
"골프": "Golf",
"제타": "Jetta",
"투아렉": "Touareg",
"파사트": "Passat",
"아테온": "Arteon",
"쿠퍼": "Cooper",
"컨트리맨": "Countryman",
"클럽맨": "Clubman",
"디스커버리": "Discovery",
"레인지로버": "Range Rover",
"이보크": "Evoque",
"벨라": "Velar",
"디펜더": "Defender",
"랭글러": "Wrangler",
"체로키": "Cherokee",
"레니게이드": "Renegade",
"컴패스": "Compass",
"루비콘": "Rubicon",
"에비에이터": "Aviator",
"르반떼": "Levante",
"기블리": "Ghibli",
"어코드": "Accord",
"캠리": "Camry",
"프리우스": "Prius",
"타스만": "Tasman",
"토러스": "Taurus",
"카마로": "Camaro",
"비틀": "Beetle",
"e-트론": "e-tron",
"글래디에이터": "Gladiator",
"노틸러스": "Nautilus",
"시로코": "Scirocco",
"아틀라스": "Atlas",
"알파드": "Alphard",
"익스페디션": "Expedition",
"익스프레스Van": "Express Van",
"짐니": "Jimny",
"쿠가": "Kuga",
"클리오": "Clio",
"파일럿": "Pilot",
"콰트로포르테": "Quattroporte",
"고스트": "Ghost",
"팬텀": "Phantom",
"레이스": "Wraith",
"": "Dawn",
"컬리넌": "Cullinan",
"스펙터": "Spectre",
"벤테이가": "Bentayga",
"컨티넨탈 GT": "Continental GT",
"플라잉스퍼": "Flying Spur",
"우라칸": "Huracan",
"아벤타도르": "Aventador",
"우루스": "Urus",
"레부엘토": "Revuelto",
"로마": "Roma",
"포르토피노": "Portofino",
"SF90 스트라달레": "SF90 Stradale",
"F8 트리뷰토": "F8 Tributo",
"그란투리스모": "GranTurismo",
"반퀴시": "Vanquish",
"밴티지": "Vantage",
"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)
_register_partner_models()
# Безопасные составные фразы. Применяются только к строкам, где есть корейский текст.
MODEL_PHRASE_TRANSLATIONS = {
"올 뉴": "All New",
"더 뉴": "New",
"디 올 뉴": "The All New",
"베리 뉴": "Very New",
"베리": "Very",
"뷰티풀": "Beautiful",
"넥스트": "Next",
"": "The ",
"디 엣지": "The Edge",
"액티브 투어러": "Active Tourer",
"투어러": "Tourer",
"그란쿠페": "GranCoupe",
"그란 쿠페": "GranCoupe",
"스포츠 칸": "Sport Khan",
"스포츠": "Sport",
"하이브리드": "Hybrid",
"일렉트릭": "Electric",
"네오": "Neo",
"트랜스폼": "Transform",
"프라임": "Prime",
"마스터": "Master",
"어반": "Urban",
"볼드": "Bold",
"아머": "Armor",
"에어": "Air",
}
# Короткие токены переводятся только как отдельные токены, не внутри других слов.
MODEL_TOKEN_TRANSLATIONS = {
"시리즈": "Series",
"클래스": "Class",
"세대": "gen",
"": "New",
"": "All",
"": "Khan",
}
MODEL_TRANSLATIONS = { MODEL_TRANSLATIONS = {
# --- Топливо / тип двигателя --- # --- Топливо / тип двигателя ---
"가솔린": "Gasoline", "가솔린": "Gasoline",
@@ -551,6 +801,21 @@ MODEL_TRANSLATIONS = {
"그란투리스모": "GranTurismo", "그란투리스모": "GranTurismo",
"반퀴시": "Vanquish", "반퀴시": "Vanquish",
"밴티지": "Vantage", "밴티지": "Vantage",
# --- Дополнительные модели, встреченные в данных Encar ---
"시에라": "Sierra",
"시빅": "Civic",
"레인저": "Ranger",
"콜레오스": "Koleos",
"그랑 콜레오스": "Grand Koleos",
"페이톤": "Phaeton",
"티록": "T-Roc",
"큐브": "Cube",
"크라운": "Crown",
"라이즈": "Rise",
"시로코": "Scirocco",
"이오스": "Eos",
"비틀": "Beetle",
"폴로": "Polo",
# --- Служебные / категории --- # --- Служебные / категории ---
"(세부등급": "(sub-trim", "(세부등급": "(sub-trim",
"없음)": "none)", "없음)": "none)",

View File

@@ -23,6 +23,7 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"httpx>=0.28.0",
"pytest>=8.3.0", "pytest>=8.3.0",
"pytest-cov>=5.0.0", "pytest-cov>=5.0.0",
] ]

View File

@@ -27,7 +27,8 @@ class TestEncarMapper(unittest.TestCase):
self.assertEqual(record.origin_id, "encar:41421262") self.assertEqual(record.origin_id, "encar:41421262")
self.assertEqual(record.parser_id, "encar:41421262") self.assertEqual(record.parser_id, "encar:41421262")
self.assertEqual(record.brand, "Hyundai") self.assertEqual(record.brand, "Hyundai")
self.assertEqual(record.model, "G90 (RS4)") # Скобки удаляются — на оригинальном сайте Encar их нет.
self.assertEqual(record.model, "G90")
self.assertEqual(record.year, 2023) self.assertEqual(record.year, 2023)
self.assertEqual(record.price, 85900000) # 8590 만원 × 10000 self.assertEqual(record.price, 85900000) # 8590 만원 × 10000
self.assertEqual(record.mileage, 37923) self.assertEqual(record.mileage, 37923)
@@ -83,10 +84,52 @@ class TestEncarMapper(unittest.TestCase):
self.assertEqual(record.parser_id, "encar:41878507") self.assertEqual(record.parser_id, "encar:41878507")
self.assertIn("carid=41878507", record.origin_url) self.assertIn("carid=41878507", record.origin_url)
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")
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",
"K7 프리미어": "K7 Premier",
}
for raw, expected in cases.items():
with self.subTest(raw=raw):
self.assertEqual(mapper._translate_model(raw), expected)
def test_translate_model_drops_untranslated_korean(self):
mapper = EncarMapper()
cases = {
# "레이" (Ray) не должен резать слово "슈팅브레이크" изнутри.
"슈팅브레이크": "",
"The New G70 슈팅브레이크": "The New G70",
"G70 (슈팅브레이크)": "G70",
}
for raw, expected in cases.items():
with self.subTest(raw=raw):
self.assertEqual(mapper._translate_model(raw), expected)
class TestEncarScraper(unittest.TestCase): class TestEncarScraper(unittest.TestCase):
def test_collect_listing_limits_results(self): def test_collect_listing_limits_results(self):
scraper = EncarScraper() scraper = EncarScraper()
scraper._fetch_listing_page = MagicMock(side_effect=[ scraper._fetch_listing_page = MagicMock(side_effect=[
{"SearchResults": [{"Id": 1}, {"Id": 2}]}, {"SearchResults": [{"Id": 1}, {"Id": 2}]},
]) ])

View File

@@ -195,11 +195,14 @@ class TestAPILoad(unittest.TestCase):
persistence: PersistenceService = app.state.persistence persistence: PersistenceService = app.state.persistence
persistence.upsert_cars_batch([_make_car(i, images=4) for i in range(200)]) persistence.upsert_cars_batch([_make_car(i, images=4) for i in range(200)])
cls.app = app
cls.client = TestClient(app) cls.client = TestClient(app)
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
import os import os
cls.client.close()
cls.app.state.persistence.engine.dispose()
if os.path.exists("test_api_load.db"): if os.path.exists("test_api_load.db"):
os.remove("test_api_load.db") os.remove("test_api_load.db")