Sverd commited on
Commit
b4fcd08
·
verified ·
1 Parent(s): 92bed69

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +182 -0
  2. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ from typing import Iterable, List
4
+
5
+ from dotenv import load_dotenv
6
+ import gradio as gr
7
+ import pandas as pd
8
+ import requests
9
+
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ SAMPLE_RESPONSE: List[dict] = [
14
+ {
15
+ "название": "дрель-шуруповерт аккумуляторная 18 В, Li-Ion, быстрозажимной патрон 13 мм, 0-400/2000 об/мин, 91/58 Нм, 2-х скоростная, 21 уровень крутящего момента, встроенная подсветка, электрический тормоз, реверс, вес 2.3 кг, металлический редуктор, без аккумулятора и зарядного устройства DHP458Z",
16
+ "вид": "дрель-шуруповерт",
17
+ "тип": "аккумуляторная",
18
+ "характеристики": "18 В, Li-Ion, быстрозажимной патрон 13 мм, 0-400/2000 об/мин, 91/58 Нм, 2-х скоростная, 21 уровень крутящего момента, встроенная подсветка, электрический тормоз, реверс, вес 2.3 кг, металлический редуктор, без аккумулятора и зарядного устройства",
19
+ "производитель": "Makita",
20
+ "модель": "DHP458Z",
21
+ "артикул": "DHP458Z",
22
+ "единицы измерения": "шт",
23
+ "вес": 2.3,
24
+ "размер упаковки": "225 x 79 x 259 мм",
25
+ "English Name": "Makita DHP458Z Cordless Drill Driver",
26
+ "описание": "Аккумуляторная дрель-шуруповерт Makita DHP458Z – профессиональный инструмент с напряжением 18 В и технологией XPT для защиты от пыли и влаги. Оборудован металлическим редуктором, двухскоростной передачей (0-2000 и 0-400 об/мин), 21 уровнем крутящего момента и встроенной подсветкой. Подходит для сверления в дереве (до 76 мм), металле (до 13 мм) и бетоне (до 16 мм). Имеет реверс, электронную регулировку оборотов, боковую рукоятку, клипсу для ремня и быстрозажимной патрон. Продается без аккумулятора и зарядного устройства. Выпущена в 2019 году как преемник модели BHP458, совместима с аккумуляторами 3 и 4 Ач.",
27
+ "ГАУ": "Инструменты (основное средство)",
28
+ }
29
+ ]
30
+
31
+ load_dotenv()
32
+
33
+ PRIORITY_COLUMNS = [
34
+ "название",
35
+ "производитель",
36
+ "модель",
37
+ "артикул",
38
+ "вид",
39
+ "тип",
40
+ "характеристики",
41
+ ]
42
+ WEBHOOK_URL = os.getenv("WEBHOOK_URL")
43
+ WEBHOOK_TIMEOUT = int(os.getenv("WEBHOOK_TIMEOUT_SECONDS", "15"))
44
+
45
+
46
+ def _reorder_columns(columns: Iterable[str]) -> List[str]:
47
+ ordered = [col for col in PRIORITY_COLUMNS if col in columns]
48
+ remaining = [col for col in columns if col not in ordered]
49
+ return ordered + remaining
50
+
51
+
52
+ def _call_webhook(target_url: str) -> List[dict]:
53
+ if not WEBHOOK_URL:
54
+ logger.info("WEBHOOK_URL not configured. Returning sample payload.")
55
+ return SAMPLE_RESPONSE
56
+
57
+ try:
58
+ response = requests.post(
59
+ WEBHOOK_URL, json={"url": target_url}, timeout=WEBHOOK_TIMEOUT
60
+ )
61
+ response.raise_for_status()
62
+ payload = response.json()
63
+ if not isinstance(payload, list):
64
+ raise ValueError("Webhook response must be a list of objects.")
65
+ return payload
66
+ except (requests.RequestException, ValueError) as exc:
67
+ logger.exception("Failed to fetch attributes from webhook.")
68
+ raise gr.Error("Не удалось получить атрибуты с вебхука.") from exc
69
+
70
+
71
+ def _dataframe_to_tsv(df: pd.DataFrame) -> str:
72
+ return df.to_csv(sep="\t", index=False, header=False)
73
+
74
+
75
+ def _format_vertical(df: pd.DataFrame) -> pd.DataFrame:
76
+ indexed = df.reset_index().rename(columns={"index": "Позиция"})
77
+ indexed["Позиция"] = indexed["Позиция"] + 1
78
+ column_order = list(df.columns)
79
+ vertical = indexed.melt(
80
+ id_vars="Позиция", var_name="Атрибут", value_name="Значение"
81
+ )
82
+ vertical["Атрибут"] = pd.Categorical(
83
+ vertical["Атрибут"], categories=column_order, ordered=True
84
+ )
85
+ vertical = (
86
+ vertical.sort_values(["Позиция", "Атрибут"])
87
+ .reset_index(drop=True)
88
+ .drop(columns="Позиция")
89
+ )
90
+ return vertical
91
+
92
+
93
+ def extract_attributes(target_url: str) -> tuple[pd.DataFrame, str]:
94
+ if not target_url or not target_url.strip():
95
+ raise gr.Error("Пожалуйста, введите URL для извлечения атрибутов.")
96
+
97
+ data = _call_webhook(target_url.strip())
98
+ if not data:
99
+ raise gr.Error("Вебхук вернул пустой результат.")
100
+
101
+ df = pd.DataFrame(data)
102
+ df = df[_reorder_columns(df.columns)]
103
+ tsv_payload = _dataframe_to_tsv(df)
104
+ vertical_df = _format_vertical(df)
105
+ return vertical_df, tsv_payload
106
+
107
+
108
+ def main() -> None:
109
+ with gr.Blocks(title="URL Attribute Extractor") as demo:
110
+ gr.HTML(
111
+ """
112
+ <style>
113
+ .tsv-hidden textarea {
114
+ height: 0 !important;
115
+ opacity: 0;
116
+ pointer-events: none;
117
+ }
118
+ .tsv-hidden {
119
+ height: 0;
120
+ margin: 0;
121
+ padding: 0;
122
+ }
123
+ #results-table table th:first-child,
124
+ #results-table table td:first-child {
125
+ min-width: 240px;
126
+ width: 30%;
127
+ white-space: normal;
128
+ }
129
+ </style>
130
+ """
131
+ )
132
+ gr.Markdown(
133
+ "### Получите атрибуты товара\n"
134
+ "Вставьте ссылку на карточку товара, чтобы извлечь структурированные данные."
135
+ )
136
+
137
+ with gr.Row():
138
+ url_input = gr.Textbox(
139
+ label="Target URL",
140
+ placeholder="https://example.com/product/123",
141
+ lines=1,
142
+ )
143
+ submit_btn = gr.Button("Извлечь атрибуты", variant="primary")
144
+
145
+ results_table = gr.Dataframe(
146
+ label="Результаты атрибутов",
147
+ interactive=False,
148
+ wrap=True,
149
+ type="pandas",
150
+ elem_id="results-table",
151
+ )
152
+ clipboard_box = gr.Textbox(
153
+ label=None,
154
+ interactive=False,
155
+ lines=1,
156
+ elem_id="tsv-output",
157
+ elem_classes=["tsv-hidden"],
158
+ container=False,
159
+ )
160
+ gr.HTML(
161
+ """
162
+ <button onclick="
163
+ navigator.clipboard.writeText(
164
+ document.querySelector('#tsv-output textarea').value || ''
165
+ );
166
+ " style="width: 100%; padding: 8px; margin-top: -8px;">
167
+ 📋 Скопировать атрибуты карточки
168
+ </button>
169
+ """
170
+ )
171
+
172
+ submit_btn.click(
173
+ fn=extract_attributes,
174
+ inputs=url_input,
175
+ outputs=[results_table, clipboard_box],
176
+ )
177
+
178
+ demo.launch()
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ pandas>=2.2.0
3
+ requests>=2.32.0
4
+ python-dotenv>=1.0.0