feat: визуальный запуск парсера в Admin-панели
render_parser_launcher() в Admin: форма (source/city/categories/limit/ enrich-флаги), кнопки Старт/Стоп, живой лог с авто-обновлением каждые 2с, статус завершения. Константы _PYTHON/_MAIN/_LOG_FILE с абс. путями. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+149
@@ -9,7 +9,10 @@
|
|||||||
"""
|
"""
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -26,6 +29,10 @@ import db_layer # noqa: E402 (рядом с app.py — Streamlit добавл
|
|||||||
|
|
||||||
DB_PATH = PARENT / config.DB_PATH
|
DB_PATH = PARENT / config.DB_PATH
|
||||||
|
|
||||||
|
_LOG_FILE = "/tmp/parser_run.log"
|
||||||
|
_PYTHON = str(Path(__file__).resolve().parent.parent / ".venv-linux" / "bin" / "python3")
|
||||||
|
_MAIN = str(Path(__file__).resolve().parent.parent / "main.py")
|
||||||
|
|
||||||
# Автомиграция: если БД старая (без 4 CRM-колонок / outreach_events / lead_in_run) —
|
# Автомиграция: если БД старая (без 4 CRM-колонок / outreach_events / lead_in_run) —
|
||||||
# init_db догонит схему через ALTER TABLE и CREATE TABLE IF NOT EXISTS.
|
# init_db догонит схему через ALTER TABLE и CREATE TABLE IF NOT EXISTS.
|
||||||
# Идемпотентно — на свежей БД ничего не делает.
|
# Идемпотентно — на свежей БД ничего не делает.
|
||||||
@@ -543,12 +550,154 @@ def render_crm():
|
|||||||
render_lead_detail(selected_lead_id)
|
render_lead_detail(selected_lead_id)
|
||||||
|
|
||||||
|
|
||||||
|
def render_parser_launcher() -> None:
|
||||||
|
st.subheader("Запуск парсера")
|
||||||
|
|
||||||
|
proc: subprocess.Popen | None = st.session_state.get("parser_proc")
|
||||||
|
running = proc is not None and proc.poll() is None
|
||||||
|
|
||||||
|
# ── Форма настроек (показывать только когда не запущен) ──────────
|
||||||
|
if not running:
|
||||||
|
import config as _cfg
|
||||||
|
|
||||||
|
with st.form("parser_form"):
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
source = col1.selectbox(
|
||||||
|
"Источник",
|
||||||
|
options=["yandex", "hh", "all"],
|
||||||
|
format_func=lambda s: {"yandex": "Яндекс.Карты", "hh": "HH.ru", "all": "Все"}[s],
|
||||||
|
)
|
||||||
|
|
||||||
|
cities_ui = [
|
||||||
|
{"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": "__custom__", "district": None},
|
||||||
|
]
|
||||||
|
city_labels = [c["label"] for c in cities_ui]
|
||||||
|
city_idx = col2.selectbox(
|
||||||
|
"Город / район",
|
||||||
|
range(len(city_labels)),
|
||||||
|
format_func=lambda i: city_labels[i],
|
||||||
|
)
|
||||||
|
city_choice = cities_ui[city_idx]
|
||||||
|
|
||||||
|
custom_city = None
|
||||||
|
if city_choice["city"] == "__custom__":
|
||||||
|
custom_city = st.text_input("Введи город вручную")
|
||||||
|
|
||||||
|
categories = st.multiselect(
|
||||||
|
"Категории (пусто = все из config)",
|
||||||
|
options=_cfg.CATEGORIES,
|
||||||
|
default=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
col3, col4 = st.columns(2)
|
||||||
|
limit = col3.number_input("Лимит на категорию", min_value=1, max_value=500, value=20)
|
||||||
|
|
||||||
|
col_e1, col_e2, col_e3 = st.columns(3)
|
||||||
|
do_enrich = col_e1.checkbox("--enrich (сайты)")
|
||||||
|
do_enrich_egrul = col_e2.checkbox("--enrich-egrul (ЕГРЮЛ)")
|
||||||
|
do_export = col_e3.checkbox("--export (CSV)")
|
||||||
|
|
||||||
|
submitted = st.form_submit_button("Запустить", type="primary")
|
||||||
|
|
||||||
|
if submitted:
|
||||||
|
city_val = custom_city if city_choice["city"] == "__custom__" else city_choice["city"]
|
||||||
|
district_val = city_choice["district"]
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
_PYTHON, _MAIN,
|
||||||
|
"--source", source,
|
||||||
|
"--city", city_val or "Москва",
|
||||||
|
"--limit", str(int(limit)),
|
||||||
|
]
|
||||||
|
if categories:
|
||||||
|
cmd += ["--category", ",".join(categories)]
|
||||||
|
if district_val:
|
||||||
|
cmd += ["--district", district_val]
|
||||||
|
if do_enrich:
|
||||||
|
cmd.append("--enrich")
|
||||||
|
if do_enrich_egrul:
|
||||||
|
cmd.append("--enrich-egrul")
|
||||||
|
if do_export:
|
||||||
|
cmd.append("--export")
|
||||||
|
|
||||||
|
log_f = open(_LOG_FILE, "w", encoding="utf-8")
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=log_f,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
cwd=str(Path(_MAIN).parent),
|
||||||
|
)
|
||||||
|
st.session_state["parser_proc"] = proc
|
||||||
|
st.session_state["parser_log_fh"] = log_f
|
||||||
|
st.session_state["parser_cmd"] = " ".join(cmd)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# ── Статус и управление ──────────────────────────────────────────
|
||||||
|
if running:
|
||||||
|
st.info(f"Парсер запущен (PID {proc.pid})")
|
||||||
|
if st.button("Остановить", type="primary", key="btn_stop_parser"):
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
fh = st.session_state.pop("parser_log_fh", None)
|
||||||
|
if fh:
|
||||||
|
fh.close()
|
||||||
|
st.session_state.pop("parser_proc", None)
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
# Процесс завершился
|
||||||
|
if proc is not None:
|
||||||
|
rc = proc.poll()
|
||||||
|
fh = st.session_state.pop("parser_log_fh", None)
|
||||||
|
if fh:
|
||||||
|
fh.close()
|
||||||
|
st.session_state.pop("parser_proc", None)
|
||||||
|
if rc == 0:
|
||||||
|
st.success("Парсер завершился успешно")
|
||||||
|
else:
|
||||||
|
st.error(f"Парсер завершился с кодом {rc}")
|
||||||
|
|
||||||
|
# ── Лог ─────────────────────────────────────────────────────────
|
||||||
|
if os.path.exists(_LOG_FILE):
|
||||||
|
with open(_LOG_FILE, "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
log_text = f.read()
|
||||||
|
if log_text:
|
||||||
|
st.text_area(
|
||||||
|
"Лог",
|
||||||
|
value=log_text,
|
||||||
|
height=300,
|
||||||
|
key="parser_log_area",
|
||||||
|
label_visibility="collapsed",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Пока запущен — автообновление каждые 2 сек
|
||||||
|
if running:
|
||||||
|
time.sleep(2)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
|
||||||
def render_admin() -> None:
|
def render_admin() -> None:
|
||||||
"""Страница управления: статистика, repass, удаление БД."""
|
"""Страница управления: статистика, repass, удаление БД."""
|
||||||
import database as _db
|
import database as _db
|
||||||
|
|
||||||
st.header("Управление / Admin")
|
st.header("Управление / Admin")
|
||||||
|
|
||||||
|
render_parser_launcher()
|
||||||
|
|
||||||
# ── Статистика ───────────────────────────────────────────────────
|
# ── Статистика ───────────────────────────────────────────────────
|
||||||
st.subheader("Статистика БД")
|
st.subheader("Статистика БД")
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user