import logging
import os
from typing import Iterable, List
from dotenv import load_dotenv
import gradio as gr
import pandas as pd
import requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
LATEST_DF_BASE = None
SAMPLE_RESPONSE: List[dict] = [
{
"название": "дрель-шуруповерт аккумуляторная 18 В, Li-Ion, быстрозажимной патрон 13 мм, 0-400/2000 об/мин, 91/58 Нм, 2-х скоростная, 21 уровень крутящего момента, встроенная подсветка, электрический тормоз, реверс, вес 2.3 кг, металлический редуктор, без аккумулятора и зарядного устройства DHP458Z",
"вид": "дрель-шуруповерт",
"тип": "аккумуляторная",
"характеристики": "18 В, Li-Ion, быстрозажимной патрон 13 мм, 0-400/2000 об/мин, 91/58 Нм, 2-х скоростная, 21 уровень крутящего момента, встроенная подсветка, электрический тормоз, реверс, вес 2.3 кг, металлический редуктор, без аккумулятора и зарядного устройства",
"производитель": "Makita",
"модель": "DHP458Z",
"артикул": "DHP458Z",
"единицы измерения": "шт",
"вес": 2.3,
"размер упаковки": "225 x 79 x 259 мм",
"English Name": "Makita DHP458Z Cordless Drill Driver",
"описание": "Аккумуляторная дрель-шуруповерт Makita DHP458Z – профессиональный инструмент с напряжением 18 В и технологией XPT для защиты от пыли и влаги. Оборудован металлическим редуктором, двухскоростной передачей (0-2000 и 0-400 об/мин), 21 уровнем крутящего момента и встроенной подсветкой. Подходит для сверления в дереве (до 76 мм), металле (до 13 мм) и бетоне (до 16 мм). Имеет реверс, электронную регулировку оборотов, боковую рукоятку, клипсу для ремня и быстрозажимной патрон. Продается без аккумулятора и зарядного устройства. Выпущена в 2019 году как преемник модели BHP458, совместима с аккумуляторами 3 и 4 Ач.",
"ГАУ": "Инструменты (основное средство)",
}
]
load_dotenv()
PRIORITY_COLUMNS = [
"название",
"производитель",
"модель",
"артикул",
"вид",
"тип",
"характеристики",
]
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
WEBHOOK_TIMEOUT = int(os.getenv("WEBHOOK_TIMEOUT_SECONDS", "15"))
def _reorder_columns(columns: Iterable[str]) -> List[str]:
ordered = [col for col in PRIORITY_COLUMNS if col in columns]
remaining = [col for col in columns if col not in ordered]
return ordered + remaining
def _call_webhook(target_url: str) -> List[dict]:
if not WEBHOOK_URL:
logger.info("WEBHOOK_URL not configured. Returning sample payload.")
return SAMPLE_RESPONSE
try:
response = requests.post(
WEBHOOK_URL, json={"url": target_url}, timeout=WEBHOOK_TIMEOUT
)
response.raise_for_status()
payload = response.json()
if not isinstance(payload, list):
raise ValueError("Webhook response must be a list of objects.")
return payload
except (requests.RequestException, ValueError) as exc:
logger.exception("Failed to fetch attributes from webhook.")
raise gr.Error("Не удалось получить атрибуты с вебхука.") from exc
def _dataframe_to_tsv(df: pd.DataFrame) -> str:
return df.to_csv(sep="\t", index=False, header=False)
def _format_vertical(df: pd.DataFrame) -> pd.DataFrame:
indexed = df.reset_index().rename(columns={"index": "Позиция"})
indexed["Позиция"] = indexed["Позиция"] + 1
column_order = list(df.columns)
vertical = indexed.melt(
id_vars="Позиция", var_name="Атрибут", value_name="Значение"
)
vertical["Атрибут"] = pd.Categorical(
vertical["Атрибут"], categories=column_order, ordered=True
)
vertical = (
vertical.sort_values(["Позиция", "Атрибут"])
.reset_index(drop=True)
.drop(columns="Позиция")
)
return vertical
def extract_attributes(target_url: str):
global LATEST_DF_BASE
if not target_url or not target_url.strip():
raise gr.Error("Пожалуйста, введите URL для извлечения атрибутов.")
data = _call_webhook(target_url.strip())
if not data:
raise gr.Error("Вебхук вернул пустой результат.")
df = pd.DataFrame(data)
df = df[_reorder_columns(df.columns)]
# Save base dataframe (without forced GAU) for future GAU updates
LATEST_DF_BASE = df.copy()
gau_choices: List[str] = []
selected_gau: str | None = None
if "ГАУ" in df.columns:
first_value = df["ГАУ"].iloc[0]
if isinstance(first_value, list):
gau_choices = [str(x).strip() for x in first_value if x]
elif pd.notna(first_value):
gau_choices = [str(first_value)]
if gau_choices:
selected_gau = gau_choices[0]
df["ГАУ"] = selected_gau
tsv_payload = _dataframe_to_tsv(df)
vertical_df = _format_vertical(df)
dropdown_update = gr.update(choices=gau_choices, value=selected_gau)
return vertical_df, tsv_payload, dropdown_update
def update_gau(selected_gau: str):
global LATEST_DF_BASE
if LATEST_DF_BASE is None:
raise gr.Error("Сначала получите атрибуты по URL.")
df = LATEST_DF_BASE.copy()
if "ГАУ" in df.columns and selected_gau:
df["ГАУ"] = selected_gau
tsv_payload = _dataframe_to_tsv(df)
vertical_df = _format_vertical(df)
return vertical_df, tsv_payload
def main() -> None:
with gr.Blocks(title="URL Attribute Extractor") as demo:
gr.HTML(
"""
"""
)
gr.Markdown(
"### Получите атрибуты товара\n"
"Вставьте ссылку на карточку товара, чтобы извлечь структурированные данные."
)
with gr.Row():
with gr.Column(scale=2):
url_input = gr.Textbox(
label="Target URL",
placeholder="https://example.com/product/123",
lines=1,
)
submit_btn = gr.Button("Извлечь атрибуты", variant="primary")
gau_dropdown = gr.Dropdown(
label="Вероятные ГАУ (выберите наиболее подходящий)",
choices=[],
interactive=True,
)
results_table = gr.Dataframe(
label="Результаты атрибутов",
interactive=False,
wrap=True,
type="pandas",
elem_id="results-table",
)
clipboard_box = gr.Textbox(
label=None,
interactive=False,
lines=1,
elem_id="tsv-output",
elem_classes=["tsv-hidden"],
container=False,
)
gr.HTML(
"""
"""
)
with gr.Column(scale=1):
gr.Markdown(
"""
### 📚 Как пользоваться инструментом
Этот сервис помогает автоматически собрать характеристики товара по ссылке и подготовить их для Excel.
#### 🛠 Пошаговая инструкция:
1. **Найдите товар**: Откройте страницу товара в интернет-магазине и скопируйте ссылку из адресной строки.
2. **Вставьте ссылку**: Поместите её в поле **Target URL** слева.
3. **Запустите поиск**: Нажмите кнопку **"Извлечь атрибуты"**.
4. **Проверьте данные**:
- Результаты появятся в таблице ниже.
- Если система найдет несколько вариантов **ГАУ**, выберите нужный в выпадающем списке над таблицей.
5. **Сохраните**: Нажмите кнопку **"📋 Скопировать атрибуты"** под таблицей, затем перейдите в Excel и нажмите `Ctrl+V` (Вставить).
> **💡 Совет:** Если возникла ошибка, убедитесь, что ссылка ведет на карточку конкретного товара, а не на общий каталог.
"""
)
submit_btn.click(
fn=extract_attributes,
inputs=url_input,
outputs=[results_table, clipboard_box, gau_dropdown],
)
gau_dropdown.change(
fn=update_gau,
inputs=gau_dropdown,
outputs=[results_table, clipboard_box],
)
demo.launch()
if __name__ == "__main__":
main()