init: Parser v1 — Lead Generation Engine
Парсер лидов МБ РФ: Яндекс.Карты + HH.ru + обогащение DaData/ЕГРЮЛ/Rusprofile + Streamlit CRM. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+484
@@ -0,0 +1,484 @@
|
||||
"""Интерактивный TUI-лаунчер парсера лидов.
|
||||
|
||||
Запуск: python launcher.py
|
||||
Зависимость: pip install questionary (устанавливается автоматически)
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import config # для синхронизации дефолтных категорий (фокус-ЦА) с CLI
|
||||
|
||||
try:
|
||||
import questionary
|
||||
from questionary import Style
|
||||
except ImportError:
|
||||
print("Устанавливаю questionary...")
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "questionary"], check=True)
|
||||
import questionary
|
||||
from questionary import Style
|
||||
|
||||
# ── Стиль ────────────────────────────────────────────────────────────
|
||||
STYLE = Style([
|
||||
("qmark", "fg:#00bfff bold"),
|
||||
("question", "fg:#ffffff bold"),
|
||||
("answer", "fg:#00ff99 bold"),
|
||||
("pointer", "fg:#00bfff bold"),
|
||||
("highlighted", "fg:#00bfff bold"),
|
||||
("selected", "fg:#00ff99"),
|
||||
("separator", "fg:#666666"),
|
||||
("instruction", "fg:#888888"),
|
||||
])
|
||||
|
||||
# ── Категории — полный список для Яндекс.Карт ────────────────────────
|
||||
CATEGORIES_ALL = [
|
||||
# Общепит
|
||||
"кафе",
|
||||
"ресторан",
|
||||
"пиццерия",
|
||||
"суши-бар",
|
||||
"столовая",
|
||||
"бар",
|
||||
"кондитерская",
|
||||
"пекарня",
|
||||
"кофейня",
|
||||
# Красота
|
||||
"салон красоты",
|
||||
"парикмахерская",
|
||||
"барбершоп",
|
||||
"ногтевой сервис",
|
||||
"студия массажа",
|
||||
"косметология",
|
||||
"спа-салон",
|
||||
"студия эпиляции",
|
||||
"солярий",
|
||||
"тату-салон",
|
||||
"студия перманентного макияжа",
|
||||
"студия ресниц",
|
||||
"свадебный салон",
|
||||
# Авто
|
||||
"автосервис",
|
||||
"шиномонтаж",
|
||||
"автомойка",
|
||||
"детейлинг",
|
||||
"магазин автозапчастей",
|
||||
"автошкола",
|
||||
# Медицина
|
||||
"стоматология",
|
||||
"медицинский центр",
|
||||
"клиника",
|
||||
"ветеринарная клиника",
|
||||
"аптека",
|
||||
"оптика",
|
||||
"массажный салон",
|
||||
"психологический центр",
|
||||
# Ремонт и строительство
|
||||
"ремонт квартир",
|
||||
"строительная компания",
|
||||
"ремонт техники",
|
||||
"ремонт телефонов",
|
||||
"ремонт обуви",
|
||||
# Спорт и досуг
|
||||
"фитнес-клуб",
|
||||
"спортзал",
|
||||
"бассейн",
|
||||
"йога-студия",
|
||||
"студия танцев",
|
||||
"студия растяжки",
|
||||
"школа единоборств",
|
||||
"батутный центр",
|
||||
"баня и сауна",
|
||||
"детский клуб",
|
||||
"детский развивающий центр",
|
||||
"музыкальная школа",
|
||||
"художественная студия",
|
||||
"языковые курсы",
|
||||
"фотостудия",
|
||||
# Услуги бизнесу
|
||||
"юридическая фирма",
|
||||
"бухгалтерия",
|
||||
"нотариус",
|
||||
"агентство недвижимости",
|
||||
"турагентство",
|
||||
"рекламное агентство",
|
||||
# Другое
|
||||
"кальянная",
|
||||
"клининг",
|
||||
"химчистка",
|
||||
"грузоперевозки",
|
||||
"флористика",
|
||||
"зоосалон",
|
||||
"частный детский сад",
|
||||
"магазин одежды",
|
||||
"магазин обуви",
|
||||
"банкетный зал",
|
||||
"охранное агентство",
|
||||
]
|
||||
|
||||
# ── Локации ───────────────────────────────────────────────────────────
|
||||
# Структура: {"label": "...", "city": "...", "district": "..."}
|
||||
# Для Москвы и СПб — district=None, передаём как --city
|
||||
# Для Подмосковья — city="Москва и МО", district=<город>
|
||||
LOCATIONS = [
|
||||
# Топ-уровень
|
||||
{"label": "🏙 Москва", "city": "Москва", "district": None},
|
||||
{"label": "🗺 Москва и МО (весь регион)", "city": "Москва и МО", "district": None},
|
||||
{"label": "🏙 Санкт-Петербург", "city": "Санкт-Петербург", "district": None},
|
||||
# Ближнее Подмосковье
|
||||
{"label": "📍 Мытищи", "city": "Москва и МО", "district": "Мытищи"},
|
||||
{"label": "📍 Химки", "city": "Москва и МО", "district": "Химки"},
|
||||
{"label": "📍 Балашиха", "city": "Москва и МО", "district": "Балашиха"},
|
||||
{"label": "📍 Подольск", "city": "Москва и МО", "district": "Подольск"},
|
||||
{"label": "📍 Красногорск", "city": "Москва и МО", "district": "Красногорск"},
|
||||
{"label": "📍 Одинцово", "city": "Москва и МО", "district": "Одинцово"},
|
||||
{"label": "📍 Люберцы", "city": "Москва и МО", "district": "Люберцы"},
|
||||
{"label": "📍 Королёв", "city": "Москва и МО", "district": "Королёв"},
|
||||
{"label": "📍 Долгопрудный", "city": "Москва и МО", "district": "Долгопрудный"},
|
||||
{"label": "📍 Реутов", "city": "Москва и МО", "district": "Реутов"},
|
||||
{"label": "📍 Щёлково", "city": "Москва и МО", "district": "Щёлково"},
|
||||
{"label": "📍 Жуковский", "city": "Москва и МО", "district": "Жуковский"},
|
||||
{"label": "📍 Домодедово", "city": "Москва и МО", "district": "Домодедово"},
|
||||
{"label": "📍 Электросталь", "city": "Москва и МО", "district": "Электросталь"},
|
||||
{"label": "📍 Пушкино", "city": "Москва и МО", "district": "Пушкино"},
|
||||
{"label": "📍 Видное", "city": "Москва и МО", "district": "Видное"},
|
||||
{"label": "📍 Троицк", "city": "Москва и МО", "district": "Троицк"},
|
||||
{"label": "📍 Зеленоград", "city": "Москва", "district": "Зеленоград"},
|
||||
# Дальнее Подмосковье
|
||||
{"label": "📍 Серпухов", "city": "Москва и МО", "district": "Серпухов"},
|
||||
{"label": "📍 Коломна", "city": "Москва и МО", "district": "Коломна"},
|
||||
{"label": "📍 Ногинск", "city": "Москва и МО", "district": "Ногинск"},
|
||||
{"label": "📍 Орехово-Зуево", "city": "Москва и МО", "district": "Орехово-Зуево"},
|
||||
{"label": "📍 Сергиев Посад", "city": "Москва и МО", "district": "Сергиев Посад"},
|
||||
{"label": "📍 Дмитров", "city": "Москва и МО", "district": "Дмитров"},
|
||||
{"label": "📍 Клин", "city": "Москва и МО", "district": "Клин"},
|
||||
{"label": "📍 Истра", "city": "Москва и МО", "district": "Истра"},
|
||||
{"label": "📍 Наро-Фоминск", "city": "Москва и МО", "district": "Наро-Фоминск"},
|
||||
{"label": "📍 Можайск", "city": "Москва и МО", "district": "Можайск"},
|
||||
{"label": "📍 Волоколамск", "city": "Москва и МО", "district": "Волоколамск"},
|
||||
{"label": "📍 Верея", "city": "Москва и МО", "district": "Верея"},
|
||||
{"label": "📍 Руза", "city": "Москва и МО", "district": "Руза"},
|
||||
{"label": "📍 Чехов", "city": "Москва и МО", "district": "Чехов"},
|
||||
{"label": "📍 Ступино", "city": "Москва и МО", "district": "Ступино"},
|
||||
{"label": "📍 Кашира", "city": "Москва и МО", "district": "Кашира"},
|
||||
{"label": "📍 Егорьевск", "city": "Москва и МО", "district": "Егорьевск"},
|
||||
{"label": "📍 Воскресенск", "city": "Москва и МО", "district": "Воскресенск"},
|
||||
{"label": "📍 Раменское", "city": "Москва и МО", "district": "Раменское"},
|
||||
{"label": "📍 Бронницы", "city": "Москва и МО", "district": "Бронницы"},
|
||||
{"label": "📍 Дубна", "city": "Москва и МО", "district": "Дубна"},
|
||||
{"label": "📍 Фрязино", "city": "Москва и МО", "district": "Фрязино"},
|
||||
{"label": "📍 Лыткарино", "city": "Москва и МО", "district": "Лыткарино"},
|
||||
# Ввести вручную
|
||||
{"label": "✏️ Другой (ввести вручную)...", "city": "__custom__", "district": None},
|
||||
]
|
||||
|
||||
# ── Районы Москвы (для дополнительного фильтра) ──────────────────────
|
||||
MOSCOW_DISTRICTS = [
|
||||
# ЦАО
|
||||
"Арбат", "Басманный", "Замоскворечье", "Красносельский",
|
||||
"Мещанский", "Пресненский", "Таганский", "Тверской",
|
||||
"Хамовники", "Якиманка",
|
||||
# САО (Северный)
|
||||
"Аэропорт", "Беговой", "Бескудниковский", "Войковский",
|
||||
"Восточное Дегунино", "Головинский", "Дмитровский",
|
||||
"Западное Дегунино", "Коптево", "Левобережный",
|
||||
"Молжаниновский", "Савёловский", "Сокол", "Тимирязевский",
|
||||
"Ховрино", "Хорошёвский",
|
||||
# СВАО
|
||||
"Алексеевский", "Алтуфьевский", "Бабушкинский", "Бибирево",
|
||||
"Бутырский", "Лианозово", "Лосиноостровский", "Марфино",
|
||||
"Марьина роща", "Останкинский", "Отрадное", "Ростокино",
|
||||
"Свиблово", "Северное Медведково", "Северный",
|
||||
"Южное Медведково", "Ярославский",
|
||||
# ВАО
|
||||
"Богородское", "Вешняки", "Восточное Измайлово", "Восточный",
|
||||
"Гольяново", "Ивановское", "Измайлово", "Косино-Ухтомский",
|
||||
"Метрогородок", "Новогиреево", "Новокосино", "Перово",
|
||||
"Преображенское", "Северное Измайлово", "Соколиная гора", "Сокольники",
|
||||
# ЮВАО
|
||||
"Выхино-Жулебино", "Капотня", "Кузьминки", "Лефортово",
|
||||
"Люблино", "Марьино", "Некрасовка", "Нижегородский",
|
||||
"Печатники", "Рязанский", "Текстильщики", "Южнопортовый",
|
||||
# ЮАО
|
||||
"Бирюлёво Восточное", "Бирюлёво Западное", "Братеево",
|
||||
"Даниловский", "Донской", "Зябликово",
|
||||
"Москворечье-Сабурово", "Нагатино-Садовники",
|
||||
"Нагатинский Затон", "Нагорный", "Орехово-Борисово Северное",
|
||||
"Орехово-Борисово Южное", "Царицыно", "Чертаново Северное",
|
||||
"Чертаново Центральное", "Чертаново Южное",
|
||||
# ЮЗАО
|
||||
"Академический", "Гагаринский", "Зюзино", "Коньково",
|
||||
"Котловка", "Ломоносовский", "Обручевский",
|
||||
"Северное Бутово", "Тёплый Стан", "Черёмушки",
|
||||
"Южное Бутово", "Ясенево",
|
||||
# ЗАО
|
||||
"Внуково", "Дорогомилово", "Крылатское", "Кунцево",
|
||||
"Можайский", "Ново-Переделкино", "Очаково-Матвеевское",
|
||||
"Проспект Вернадского", "Раменки", "Солнцево",
|
||||
"Тропарёво-Никулино", "Филёвский парк", "Фили-Давыдково",
|
||||
# СЗАО
|
||||
"Куркино", "Митино", "Покровское-Стрешнево", "Северное Тушино",
|
||||
"Строгино", "Хорошёво-Мнёвники", "Щукино", "Южное Тушино",
|
||||
# Новая Москва (ТАО + НАО)
|
||||
"Троицк", "Щербинка", "Коммунарка", "Сосенское",
|
||||
# Зеленоградский АО
|
||||
"Крюково", "Матушкино", "Савёлки", "Силино", "Старое Крюково",
|
||||
]
|
||||
|
||||
|
||||
# ── HH signal-запросы (из config) ────────────────────────────────────
|
||||
HH_SIGNAL_QUERIES = [
|
||||
"оператор ПК",
|
||||
"оператор колл-центра",
|
||||
"оператор технической поддержки",
|
||||
"менеджер чата",
|
||||
"менеджер по продажам без CRM",
|
||||
"помощник руководителя",
|
||||
"ассистент руководителя",
|
||||
"офис-менеджер",
|
||||
"администратор записи",
|
||||
"администратор салона красоты",
|
||||
"администратор клиники",
|
||||
"ресепшн",
|
||||
"бухгалтер 1С",
|
||||
"помощник бухгалтера",
|
||||
]
|
||||
|
||||
|
||||
def ask(fn, *args, **kwargs):
|
||||
result = fn(*args, **kwargs, style=STYLE).ask()
|
||||
if result is None:
|
||||
print("\n[отмена]")
|
||||
sys.exit(0)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
print("\n" + "═" * 56)
|
||||
print(" 🔍 Парсер лидов 44AS — Интерактивный лаунчер")
|
||||
print("═" * 56)
|
||||
print(" Пробел — выбрать/снять | Enter — подтвердить\n")
|
||||
|
||||
# ── Шаг 1: Источник ──────────────────────────────────────────────
|
||||
sources = ask(
|
||||
questionary.checkbox,
|
||||
"1/8 Источники:",
|
||||
choices=[
|
||||
questionary.Choice("🗺️ Яндекс.Карты", value="yandex", checked=True),
|
||||
questionary.Choice("💼 HH.ru", value="hh", checked=False),
|
||||
],
|
||||
)
|
||||
if not sources:
|
||||
print("Нужно выбрать хотя бы один источник.")
|
||||
sys.exit(1)
|
||||
|
||||
use_yandex = "yandex" in sources
|
||||
use_hh = "hh" in sources
|
||||
|
||||
# ── Шаг 2: Категории ─────────────────────────────────────────────
|
||||
selected_categories = []
|
||||
selected_hh_queries = []
|
||||
|
||||
if use_yandex:
|
||||
selected_categories = ask(
|
||||
questionary.checkbox,
|
||||
"2/8 Категории Я.Карт (пробел = выбрать):",
|
||||
choices=[
|
||||
questionary.Choice(cat, value=cat, checked=(cat in config.CATEGORIES))
|
||||
for cat in CATEGORIES_ALL
|
||||
],
|
||||
)
|
||||
if not selected_categories:
|
||||
print("Нужно выбрать хотя бы одну категорию.")
|
||||
sys.exit(1)
|
||||
|
||||
if use_hh:
|
||||
selected_hh_queries = ask(
|
||||
questionary.checkbox,
|
||||
"2/8 Signal-запросы для HH.ru:",
|
||||
choices=[
|
||||
questionary.Choice(q, value=q, checked=True)
|
||||
for q in HH_SIGNAL_QUERIES
|
||||
],
|
||||
)
|
||||
if not selected_hh_queries:
|
||||
print("Нужно выбрать хотя бы один запрос.")
|
||||
sys.exit(1)
|
||||
|
||||
# ── Шаг 3: Локация ───────────────────────────────────────────────
|
||||
loc_labels = [loc["label"] for loc in LOCATIONS]
|
||||
chosen_label = ask(
|
||||
questionary.select,
|
||||
"3/8 Локация:",
|
||||
choices=loc_labels,
|
||||
)
|
||||
chosen_loc = next(loc for loc in LOCATIONS if loc["label"] == chosen_label)
|
||||
|
||||
if chosen_loc["city"] == "__custom__":
|
||||
custom = ask(questionary.text, " Введите название (город или район МО):").strip()
|
||||
# Для незнакомых городов используем fallback: Москва и МО + district
|
||||
city = "Москва и МО"
|
||||
district = custom
|
||||
else:
|
||||
city = chosen_loc["city"]
|
||||
district = chosen_loc["district"]
|
||||
|
||||
# Дополнительные районы (только для Москвы — можно выбрать несколько,
|
||||
# парсер запустится для каждого по очереди)
|
||||
extra_districts: list[str] = []
|
||||
if city == "Москва" and district is None:
|
||||
extra_districts = ask(
|
||||
questionary.checkbox,
|
||||
"4/8 Районы Москвы (пробел — выбрать, можно несколько; Enter без выбора = весь город):",
|
||||
choices=[
|
||||
questionary.Choice(d, value=d, checked=False)
|
||||
for d in MOSCOW_DISTRICTS
|
||||
],
|
||||
)
|
||||
else:
|
||||
print(f"4/8 Район: {'авто → ' + district if district else 'весь регион'}")
|
||||
|
||||
# final_districts — список районов для запуска (None = без района)
|
||||
if extra_districts:
|
||||
final_districts = extra_districts
|
||||
elif district:
|
||||
final_districts = [district]
|
||||
else:
|
||||
final_districts = [None]
|
||||
|
||||
# ── Шаг 5: Лимит ─────────────────────────────────────────────────
|
||||
limit_raw = ask(
|
||||
questionary.text,
|
||||
"5/8 Лимит карточек на категорию:",
|
||||
default="100",
|
||||
validate=lambda v: v.isdigit() and int(v) > 0 or "Введите целое число > 0",
|
||||
).strip()
|
||||
limit = limit_raw
|
||||
|
||||
# ── Шаг 6: Опции pipeline ────────────────────────────────────────
|
||||
pipeline_opts = ask(
|
||||
questionary.checkbox,
|
||||
"6/8 Опции pipeline:",
|
||||
choices=[
|
||||
questionary.Choice("🔍 Дозаполнить сайты HH-компаний (--hh-enrich-websites)",
|
||||
value="hh_enrich_websites", checked=True),
|
||||
questionary.Choice("🔎 Найти сайты через DuckDuckGo (--find-sites)",
|
||||
value="find_sites", checked=True),
|
||||
questionary.Choice("🔧 Анализ сайтов + email/телефоны с сайта (--enrich)",
|
||||
value="enrich", checked=True),
|
||||
questionary.Choice("🏛 ЕГРЮЛ обогащение через DaData + Rusprofile (--enrich-egrul)",
|
||||
value="enrich_egrul", checked=True),
|
||||
questionary.Choice("🔄 Пересчёт score (--rescore)",
|
||||
value="rescore", checked=True),
|
||||
questionary.Choice("📄 CSV каждого прогона в exports/YYYY-MM/ (--export)",
|
||||
value="export", checked=True),
|
||||
questionary.Choice("📦 Master-файл всех лидов в exports/_master/ (--export-master)",
|
||||
value="export_master", checked=False),
|
||||
questionary.Choice("🔁 Пересканировать уже обогащённые сайты (--rescan-sites)",
|
||||
value="rescan_sites", checked=False),
|
||||
],
|
||||
)
|
||||
do_enrich = "enrich" in pipeline_opts
|
||||
do_enrich_egrul = "enrich_egrul" in pipeline_opts
|
||||
do_rescore = "rescore" in pipeline_opts
|
||||
do_export = "export" in pipeline_opts
|
||||
do_export_master = "export_master" in pipeline_opts
|
||||
do_rescan_sites = "rescan_sites" in pipeline_opts
|
||||
do_hh_enrich_websites = "hh_enrich_websites" in pipeline_opts
|
||||
do_find_sites = "find_sites" in pipeline_opts
|
||||
|
||||
# ── Шаг 7: Мин. score ────────────────────────────────────────────
|
||||
min_score = "0"
|
||||
if do_export:
|
||||
min_score = ask(
|
||||
questionary.text,
|
||||
"7/8 Минимальный score для экспорта:",
|
||||
default="5",
|
||||
validate=lambda v: v.isdigit() or "Введите целое число",
|
||||
)
|
||||
|
||||
# ── Шаг 8: Сборка команды(-команд) ───────────────────────────────
|
||||
# Для каждого выбранного источника + района — отдельная команда
|
||||
# (--category один общий, поэтому источники разделяются на отдельные запуски).
|
||||
sources_for_run: list[tuple[str, list[str]]] = []
|
||||
if use_yandex:
|
||||
sources_for_run.append(("yandex", selected_categories))
|
||||
if use_hh:
|
||||
sources_for_run.append(("hh", selected_hh_queries))
|
||||
|
||||
def build_cmd(district_value: str | None, source: str, categories: list[str]) -> list[str]:
|
||||
"""Собрать команду для одной пары (район, источник)."""
|
||||
cmd = [sys.executable, "main.py", "--source", source]
|
||||
|
||||
if categories:
|
||||
cmd += ["--category", ",".join(categories)]
|
||||
|
||||
cmd += ["--city", city]
|
||||
if district_value:
|
||||
cmd += ["--district", district_value]
|
||||
cmd += ["--limit", limit]
|
||||
# Порядок флагов: rescan → hh-sites → find-sites → enrich → enrich-egrul.
|
||||
# main.py всё равно строго упорядочит шаги внутри, но логически читается лучше.
|
||||
if do_rescan_sites: cmd.append("--rescan-sites")
|
||||
if do_hh_enrich_websites: cmd.append("--hh-enrich-websites")
|
||||
if do_find_sites: cmd.append("--find-sites")
|
||||
if do_enrich: cmd.append("--enrich")
|
||||
if do_enrich_egrul: cmd.append("--enrich-egrul")
|
||||
if do_rescore: cmd.append("--rescore")
|
||||
if do_export: cmd += ["--export", "--min-score", min_score]
|
||||
if do_export_master: cmd.append("--export-master")
|
||||
return cmd
|
||||
|
||||
# Декартово произведение: район × источник = N команд
|
||||
runs = [
|
||||
(d, src, build_cmd(d, src, cats))
|
||||
for d in final_districts
|
||||
for src, cats in sources_for_run
|
||||
]
|
||||
|
||||
# ── Предпросмотр ─────────────────────────────────────────────────
|
||||
print("\n" + "─" * 56)
|
||||
print(f"8/8 Запусков парсера: {len(runs)}")
|
||||
print()
|
||||
for d, src, cmd in runs:
|
||||
loc_label = d or f"{city} (весь регион)"
|
||||
src_label = {"yandex": "🗺 Я.Карты", "hh": "💼 HH"}.get(src, src)
|
||||
print(f" {src_label} 📍 {loc_label}")
|
||||
print(f" {' '.join(cmd)}")
|
||||
print()
|
||||
|
||||
parts = []
|
||||
if use_yandex: parts.append(f"Я.Карты: {len(selected_categories)} кат.")
|
||||
if use_hh: parts.append(f"HH: {len(selected_hh_queries)} запр.")
|
||||
if len(final_districts) > 1:
|
||||
parts.append(f"📍 {city} × {len(final_districts)} районов")
|
||||
else:
|
||||
d0 = final_districts[0]
|
||||
parts.append(f"📍 {city}{' / ' + d0 if d0 else ''}")
|
||||
parts.append(f"лимит: {limit}")
|
||||
if do_rescan_sites: parts.append("rescan-sites 🔁")
|
||||
if do_hh_enrich_websites: parts.append("HH-sites 🔍")
|
||||
if do_find_sites: parts.append("DDG-sites 🔎")
|
||||
if do_enrich: parts.append("enrich ✓")
|
||||
if do_enrich_egrul: parts.append("DaData/ЕГРЮЛ ✓")
|
||||
if do_rescore: parts.append("rescore ✓")
|
||||
if do_export: parts.append(f"export (≥{min_score}) ✓")
|
||||
if do_export_master: parts.append("master 📦")
|
||||
print(" " + " | ".join(parts))
|
||||
print("─" * 56)
|
||||
|
||||
confirm = ask(questionary.confirm, "Запустить?", default=True)
|
||||
if not confirm:
|
||||
print("[отмена]")
|
||||
sys.exit(0)
|
||||
|
||||
# ── Запуск ───────────────────────────────────────────────────────
|
||||
for idx, (d, src, cmd) in enumerate(runs, start=1):
|
||||
if len(runs) > 1:
|
||||
src_label = {"yandex": "🗺 Я.Карты", "hh": "💼 HH"}.get(src, src)
|
||||
print(f"\n{'═' * 56}")
|
||||
print(f" ▶ Прогон {idx}/{len(runs)}: {src_label} · {d or 'весь регион'}")
|
||||
print(f"{'═' * 56}\n")
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user