limitedonly41 commited on
Commit
efb2be6
·
verified ·
1 Parent(s): b0c9b82

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +413 -0
app.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Создание Gradio интерфейса
208
+ with gr.Blocks(
209
+ title="Категоризатор веб-сайтов",
210
+ theme=gr.themes.Soft()
211
+ ) as demo:
212
+
213
+ gr.HTML("<h1 style='text-align: center; color: #2d5aa0;'>🌐 Категоризатор веб-сайтов</h1>")
214
+ gr.HTML("<p style='text-align: center;'>Подключите <b>публичную</b> Google таблицу с URL и назначайте категории сайтам</p>")
215
+
216
+ # Инструкция для пользователя
217
+ with gr.Accordion("📖 Как использовать", open=False):
218
+ gr.HTML("""
219
+ <div style='padding: 15px; background: #f8f9fa; border-radius: 8px;'>
220
+ <h4>Подготовка Google таблицы:</h4>
221
+ <ol>
222
+ <li>Откройте вашу Google таблицу</li>
223
+ <li>Нажмите "Поделиться" → "Доступ по ссылке" → "Просматривать могут все"</li>
224
+ <li>Скопируйте ссылку</li>
225
+ </ol>
226
+ <h4>Структура таблицы:</h4>
227
+ <ul>
228
+ <li><b>Столбец A:</b> URL сайтов</li>
229
+ <li><b>Столбец B:</b> Категории (заполняется приложением)</li>
230
+ </ul>
231
+ <h4>Использование:</h4>
232
+ <ol>
233
+ <li>Вставьте ссылку на таблицу и подключитесь</li>
234
+ <li>Просматривайте сайты в окне предпросмотра</li>
235
+ <li>Выбирайте катего��ию из списка</li>
236
+ <li>Скачайте результаты в конце работы</li>
237
+ </ol>
238
+ </div>
239
+ """)
240
+
241
+ with gr.Row():
242
+ with gr.Column(scale=1):
243
+ # Панель подключения
244
+ gr.HTML("<h3>📊 Подключение к таблице</h3>")
245
+
246
+ sheet_url_input = gr.Textbox(
247
+ label="URL Google таблицы",
248
+ placeholder="https://docs.google.com/spreadsheets/d/.../edit?usp=sharing",
249
+ value="",
250
+ info="Таблица должна быть публичной (доступна всем по ссылке)"
251
+ )
252
+
253
+ connect_btn = gr.Button("🔗 Подключиться", variant="primary", size="lg")
254
+ connection_status = gr.HTML("")
255
+
256
+ # Панель навигации
257
+ with gr.Group():
258
+ gr.HTML("<h3>🧭 Навигация</h3>")
259
+
260
+ with gr.Row():
261
+ prev_btn = gr.Button("⬅️ Предыдущий", size="sm")
262
+ next_btn = gr.Button("Следующий ➡️", size="sm")
263
+
264
+ record_info = gr.HTML("")
265
+
266
+ current_url_display = gr.Textbox(
267
+ label="Текущий URL",
268
+ interactive=False
269
+ )
270
+
271
+ # Панель категоризации
272
+ with gr.Group():
273
+ gr.HTML("<h3>🏷️ Категоризация</h3>")
274
+
275
+ category_dropdown = gr.Dropdown(
276
+ choices=app.categories,
277
+ label="Выберите категорию",
278
+ value=""
279
+ )
280
+
281
+ save_status = gr.HTML("")
282
+
283
+ # Панель экспорта
284
+ with gr.Group():
285
+ gr.HTML("<h3>💾 Экспорт результатов</h3>")
286
+
287
+ export_btn = gr.Button("📥 Скачать результаты (CSV)", variant="secondary")
288
+ export_file = gr.File(label="Файл с результатами", visible=False)
289
+
290
+ with gr.Column(scale=2):
291
+ gr.HTML("<h3>🌍 Предварительный просмотр сайта</h3>")
292
+
293
+ website_viewer = gr.HTML(
294
+ value="""<div style='height: 600px; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: 2px dashed #ccc; border-radius: 12px; color: white; text-align: center;'>
295
+ <h3 style='margin-bottom: 20px;'>🚀 Добро пожаловать!</h3>
296
+ <p style='font-size: 18px; margin-bottom: 10px;'>Подключите Google таблицу для просмотра сайтов</p>
297
+ <p style='font-size: 14px; opacity: 0.8;'>Здесь будет отображаться предварительный просмотр сайтов</p>
298
+ </div>"""
299
+ )
300
+
301
+ gr.HTML("""
302
+ <p style='text-align: center; color: #666; font-size: 12px; margin-top: 10px;'>
303
+ ⚠️ Некоторые сайты могут блокировать отображение в iframe по соображениям безопасности
304
+ </p>
305
+ """)
306
+
307
+ # Скрытое состояние для CSV данных
308
+ csv_data = gr.State("")
309
+
310
+ # Обработчики событий
311
+ def handle_connect(url):
312
+ """Обработчик подключения к таблице"""
313
+ status, iframe_url = app.connect_to_sheet(url)
314
+
315
+ if "✅" in status: # Успешное подключение
316
+ url_display, category, info = app.get_current_info()
317
+
318
+ if iframe_url:
319
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="600px" frameborder="0" style="border-radius: 8px;"></iframe>'
320
+ else:
321
+ iframe_html = "<div style='height: 600px; display: flex; align-items: center; justify-content: center; background: #f8d7da; border: 2px dashed #dc3545; border-radius: 8px; color: #721c24;'><p>❌ URL не найден или некорректен</p></div>"
322
+
323
+ info_html = f'<div style="color: green; background: #d4edda; padding: 10px; border-radius: 5px; border: 1px solid #c3e6cb;">{info}</div>' if info else ""
324
+
325
+ return (
326
+ gr.HTML(f'<div style="color: green; background: #d4edda; padding: 10px; border-radius: 5px; border: 1px solid #c3e6cb;">{status}</div>'),
327
+ iframe_html,
328
+ url_display,
329
+ category,
330
+ info_html
331
+ )
332
+ else: # Ошибка
333
+ return (
334
+ gr.HTML(f'<div style="color: #dc3545; background: #f8d7da; padding: 10px; border-radius: 5px; border: 1px solid #f5c6cb;">{status}</div>'),
335
+ website_viewer.value,
336
+ "",
337
+ "",
338
+ gr.HTML("")
339
+ )
340
+
341
+ def handle_navigation(direction):
342
+ """Обработчик навигации"""
343
+ if direction == "next":
344
+ url_display, category, info, iframe_url = app.next_record()
345
+ else:
346
+ url_display, category, info, iframe_url = app.previous_record()
347
+
348
+ if iframe_url:
349
+ iframe_html = f'<iframe src="{iframe_url}" width="100%" height="600px" frameborder="0" style="border-radius: 8px;"></iframe>'
350
+ else:
351
+ iframe_html = "<div style='height: 600px; display: flex; align-items: center; justify-content: center; background: #f8d7da; border: 2px dashed #dc3545; border-radius: 8px; color: #721c24;'><p>❌ URL не найден</p></div>"
352
+
353
+ info_html = f'<div style="color: green; background: #d4edda; padding: 10px; border-radius: 5px; border: 1px solid #c3e6cb;">{info}</div>' if info else ""
354
+
355
+ return (
356
+ iframe_html,
357
+ url_display,
358
+ category,
359
+ info_html
360
+ )
361
+
362
+ def handle_category_change(category):
363
+ """Обработчик изменения категории"""
364
+ if category:
365
+ status, csv_content = app.save_category(category)
366
+ if "✅" in status:
367
+ status_html = f'<div style="color: green; background: #d4edda; padding: 10px; border-radius: 5px; border: 1px solid #c3e6cb;">{status}</div>'
368
+ else:
369
+ status_html = f'<div style="color: #dc3545; background: #f8d7da; padding: 10px; border-radius: 5px; border: 1px solid #f5c6cb;">{status}</div>'
370
+ return status_html, csv_content
371
+ return gr.HTML(""), ""
372
+
373
+ def handle_export():
374
+ """Обработчик экспорта результатов"""
375
+ csv_content = app.export_results()
376
+ if csv_content:
377
+ # Сохраняем во временный файл
378
+ with open("results.csv", "w", encoding="utf-8") as f:
379
+ f.write(csv_content)
380
+ return gr.File(value="results.csv", visible=True)
381
+ return gr.File(visible=False)
382
+
383
+ # Привязка событий
384
+ connect_btn.click(
385
+ handle_connect,
386
+ inputs=[sheet_url_input],
387
+ outputs=[connection_status, website_viewer, current_url_display, category_dropdown, record_info]
388
+ )
389
+
390
+ next_btn.click(
391
+ lambda: handle_navigation("next"),
392
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info]
393
+ )
394
+
395
+ prev_btn.click(
396
+ lambda: handle_navigation("previous"),
397
+ outputs=[website_viewer, current_url_display, category_dropdown, record_info]
398
+ )
399
+
400
+ category_dropdown.change(
401
+ handle_category_change,
402
+ inputs=[category_dropdown],
403
+ outputs=[save_status, csv_data]
404
+ )
405
+
406
+ export_btn.click(
407
+ handle_export,
408
+ outputs=[export_file]
409
+ )
410
+
411
+ # Запуск приложения
412
+ if __name__ == "__main__":
413
+ demo.launch()