Lukeetah commited on
Commit
a99f3d6
·
verified ·
1 Parent(s): 4792bae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +366 -116
app.py CHANGED
@@ -1,130 +1,380 @@
1
- import gradio as gr
2
  import os
3
- import tempfile
4
- import time
5
- from web_scraper_tool import WebScrapperTool
 
 
 
 
6
 
7
- # Inicializar el scraper
8
- scraper = WebScrapperTool("temp_output")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- def scrape_url(url, output_format, progress=gr.Progress()):
11
- """Función principal que procesa la URL ingresada"""
12
- progress(0, desc="Iniciando...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- # Validar URL
15
- if not url.startswith(('http://', 'https://')):
16
- return None, "Error: La URL debe comenzar con http:// o https://"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- try:
19
- progress(0.2, desc="Analizando URL...")
20
- # Detectar si es una imagen
21
- is_image = scraper.is_image_url(url)
22
-
23
- progress(0.4, desc="Iniciando descarga...")
24
-
25
- temp_dir = tempfile.mkdtemp()
26
- timestamp = int(time.time())
27
-
28
- if is_image:
29
- progress(0.6, desc="Procesando imagen...")
30
- filename = f"imagen_{timestamp}.txt"
31
- output_path = os.path.join(temp_dir, filename)
32
-
33
- # Obtenemos metadatos de la imagen
34
- metadata = scraper.get_image_metadata(url)
35
- with open(output_path, 'w', encoding='utf-8') as f:
36
- f.write(f"URL de la imagen: {url}\n\n")
37
- f.write("Metadatos de la imagen:\n")
38
- for key, value in metadata.items():
39
- f.write(f"{key}: {value}\n")
40
-
41
- progress(1.0, desc="¡Listo!")
42
- return output_path, f"✅ Archivo generado exitosamente. Se detectó que la URL es una imagen."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  else:
44
- if output_format == "txt":
45
- progress(0.6, desc="Extrayendo texto...")
46
- filename = f"contenido_{timestamp}.txt"
47
- output_path = os.path.join(temp_dir, filename)
48
- scraper.scrape_to_text(url, output_path)
49
- else: # PDF
50
- progress(0.6, desc="Generando PDF...")
51
- filename = f"contenido_{timestamp}.pdf"
52
- output_path = os.path.join(temp_dir, filename)
53
- scraper.scrape_to_pdf(url, output_path)
54
-
55
- progress(1.0, desc="¡Listo!")
56
- return output_path, f"✅ Archivo generado exitosamente en formato {output_format.upper()}"
 
 
 
57
 
58
- except Exception as e:
59
- return None, f"❌ Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- # Estilos CSS personalizados para una apariencia minimalista
62
- css = """
63
- .gradio-container {
64
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
65
- max-width: 800px;
66
- margin: 0 auto;
67
- }
68
- .main-header {
69
- text-align: center;
70
- margin-bottom: 2rem;
71
- }
72
- .app-description {
73
- margin-bottom: 2rem;
74
- text-align: center;
75
- color: #666;
76
- }
77
- .gr-button {
78
- border-radius: 4px !important;
79
- }
80
- .gr-button-primary {
81
- background: linear-gradient(90deg, #5c1edb, #775af5) !important;
82
- }
83
- footer {
84
- margin-top: 3rem;
85
- text-align: center;
86
- font-size: 0.8rem;
87
- color: #888;
88
- }
89
- """
90
-
91
- # Definir la interfaz de Gradio
92
- with gr.Blocks(css=css) as demo:
93
- gr.HTML("<h1 class='main-header'>🕸️ Web Scraper Tool</h1>")
94
- gr.HTML("<p class='app-description'>Ingresa una URL para extraer su contenido en formato PDF o texto plano. La herramienta detectará automáticamente si se trata de una imagen.</p>")
95
-
96
- with gr.Row():
97
- url_input = gr.Textbox(
98
- label="URL",
99
- placeholder="https://ejemplo.com",
100
- info="Ingresa la URL que deseas procesar"
101
- )
102
 
103
- with gr.Row():
104
- format_select = gr.Radio(
105
- ["txt", "pdf"],
106
- label="Formato de salida",
107
- value="txt",
108
- info="Selecciona el formato para guardar el contenido"
109
- )
110
 
111
- with gr.Row():
112
- submit_btn = gr.Button("Procesar URL", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- with gr.Row():
115
- output_message = gr.Textbox(label="Estado")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- with gr.Row():
118
- file_output = gr.File(label="Archivo generado")
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- submit_btn.click(
121
- fn=scrape_url,
122
- inputs=[url_input, format_select],
123
- outputs=[file_output, output_message]
124
- )
125
-
126
- gr.HTML("<footer>Desarrollado con <a href='https://gradio.app'>Gradio</a> y <a href='https://huggingface.co/spaces'>Hugging Face Spaces</a></footer>")
127
 
128
- # Iniciar la aplicación
129
- if __name__ == "__main__":
130
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ from weasyprint import HTML, CSS
5
+ from urllib.parse import urlparse, urlunparse
6
+ import re
7
+ from PIL import Image
8
+ import io
9
 
10
+ class WebScrapperTool:
11
+ def __init__(self, output_dir="output"):
12
+ self.output_dir = output_dir
13
+ if not os.path.exists(output_dir):
14
+ os.makedirs(output_dir)
15
+
16
+ # Headers para evitar bloqueos
17
+ self.headers = {
18
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
19
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
20
+ 'Accept-Language': 'es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3',
21
+ 'Accept-Encoding': 'gzip, deflate',
22
+ 'DNT': '1',
23
+ 'Connection': 'keep-alive',
24
+ 'Upgrade-Insecure-Requests': '1'
25
+ }
26
 
27
+ def normalize_url(self, url):
28
+ """Normaliza URLs manejando todos los casos de mayúsculas y formatos incorrectos"""
29
+ if not url:
30
+ raise ValueError("URL no puede estar vacía")
31
+
32
+ url = url.strip()
33
+
34
+ # Convertir esquemas a minúsculas pero mantener el resto
35
+ if url.lower().startswith('http://'):
36
+ url = 'http://' + url[7:]
37
+ elif url.lower().startswith('https://'):
38
+ url = 'https://' + url[8:]
39
+ elif not url.startswith(('http://', 'https://')):
40
+ # Si no tiene esquema, agregar https por defecto
41
+ url = 'https://' + url
42
+
43
+ try:
44
+ parsed = urlparse(url)
45
+
46
+ # Normalizar componentes
47
+ scheme = parsed.scheme.lower()
48
+ netloc = parsed.netloc.lower() if parsed.netloc else ''
49
+ path = parsed.path
50
+ params = parsed.params
51
+ query = parsed.query
52
+ fragment = parsed.fragment
53
+
54
+ # Si netloc está vacío pero hay path, intentar corregir
55
+ if not netloc and path:
56
+ parts = path.split('/', 1)
57
+ netloc = parts[0].lower()
58
+ path = '/' + parts[1] if len(parts) > 1 else ''
59
+
60
+ normalized_url = urlunparse((scheme, netloc, path, params, query, fragment))
61
+ return normalized_url
62
+
63
+ except Exception as e:
64
+ raise ValueError(f"URL inválida: {url}. Error: {str(e)}")
65
 
66
+ def is_image_url(self, url):
67
+ """Detecta si una URL es una imagen"""
68
+ image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.tiff', '.ico'}
69
+
70
+ # Verificar por extensión
71
+ parsed_url = urlparse(url.lower())
72
+ path = parsed_url.path
73
+ if any(path.endswith(ext) for ext in image_extensions):
74
+ return True
75
+
76
+ # Verificar por content-type si es posible
77
+ try:
78
+ response = requests.head(url, headers=self.headers, timeout=10)
79
+ content_type = response.headers.get('content-type', '').lower()
80
+ if content_type.startswith('image/'):
81
+ return True
82
+ except:
83
+ pass
84
+
85
+ return False
86
 
87
+ def get_clean_html_for_pdf(self, html_content, base_url):
88
+ """Limpia HTML específicamente para conversión PDF robusta"""
89
+ soup = BeautifulSoup(html_content, 'html.parser')
90
+
91
+ # Remover elementos problemáticos para PDF
92
+ for element in soup(['script', 'style', 'noscript', 'iframe', 'embed', 'object']):
93
+ element.decompose()
94
+
95
+ # Remover atributos problemáticos
96
+ for tag in soup.find_all():
97
+ # Mantener solo atributos seguros
98
+ safe_attrs = ['href', 'src', 'alt', 'title', 'class', 'id']
99
+ attrs_to_remove = [attr for attr in tag.attrs if attr not in safe_attrs]
100
+ for attr in attrs_to_remove:
101
+ del tag[attr]
102
+
103
+ # Agregar CSS básico para mejor renderizado PDF
104
+ css_style = """
105
+ <style>
106
+ body {
107
+ font-family: Arial, sans-serif;
108
+ line-height: 1.6;
109
+ margin: 20px;
110
+ color: #333;
111
+ }
112
+ h1, h2, h3, h4, h5, h6 {
113
+ color: #2c3e50;
114
+ margin-top: 20px;
115
+ }
116
+ p {
117
+ margin-bottom: 10px;
118
+ }
119
+ a {
120
+ color: #3498db;
121
+ text-decoration: none;
122
+ }
123
+ img {
124
+ max-width: 100%;
125
+ height: auto;
126
+ }
127
+ table {
128
+ border-collapse: collapse;
129
+ width: 100%;
130
+ }
131
+ th, td {
132
+ border: 1px solid #ddd;
133
+ padding: 8px;
134
+ text-align: left;
135
+ }
136
+ </style>
137
+ """
138
+
139
+ # Insertar CSS en el head
140
+ if soup.head:
141
+ soup.head.insert(0, BeautifulSoup(css_style, 'html.parser'))
142
  else:
143
+ # Si no hay head, crear uno
144
+ head = soup.new_tag('head')
145
+ head.insert(0, BeautifulSoup(css_style, 'html.parser'))
146
+ if soup.html:
147
+ soup.html.insert(0, head)
148
+ else:
149
+ # Crear estructura HTML completa
150
+ html_tag = soup.new_tag('html')
151
+ html_tag.insert(0, head)
152
+ body = soup.new_tag('body')
153
+ body.extend(soup.contents[:])
154
+ html_tag.append(body)
155
+ soup.clear()
156
+ soup.append(html_tag)
157
+
158
+ return str(soup)
159
 
160
+ def scrape_to_pdf(self, url, filename=None):
161
+ """Convierte página web a PDF con manejo robusto de errores"""
162
+ try:
163
+ normalized_url = self.normalize_url(url)
164
+
165
+ # Verificar si es imagen
166
+ if self.is_image_url(normalized_url):
167
+ return self._handle_image_to_pdf(normalized_url, filename)
168
+
169
+ # Obtener contenido web
170
+ response = requests.get(normalized_url, headers=self.headers, timeout=30)
171
+ response.raise_for_status()
172
+ response.encoding = response.apparent_encoding or 'utf-8'
173
+
174
+ # Limpiar HTML para PDF
175
+ clean_html = self.get_clean_html_for_pdf(response.text, normalized_url)
176
+
177
+ # Generar nombre de archivo
178
+ if not filename:
179
+ domain = urlparse(normalized_url).netloc.replace('www.', '')
180
+ filename = f"scraped_{domain.replace('.', '_')}.pdf"
181
+
182
+ if not filename.endswith('.pdf'):
183
+ filename += '.pdf'
184
+
185
+ pdf_path = os.path.join(self.output_dir, filename)
186
+
187
+ # Configurar WeasyPrint con opciones robustas
188
+ html_doc = HTML(string=clean_html, base_url=normalized_url)
189
+
190
+ # CSS adicional para mejorar renderizado
191
+ css = CSS(string='''
192
+ @page {
193
+ margin: 2cm;
194
+ size: A4;
195
+ }
196
+ body {
197
+ font-size: 12pt;
198
+ }
199
+ ''')
200
+
201
+ html_doc.write_pdf(pdf_path, stylesheets=[css])
202
+
203
+ return {
204
+ 'status': 'success',
205
+ 'file': pdf_path,
206
+ 'url': normalized_url,
207
+ 'message': f'PDF generado exitosamente: {filename}'
208
+ }
209
+
210
+ except requests.RequestException as e:
211
+ return {
212
+ 'status': 'error',
213
+ 'message': f'Error al acceder a la URL: {str(e)}',
214
+ 'url': url
215
+ }
216
+ except Exception as e:
217
+ return {
218
+ 'status': 'error',
219
+ 'message': f'Error al generar PDF: {str(e)}',
220
+ 'url': url
221
+ }
222
 
223
+ def scrape_to_text(self, url, filename=None):
224
+ """Convierte página web a texto plano"""
225
+ try:
226
+ normalized_url = self.normalize_url(url)
227
+
228
+ # Verificar si es imagen
229
+ if self.is_image_url(normalized_url):
230
+ return self._handle_image_to_text(normalized_url, filename)
231
+
232
+ # Obtener contenido web
233
+ response = requests.get(normalized_url, headers=self.headers, timeout=30)
234
+ response.raise_for_status()
235
+ response.encoding = response.apparent_encoding or 'utf-8'
236
+
237
+ # Extraer texto limpio
238
+ soup = BeautifulSoup(response.text, 'html.parser')
239
+
240
+ # Remover elementos no deseados
241
+ for element in soup(['script', 'style', 'noscript', 'header', 'footer', 'nav']):
242
+ element.decompose()
243
+
244
+ # Extraer texto con separadores
245
+ text_content = soup.get_text(separator='\n', strip=True)
246
+
247
+ # Limpiar texto
248
+ lines = [line.strip() for line in text_content.split('\n') if line.strip()]
249
+ clean_text = '\n'.join(lines)
250
+
251
+ # Agregar metadatos
252
+ metadata = f"""URL: {normalized_url}
253
+ Fecha de extracción: {requests.utils.default_headers()['User-Agent']}
254
+ Caracteres extraídos: {len(clean_text)}
 
 
 
 
 
 
 
 
 
255
 
256
+ {'='*50}
 
 
 
 
 
 
257
 
258
+ {clean_text}"""
259
+
260
+ # Generar nombre de archivo
261
+ if not filename:
262
+ domain = urlparse(normalized_url).netloc.replace('www.', '')
263
+ filename = f"scraped_{domain.replace('.', '_')}.txt"
264
+
265
+ if not filename.endswith('.txt'):
266
+ filename += '.txt'
267
+
268
+ txt_path = os.path.join(self.output_dir, filename)
269
+
270
+ with open(txt_path, 'w', encoding='utf-8') as f:
271
+ f.write(metadata)
272
+
273
+ return {
274
+ 'status': 'success',
275
+ 'file': txt_path,
276
+ 'url': normalized_url,
277
+ 'message': f'Texto extraído exitosamente: {filename}'
278
+ }
279
+
280
+ except Exception as e:
281
+ return {
282
+ 'status': 'error',
283
+ 'message': f'Error al extraer texto: {str(e)}',
284
+ 'url': url
285
+ }
286
 
287
+ def _handle_image_to_pdf(self, url, filename):
288
+ """Maneja conversión de imagen a PDF"""
289
+ try:
290
+ response = requests.get(url, headers=self.headers, timeout=30)
291
+ response.raise_for_status()
292
+
293
+ # Crear HTML con la imagen
294
+ html_content = f"""
295
+ <html>
296
+ <head>
297
+ <style>
298
+ body {{ margin: 0; padding: 20px; text-align: center; }}
299
+ img {{ max-width: 100%; height: auto; }}
300
+ .info {{ margin-top: 20px; font-family: Arial, sans-serif; }}
301
+ </style>
302
+ </head>
303
+ <body>
304
+ <img src="{url}" alt="Imagen extraída">
305
+ <div class="info">
306
+ <p><strong>URL:</strong> {url}</p>
307
+ <p><strong>Tipo:</strong> Imagen</p>
308
+ </div>
309
+ </body>
310
+ </html>
311
+ """
312
+
313
+ if not filename:
314
+ filename = "image_scraped.pdf"
315
+
316
+ pdf_path = os.path.join(self.output_dir, filename)
317
+ HTML(string=html_content).write_pdf(pdf_path)
318
+
319
+ return {
320
+ 'status': 'success',
321
+ 'file': pdf_path,
322
+ 'url': url,
323
+ 'message': f'Imagen convertida a PDF: {filename}'
324
+ }
325
+
326
+ except Exception as e:
327
+ return {
328
+ 'status': 'error',
329
+ 'message': f'Error al procesar imagen: {str(e)}',
330
+ 'url': url
331
+ }
332
 
333
+ def _handle_image_to_text(self, url, filename):
334
+ """Maneja conversión de imagen a archivo de texto con metadatos"""
335
+ try:
336
+ response = requests.get(url, headers=self.headers, timeout=30)
337
+ response.raise_for_status()
338
+
339
+ # Obtener información de la imagen
340
+ try:
341
+ img = Image.open(io.BytesIO(response.content))
342
+ img_info = f"""IMAGEN DETECTADA
343
+ URL: {url}
344
+ Formato: {img.format}
345
+ Dimensiones: {img.size[0]}x{img.size[1]} píxeles
346
+ Modo: {img.mode}
347
+ Tamaño del archivo: {len(response.content)} bytes
348
 
349
+ Esta URL contiene una imagen, no texto extraíble.
350
+ Para procesar el contenido visual, considera usar herramientas de OCR.
351
+ """
352
+ except:
353
+ img_info = f"""IMAGEN DETECTADA
354
+ URL: {url}
355
+ Tamaño del archivo: {len(response.content)} bytes
356
 
357
+ Esta URL contiene una imagen, no texto extraíble.
358
+ """
359
+
360
+ if not filename:
361
+ filename = "image_info.txt"
362
+
363
+ txt_path = os.path.join(self.output_dir, filename)
364
+
365
+ with open(txt_path, 'w', encoding='utf-8') as f:
366
+ f.write(img_info)
367
+
368
+ return {
369
+ 'status': 'success',
370
+ 'file': txt_path,
371
+ 'url': url,
372
+ 'message': f'Información de imagen guardada: {filename}'
373
+ }
374
+
375
+ except Exception as e:
376
+ return {
377
+ 'status': 'error',
378
+ 'message': f'Error al procesar imagen: {str(e)}',
379
+ 'url': url
380
+ }