|
|
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()
|
|
|
|