Implement web crawler with database integration and URL extraction
This commit is contained in:
Binary file not shown.
+144
@@ -0,0 +1,144 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from crawler_db_model import WebsPorVisitar, WebsVisitadas, engine
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Cargar variables de entorno
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Crear sesión de base de datos
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
def extract_urls(url):
|
||||||
|
"""Extrae todas las URLs de una página web."""
|
||||||
|
try:
|
||||||
|
headers = {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
response = requests.get(url, headers=headers, timeout=10)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return [], response.status_code
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
urls = set()
|
||||||
|
for link in soup.find_all('a', href=True):
|
||||||
|
full_url = urljoin(url, link['href'])
|
||||||
|
parsed_url = urlparse(full_url)
|
||||||
|
# Filtrar URLs inválidas o fuera de dominio
|
||||||
|
if parsed_url.scheme in ['http', 'https']:
|
||||||
|
urls.add(full_url)
|
||||||
|
|
||||||
|
return list(urls), response.status_code
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al procesar {url}: {e}")
|
||||||
|
return [], None
|
||||||
|
|
||||||
|
def hash_content(content):
|
||||||
|
"""Genera un hash SHA-256 del contenido para verificar duplicados."""
|
||||||
|
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
def generate_summary(soup):
|
||||||
|
"""Genera un resumen más detallado del contenido de la página."""
|
||||||
|
# Extraer título
|
||||||
|
titulo = soup.title.string if soup.title else "Sin título"
|
||||||
|
|
||||||
|
# Extraer meta descripción
|
||||||
|
meta_description = soup.find('meta', attrs={'name': 'description'})
|
||||||
|
descripcion = meta_description['content'] if meta_description else "No hay descripción disponible."
|
||||||
|
|
||||||
|
# Extraer encabezados principales
|
||||||
|
headers = {
|
||||||
|
'h1': [h1.get_text(strip=True) for h1 in soup.find_all('h1')],
|
||||||
|
'h2': [h2.get_text(strip=True) for h2 in soup.find_all('h2')],
|
||||||
|
'h3': [h3.get_text(strip=True) for h3 in soup.find_all('h3')]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extraer texto de párrafos principales
|
||||||
|
paragraphs = [p.get_text(strip=True) for p in soup.find_all('p')][:5]
|
||||||
|
|
||||||
|
# Resumen estructurado
|
||||||
|
summary = {
|
||||||
|
"titulo": titulo,
|
||||||
|
"descripcion": descripcion,
|
||||||
|
"encabezados": headers,
|
||||||
|
"parrafos": paragraphs
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
|
||||||
|
def process_url(url):
|
||||||
|
"""Procesa una URL: extrae datos y las inserta en las tablas correspondientes."""
|
||||||
|
try:
|
||||||
|
headers = {
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
urls, status_code = extract_urls(url)
|
||||||
|
|
||||||
|
# Obtener datos del sitio
|
||||||
|
response = requests.get(url, headers=headers, timeout=10)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
dominio = urlparse(url).netloc
|
||||||
|
summary = generate_summary(soup)
|
||||||
|
ip = requests.get(f"https://api.ipify.org?domain={dominio}&format=json").json().get('ip', "")
|
||||||
|
contenido_hash = hash_content(response.text)
|
||||||
|
|
||||||
|
# Insertar en WebsVisitadas
|
||||||
|
visitada = WebsVisitadas(
|
||||||
|
url=url,
|
||||||
|
dominio=dominio,
|
||||||
|
titulo=summary['titulo'],
|
||||||
|
resumen=f"Descripción: {summary['descripcion']}, Encabezados: {summary['encabezados']}, Párrafos: {summary['parrafos']}",
|
||||||
|
ip=ip,
|
||||||
|
codigo_http=status_code,
|
||||||
|
contenido_hash=contenido_hash
|
||||||
|
)
|
||||||
|
session.add(visitada)
|
||||||
|
|
||||||
|
# Insertar nuevas URLs en WebsPorVisitar si no están repetidas
|
||||||
|
for new_url in urls:
|
||||||
|
if not session.query(WebsPorVisitar).filter_by(url=new_url).first() and \
|
||||||
|
not session.query(WebsVisitadas).filter_by(url=new_url).first():
|
||||||
|
session.add(WebsPorVisitar(url=new_url))
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error procesando {url}: {e}")
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
def crawl():
|
||||||
|
"""Crawl principal que procesa URLs hasta agotar la lista de webs por visitar."""
|
||||||
|
while True:
|
||||||
|
# Obtener la siguiente URL a visitar
|
||||||
|
web_por_visitar = session.query(WebsPorVisitar).first()
|
||||||
|
if not web_por_visitar:
|
||||||
|
print("No hay más webs por visitar.")
|
||||||
|
break
|
||||||
|
|
||||||
|
url = web_por_visitar.url
|
||||||
|
print(f"Visitando: {url}")
|
||||||
|
|
||||||
|
# Procesar la URL y eliminarla de la lista de pendientes
|
||||||
|
process_url(url)
|
||||||
|
session.delete(web_por_visitar)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# URL inicial
|
||||||
|
url_inicial = "http://example.com"
|
||||||
|
|
||||||
|
# Insertar URL inicial si no existe
|
||||||
|
if not session.query(WebsPorVisitar).filter_by(url=url_inicial).first():
|
||||||
|
session.add(WebsPorVisitar(url=url_inicial))
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Iniciar el crawler
|
||||||
|
crawl()
|
||||||
@@ -2,6 +2,8 @@ from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, B
|
|||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
@@ -27,18 +29,17 @@ class WebsVisitadas(Base):
|
|||||||
contenido_hash = Column(String(64), nullable=True) # Hash del contenido para detectar cambios
|
contenido_hash = Column(String(64), nullable=True) # Hash del contenido para detectar cambios
|
||||||
es_dinamico = Column(Boolean, default=False) # Si es una página generada dinámicamente
|
es_dinamico = Column(Boolean, default=False) # Si es una página generada dinámicamente
|
||||||
|
|
||||||
|
# Cargar variables de entorno desde el archivo .env
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Obtener la URL de la base de datos desde las variables de entorno
|
||||||
|
database_url = os.getenv('DATABASE_URL')
|
||||||
|
|
||||||
# Crear el motor de base de datos y las tablas
|
# Crear el motor de base de datos y las tablas
|
||||||
engine = create_engine('postgresql://usuario:password@localhost:5432/nombre_bd')
|
engine = create_engine(database_url)
|
||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
# Crear una sesión
|
# Crear una sesión
|
||||||
Session = sessionmaker(bind=engine)
|
Session = sessionmaker(bind=engine)
|
||||||
session = Session()
|
session = Session()
|
||||||
|
|
||||||
# Ejemplo de cómo agregar una entrada
|
|
||||||
if __name__ == "__main__":
|
|
||||||
nueva_web_por_visitar = WebsPorVisitar(url="http://example.com")
|
|
||||||
session.add(nueva_web_por_visitar)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
print("Nueva URL agregada a 'webs_por_visitar'.")
|
|
||||||
Reference in New Issue
Block a user