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