fix: устранены все найденные аудитом баги и тихие падения
- SQL injection паттерн → параметризованные запросы во всех местах - except: pass/continue → logger.warning() везде, ничего не тонет молча - WAL mode + индекс domain_dedup_key в database.py - try/finally для conn в main.py, утечка соединения устранена - backoff 30с при 403/429 от Rusprofile/ЕГРЮЛ - ликвидированные компании → egrul_status="liquidated" - max_candidates в contacts_finder считает только реальных кандидатов - DB_PATH абсолютный (Path(__file__).parent), HH_PAUSE_BETWEEN_QUERIES в config - HH_SIGNAL_QUERIES дубль убран из launcher.py → импорт из config - path traversal защита в egrul_enricher debug_dump_html Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -205,8 +205,7 @@ def run_hh(conn, queries: list[str], city: str, max_pages: int = None) -> list[i
|
||||
|
||||
# Пауза между запросами (anti-rate-limit)
|
||||
if idx < len(queries):
|
||||
import time
|
||||
time.sleep(3)
|
||||
time.sleep(config.HH_PAUSE_BETWEEN_QUERIES)
|
||||
|
||||
return run_ids
|
||||
|
||||
@@ -272,7 +271,7 @@ def run_enrichment(conn, limit: int | None = None) -> int:
|
||||
conn.execute(f"UPDATE leads SET {sets} WHERE id = ?", vals)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
logger.debug(f" ⚠ update inn/ogrn: {e}")
|
||||
logger.warning(f" ⚠ update inn/ogrn: {e}")
|
||||
|
||||
# Если нашли ИНН на сайте, сразу идём в Rusprofile by-inn за директором
|
||||
rusprofile_note = ""
|
||||
@@ -302,7 +301,7 @@ def run_enrichment(conn, limit: int | None = None) -> int:
|
||||
director = egrul_info.get("director_name") or "—"
|
||||
rusprofile_note = f" +ЕГРЮЛ:директор={director[:30]}"
|
||||
except Exception as e:
|
||||
logger.debug(f" ⚠ Rusprofile by-inn: {e}")
|
||||
logger.warning(f" ⚠ Rusprofile by-inn: {e}")
|
||||
|
||||
# Пересчитать score (теперь с Tier 2 данными + ИНН с сайта)
|
||||
full_lead = dict(conn.execute("SELECT * FROM leads WHERE id = ?", (lead_id,)).fetchone())
|
||||
@@ -888,7 +887,7 @@ def main():
|
||||
# Раньше флаг ставился всегда → `--full --source yandex` гонял HH-обогащение по
|
||||
# ВСЕЙ базе HH-лидов без сайта (~час впустую). Теперь — только при source hh/all
|
||||
# (как и обещает README: «+ --hh-enrich-websites если source=hh»).
|
||||
args.hh_enrich_websites = args.source in ("hh", "all")
|
||||
args.hh_enrich_websites = args.hh_enrich_websites or args.source in ("hh", "all")
|
||||
args.find_sites = True # ⬅ ищем сайты для лидов где website пуст (DDG fallback)
|
||||
args.enrich = True # → website-analyzer достаёт email/phone со страниц
|
||||
args.enrich_egrul = True
|
||||
@@ -914,156 +913,154 @@ def main():
|
||||
# Инициализация БД
|
||||
init_db(config.DB_PATH)
|
||||
conn = get_connection(config.DB_PATH)
|
||||
try:
|
||||
# Только статистика — выходим сразу
|
||||
if args.stats:
|
||||
stats = get_stats(conn)
|
||||
print("\n📊 СТАТИСТИКА:")
|
||||
for k, v in stats.items():
|
||||
print(f" {k}: {v}")
|
||||
return
|
||||
|
||||
# Только статистика — выходим сразу
|
||||
if args.stats:
|
||||
# Cleanup кривых ФИО директоров (одноразовая операция)
|
||||
if args.cleanup_directors:
|
||||
cleared = cleanup_bad_director_names(conn)
|
||||
logger.info(f"🧹 Очищено директоров с должностями вместо ФИО: {cleared}")
|
||||
# Не выходим — даём дальше pipeline отработать (например --enrich-egrul)
|
||||
|
||||
# Одноразовая миграция категорий: lead.category = sources_log.query
|
||||
if args.fix_categories:
|
||||
fixed = fix_categories_from_runs(conn)
|
||||
logger.info(f"🏷️ Категории переписаны на поисковый запрос у {fixed} лидов.")
|
||||
|
||||
# Сброс site_checked_at для перепрогона enricher на уже обогащённых лидах
|
||||
if args.rescan_sites:
|
||||
cursor = conn.execute(
|
||||
"UPDATE leads SET site_checked_at = NULL WHERE site_checked_at IS NOT NULL"
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"🔁 Сброшено site_checked_at у {cursor.rowcount} лидов — следующий --enrich пройдёт по ним заново.")
|
||||
|
||||
# Если не указан ни один из шагов — показать help
|
||||
if not (
|
||||
args.source
|
||||
or args.enrich
|
||||
or args.enrich_egrul
|
||||
or args.enrich_finance
|
||||
or args.rescore
|
||||
or args.export
|
||||
or args.cleanup_directors
|
||||
or args.fix_categories
|
||||
or args.rescan_sites
|
||||
or args.hh_enrich_websites
|
||||
or args.find_sites
|
||||
or args.export_master
|
||||
or args.export_run is not None
|
||||
):
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Собираем run_id всех прогонов этой сессии — для auto-export CSV по прогонам
|
||||
collected_run_ids: list[int] = []
|
||||
|
||||
# ── Pipeline — шаги выполняются последовательно ─────────────────
|
||||
# Шаг 1: Парсинг (если указан --source)
|
||||
if args.source:
|
||||
if args.category:
|
||||
categories = [c.strip() for c in args.category.split(",") if c.strip()]
|
||||
else:
|
||||
categories = config.CATEGORIES
|
||||
|
||||
if args.source in ("yandex", "all"):
|
||||
collected_run_ids.extend(
|
||||
run_yandex(conn, categories, args.city, args.limit, district=args.district)
|
||||
)
|
||||
if args.source in ("hh", "all"):
|
||||
# Для HH "категории" — это signal-запросы (оператор / администратор / etc.)
|
||||
# Если пользователь явно указал --category — используем его список.
|
||||
# Иначе — стандартный config.HH_SIGNAL_QUERIES.
|
||||
hh_queries = categories if args.category else config.HH_SIGNAL_QUERIES
|
||||
collected_run_ids.extend(
|
||||
run_hh(conn, hh_queries, args.city, max_pages=args.limit if args.category else None)
|
||||
)
|
||||
if args.source in ("2gis", "all"):
|
||||
logger.warning("⚠️ Парсер 2GIS пока не реализован (Phase 1.4)")
|
||||
if args.source in ("vk", "all"):
|
||||
logger.warning("⚠️ Парсер VK пока не реализован (Phase 1.5)")
|
||||
|
||||
# --limit относится только к парсингу (карточек на категорию).
|
||||
# Enrichment всегда обрабатывает ВСЕХ непроверенных, чтобы не оставлять хвосты.
|
||||
|
||||
# Шаг 1.5: HH employer-page enrichment (дозаполняет website у HH-лидов).
|
||||
# Должен идти ДО Tier 2 (--enrich), иначе у HH-лидов нет сайта и Tier 2
|
||||
# их пропустит → email не соберутся.
|
||||
if args.hh_enrich_websites:
|
||||
run_hh_websites(conn, limit=args.hh_limit)
|
||||
|
||||
# Шаг 1.6: Поиск сайтов через DDG (для лидов без website).
|
||||
# Должен идти ДО --enrich по той же причине что и hh_enrich_websites:
|
||||
# сначала найти сайт → потом website-analyzer вытащит email/phone.
|
||||
#
|
||||
# ВАЖНО: ограничиваем поиск только лидами ТЕКУЩЕЙ сессии (collected_run_ids).
|
||||
# Иначе пойдём по всей БД (2500+ лидов = час+ DDG-запросов).
|
||||
# Если --find-sites вызван БЕЗ --source (например, для добивки старых лидов),
|
||||
# тогда run_ids=None и идём по всем (медленно, но осознанно).
|
||||
if args.find_sites:
|
||||
run_find_sites(
|
||||
conn,
|
||||
limit=args.find_sites_limit,
|
||||
run_ids=collected_run_ids if collected_run_ids else None,
|
||||
source_filter=args.find_sites_source,
|
||||
)
|
||||
|
||||
# Шаг 2: Enrichment сайтов (если указан --enrich)
|
||||
if args.enrich:
|
||||
run_enrichment(conn, limit=None)
|
||||
|
||||
# Шаг 3: Enrichment ЕГРЮЛ (если указан --enrich-egrul)
|
||||
if args.enrich_egrul:
|
||||
run_egrul_enrichment(conn, limit=None)
|
||||
|
||||
# Шаг 3.5: Финансы — сотрудники + оборот по ООО (если указан --enrich-finance)
|
||||
if args.enrich_finance:
|
||||
run_financials(conn, limit=None)
|
||||
|
||||
# Шаг 4: Rescore (если указан --rescore)
|
||||
if args.rescore:
|
||||
run_rescore(conn)
|
||||
|
||||
# Финальная статистика
|
||||
stats = get_stats(conn)
|
||||
print("\n📊 СТАТИСТИКА:")
|
||||
print("\n" + "═" * 60)
|
||||
print("📊 РЕЗУЛЬТАТ:")
|
||||
for k, v in stats.items():
|
||||
print(f" {k}: {v}")
|
||||
print("═" * 60)
|
||||
|
||||
# Шаг 5: Экспорт (если указан --export)
|
||||
# Новое поведение: если в сессии были прогоны парсера → отдельный CSV
|
||||
# каждого прогона в exports/YYYY-MM/ с шапкой-метаданными.
|
||||
# Старое поведение (backward compat): --export без парсинга → плоский CSV
|
||||
# по min_score в exports/YYYY-MM/leads_<timestamp>.csv.
|
||||
if args.export:
|
||||
if collected_run_ids:
|
||||
print(f"\n📤 Экспорт {len(collected_run_ids)} прогонов сессии:")
|
||||
for run_id in collected_run_ids:
|
||||
export_run(config.DB_PATH, run_id)
|
||||
# Сводный файл «всё, что собрано за день» в папку дня
|
||||
export_day(config.DB_PATH)
|
||||
else:
|
||||
export_to_csv(config.DB_PATH, min_score=args.min_score)
|
||||
|
||||
# Шаг 6: master-snapshot всей БД (--export-master)
|
||||
if args.export_master:
|
||||
export_master(config.DB_PATH)
|
||||
|
||||
# Шаг 7: пересоздать CSV конкретного прогона (--export-run N)
|
||||
if args.export_run is not None:
|
||||
export_run(config.DB_PATH, args.export_run)
|
||||
finally:
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Cleanup кривых ФИО директоров (одноразовая операция)
|
||||
if args.cleanup_directors:
|
||||
cleared = cleanup_bad_director_names(conn)
|
||||
logger.info(f"🧹 Очищено директоров с должностями вместо ФИО: {cleared}")
|
||||
# Не выходим — даём дальше pipeline отработать (например --enrich-egrul)
|
||||
|
||||
# Одноразовая миграция категорий: lead.category = sources_log.query
|
||||
if args.fix_categories:
|
||||
fixed = fix_categories_from_runs(conn)
|
||||
logger.info(f"🏷️ Категории переписаны на поисковый запрос у {fixed} лидов.")
|
||||
|
||||
# Сброс site_checked_at для перепрогона enricher на уже обогащённых лидах
|
||||
if args.rescan_sites:
|
||||
cursor = conn.execute(
|
||||
"UPDATE leads SET site_checked_at = NULL WHERE site_checked_at IS NOT NULL"
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"🔁 Сброшено site_checked_at у {cursor.rowcount} лидов — следующий --enrich пройдёт по ним заново.")
|
||||
|
||||
# Если не указан ни один из шагов — показать help
|
||||
if not (
|
||||
args.source
|
||||
or args.enrich
|
||||
or args.enrich_egrul
|
||||
or args.enrich_finance
|
||||
or args.rescore
|
||||
or args.export
|
||||
or args.cleanup_directors
|
||||
or args.fix_categories
|
||||
or args.rescan_sites
|
||||
or args.hh_enrich_websites
|
||||
or args.find_sites
|
||||
or args.export_master
|
||||
or args.export_run is not None
|
||||
):
|
||||
parser.print_help()
|
||||
conn.close()
|
||||
sys.exit(1)
|
||||
|
||||
# Собираем run_id всех прогонов этой сессии — для auto-export CSV по прогонам
|
||||
collected_run_ids: list[int] = []
|
||||
|
||||
# ── Pipeline — шаги выполняются последовательно ─────────────────
|
||||
# Шаг 1: Парсинг (если указан --source)
|
||||
if args.source:
|
||||
if args.category:
|
||||
categories = [c.strip() for c in args.category.split(",") if c.strip()]
|
||||
else:
|
||||
categories = config.CATEGORIES
|
||||
|
||||
if args.source in ("yandex", "all"):
|
||||
collected_run_ids.extend(
|
||||
run_yandex(conn, categories, args.city, args.limit, district=args.district)
|
||||
)
|
||||
if args.source in ("hh", "all"):
|
||||
# Для HH "категории" — это signal-запросы (оператор / администратор / etc.)
|
||||
# Если пользователь явно указал --category — используем его список.
|
||||
# Иначе — стандартный config.HH_SIGNAL_QUERIES.
|
||||
hh_queries = categories if args.category else config.HH_SIGNAL_QUERIES
|
||||
collected_run_ids.extend(
|
||||
run_hh(conn, hh_queries, args.city, max_pages=args.limit if args.category else None)
|
||||
)
|
||||
if args.source in ("2gis", "all"):
|
||||
logger.warning("⚠️ Парсер 2GIS пока не реализован (Phase 1.4)")
|
||||
if args.source in ("vk", "all"):
|
||||
logger.warning("⚠️ Парсер VK пока не реализован (Phase 1.5)")
|
||||
|
||||
# --limit относится только к парсингу (карточек на категорию).
|
||||
# Enrichment всегда обрабатывает ВСЕХ непроверенных, чтобы не оставлять хвосты.
|
||||
|
||||
# Шаг 1.5: HH employer-page enrichment (дозаполняет website у HH-лидов).
|
||||
# Должен идти ДО Tier 2 (--enrich), иначе у HH-лидов нет сайта и Tier 2
|
||||
# их пропустит → email не соберутся.
|
||||
if args.hh_enrich_websites:
|
||||
run_hh_websites(conn, limit=args.hh_limit)
|
||||
|
||||
# Шаг 1.6: Поиск сайтов через DDG (для лидов без website).
|
||||
# Должен идти ДО --enrich по той же причине что и hh_enrich_websites:
|
||||
# сначала найти сайт → потом website-analyzer вытащит email/phone.
|
||||
#
|
||||
# ВАЖНО: ограничиваем поиск только лидами ТЕКУЩЕЙ сессии (collected_run_ids).
|
||||
# Иначе пойдём по всей БД (2500+ лидов = час+ DDG-запросов).
|
||||
# Если --find-sites вызван БЕЗ --source (например, для добивки старых лидов),
|
||||
# тогда run_ids=None и идём по всем (медленно, но осознанно).
|
||||
if args.find_sites:
|
||||
run_find_sites(
|
||||
conn,
|
||||
limit=args.find_sites_limit,
|
||||
run_ids=collected_run_ids if collected_run_ids else None,
|
||||
source_filter=args.find_sites_source,
|
||||
)
|
||||
|
||||
# Шаг 2: Enrichment сайтов (если указан --enrich)
|
||||
if args.enrich:
|
||||
run_enrichment(conn, limit=None)
|
||||
|
||||
# Шаг 3: Enrichment ЕГРЮЛ (если указан --enrich-egrul)
|
||||
if args.enrich_egrul:
|
||||
run_egrul_enrichment(conn, limit=None)
|
||||
|
||||
# Шаг 3.5: Финансы — сотрудники + оборот по ООО (если указан --enrich-finance)
|
||||
if args.enrich_finance:
|
||||
run_financials(conn, limit=None)
|
||||
|
||||
# Шаг 4: Rescore (если указан --rescore)
|
||||
if args.rescore:
|
||||
run_rescore(conn)
|
||||
|
||||
# Финальная статистика
|
||||
stats = get_stats(conn)
|
||||
print("\n" + "═" * 60)
|
||||
print("📊 РЕЗУЛЬТАТ:")
|
||||
for k, v in stats.items():
|
||||
print(f" {k}: {v}")
|
||||
print("═" * 60)
|
||||
|
||||
# Шаг 5: Экспорт (если указан --export)
|
||||
# Новое поведение: если в сессии были прогоны парсера → отдельный CSV
|
||||
# каждого прогона в exports/YYYY-MM/ с шапкой-метаданными.
|
||||
# Старое поведение (backward compat): --export без парсинга → плоский CSV
|
||||
# по min_score в exports/YYYY-MM/leads_<timestamp>.csv.
|
||||
if args.export:
|
||||
if collected_run_ids:
|
||||
print(f"\n📤 Экспорт {len(collected_run_ids)} прогонов сессии:")
|
||||
for run_id in collected_run_ids:
|
||||
export_run(config.DB_PATH, run_id)
|
||||
# Сводный файл «всё, что собрано за день» в папку дня
|
||||
export_day(config.DB_PATH)
|
||||
else:
|
||||
export_to_csv(config.DB_PATH, min_score=args.min_score)
|
||||
|
||||
# Шаг 6: master-snapshot всей БД (--export-master)
|
||||
if args.export_master:
|
||||
export_master(config.DB_PATH)
|
||||
|
||||
# Шаг 7: пересоздать CSV конкретного прогона (--export-run N)
|
||||
if args.export_run is not None:
|
||||
export_run(config.DB_PATH, args.export_run)
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user