Compare commits
5 Commits
f1e6b9db9b
...
ec49ed9bc5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec49ed9bc5 | ||
|
|
bc5c3698c3 | ||
|
|
12b313ad52 | ||
|
|
0c08fc7f63 | ||
|
|
66dddbc0f9 |
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
@@ -26,21 +26,28 @@ def list_cars(
|
||||
# Список автомобилей с пагинацией и фильтрами
|
||||
with persistence.session_scope() as session:
|
||||
query = select(Car)
|
||||
count_query = select(func.count(Car.id))
|
||||
|
||||
if brand:
|
||||
escaped_brand = brand.replace("%", r"\%").replace("_", r"\_")
|
||||
query = query.where(Car.brand.ilike(f"%{escaped_brand}%", escape="\\"))
|
||||
cond = Car.brand.ilike(f"%{escaped_brand}%", escape="\\")
|
||||
query = query.where(cond)
|
||||
count_query = count_query.where(cond)
|
||||
if model:
|
||||
escaped_model = model.replace("%", r"\%").replace("_", r"\_")
|
||||
query = query.where(Car.model.ilike(f"%{escaped_model}%", escape="\\"))
|
||||
cond = Car.model.ilike(f"%{escaped_model}%", escape="\\")
|
||||
query = query.where(cond)
|
||||
count_query = count_query.where(cond)
|
||||
if year_min is not None:
|
||||
query = query.where(Car.year >= year_min)
|
||||
count_query = count_query.where(Car.year >= year_min)
|
||||
if year_max is not None:
|
||||
query = query.where(Car.year <= year_max)
|
||||
count_query = count_query.where(Car.year <= year_max)
|
||||
if is_sold is not None:
|
||||
query = query.where(Car.is_sold == is_sold)
|
||||
count_query = count_query.where(Car.is_sold == is_sold)
|
||||
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total = session.execute(count_query).scalar() or 0
|
||||
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
@@ -919,6 +919,41 @@ class EncarScraper:
|
||||
excluded_brands: set[str] | None = None,
|
||||
runtime_filters: FiltersConfig | None = None,
|
||||
probe_all_photos: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Публичная обёртка: гарантирует закрытие HTTP pool даже при ошибках."""
|
||||
try:
|
||||
return self._sync_listing_impl(
|
||||
limit=limit,
|
||||
filters=filters,
|
||||
lane=lane,
|
||||
only_new=only_new,
|
||||
batch_size=batch_size,
|
||||
redis_client=redis_client,
|
||||
allowed_brands=allowed_brands,
|
||||
excluded_brands=excluded_brands,
|
||||
runtime_filters=runtime_filters,
|
||||
probe_all_photos=probe_all_photos,
|
||||
)
|
||||
finally:
|
||||
if self._batch_pool is not None:
|
||||
try:
|
||||
self._batch_pool.close()
|
||||
except Exception:
|
||||
logger.debug("Failed to close batch pool", exc_info=True)
|
||||
self._batch_pool = None
|
||||
|
||||
def _sync_listing_impl(
|
||||
self,
|
||||
limit: int | None = None,
|
||||
filters: EncarFilters | None = None,
|
||||
lane: str = "encar",
|
||||
only_new: bool = False,
|
||||
batch_size: int = 1000,
|
||||
redis_client: Any | None = None,
|
||||
allowed_brands: set[str] | None = None,
|
||||
excluded_brands: set[str] | None = None,
|
||||
runtime_filters: FiltersConfig | None = None,
|
||||
probe_all_photos: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Полная синхронизация листинга Encar.
|
||||
|
||||
@@ -988,11 +1023,21 @@ class EncarScraper:
|
||||
max_consecutive_errors = 5
|
||||
consecutive_zero_new = 0
|
||||
max_consecutive_zero_new = 5
|
||||
# Защита от бесконечной пагинации: Encar API отдаёт максимум
|
||||
# ~10k записей на query, шарды строятся ≤9500 → не более ~10
|
||||
# страниц при page_size=1000. Жёсткий cap в 50 — паранойя.
|
||||
max_pages_per_shard = 50
|
||||
|
||||
shard_query = shard_filters.build_query()
|
||||
shard_collected = 0
|
||||
|
||||
while True:
|
||||
if page >= max_pages_per_shard:
|
||||
logger.warning(
|
||||
"Shard %d hit hard page cap (%d), moving to next",
|
||||
shard_idx, max_pages_per_shard,
|
||||
)
|
||||
break
|
||||
offset = page * page_size
|
||||
try:
|
||||
response = self._fetch_listing_page(
|
||||
@@ -1152,11 +1197,6 @@ class EncarScraper:
|
||||
|
||||
self._clear_checkpoint(redis_client)
|
||||
|
||||
# Закрываем pool batch API
|
||||
if self._batch_pool is not None:
|
||||
self._batch_pool.close()
|
||||
self._batch_pool = None
|
||||
|
||||
logger.info(
|
||||
"Full sync complete: %d shards, %d collected, %d synced, %d failed, %d marked sold",
|
||||
len(shards), items_collected, synced, failed, marked_sold,
|
||||
|
||||
@@ -46,7 +46,10 @@ class PersistenceService:
|
||||
try:
|
||||
Base.metadata.create_all(self.engine)
|
||||
except Exception:
|
||||
logger.debug("create_tables skipped (schema already exists)")
|
||||
# Не глотаем: таблицы могут уже существовать (ок), либо есть проблема
|
||||
# доступа — пусть вышестоящий код видит её при первой операции, но
|
||||
# сигнализируем в warning чтобы упростить диагностику.
|
||||
logger.warning("create_tables failed (continuing — schema may already exist)", exc_info=True)
|
||||
|
||||
@contextmanager
|
||||
def session_scope(self) -> Iterator[Session]:
|
||||
|
||||
@@ -5,21 +5,28 @@
|
||||
# Топливо
|
||||
|
||||
FUEL_TYPE_MAP = {
|
||||
"가솔린": "petrol",
|
||||
"gasoline": "petrol",
|
||||
"디젤": "diesel",
|
||||
"diesel": "diesel",
|
||||
"전기": "electric",
|
||||
"electric": "electric",
|
||||
"하이브리드": "hybrid",
|
||||
"hybrid": "hybrid",
|
||||
"lpg": "lpg",
|
||||
"LPG": "lpg",
|
||||
"LPG(일반인 구입)": "lpg",
|
||||
"LPG(장애인 구입)": "lpg",
|
||||
"가솔린+전기": "hybrid",
|
||||
"디젤+전기": "hybrid",
|
||||
"CNG": "cng",
|
||||
"가솔린": "Gasoline",
|
||||
"gasoline": "Gasoline",
|
||||
"디젤": "Diesel",
|
||||
"경유": "Diesel",
|
||||
"diesel": "Diesel",
|
||||
"전기": "Electric",
|
||||
"전기차": "Electric",
|
||||
"electric": "Electric",
|
||||
"하이브리드": "Hybrid",
|
||||
"hybrid": "Hybrid",
|
||||
"가솔린 하이브리드": "Gasoline Hybrid",
|
||||
"플러그인 하이브리드": "Plug-in Hybrid",
|
||||
"lpg": "LPG",
|
||||
"LPG": "LPG",
|
||||
"LPG(일반인 구입)": "LPG",
|
||||
"LPG(장애인 구입)": "LPG",
|
||||
"LNG": "LNG",
|
||||
"수소": "Hydrogen",
|
||||
"가솔린+전기": "Gasoline + Electric",
|
||||
"가솔린 + 전기": "Gasoline + Electric",
|
||||
"디젤+전기": "Hybrid",
|
||||
"CNG": "CNG",
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +35,12 @@ FUEL_TYPE_MAP = {
|
||||
GEARBOX_MAP_KO = {
|
||||
"오토": "AT",
|
||||
"자동": "AT",
|
||||
"자동변속": "AT",
|
||||
"수동": "MT",
|
||||
"세미오토": "AT",
|
||||
"cvt": "CVT",
|
||||
"CVT": "CVT",
|
||||
"듀얼클러치": "AT",
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +50,9 @@ SELL_TYPE_MAP = {
|
||||
"일반": "STOCK",
|
||||
"경매": "AUCTION",
|
||||
"입찰": "TENDER",
|
||||
"리스": "STOCK",
|
||||
"렌트": "STOCK",
|
||||
"할부": "STOCK",
|
||||
}
|
||||
|
||||
|
||||
@@ -53,104 +66,114 @@ CITY_TO_COUNTRY = {
|
||||
}
|
||||
|
||||
|
||||
# Бренды: корейский → русский
|
||||
# Бренды: корейский → английский
|
||||
|
||||
BRAND_TRANSLATIONS = {
|
||||
"현대": "Хёндэ",
|
||||
"기아": "Киа",
|
||||
"쉐보레(GM대우)": "Шевроле",
|
||||
"벤츠": "Мерседес-Бенц",
|
||||
"현대": "Hyundai",
|
||||
"기아": "Kia",
|
||||
"쉐보레(GM대우)": "Chevrolet",
|
||||
"벤츠": "Mercedes-Benz",
|
||||
"메르세데스-벤츠": "Mercedes-Benz",
|
||||
"메르세데스 벤츠": "Mercedes-Benz",
|
||||
"메르세데스-AMG": "Mercedes-AMG",
|
||||
"메르세데스 AMG": "Mercedes-AMG",
|
||||
"AMG": "AMG",
|
||||
"BMW": "BMW",
|
||||
"아우디": "Ауди",
|
||||
"폭스바겐": "Фольксваген",
|
||||
"토요타": "Тойота",
|
||||
"도요타": "Тойота",
|
||||
"닛산": "Ниссан",
|
||||
"혼다": "Хонда",
|
||||
"포르쉐": "Порше",
|
||||
"랜드로버": "Ленд Ровер",
|
||||
"재규어": "Ягуар",
|
||||
"볼보": "Вольво",
|
||||
"푸조": "Пежо",
|
||||
"시트로엥": "Ситроен",
|
||||
"르노": "Рено",
|
||||
"미니": "Мини",
|
||||
"링컨": "Линкольн",
|
||||
"캐딜락": "Кадиллак",
|
||||
"크라이슬러": "Крайслер",
|
||||
"지프": "Джип",
|
||||
"포드": "Форд",
|
||||
"테슬라": "Тесла",
|
||||
"폴크스바겐": "Фольксваген",
|
||||
"스코다": "Шкода",
|
||||
"아우디": "Audi",
|
||||
"폭스바겐": "Volkswagen",
|
||||
"토요타": "Toyota",
|
||||
"도요타": "Toyota",
|
||||
"닛산": "Nissan",
|
||||
"혼다": "Honda",
|
||||
"포르쉐": "Porsche",
|
||||
"랜드로버": "Land Rover",
|
||||
"재규어": "Jaguar",
|
||||
"볼보": "Volvo",
|
||||
"푸조": "Peugeot",
|
||||
"시트로엥": "Citroen",
|
||||
"르노": "Renault",
|
||||
"미니": "MINI",
|
||||
"링컨": "Lincoln",
|
||||
"캐딜락": "Cadillac",
|
||||
"크라이슬러": "Chrysler",
|
||||
"지프": "Jeep",
|
||||
"포드": "Ford",
|
||||
"테슬라": "Tesla",
|
||||
"폴크스바겐": "Volkswagen",
|
||||
"스코다": "Skoda",
|
||||
"세아트": "SEAT",
|
||||
"오펠": "Опель",
|
||||
"피아트": "Фиат",
|
||||
"알파로메오": "Альфа Ромео",
|
||||
"마세라티": "Мазерати",
|
||||
"람보르기니": "Ламборгини",
|
||||
"페라리": "Феррари",
|
||||
"벤틀리": "Бентли",
|
||||
"롤스로이스": "Роллс-Ройс",
|
||||
"애스턴마틴": "Астон Мартин",
|
||||
"맥라렌": "Макларен",
|
||||
"부가티": "Бугатти",
|
||||
"코닉세그": "Кёнигсегг",
|
||||
"파가니": "Пагани",
|
||||
"오펠": "Opel",
|
||||
"피아트": "Fiat",
|
||||
"알파로메오": "Alfa Romeo",
|
||||
"마세라티": "Maserati",
|
||||
"람보르기니": "Lamborghini",
|
||||
"페라리": "Ferrari",
|
||||
"벤틀리": "Bentley",
|
||||
"롤스로이스": "Rolls-Royce",
|
||||
"애스턴마틴": "Aston Martin",
|
||||
"애스톤마틴": "Aston Martin",
|
||||
"맥라렌": "McLaren",
|
||||
"부가티": "Bugatti",
|
||||
"코닉세그": "Koenigsegg",
|
||||
"파가니": "Pagani",
|
||||
"헤네시": "Hennessey",
|
||||
"쉘비": "Shelby",
|
||||
"다임러": "Daimler",
|
||||
"마이바흐": "Майбах",
|
||||
"스마트": "Смарт",
|
||||
"마이바흐": "Maybach",
|
||||
"스마트": "Smart",
|
||||
"DS": "DS",
|
||||
"렉서스": "Лексус",
|
||||
"인피니티": "Инфинити",
|
||||
"아큐라": "Акура",
|
||||
"스바루": "Субару",
|
||||
"미쓰비시": "Мицубиси",
|
||||
"마쓰다": "Мазда",
|
||||
"이스즈": "Исузу",
|
||||
"렉서스": "Lexus",
|
||||
"인피니티": "Infiniti",
|
||||
"아큐라": "Acura",
|
||||
"스바루": "Subaru",
|
||||
"미쓰비시": "Mitsubishi",
|
||||
"마쓰다": "Mazda",
|
||||
"이스즈": "Isuzu",
|
||||
"다이하츠": "Daihatsu",
|
||||
"스즈키": "Сузуки",
|
||||
"스즈키": "Suzuki",
|
||||
"KG모빌리티(쌍용)": "KG Mobility",
|
||||
"르노코리아(삼성)": "Рено Корея",
|
||||
"KG모빌리티": "KG Mobility",
|
||||
"르노코리아(삼성)": "Renault Korea",
|
||||
"한국GM": "GM Korea",
|
||||
"쌍용": "СсангЙонг",
|
||||
"대우": "Дэу",
|
||||
"제네시스": "Генезис",
|
||||
"르노삼성": "Рено Самсунг",
|
||||
"로터스": "Лотус",
|
||||
"리비안": "Ривиан",
|
||||
"루시드": "Люсид",
|
||||
"리막": "Римак",
|
||||
"알파 로메오": "Альфа Ромео",
|
||||
"마쯔다": "Мазда",
|
||||
"어큐라": "Акура",
|
||||
"다이하쯔": "Дайхатсу",
|
||||
"쌍용": "SsangYong",
|
||||
"대우": "Daewoo",
|
||||
"제네시스": "Genesis",
|
||||
"르노삼성": "Renault Samsung",
|
||||
"로터스": "Lotus",
|
||||
"리비안": "Rivian",
|
||||
"루시드": "Lucid",
|
||||
"리막": "Rimac",
|
||||
"알파 로메오": "Alfa Romeo",
|
||||
"마쯔다": "Mazda",
|
||||
"어큐라": "Acura",
|
||||
"다이하쯔": "Daihatsu",
|
||||
# --- Новые бренды (с сайта Encar) ---
|
||||
"쉐보레": "Шевроле",
|
||||
"시트로엥/DS": "Ситроен/DS",
|
||||
"미쯔비시": "Мицубиси",
|
||||
"미쯔오까": "Мицуока",
|
||||
"동풍소콘": "Донгфенг Сокон",
|
||||
"쉐보레": "Chevrolet",
|
||||
"시트로엥/DS": "Citroen/DS",
|
||||
"미쯔비시": "Mitsubishi",
|
||||
"미쯔오까": "Mitsuoka",
|
||||
"동풍소콘": "Dongfeng Sokon",
|
||||
"북기은상": "BAIC",
|
||||
"신위안": "Skywell",
|
||||
"이네오스": "Инеос",
|
||||
"지리": "Джили",
|
||||
"머큐리": "Меркурий",
|
||||
"이네오스": "Ineos",
|
||||
"지리": "Geely",
|
||||
"홍치": "Hongqi",
|
||||
"BYD": "BYD",
|
||||
"머큐리": "Mercury",
|
||||
"사이언": "Scion",
|
||||
"뷰익": "Бьюик",
|
||||
"뷰익": "Buick",
|
||||
"새턴": "Saturn",
|
||||
"올즈모빌": "Oldsmobile",
|
||||
"폰티악": "Понтиак",
|
||||
"폰티악": "Pontiac",
|
||||
"포톤": "Foton",
|
||||
"MG로버": "MG Rover",
|
||||
"닷지": "Додж",
|
||||
"험머": "Хаммер",
|
||||
"폴스타": "Полстар",
|
||||
"사브": "Сааб",
|
||||
"기타 제조사": "Другие",
|
||||
"기타 수입차": "Другие импортные",
|
||||
"닷지": "Dodge",
|
||||
"램": "Ram",
|
||||
"험머": "Hummer",
|
||||
"폴스타": "Polestar",
|
||||
"사브": "Saab",
|
||||
"기타 제조사": "Other",
|
||||
"기타 수입차": "Other Imported",
|
||||
}
|
||||
|
||||
|
||||
@@ -252,276 +275,298 @@ def expand_allowed_brands(brands: set[str] | None) -> set[str] | None:
|
||||
|
||||
|
||||
|
||||
# Модели / комплектации: корейский → русский
|
||||
# Модели / комплектации: корейский → английский
|
||||
|
||||
MODEL_TRANSLATIONS = {
|
||||
# --- Топливо / тип двигателя ---
|
||||
"가솔린": "бензин",
|
||||
"디젤": "дизель",
|
||||
"하이브리드": "гибрид",
|
||||
"전기": "электро",
|
||||
"터보": "турбо",
|
||||
"일렉트릭": "электрик",
|
||||
"일렉트리파이드": "электрифайд",
|
||||
"가솔린": "Gasoline",
|
||||
"디젤": "Diesel",
|
||||
"하이브리드": "Hybrid",
|
||||
"전기": "Electric",
|
||||
"터보": "Turbo",
|
||||
"일렉트릭": "Electric",
|
||||
"일렉트리파이드": "Electrified",
|
||||
"블루텍": "BlueTEC",
|
||||
"블루모션": "BlueMotion",
|
||||
# --- Кузов / конструкция ---
|
||||
"쿠페": "купе",
|
||||
"세단": "седан",
|
||||
"왜건": "универсал",
|
||||
"웨건": "универсал",
|
||||
"해치백": "хэтчбек",
|
||||
"컨버터블": "кабриолет",
|
||||
"카브리올레": "кабриолет",
|
||||
"크로스오버": "кроссовер",
|
||||
"리무진": "лимузин",
|
||||
"하이리무진": "хай-лимузин",
|
||||
"하이리무진(특장업체)": "хай-лимузин(спецтехника)",
|
||||
"스포트백": "спортбэк",
|
||||
"스파이더": "спайдер",
|
||||
"밴": "вэн",
|
||||
"픽업": "пикап",
|
||||
"캠핑카": "кемпер",
|
||||
"쿠페": "Coupe",
|
||||
"세단": "Sedan",
|
||||
"왜건": "Wagon",
|
||||
"웨건": "Wagon",
|
||||
"해치백": "Hatchback",
|
||||
"컨버터블": "Convertible",
|
||||
"카브리올레": "Cabriolet",
|
||||
"크로스오버": "Crossover",
|
||||
"리무진": "Limousine",
|
||||
"하이리무진": "High Limousine",
|
||||
"하이리무진(특장업체)": "High Limousine (Special)",
|
||||
"스포트백": "Sportback",
|
||||
"스파이더": "Spyder",
|
||||
"밴": "Van",
|
||||
"픽업": "Pickup",
|
||||
"캠핑카": "Camper",
|
||||
# --- КПП / привод ---
|
||||
"오토": "автомат",
|
||||
"수동": "механика",
|
||||
"후륜": "задний привод",
|
||||
"전륜": "передний привод",
|
||||
"사륜": "полный привод",
|
||||
"오토": "Auto",
|
||||
"수동": "Manual",
|
||||
"후륜": "RWD",
|
||||
"전륜": "FWD",
|
||||
"사륜": "AWD",
|
||||
"4모션": "4Motion",
|
||||
# --- Трим / комплектация ---
|
||||
"프레스티지": "престиж",
|
||||
"시그니처": "сигнатур",
|
||||
"럭셔리": "люкс",
|
||||
"스포츠": "спорт",
|
||||
"롱레인지": "дальний запас хода",
|
||||
"스탠다드": "стандарт",
|
||||
"플러스": "плюс",
|
||||
"리미티드": "лимитед",
|
||||
"스페셜": "спешл",
|
||||
"모던": "модерн",
|
||||
"익스클루시브": "эксклюзив",
|
||||
"캘리그래피": "каллиграфи",
|
||||
"노블레스": "ноблесс",
|
||||
"트림": "трим",
|
||||
"프리미엄": "премиум",
|
||||
"프리미어": "премьер",
|
||||
"플래티넘": "платинум",
|
||||
"플래티넘Ⅱ": "платинум II",
|
||||
"인스퍼레이션": "инспирейшн",
|
||||
"트렌디": "тренди",
|
||||
"다이나믹": "динамик",
|
||||
"프라임": "прайм",
|
||||
"초이스": "чойс",
|
||||
"르블랑": "Ле Блан",
|
||||
"프레지던트": "президент",
|
||||
"인스크립션": "инскрипшн",
|
||||
"볼드": "болд",
|
||||
"밸류": "вэлью",
|
||||
"퓨어": "пьюр",
|
||||
"엑셀런스": "экселленс",
|
||||
"얼티메이트": "алтимейт",
|
||||
"에센셜": "эссеншл",
|
||||
"슈프림": "суприм",
|
||||
"에어": "эйр",
|
||||
"어반": "урбан",
|
||||
"탑": "топ",
|
||||
"코어": "кор",
|
||||
"엘리트": "элит",
|
||||
"셀렉션": "селекшн",
|
||||
"브라이트": "брайт",
|
||||
"클래식": "классик",
|
||||
"고급형": "высшая комплектация",
|
||||
"최고급형": "максимальная комплектация",
|
||||
"기본형": "базовая комплектация",
|
||||
"디럭스": "делюкс",
|
||||
"디럭스팩": "делюкс-пакет",
|
||||
"마이스터": "мастер",
|
||||
"마스터": "мастер",
|
||||
"마스터즈": "мастерс",
|
||||
"어메이징": "амейзинг",
|
||||
"파워": "пауэр",
|
||||
"리저브": "резерв",
|
||||
"퍼펙트": "перфект",
|
||||
"리얼": "реал",
|
||||
"울트라": "ультра",
|
||||
"세이프티": "сейфти",
|
||||
"퍼포먼스": "перформанс",
|
||||
"인텔리전트": "интеллиджент",
|
||||
"컬렉션": "коллекшн",
|
||||
"브릴리언트": "бриллиант",
|
||||
"어드밴티지": "адвантейдж",
|
||||
"파이니스트": "файнест",
|
||||
"런치팩": "ланч-пак",
|
||||
"페이버드": "фейворд",
|
||||
"엘레강스": "элеганс",
|
||||
"하이": "хай",
|
||||
"블랙라벨": "Блэк Лейбл",
|
||||
"플럭스": "флюкс",
|
||||
"테크": "тек",
|
||||
"프로": "про",
|
||||
"베스트": "бест",
|
||||
"라이즈": "райз",
|
||||
"액티브": "актив",
|
||||
"롱": "лонг",
|
||||
"패키지": "пакет",
|
||||
"에디션": "эдишн",
|
||||
"디자인": "дизайн",
|
||||
"라운지": "лаунж",
|
||||
"아트": "арт",
|
||||
"팝": "поп",
|
||||
"조이": "джой",
|
||||
"레드라인": "редлайн",
|
||||
"스타일": "стайл",
|
||||
"블랙": "блэк",
|
||||
"라이트": "лайт",
|
||||
"온라인": "онлайн",
|
||||
"론지튜드": "лонгитюд",
|
||||
"오버랜드": "оверленд",
|
||||
"컴페티션": "компетишн",
|
||||
"올스페이스": "оллспейс",
|
||||
"스타": "стар",
|
||||
"프레스티지": "Prestige",
|
||||
"시그니처": "Signature",
|
||||
"럭셔리": "Luxury",
|
||||
"스포츠": "Sport",
|
||||
"롱레인지": "Long Range",
|
||||
"스탠다드": "Standard",
|
||||
"플러스": "Plus",
|
||||
"리미티드": "Limited",
|
||||
"스페셜": "Special",
|
||||
"모던": "Modern",
|
||||
"익스클루시브": "Exclusive",
|
||||
"캘리그래피": "Calligraphy",
|
||||
"노블레스": "Noblesse",
|
||||
"트림": "Trim",
|
||||
"프리미엄": "Premium",
|
||||
"프리미어": "Premier",
|
||||
"플래티넘": "Platinum",
|
||||
"플래티넘Ⅱ": "Platinum II",
|
||||
"인스퍼레이션": "Inspiration",
|
||||
"트렌디": "Trendy",
|
||||
"다이나믹": "Dynamic",
|
||||
"프라임": "Prime",
|
||||
"초이스": "Choice",
|
||||
"르블랑": "Le Blanc",
|
||||
"프레지던트": "President",
|
||||
"인스크립션": "Inscription",
|
||||
"볼드": "Bold",
|
||||
"밸류": "Value",
|
||||
"퓨어": "Pure",
|
||||
"엑셀런스": "Excellence",
|
||||
"얼티메이트": "Ultimate",
|
||||
"에센셜": "Essential",
|
||||
"슈프림": "Supreme",
|
||||
"에어": "Air",
|
||||
"어반": "Urban",
|
||||
"탑": "Top",
|
||||
"코어": "Core",
|
||||
"엘리트": "Elite",
|
||||
"셀렉션": "Selection",
|
||||
"브라이트": "Bright",
|
||||
"클래식": "Classic",
|
||||
"고급형": "Deluxe",
|
||||
"최고급형": "Top Grade",
|
||||
"기본형": "Base",
|
||||
"디럭스": "Deluxe",
|
||||
"디럭스팩": "Deluxe Pack",
|
||||
"마이스터": "Meister",
|
||||
"마스터": "Master",
|
||||
"마스터즈": "Masters",
|
||||
"어메이징": "Amazing",
|
||||
"파워": "Power",
|
||||
"리저브": "Reserve",
|
||||
"퍼펙트": "Perfect",
|
||||
"리얼": "Real",
|
||||
"울트라": "Ultra",
|
||||
"세이프티": "Safety",
|
||||
"퍼포먼스": "Performance",
|
||||
"인텔리전트": "Intelligent",
|
||||
"컬렉션": "Collection",
|
||||
"브릴리언트": "Brilliant",
|
||||
"어드밴티지": "Advantage",
|
||||
"파이니스트": "Finest",
|
||||
"런치팩": "Launch Pack",
|
||||
"페이버드": "Favored",
|
||||
"엘레강스": "Elegance",
|
||||
"하이": "High",
|
||||
"블랙라벨": "Black Label",
|
||||
"플럭스": "Flux",
|
||||
"테크": "Tech",
|
||||
"프로": "Pro",
|
||||
"베스트": "Best",
|
||||
"라이즈": "Rise",
|
||||
"액티브": "Active",
|
||||
"롱": "Long",
|
||||
"패키지": "Package",
|
||||
"에디션": "Edition",
|
||||
"디자인": "Design",
|
||||
"라운지": "Lounge",
|
||||
"아트": "Art",
|
||||
"팝": "Pop",
|
||||
"조이": "Joy",
|
||||
"레드라인": "Redline",
|
||||
"스타일": "Style",
|
||||
"블랙": "Black",
|
||||
"라이트": "Light",
|
||||
"온라인": "Online",
|
||||
"론지튜드": "Longitude",
|
||||
"오버랜드": "Overland",
|
||||
"컴페티션": "Competition",
|
||||
"올스페이스": "Allspace",
|
||||
"스타": "Star",
|
||||
# --- Поколение / номер ---
|
||||
"1세대": "1 поколение",
|
||||
"2세대": "2 поколение",
|
||||
"3세대": "3 поколение",
|
||||
"4세대": "4 поколение",
|
||||
"5세대": "5 поколение",
|
||||
"6세대": "6 поколение",
|
||||
"7세대": "7 поколение",
|
||||
"8세대": "8 поколение",
|
||||
"10세대": "10 поколение",
|
||||
"1세대": "1st gen",
|
||||
"2세대": "2nd gen",
|
||||
"3세대": "3rd gen",
|
||||
"4세대": "4th gen",
|
||||
"5세대": "5th gen",
|
||||
"6세대": "6th gen",
|
||||
"7세대": "7th gen",
|
||||
"8세대": "8th gen",
|
||||
"10세대": "10th gen",
|
||||
"더": "",
|
||||
"뉴": "нью",
|
||||
"넥스트": "некст",
|
||||
"올": "олл",
|
||||
"그란": "гран",
|
||||
"그랜드": "гранд",
|
||||
"뉴": "New",
|
||||
"넥스트": "Next",
|
||||
"올": "All",
|
||||
"그란": "Gran",
|
||||
"그랜드": "Grand",
|
||||
# --- Посадочных мест ---
|
||||
"2인승": "2-местный",
|
||||
"3인승": "3-местный",
|
||||
"5인승": "5-местный",
|
||||
"6인승": "6-местный",
|
||||
"7인승": "7-местный",
|
||||
"9인승": "9-местный",
|
||||
"11인승": "11-местный",
|
||||
"12인승": "12-местный",
|
||||
"2인승": "2-seater",
|
||||
"3인승": "3-seater",
|
||||
"5인승": "5-seater",
|
||||
"6인승": "6-seater",
|
||||
"7인승": "7-seater",
|
||||
"9인승": "9-seater",
|
||||
"11인승": "11-seater",
|
||||
"12인승": "12-seater",
|
||||
# --- Двери ---
|
||||
"4도어": "4-дверный",
|
||||
"5도어": "5-дверный",
|
||||
"4도어": "4-door",
|
||||
"5도어": "5-door",
|
||||
# --- Модели Hyundai ---
|
||||
"팰리세이드": "Палисейд",
|
||||
"싼타페": "Санта Фе",
|
||||
"투싼": "Туксон",
|
||||
"아반떼": "Аванте",
|
||||
"쏘나타": "Соната",
|
||||
"그랜저": "Грэнджер",
|
||||
"팰리세이드": "Palisade",
|
||||
"싼타페": "Santa Fe",
|
||||
"투싼": "Tucson",
|
||||
"아반떼": "Avante",
|
||||
"쏘나타": "Sonata",
|
||||
"그랜저": "Grandeur",
|
||||
"아이오닉5": "Ioniq 5",
|
||||
"아이오닉6": "Ioniq 6",
|
||||
"아이오닉": "Ioniq",
|
||||
"넥쏘": "Нексо",
|
||||
"벨로스터": "Велостер",
|
||||
"코나": "Кона",
|
||||
"캐스퍼": "Каспер",
|
||||
"베뉴": "Венью",
|
||||
"스타렉스": "Старекс",
|
||||
"스타리아": "Стариа",
|
||||
"에쿠스(신형)": "Экус",
|
||||
"엑센트(신형)": "Акцент",
|
||||
"아슬란": "Аслан",
|
||||
"베라크루즈": "Веракруз",
|
||||
"넥쏘": "Nexo",
|
||||
"벨로스터": "Veloster",
|
||||
"코나": "Kona",
|
||||
"캐스퍼": "Casper",
|
||||
"베뉴": "Venue",
|
||||
"스타렉스": "Starex",
|
||||
"스타리아": "Staria",
|
||||
"에쿠스(신형)": "Equus",
|
||||
"엑센트(신형)": "Accent",
|
||||
"아슬란": "Aslan",
|
||||
"베라크루즈": "Veracruz",
|
||||
# --- Модели Kia ---
|
||||
"카니발": "Карнивал",
|
||||
"쏘렌토": "Соренто",
|
||||
"스포티지": "Спортейдж",
|
||||
"모닝": "Морнинг",
|
||||
"뉴모닝": "нью Морнинг",
|
||||
"레이": "Рэй",
|
||||
"셀토스": "Селтос",
|
||||
"니로": "Ниро",
|
||||
"스팅어": "Стингер",
|
||||
"쏘울": "Соул",
|
||||
"모하비": "Мохаве",
|
||||
"프라이드": "Прайд",
|
||||
"스토닉": "Стоник",
|
||||
"카니발": "Carnival",
|
||||
"쏘렌토": "Sorento",
|
||||
"스포티지": "Sportage",
|
||||
"모닝": "Morning",
|
||||
"뉴모닝": "New Morning",
|
||||
"레이": "Ray",
|
||||
"셀토스": "Seltos",
|
||||
"니로": "Niro",
|
||||
"스팅어": "Stinger",
|
||||
"쏘울": "Soul",
|
||||
"모하비": "Mohave",
|
||||
"프라이드": "Pride",
|
||||
"스토닉": "Stonic",
|
||||
# --- Модели Chevrolet/GM ---
|
||||
"스파크": "Спарк",
|
||||
"말리부": "Малибу",
|
||||
"트랙스": "Тракс",
|
||||
"크루즈": "Круз",
|
||||
"올란도": "Орландо",
|
||||
"트레일블레이저": "Трейлблейзер",
|
||||
"콜로라도": "Колорадо",
|
||||
"트래버스": "Траверс",
|
||||
"캡티바": "Каптива",
|
||||
"이쿼녹스": "Эквинокс",
|
||||
"아베오": "Авео",
|
||||
"볼트": "Вольт",
|
||||
"임팔라": "Импала",
|
||||
"맥스크루즈": "МаксКруз",
|
||||
"스파크": "Spark",
|
||||
"말리부": "Malibu",
|
||||
"트랙스": "Trax",
|
||||
"크루즈": "Cruze",
|
||||
"올란도": "Orlando",
|
||||
"트레일블레이저": "Trailblazer",
|
||||
"콜로라도": "Colorado",
|
||||
"트래버스": "Traverse",
|
||||
"캡티바": "Captiva",
|
||||
"이쿼녹스": "Equinox",
|
||||
"아베오": "Aveo",
|
||||
"볼트": "Volt",
|
||||
"임팔라": "Impala",
|
||||
"맥스크루즈": "Max Cruz",
|
||||
# --- Модели Genesis ---
|
||||
"제네시스": "Генезис",
|
||||
# --- Модели иностранных авто (транслитерация) ---
|
||||
"익스플로러": "Эксплорер",
|
||||
"머스탱": "Мустанг",
|
||||
"카이엔": "Каен",
|
||||
"파나메라": "Панамера",
|
||||
"마칸": "Макан",
|
||||
"타이칸": "Тайкан",
|
||||
"박스터": "Бокстер",
|
||||
"카레라": "Каррера",
|
||||
"티구안": "Тигуан",
|
||||
"골프": "Гольф",
|
||||
"제타": "Джетта",
|
||||
"투아렉": "Туарег",
|
||||
"파사트": "Пассат",
|
||||
"아테온": "Артеон",
|
||||
"콰트로": "кваттро",
|
||||
"아방가르드": "Авангард",
|
||||
"쿠퍼": "Купер",
|
||||
"컨트리맨": "Кантримен",
|
||||
"클럽맨": "Клабмен",
|
||||
"디스커버리": "Дискавери",
|
||||
"레인지로버": "Рейндж Ровер",
|
||||
"이보크": "Эвок",
|
||||
"벨라": "Велар",
|
||||
"디펜더": "Дефендер",
|
||||
"랭글러": "Рэнглер",
|
||||
"체로키": "Чероки",
|
||||
"체로키(KL)": "Чероки (KL)",
|
||||
"체로키(WL)": "Чероки (WL)",
|
||||
"레니게이드": "Ренегат",
|
||||
"컴패스": "Компас",
|
||||
"루비콘": "Рубикон",
|
||||
"에비에이터": "Авиатор",
|
||||
"에스컬레이드": "Эскалейд",
|
||||
"컨티넨탈": "Континенталь",
|
||||
"르반떼": "Леванте",
|
||||
"기블리": "Гибли",
|
||||
"알티마": "Альтима",
|
||||
"오딧세이": "Одиссей",
|
||||
"어코드": "Аккорд",
|
||||
"캠리": "Камри",
|
||||
"시에나": "Сиенна",
|
||||
"프리우스": "Приус",
|
||||
"크로스컨트리": "Кросс Кантри",
|
||||
"타스만": "Тасман",
|
||||
"제네시스": "Genesis",
|
||||
# --- Модели иностранных авто ---
|
||||
"익스플로러": "Explorer",
|
||||
"머스탱": "Mustang",
|
||||
"카이엔": "Cayenne",
|
||||
"파나메라": "Panamera",
|
||||
"마칸": "Macan",
|
||||
"타이칸": "Taycan",
|
||||
"박스터": "Boxster",
|
||||
"카이맨": "Cayman",
|
||||
"카레라": "Carrera",
|
||||
"티구안": "Tiguan",
|
||||
"골프": "Golf",
|
||||
"제타": "Jetta",
|
||||
"투아렉": "Touareg",
|
||||
"파사트": "Passat",
|
||||
"아테온": "Arteon",
|
||||
"콰트로": "quattro",
|
||||
"아방가르드": "Avantgarde",
|
||||
"쿠퍼": "Cooper",
|
||||
"컨트리맨": "Countryman",
|
||||
"클럽맨": "Clubman",
|
||||
"디스커버리": "Discovery",
|
||||
"레인지로버": "Range Rover",
|
||||
"이보크": "Evoque",
|
||||
"벨라": "Velar",
|
||||
"디펜더": "Defender",
|
||||
"랭글러": "Wrangler",
|
||||
"체로키": "Cherokee",
|
||||
"체로키(KL)": "Cherokee (KL)",
|
||||
"체로키(WL)": "Cherokee (WL)",
|
||||
"레니게이드": "Renegade",
|
||||
"컴패스": "Compass",
|
||||
"루비콘": "Rubicon",
|
||||
"에비에이터": "Aviator",
|
||||
"에스컬레이드": "Escalade",
|
||||
"컨티넨탈": "Continental",
|
||||
"르반떼": "Levante",
|
||||
"기블리": "Ghibli",
|
||||
"알티마": "Altima",
|
||||
"오딧세이": "Odyssey",
|
||||
"어코드": "Accord",
|
||||
"캠리": "Camry",
|
||||
"시에나": "Sienna",
|
||||
"프리우스": "Prius",
|
||||
"크로스컨트리": "Cross Country",
|
||||
"타스만": "Tasman",
|
||||
"고스트": "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",
|
||||
"그란투리스모": "GranTurismo",
|
||||
"반퀴시": "Vanquish",
|
||||
"밴티지": "Vantage",
|
||||
# --- Служебные / категории ---
|
||||
"(세부등급": "(подуровень",
|
||||
"없음)": "отсутствует)",
|
||||
"(렌터카)": "(прокат)",
|
||||
"(렌터카용)": "(для проката)",
|
||||
"LPG(렌터카용)": "LPG(для проката)",
|
||||
"LPI(렌터카용)": "LPI(для проката)",
|
||||
"(택시형)": "(такси)",
|
||||
"택시형": "такси",
|
||||
"어린이보호차": "детский транспорт",
|
||||
"카고": "карго",
|
||||
"모델": "модель",
|
||||
"스마트": "Смарт",
|
||||
"기아": "Киа",
|
||||
"마이바흐": "Майбах",
|
||||
"레인지": "Рейндж",
|
||||
"(세부등급": "(sub-trim",
|
||||
"없음)": "none)",
|
||||
"(렌터카)": "(rental)",
|
||||
"(렌터카용)": "(for rental)",
|
||||
"LPG(렌터카용)": "LPG (for rental)",
|
||||
"LPI(렌터카용)": "LPI (for rental)",
|
||||
"(택시형)": "(taxi)",
|
||||
"택시형": "taxi",
|
||||
"어린이보호차": "child transport",
|
||||
"카고": "Cargo",
|
||||
"모델": "Model",
|
||||
"스마트": "Smart",
|
||||
"기아": "Kia",
|
||||
"마이바흐": "Maybach",
|
||||
"레인지": "Range",
|
||||
}
|
||||
|
||||
|
||||
@@ -586,15 +631,22 @@ COLOR_TRANSLATIONS = {
|
||||
|
||||
BODY_TYPE_MAP = {
|
||||
"세단": "SEDAN",
|
||||
"SUV": "SUV",
|
||||
"SUV ": "SUV",
|
||||
"쿠페": "COUPE",
|
||||
"컨버터블": "CONVERTIBLE",
|
||||
"카브리올레": "CONVERTIBLE",
|
||||
"오픈카": "OPEN",
|
||||
"해치백": "HATCHBACK",
|
||||
"왜건": "WAGON",
|
||||
"컨버터블": "CONVERTIBLE",
|
||||
"쿠페": "COUPE",
|
||||
"스테이션왜건": "STATION_WAGON",
|
||||
"SUV": "SUV",
|
||||
"SUV ": "SUV",
|
||||
"크로스오버": "SUV",
|
||||
"픽업트럭": "PICKUP",
|
||||
"픽업": "PICKUP",
|
||||
"밴": "VAN",
|
||||
"미니밴": "MINIVAN",
|
||||
"트럭": "TRUCK",
|
||||
"쿠페형 SUV": "SUV",
|
||||
"버스": "BUS",
|
||||
"기타": "OTHER",
|
||||
"승용차": "SEDAN",
|
||||
|
||||
@@ -5,6 +5,7 @@ from threading import Event, Thread
|
||||
import uuid
|
||||
|
||||
from celery import shared_task
|
||||
from celery.exceptions import MaxRetriesExceededError, Retry
|
||||
from celery.signals import worker_process_init
|
||||
from redis import Redis
|
||||
|
||||
@@ -24,6 +25,18 @@ _redis: Redis | None = None
|
||||
@worker_process_init.connect
|
||||
def _reset_globals_after_fork(**kwargs):
|
||||
global _persistence, _redis
|
||||
# После fork закрываем унаследованные TCP-сокеты родителя (иначе дети
|
||||
# делят одни connection pool'ы → повреждение данных).
|
||||
if _persistence is not None:
|
||||
try:
|
||||
_persistence.engine.dispose()
|
||||
except Exception:
|
||||
logger.debug("engine.dispose after fork failed", exc_info=True)
|
||||
if _redis is not None:
|
||||
try:
|
||||
_redis.close()
|
||||
except Exception:
|
||||
logger.debug("redis.close after fork failed", exc_info=True)
|
||||
_persistence = None
|
||||
_redis = None
|
||||
|
||||
@@ -156,6 +169,10 @@ def encar_sync_listing_task(
|
||||
heartbeat_stop: Event | None = None
|
||||
heartbeat_thread: Thread | None = None
|
||||
run_id: int | None = None
|
||||
# Планировать следующий прогон только когда текущий прогон завершён
|
||||
# (success или final failure после исчерпания retries). Retry сам
|
||||
# перепланирует задачу — не дублируем apply_async в этом случае.
|
||||
reschedule_next = True
|
||||
|
||||
try:
|
||||
heartbeat_stop, heartbeat_thread = _start_lock_heartbeat(
|
||||
@@ -244,7 +261,14 @@ def encar_sync_listing_task(
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to record sync_run failure", exc_info=True)
|
||||
raise self.retry(exc=exc)
|
||||
try:
|
||||
# Retry сам перепланирует задачу — не планируем следующий прогон
|
||||
reschedule_next = False
|
||||
raise self.retry(exc=exc)
|
||||
except MaxRetriesExceededError:
|
||||
# Retries исчерпаны — запускаем следующий прогон по расписанию
|
||||
reschedule_next = True
|
||||
raise
|
||||
finally:
|
||||
if heartbeat_stop is not None:
|
||||
heartbeat_stop.set()
|
||||
@@ -252,20 +276,21 @@ def encar_sync_listing_task(
|
||||
heartbeat_thread.join(timeout=max(1.0, min(5.0, lock_ttl / 10)))
|
||||
if lock_acquired:
|
||||
_release_lock_if_owner(redis_client, ENCAR_SYNC_LOCK_KEY, owner_token)
|
||||
try:
|
||||
interval_minutes = Settings().celery.encar_beat_interval_minutes
|
||||
countdown = max(60, int(interval_minutes) * 60)
|
||||
self.apply_async(
|
||||
kwargs={"car_type": car_type},
|
||||
countdown=countdown,
|
||||
queue="encar",
|
||||
)
|
||||
logger.info(
|
||||
"Next encar_sync_listing_task scheduled in %d minutes",
|
||||
countdown // 60,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to schedule next sync run", exc_info=True)
|
||||
if reschedule_next:
|
||||
try:
|
||||
interval_minutes = Settings().celery.encar_beat_interval_minutes
|
||||
countdown = max(60, int(interval_minutes) * 60)
|
||||
self.apply_async(
|
||||
kwargs={"car_type": car_type},
|
||||
countdown=countdown,
|
||||
queue="encar",
|
||||
)
|
||||
logger.info(
|
||||
"Next encar_sync_listing_task scheduled in %d minutes",
|
||||
countdown // 60,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to schedule next sync run", exc_info=True)
|
||||
|
||||
|
||||
@shared_task(
|
||||
|
||||
Reference in New Issue
Block a user