File size: 11,541 Bytes
b4fcd08 e7de712 b4fcd08 e7de712 b4fcd08 e7de712 b4fcd08 ab5d38a b4fcd08 e7de712 b4fcd08 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
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(
"""
<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()
|