ia integrada

This commit is contained in:
Your Name
2025-02-05 20:57:15 +01:00
parent e3bee454d9
commit 987e24bd00
14 changed files with 581 additions and 273 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
.venv
entorno
__pypchache__

18
Dockerfile Normal file
View File

@ -0,0 +1,18 @@
# Usa una imagen oficial de Python
FROM python:3.9
# Configurar el directorio de trabajo en el contenedor
WORKDIR /app
# Copiar los archivos de la aplicación al contenedor
COPY app/ /app/
# Instalar dependencias si es necesario (ajusta según tu requerimiento)
RUN pip install mysql-connector-python schedule
# Copiar el archivo de crontab y configurarlo
COPY crontab.txt /etc/cron.d/crontab
RUN chmod 0644 /etc/cron.d/crontab && crontab /etc/cron.d/crontab
# Iniciar cron y ejecutar el script en segundo plano
CMD cron && tail -f /var/log/cron.log

0
app/__init__.py Normal file
View File

34
app/database.py Normal file
View File

@ -0,0 +1,34 @@
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Cargar variables del archivo .env
load_dotenv()
# Configuración de MySQL
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
MYSQL_USER = os.getenv("MYSQL_USER", "root")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "manabo")
MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "noticias")
MYSQL_PORT = os.getenv("MYSQL_PORT", "3306")
DATABASE_URL = f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}"
# Crear el motor de base de datos
engine = create_engine(DATABASE_URL)
# Crear la sesión
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base para los modelos
Base = declarative_base()
# Dependencia para obtener la sesión de la BD
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

12
app/main.py Normal file
View File

@ -0,0 +1,12 @@
from fastapi import FastAPI
from .database import Base, engine
from .routes import router
# Crear las tablas en MySQL si no existen
Base.metadata.create_all(bind=engine)
# Inicializar FastAPI
app = FastAPI()
# Incluir rutas
app.include_router(router)

14
app/models.py Normal file
View File

@ -0,0 +1,14 @@
from sqlalchemy import Column, Integer, String, Text, DateTime
from datetime import datetime
from .database import Base
class NewsItem(Base):
__tablename__ = "news"
id = Column(Integer, primary_key=True, autoincrement=True) # ID autoincremental
titulo = Column(String(255), unique=True, nullable=False)
contenido = Column(Text, nullable=False)
autor = Column(String(255), nullable=True)
fuente = Column(String(255), nullable=True)
fecha = Column(DateTime, default=datetime.utcnow)
link = Column(String(500), unique=True, nullable=False)

41
app/routes.py Normal file
View File

@ -0,0 +1,41 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from .database import get_db
from .models import NewsItem
from pydantic import BaseModel
from datetime import datetime
router = APIRouter()
# Modelo de datos de entrada
class NewsItemCreate(BaseModel):
titulo: str
contenido: str
autor: str | None = None
fuente: str | None = None
fecha: datetime | None = None
link: str
@router.post("/news/")
def create_news_item(item: NewsItemCreate, db: Session = Depends(get_db)):
# Verificar si el título ya existe
existing_item = db.query(NewsItem).filter(NewsItem.titulo == item.titulo).first()
if existing_item:
raise HTTPException(status_code=400, detail="El título ya existe en la base de datos")
# Crear nuevo objeto
new_item = NewsItem(
titulo=item.titulo,
contenido=item.contenido,
autor=item.autor,
fuente=item.fuente,
fecha=item.fecha or datetime.utcnow(),
link=item.link
)
db.add(new_item)
db.commit()
db.refresh(new_item)
return {"message": "Noticia creada con éxito", "id": new_item.id, "titulo": new_item.titulo}

65
docker-compose.yml Normal file
View File

@ -0,0 +1,65 @@
version: "3.9"
services:
app:
build: .
container_name: mi_scraper
depends_on:
- mysql
environment:
- MYSQL_HOST=mysql
- MYSQL_USER=root
- MYSQL_PASSWORD=admin123
- MYSQL_DATABASE=scraper_db
volumes:
- ./app:/app
restart: always
networks:
- metanet1
mysql:
image: mysql:8
container_name: mi_mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: admin123
MYSQL_DATABASE: scraper_db
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- metanet1
metabase:
image: metabase/metabase:latest
container_name: metabase
hostname: metabase
volumes:
- /dev/urandom:/dev/random:ro
ports:
- "3000:3000"
environment:
MB_DB_TYPE: mysql
MB_DB_DBNAME: scraper_db
MB_DB_PORT: 3306
MB_DB_USER: root
MB_DB_PASS: admin123
MB_DB_HOST: mysql
networks:
- metanet1
depends_on:
- mysql
healthcheck:
test: curl --fail -I http://localhost:3000/api/health || exit 1
interval: 15s
timeout: 5s
retries: 5
networks:
metanet1:
driver: bridge
volumes:
mysql_data:

View File

@ -1,40 +1,27 @@
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import requests
import json
# Carga el modelo y el tokenizador (ajusta la ruta si es local)
modelo_nombre = "meta-llama/Llama-3-8B" # O usa un modelo local como "ruta/al/modelo"
tokenizer = AutoTokenizer.from_pretrained(modelo_nombre)
modelo = AutoModelForCausalLM.from_pretrained(modelo_nombre, torch_dtype=torch.float16, device_map="auto")
def is_security_related(prompt):
url = "http://localhost:11434/api/generate"
data = {
"model": "llama3",
"prompt": f"Does the following topic relate to national defense, armed forces, police, espionage, or intelligence? Answer only with 'true' or 'false'. Topic: {prompt}",
}
response = requests.post(url, json=data)
# Umbral de logits (ajusta según pruebas)
UMBRAL_LOGITS = -1.0
try:
# Dividir la respuesta en líneas y parsear cada una
for line in response.text.strip().split("\n"):
json_data = json.loads(line)
if "response" in json_data and json_data["response"].strip():
return json_data["response"].strip().lower() == "true"
def evaluar_seguridad_nacional(texto):
prompt = f"Evalúa si el siguiente texto está relacionado con defensa nacional, inteligencia, espionaje, fuerzas de seguridad, policía, ejército o fuerzas armadas. Responde solo con '' o 'no'.\n\nTexto: {texto}\n\nRespuesta:"
# Tokenización
inputs = tokenizer(prompt, return_tensors="pt").to(modelo.device)
# Inferencia con el modelo
with torch.no_grad():
outputs = modelo(**inputs)
# Obtener logits del último token generado
logits = outputs.logits[:, -1, :] # Última posición
# Obtener puntuaciones para "sí" y "no"
id_si = tokenizer.convert_tokens_to_ids("")
id_no = tokenizer.convert_tokens_to_ids("no")
logit_si = logits[0, id_si].item() if id_si in tokenizer.get_vocab() else -float("inf")
logit_no = logits[0, id_no].item() if id_no in tokenizer.get_vocab() else -float("inf")
# Decidir según los logits
if logit_si > logit_no and logit_si > UMBRAL_LOGITS:
return True
except json.JSONDecodeError as e:
print("JSON Decode Error:", e)
return False
# Ejemplo de uso
texto_ejemplo = "El ejército ha desplegado unidades en la frontera para proteger la soberanía nacional."
resultado = evaluar_seguridad_nacional(texto_ejemplo)
print(f"¿El texto está relacionado con seguridad nacional? {resultado}")
# Prueba con un ejemplo
prompt = "La Laguna Tenerife inició la segunda vuelta de la Liga ACB defendiendo la cuarta plaza de la competición y hoy (12.00 horas), ante el Río Breogán, ese será su objetivo: ganar y seguir estando entre los cuatro primeros y vigilando a los otros tres rivales: Real Madrid, Valencia y Unicaja-, a un solo un triunfo de distancia.\nEn el partido de hoy, el conjunto aurinegro afronta un partido complicado, pues el Río Breogán viene jugando a un buen nivel. De los cuatro últimos partidos ha ganado tres -ante el Bilbao (76-71), Coruña (83-80) y Andorra (93-101)- y han caído frente al Baskonia (97-91).\nSituados en el puesto doce, el Breogán no será un rival fácil para los aurinegros que, pese a que también están jugando bien sobre todo en casa, saben que tendrán que estar al máximo nivel para poder sacar el partido adelante.\nGanar en confianza desde la defensa y jugar con equilibrio en ataque serán dos de los aspectos que deben conseguir los aurinegros en un enfrentamiento complicado, ante un rival que va hacia arriba y que sabe también las necesidades suyas para no complicarse en la zona de farolillo.\nEl entrenador del CB Canarias,Txus Vidorreta, tendrá a todos sus efectivos y, si el partido va como se espera, podrá seguir dando minutos en las rotaciones del equipo a Jaime Fernández, que ha podido entrenar.\nA dos jornadas de la disputa de la Copa del Rey en Gran Canaria, La Laguna Tenerife quiere irse preparando para esa cita con un primer enfrentamiento, en cuartos de final, contra el FC Barcelona.\nHoy será uno de esos “ensayos” importantes para saber cómo se encuentra el equipo para esa cita en la isla vecina, aunque ahora mismo el objetivo sigue estándo en la defensa de ese cuarto lugar de la Liga Endesa.\nUn desafío ante el cuadro de Luis Casimiro que necesitará igualmente del siempre entregado público insular, después de que la afición canarista confirmara desde el lunes pasado un nuevo sold out, el vigésimo primero consecutivo en partidos ACB entre el final de la campaña pasada y el vigente curso."
print(is_security_related(prompt))

File diff suppressed because one or more lines are too long

93
scrapper/autorsearcher.py Normal file
View File

@ -0,0 +1,93 @@
import json
import requests
import sys
from bs4 import BeautifulSoup
def download_html_as_human(url):
"""
Descarga el HTML de una página web simulando un navegador real y usando cookies de sesión.
"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
}
session = requests.Session()
response = session.get(url, headers=headers)
if response.status_code == 200:
return response.text
else:
return None
def extract_author_from_json(json_data):
"""
Extrae el autor del JSON-LD, incluso si está en una lista.
"""
if isinstance(json_data, list):
for item in json_data:
author = extract_author_from_json(item)
if author:
return author
elif isinstance(json_data, dict):
if 'author' in json_data:
author_data = json_data['author']
if isinstance(author_data, list):
for author in author_data:
if isinstance(author, dict) and 'name' in author:
return author['name']
elif isinstance(author_data, dict) and 'name' in author_data:
return author_data['name']
return None
def get_author_from_json_ld(soup):
"""
Extrae el autor de los metadatos JSON-LD, considerando estructuras con listas y objetos.
"""
scripts = soup.find_all('script', type='application/ld+json')
for script in scripts:
try:
json_data = json.loads(script.string)
author = extract_author_from_json(json_data)
if author:
return author
except json.JSONDecodeError:
continue
return None
def get_author_from_meta(soup):
"""
Extrae el autor de la etiqueta <meta> con el atributo property="nrbi:authors".
"""
meta_author = soup.find('meta', property='nrbi:authors')
if meta_author and 'content' in meta_author.attrs:
return meta_author['content']
return None
def get_author_from_url(url):
"""
Busca el autor en los metadatos JSON-LD y en la etiqueta <meta> de una URL.
"""
html_content = download_html_as_human(url)
if not html_content:
print("error")
return "No se pudo descargar la página."
soup = BeautifulSoup(html_content, 'html.parser')
author = get_author_from_json_ld(soup)
if author:
return author
author = get_author_from_meta(soup)
if author:
return author
return "Autor no encontrado en los metadatos."
if __name__ == "__main__":
if len(sys.argv) > 1:
url = sys.argv[1]
print(get_author_from_url(url))
else:
print("Uso: python autorsearcher.py <URL>")

27
scrapper/iacorrector.py Normal file
View File

@ -0,0 +1,27 @@
import requests
import json
def is_security_related(prompt):
url = "http://localhost:11434/api/generate"
data = {
"model": "llama3",
"prompt": f"Does the following topic relate to national defense, armed forces, police, espionage, or intelligence? Answer only with 'true' or 'false'. Topic: {prompt}",
}
response = requests.post(url, json=data)
try:
# Dividir la respuesta en líneas y parsear cada una
for line in response.text.strip().split("\n"):
json_data = json.loads(line)
if "response" in json_data and json_data["response"].strip():
return json_data["response"].strip().lower() == "true"
except json.JSONDecodeError as e:
print("JSON Decode Error:", e)
return False
# Prueba con un ejemplo
prompt = "La Laguna Tenerife inició la segunda vuelta de la Liga ACB defendiendo la cuarta plaza de la competición y hoy (12.00 horas), ante el Río Breogán, ese será su objetivo: ganar y seguir estando entre los cuatro primeros y vigilando a los otros tres rivales: Real Madrid, Valencia y Unicaja-, a un solo un triunfo de distancia.\nEn el partido de hoy, el conjunto aurinegro afronta un partido complicado, pues el Río Breogán viene jugando a un buen nivel. De los cuatro últimos partidos ha ganado tres -ante el Bilbao (76-71), Coruña (83-80) y Andorra (93-101)- y han caído frente al Baskonia (97-91).\nSituados en el puesto doce, el Breogán no será un rival fácil para los aurinegros que, pese a que también están jugando bien sobre todo en casa, saben que tendrán que estar al máximo nivel para poder sacar el partido adelante.\nGanar en confianza desde la defensa y jugar con equilibrio en ataque serán dos de los aspectos que deben conseguir los aurinegros en un enfrentamiento complicado, ante un rival que va hacia arriba y que sabe también las necesidades suyas para no complicarse en la zona de farolillo.\nEl entrenador del CB Canarias,Txus Vidorreta, tendrá a todos sus efectivos y, si el partido va como se espera, podrá seguir dando minutos en las rotaciones del equipo a Jaime Fernández, que ha podido entrenar.\nA dos jornadas de la disputa de la Copa del Rey en Gran Canaria, La Laguna Tenerife quiere irse preparando para esa cita con un primer enfrentamiento, en cuartos de final, contra el FC Barcelona.\nHoy será uno de esos “ensayos” importantes para saber cómo se encuentra el equipo para esa cita en la isla vecina, aunque ahora mismo el objetivo sigue estándo en la defensa de ese cuarto lugar de la Liga Endesa.\nUn desafío ante el cuadro de Luis Casimiro que necesitará igualmente del siempre entregado público insular, después de que la afición canarista confirmara desde el lunes pasado un nuevo sold out, el vigésimo primero consecutivo en partidos ACB entre el final de la campaña pasada y el vigente curso."
print(is_security_related(prompt))

165
scrapper/webscrapper.py Normal file
View File

@ -0,0 +1,165 @@
import requests
from bs4 import BeautifulSoup
import json
import os
import time
import subprocess
from googlenewsdecoder import gnewsdecoder
from iacorrector import is_security_related # Importa la función desde iacorrector.py
from datetime import datetime
import pytz
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"
}
def get_author_from_script(url):
"""
Llama a autorsearcher.py con la URL de la noticia y devuelve el autor encontrado.
"""
try:
result = subprocess.run(["python", "autorsearcher.py", url], capture_output=True, text=True)
author = result.stdout.strip()
return author if author else "Desconocido"
except Exception as e:
print(f"Error al obtener el autor para {url}: {e}")
return "Desconocido"
def get_url_from_google_news(url):
interval_time = 1
try:
decoded_url = gnewsdecoder(url, interval=interval_time)
if decoded_url.get("status"):
return decoded_url["decoded_url"]
else:
return "N/C"
except Exception as e:
print(f"Error occurred: {e}")
def get_article_content(url):
"""
Extrae el texto principal del artículo desde la URL final.
"""
try:
response = requests.get(url, headers=HEADERS)
if response.status_code != 200:
print(f"Error al acceder a {url}: Código {response.status_code}")
return "No se pudo obtener el contenido"
soup = BeautifulSoup(response.text, "html.parser")
# Buscar los elementos más comunes donde se almacena el contenido del artículo
possible_containers = [
soup.find("article"), # Etiqueta <article> (común en blogs y periódicos)
soup.find("div", class_="post-content"), # Clases comunes en WordPress
soup.find("div", class_="entry-content"),
soup.find("div", class_="content"),
soup.find("div", id="article-body")
]
for container in possible_containers:
if container:
paragraphs = container.find_all("p")
article_text = "\n".join(p.get_text(strip=True) for p in paragraphs)
return article_text if article_text else "No se encontró contenido relevante"
return "No se encontró contenido relevante"
except Exception as e:
print(f"Error al extraer contenido de {url}: {e}")
return "Error al extraer contenido"
def search_news(query):
"""
Busca noticias relacionadas con una palabra clave en Google News.
"""
base_url = f"https://news.google.com/rss/search?q={query.replace(' ', '+')}&hl=es&gl=ES&ceid=ES%3Aes"
response = requests.get(base_url, headers=HEADERS)
if response.status_code != 200:
print(f"Error al acceder a la página para la consulta '{query}': {response.status_code}")
return []
soup = BeautifulSoup(response.content, 'xml')
articles = soup.find_all("item")
news_list = []
for article in articles[:30]: # Limitar a los primeros 30 artículos
try:
title = article.title.get_text(strip=True)
content = article.description.get_text(strip=True) if article.description else "Sin descripción"
link = article.link.get_text(strip=True)
source_info = article.source.get_text(strip=True) if article.source else "Desconocido"
date = article.pubDate.get_text(strip=True) if article.pubDate else "Fecha no disponible"
date_parsed = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S GMT')
date_parsed = date_parsed.replace(tzinfo=pytz.UTC)
# Obtener la URL final del artículo
final_url = get_url_from_google_news(link)
# Obtener el autor usando autorsearcher.py
author = get_author_from_script(final_url)
content = get_article_content(final_url)
# Verificar si el artículo es válido usando iacorrector
if is_security_related(content): # Solo si el artículo es válido
news_item = {
"titulo": title,
"contenido": content,
"autor": author,
"fuente": source_info,
"fecha": date_parsed.isoformat(),
"link": final_url # Guardamos la URL final en lugar de la de Google News
}
news_list.append(news_item)
except Exception as e:
print(f"Error al procesar un artículo para '{query}': {e}")
return news_list
def insertar_datos(news_item):
API_URL = "http://127.0.0.1:8001/news/"
response = requests.post(API_URL, json=news_item)
if response.status_code == 200:
print(f"Noticia '{news_item['titulo']}' creada con éxito.")
else:
print(f"Error al insertar '{news_item['titulo']}':", response.status_code, response.json())
def search_from_keywords_file():
"""
Lee palabras clave del archivo 'keywords.txt' y realiza búsquedas para cada una.
"""
all_news = [] # Lista para almacenar todas las noticias recolectadas
try:
with open("keywords.txt", "r", encoding="utf-8") as file:
keywords = file.readlines()
# Eliminar posibles saltos de línea y espacios extra
keywords = [keyword.strip() for keyword in keywords]
for keyword in keywords:
print(f"\nBuscando noticias sobre: {keyword}")
news_list = search_news(keyword)
all_news.extend(news_list) # Añadir las noticias encontradas para cada palabra clave
time.sleep(2) # Pausa para evitar bloqueos por demasiadas solicitudes en poco tiempo
# Guardar todas las noticias en un archivo JSON
for news in all_news:
insertar_datos(news)
except FileNotFoundError:
print("No se encontró el archivo 'keywords.txt'.")
except Exception as e:
print(f"Error al leer el archivo 'keywords.txt': {e}")
# Ejecutar la búsqueda desde el archivo
search_from_keywords_file()

View File

@ -5,6 +5,7 @@ import os
import time
import subprocess
from googlenewsdecoder import gnewsdecoder
from iacorrector import is_security_related # Importa la función desde iacorrector.py
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"
@ -90,23 +91,25 @@ def search_news(query):
source_info = article.source.get_text(strip=True) if article.source else "Desconocido"
date = article.pubDate.get_text(strip=True) if article.pubDate else "Fecha no disponible"
# Obtener la URL final del artículo
final_url = get_url_from_google_news(link)
# Obtener el autor usando autorsearcher.py
author = get_author_from_script(final_url)
content = get_article_content(final_url)
news_item = {
"titulo": title,
"contenido": content,
"autor": author,
"fuente": source_info,
"fecha": date,
"link": final_url # Guardamos la URL final en lugar de la de Google News
}
news_list.append(news_item)
# Verificar si el artículo es válido usando iacorrector
if is_security_related(content): # Solo si el artículo es válido
news_item = {
"titulo": title,
"contenido": content,
"autor": author,
"fuente": source_info,
"fecha": date,
"link": final_url # Guardamos la URL final en lugar de la de Google News
}
news_list.append(news_item)
except Exception as e:
print(f"Error al procesar un artículo para '{query}': {e}")