limitedonly41 commited on
Commit
0ef6bf3
·
verified ·
1 Parent(s): 5a17118

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +376 -0
app.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import io
4
+ import re
5
+ from typing import List, Optional, Tuple
6
+ import logging
7
+
8
+ # Настройка логирования
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class WebsiteCategorizerApp:
13
+ def __init__(self):
14
+ self.sheet_url = ""
15
+ self.sheet_data = []
16
+ self.current_index = 0
17
+ self.categories = ["новости", "коммерция", "другое"]
18
+ self.results_data = [] # Для сохранения результатов
19
+
20
+ def convert_google_sheet_url(self, sheet_url: str) -> str:
21
+ """Конвертирует URL Google таблицы в CSV экспорт URL"""
22
+ try:
23
+ # Различные форматы URL Google таблиц
24
+ if "/edit#gid=" in sheet_url:
25
+ csv_url = sheet_url.replace("/edit#gid=", "/export?format=csv&gid=")
26
+ elif "/edit?usp=sharing" in sheet_url:
27
+ csv_url = sheet_url.replace("/edit?usp=sharing", "/export?format=csv")
28
+ elif "/edit" in sheet_url:
29
+ csv_url = sheet_url.replace("/edit", "/export?format=csv")
30
+ else:
31
+ # Если URL уже в формате экспорта
32
+ csv_url = sheet_url
33
+
34
+ return csv_url
35
+ except Exception as e:
36
+ logger.error(f"Ошибка конвертации URL: {e}")
37
+ return ""
38
+
39
+ def connect_to_sheet(self, sheet_url: str) -> Tuple[str, str]:
40
+ """Подключается к Google таблице через CSV экспорт"""
41
+ try:
42
+ if not sheet_url:
43
+ return "❌ Ошибка: Введите URL Google таблицы", ""
44
+
45
+ # Конвертируем URL в CSV формат
46
+ csv_url = self.convert_google_sheet_url(sheet_url)
47
+
48
+ if not csv_url:
49
+ return "❌ Ошибка: Неверный формат URL", ""
50
+
51
+ # Загружаем данные через pandas
52
+ df = pd.read_csv(csv_url)
53
+
54
+ if df.empty:
55
+ return "❌ Ошибка: Таблица пуста", ""
56
+
57
+ # Убедимся что есть минимум 2 столбца
58
+ if len(df.columns) < 2:
59
+ return "❌ Ошибка: Нужно минимум 2 столбца (URL и категория)", ""
60
+
61
+ # Сохранение данных
62
+ self.sheet_data = []
63
+ self.results_data = []
64
+
65
+ # Получаем названия столбцов
66
+ url_column = df.columns[0]
67
+ category_column = df.columns[1]
68
+
69
+ for index, row in df.iterrows():
70
+ url = str(row[url_column]).strip() if pd.notna(row[url_column]) else ""
71
+ category = str(row[category_column]).strip() if pd.notna(row[category_column]) else ""
72
+
73
+ if url and url.lower() not in ['url', 'nan']:
74
+ self.sheet_data.append({
75
+ "index": index,
76
+ "url": url,
77
+ "category": category if category.lower() != 'nan' else ""
78
+ })
79
+
80
+ # Добавляем в результаты
81
+ self.results_data.append({
82
+ "url": url,
83
+ "category": category if category.lower() != 'nan' else ""
84
+ })
85
+
86
+ if not self.sheet_data:
87
+ return "❌ Ошибка: Не найдены валидные URL", ""
88
+
89
+ self.current_index = 0
90
+ self.sheet_url = sheet_url
91
+
92
+ success_msg = f"✅ Подключено успешно! Найдено {len(self.sheet_data)} записей"
93
+ first_url = self.get_current_url_for_display()
94
+
95
+ return success_msg, first_url
96
+
97
+ except Exception as e:
98
+ logger.error(f"Ошибка подключения к таблице: {e}")
99
+ error_msg = f"❌ Ошибка: {str(e)}"
100
+ error_msg += "\n\nУбедитесь что:\n- Таблица публичная (доступна всем по ссылке)\n- URL корректный"
101
+ return error_msg, ""
102
+
103
+ def get_current_url_for_display(self) -> str:
104
+ """Возвращает текущий URL для отображения в iframe"""
105
+ if not self.sheet_data or self.current_index >= len(self.sheet_data):
106
+ return ""
107
+
108
+ url = self.sheet_data[self.current_index]["url"]
109
+
110
+ # Добавляем http:// если протокол не указан
111
+ if url and not url.startswith(("http://", "https://")):
112
+ url = "http://" + url
113
+
114
+ return url
115
+
116
+ def get_current_info(self) -> Tuple[str, str, str]:
117
+ """Возвращает информацию о текущей записи"""
118
+ if not self.sheet_data:
119
+ return "", "", "Нет данных"
120
+
121
+ if self.current_index >= len(self.sheet_data):
122
+ self.current_index = 0
123
+
124
+ current = self.sheet_data[self.current_index]
125
+ url = current["url"]
126
+ category = current["category"]
127
+
128
+ info = f"{self.current_index + 1}/{len(self.sheet_data)}"
129
+
130
+ return url, category, info
131
+
132
+ def navigate_to_index(self, index: int) -> Tuple[str, str, str, str]:
133
+ """Переходит к записи по индексу"""
134
+ if not self.sheet_data:
135
+ return "", "", "", "Нет данных"
136
+
137
+ # Ограничиваем индекс допустимыми значениями
138
+ index = max(0, min(index, len(self.sheet_data) - 1))
139
+ self.current_index = index
140
+
141
+ url, category, info = self.get_current_info()
142
+ iframe_url = self.get_current_url_for_display()
143
+
144
+ return url, category, info, iframe_url
145
+
146
+ def previous_record(self) -> Tuple[str, str, str, str]:
147
+ """Переход к предыдущей записи"""
148
+ if not self.sheet_data:
149
+ return "", "", "", "Нет данных"
150
+
151
+ if self.current_index > 0:
152
+ self.current_index -= 1
153
+ else:
154
+ self.current_index = len(self.sheet_data) - 1
155
+
156
+ return self.navigate_to_index(self.current_index)
157
+
158
+ def next_record(self) -> Tuple[str, str, str, str]:
159
+ """Переход к следующей записи"""
160
+ if not self.sheet_data:
161
+ return "", "", "", "Нет данных"
162
+
163
+ if self.current_index < len(self.sheet_data) - 1:
164
+ self.current_index += 1
165
+ else:
166
+ self.current_index = 0
167
+
168
+ return self.navigate_to_index(self.current_index)
169
+
170
+ def save_category(self, category: str) -> Tuple[str, str]:
171
+ """Сохраняет категорию локально и возвращает CSV для скачивания"""
172
+ if not self.sheet_data:
173
+ return "❌ Нет данных для сохранения", ""
174
+
175
+ try:
176
+ # Обновляем локальные данные
177
+ self.sheet_data[self.current_index]["category"] = category
178
+ self.results_data[self.current_index]["category"] = category
179
+
180
+ # Создаем CSV файл с результатами
181
+ df_results = pd.DataFrame(self.results_data)
182
+ csv_buffer = io.StringIO()
183
+ df_results.to_csv(csv_buffer, index=False, encoding='utf-8')
184
+ csv_content = csv_buffer.getvalue()
185
+
186
+ status_msg = f"✅ '{category}' сохранено"
187
+
188
+ return status_msg, csv_content
189
+
190
+ except Exception as e:
191
+ logger.error(f"Ошибка сохранения категории: {e}")
192
+ return f"❌ Ошибка: {str(e)}", ""
193
+
194
+ def export_results(self) -> str:
195
+ """Экспортирует все результаты в CSV"""
196
+ if not self.results_data:
197
+ return ""
198
+
199
+ df_results = pd.DataFrame(self.results_data)
200
+ csv_buffer = io.StringIO()
201
+ df_results.to_csv(csv_buffer, index=False, encoding='utf-8')
202
+ return csv_buffer.getvalue()
203
+
204
+ # Создаем экземпляр приложения
205
+ app = WebsiteCategorizerApp()
206
+
207
+ # Создание простого и надежного интерфейса
208
+ with gr.Blocks(title="Категоризатор сайтов", theme=gr.themes.Soft()) as demo:
209
+
210
+ # Заголовок
211
+ gr.HTML("<h2 style='text-align: center; margin: 10px 0; color: #2d5aa0;'>🌐 Категоризатор сайтов</h2>")
212
+
213
+ with gr.Row():
214
+ # КОМПАКТНАЯ ЛЕВАЯ ПАНЕЛЬ (scale=1 - минимальный размер)
215
+ with gr.Column(scale=1):
216
+
217
+ # Подключение
218
+ gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>📊 Подключение</h4>")
219
+ sheet_url_input = gr.Textbox(
220
+ label="URL Google таблицы",
221
+ placeholder="https://docs.google.com/spreadsheets/...",
222
+ lines=2
223
+ )
224
+ connect_btn = gr.Button("🔗 Подключить", variant="primary")
225
+ connection_status = gr.HTML("")
226
+
227
+ # Навигация
228
+ gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>🧭 Навигация</h4>")
229
+ with gr.Row():
230
+ prev_btn = gr.Button("⬅️", size="sm")
231
+ next_btn = gr.Button("➡️", size="sm")
232
+
233
+ record_info = gr.HTML("")
234
+ current_url_display = gr.Textbox(
235
+ label="Текущий URL",
236
+ interactive=False,
237
+ lines=1
238
+ )
239
+
240
+ # Категория
241
+ gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>🏷️ Категория</h4>")
242
+ category_dropdown = gr.Dropdown(
243
+ choices=app.categories,
244
+ label="Выберите категорию"
245
+ )
246
+ save_status = gr.HTML("")
247
+
248
+ # Экспорт
249
+ gr.HTML("<h4 style='font-size: 14px; margin: 5px 0;'>💾 Экспорт</h4>")
250
+ export_btn = gr.Button("📥 Скачать CSV")
251
+ export_file = gr.File(visible=False)
252
+
253
+ # БОЛЬШОЕ ОКНО ПРОСМОТРА (scale=5 - в 5 раз больше левой)
254
+ with gr.Column(scale=5):
255
+ gr.HTML("<h3 style='margin-bottom: 8px;'>🌍 Предварительный просмотр</h3>")
256
+
257
+ # Большое окно - 900px высота
258
+ website_viewer = gr.HTML(
259
+ value="""
260
+ <div style='height: 900px; display: flex; flex-direction: column; align-items: center; justify-content: center;
261
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
262
+ border: 2px dashed #ccc; border-radius: 12px; color: white; text-align: center;'>
263
+ <h1 style='margin-bottom: 24px;'>🚀 Добро пожаловать!</h1>
264
+ <p style='font-size: 20px; margin-bottom: 16px;'>Подключите Google таблицу</p>
265
+ <p style='font-size: 16px; opacity: 0.8;'>Окно просмотра 900px высотой</p>
266
+ </div>
267
+ """
268
+ )
269
+
270
+ # Скрытое состояние для CSV данных
271
+ csv_data = gr.State("")
272
+
273
+ # Обработчики событий
274
+ def handle_connect(url):
275
+ """Обработчик подключения к таблице"""
276
+ status, iframe_url = app.connect_to_sheet(url)
277
+
278
+ if "✅" in status: # Успешное подключение
279
+ url_display, category, info = app.get_current_info()
280
+
281
+ if iframe_url:
282
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" frameborder="0" style="border-radius: 8px;"></iframe>'
283
+ else:
284
+ iframe_html = "<div style='height: 900px; display: flex; align-items: center; justify-content: center; background: #f8d7da; border: 2px dashed #dc3545; border-radius: 8px; color: #721c24;'><p style='font-size: 18px;'>❌ URL не найден</p></div>"
285
+
286
+ info_html = f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">{info}</div>'
287
+
288
+ return (
289
+ gr.HTML(f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">✅ Готово!</div>'),
290
+ iframe_html,
291
+ url_display,
292
+ category,
293
+ info_html
294
+ )
295
+ else: # Ошибка
296
+ return (
297
+ gr.HTML(f'<div style="color: #dc3545; background: #f8d7da; padding: 8px; border-radius: 4px; margin: 4px 0;">❌ Ошибка</div>'),
298
+ website_viewer.value,
299
+ "",
300
+ "",
301
+ gr.HTML("")
302
+ )
303
+
304
+ def handle_navigation(direction):
305
+ """Обработчик навигации"""
306
+ if direction == "next":
307
+ url_display, category, info, iframe_url = app.next_record()
308
+ else:
309
+ url_display, category, info, iframe_url = app.previous_record()
310
+
311
+ if iframe_url:
312
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="900px" frameborder="0" style="border-radius: 8px;"></iframe>'
313
+ else:
314
+ iframe_html = "<div style='height: 900px; display: flex; align-items: center; justify-content: center; background: #f8d7da; border: 2px dashed #dc3545; border-radius: 8px; color: #721c24;'><p style='font-size: 18px;'>❌ URL не найден</p></div>"
315
+
316
+ info_html = f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">{info}</div>'
317
+
318
+ return (
319
+ iframe_html,
320
+ url_display,
321
+ category,
322
+ info_html
323
+ )
324
+
325
+ def handle_category_change(category):
326
+ """Обработчик изменения категории"""
327
+ if category:
328
+ status, csv_content = app.save_category(category)
329
+ if "✅" in status:
330
+ status_html = f'<div style="color: green; background: #d4edda; padding: 8px; border-radius: 4px; margin: 4px 0;">{status}</div>'
331
+ else:
332
+ status_html = f'<div style="color: #dc3545; background: #f8d7da; padding: 8px; border-radius: 4px; margin: 4px 0;">{status}</div>'
333
+ return status_html, csv_content
334
+ return gr.HTML(""), ""
335
+
336
+ def handle_export():
337
+ """Обработчик экспорта результатов"""
338
+ csv_content = app.export_results()
339
+ if csv_content:
340
+ # Сохраняем во временный файл
341
+ with open("results.csv", "w", encoding="utf-8") as f:
342
+ f.write(csv_content)
343
+ return gr.File(value="results.csv", visible=True)
344
+ return gr.File(visible=False)
345
+
346
+ # Привязка событий
347
+ connect_btn.click(
348
+ handle_connect,
349
+ inputs=[sheet_url_input],
350
+ outputs=[connection_status, website_viewer, current_url_display, category_dropdown, record_info]
351
+ )
352
+
353
+ next_btn.click(
354
+ lambda: handle_navigation("next"),
355
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info]
356
+ )
357
+
358
+ prev_btn.click(
359
+ lambda: handle_navigation("previous"),
360
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info]
361
+ )
362
+
363
+ category_dropdown.change(
364
+ handle_category_change,
365
+ inputs=[category_dropdown],
366
+ outputs=[save_status, csv_data]
367
+ )
368
+
369
+ export_btn.click(
370
+ handle_export,
371
+ outputs=[export_file]
372
+ )
373
+
374
+ # Запуск приложения
375
+ if __name__ == "__main__":
376
+ demo.launch()