| 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)]
|
|
|
|
|
| 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(
|
| """
|
| <style>
|
| .tsv-hidden textarea {
|
| height: 0 !important;
|
| opacity: 0;
|
| pointer-events: none;
|
| }
|
| .tsv-hidden {
|
| height: 0;
|
| margin: 0;
|
| padding: 0;
|
| }
|
| #results-table table th:first-child,
|
| #results-table table td:first-child {
|
| min-width: 240px;
|
| width: 30%;
|
| white-space: normal;
|
| }
|
| </style>
|
| """
|
| )
|
| 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(
|
| """
|
| <button onclick="
|
| navigator.clipboard.writeText(
|
| document.querySelector('#tsv-output textarea').value || ''
|
| );
|
| " style="width: 100%; padding: 8px; margin-top: -8px;">
|
| 📋 Скопировать атрибуты карточки
|
| </button>
|
| """
|
| )
|
|
|
| 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()
|
|
|