- docs/ → INFO/DOCS/CONTEXT/ (documentación técnica en markdown) - FLUJOS/DOCS/ + FLUJOS_DATOS/DOCS/ → INFO/DOCS/ (txts de arquitectura) - POCS/ → INFO/POCS/ (pruebas de concepto) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.9 KiB
Scraper de Noticias — Contexto técnico FLUJOS
Fecha: 2026-04-21
Archivo: FLUJOS_DATOS/NOTICIAS/main_noticias.py
Entorno: FLUJOS_DATOS/myenv/ (Python 3.11, venv)
Qué hace
Scraper web recursivo que:
- Visita ~90 URLs de medios de comunicación internacionales
- Explora sus páginas recursivamente hasta profundidad 6
- Descarga artículos (texto) y ficheros adjuntos (PDF, CSV, DOCX, XLSX, ZIP)
- Traduce a español si el contenido está en otro idioma
- Limpia y tokeniza con BERT
- Guarda en disco y MongoDB (
noticias)
Lista de fuentes (90 medios)
urls = [
# Bases de datos de investigación
'https://reactionary.international/database/',
'https://aleph.occrp.org/', # OCCRP — periodismo de investigación
'https://offshoreleaks.icij.org/', # ICIJ — paraísos fiscales
# Prensa española
'https://www.publico.es/', 'https://www.elsaltodiario.com/',
'https://elpais.com/', 'https://www.elmundo.es/', 'https://www.abc.es/',
'https://www.lavanguardia.com/', 'https://www.elconfidencial.com/',
'https://www.eldiario.es/', 'https://www.rtve.es/', ...
# Prensa internacional
'https://www.nytimes.com/', 'https://www.theguardian.com/',
'https://www.lemonde.fr/', 'https://www.spiegel.de/',
'https://www.washingtonpost.com/', 'https://www.aljazeera.com/',
'https://www.bbc.com/', 'https://www.reuters.com/',
'https://www.ft.com/', 'https://www.economist.com/', ...
# Prensa tech / seguridad
'https://www.wired.com/', 'https://www.theregister.com/',
'https://www.arstechnica.com/', 'https://www.zdnet.com/',
'https://www.cyberdefensemagazine.com/', 'https://www.darkreading.com/', ...
]
Total: ~90 URLs seed. Cada una se explora recursivamente hasta 6 niveles de profundidad.
Flujo de scraping recursivo
def explore_and_extract_articles(url, articles_folder, files_folder,
processed_urls, size_limit, depth=0, max_depth=6):
para cada URL seed:
explore_and_extract_articles(url, depth=0, max_depth=6)
└── HTMLSession.get(url).html.render() # ejecuta JavaScript con Chromium headless
para cada link encontrado:
if link ya procesado: skip
processed_urls.add(link)
if extensión es PDF/CSV/DOCX/XLSX/ZIP/HTML/MD:
download_and_save_file(link, files_folder)
else:
extract_and_save_article(link, articles_folder)
explore_and_extract_articles(link, depth+1) # recursivo
if tamaño total > 50 GB: parar
explore_wayback_machine(url, articles_folder) # fallback Wayback Machine
Renderizado JavaScript
Usa requests-html con Chromium headless (Pyppeteer) para renderizar páginas que cargan contenido con JavaScript. Esto permite scraping de medios que usan SPA/React.
session = HTMLSession()
response = session.get(url, timeout=30)
response.html.render(timeout=30, sleep=1) # espera 1s a que cargue el JS
links = response.html.absolute_links
Extracción y limpieza de artículos
def extract_and_save_article(url, articles_folder):
response = requests.get(url, timeout=30)
soup = BeautifulSoup(response.content, 'html.parser')
title = soup.find('title').get_text().strip()
paragraphs = soup.find_all('p')
content = ' '.join([p.get_text() for p in paragraphs])
translated = translate_text(content) # → español
cleaned = clean_text(translated) # → limpieza + stopwords
filename = clean_filename(title) + '.txt'
guardar en articles_folder/filename
Traducción automática
from deep_translator import GoogleTranslator
def translate_text(text):
return GoogleTranslator(source='auto', target='es').translate(text)
Usa Google Translate vía deep-translator. Detecta idioma automáticamente. Fallo → devuelve el texto original sin traducir.
Limpieza de texto
def clean_text(text):
text = re.sub(r'<!\[\s*CDATA\s*\[.*?\]\]>', '', text, flags=re.S) # CDATA
soup = BeautifulSoup(text, 'html.parser')
text = soup.get_text(separator=" ") # HTML → texto plano
text = text.lower()
text = re.sub(r'http\S+', '', text) # elimina URLs
text = re.sub(r'[^a-záéíóúñü\s]', '', text) # solo letras + espacios
text = re.sub(r'\s+', ' ', text).strip()
words = [w for w in text.split() if w not in STOPWORDS]
return ' '.join(words)
Procesamiento de ficheros descargados
def process_files(files_folder, destination_folder):
for file in os.walk(files_folder):
if .pdf: content = read_pdf(file_path) # PyPDF2
elif .csv: content = read_csv(file_path) # csv.reader
elif .txt: content = open(file_path).read()
elif .docx: content = read_docx(file_path) # python-docx
elif .xlsx: content = read_xlsx(file_path) # openpyxl
elif .zip: content = read_zip(file_path) # zipfile
elif .html/.md: content = format_content(html2text)
translated = translate_text(content)
cleaned = clean_text(translated)
tokenize_and_save(cleaned, file, destination_folder)
Tokenización BERT
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('dccuchile/bert-base-spanish-wwm-cased')
def tokenize_and_save(text, filename, destination_folder):
tokens = tokenizer.encode(text, truncation=True, max_length=512, add_special_tokens=True)
tokens_str = ' '.join(map(str, tokens))
open(f'{destination_folder}/{filename}', 'w').write(tokens_str)
Mismo modelo BERT en español que el scraper de Wikipedia. Trunca a 512 tokens.
Deduplicación por URL
def register_processed_notifications(base_folder, urls):
"""Lee/escribe processed_articles.txt para evitar re-procesar URLs."""
txt_path = os.path.join(base_folder, "processed_articles.txt")
processed_urls = set(open(txt_path).read().splitlines())
urls_to_process = [u for u in urls if u not in processed_urls]
# Añade nuevas URLs al fichero
return urls_to_process
Las URLs ya procesadas se guardan en NOTICIAS/processed_articles.txt. Esto es la deduplicación a nivel de seed URL, pero NO previene artículos duplicados desde diferentes URLs.
Límites configurados
FOLDER_SIZE_LIMIT = 50 * 1024 * 1024 * 1024 # 50 GB máximo en disco
max_depth = 6 # profundidad recursiva máxima
Estructura de ficheros en disco (ignorada por git)
FLUJOS_DATOS/NOTICIAS/
├── articulos/ # .gitignore — .txt por artículo scrapeado
├── archivos/ # .gitignore — PDF, CSV, DOCX, etc. descargados
├── tokenized/ # .gitignore — IDs BERT por documento
├── processed_articles.txt # .gitignore — URLs ya procesadas
├── noticias_procesadas.txt # .gitignore
├── main_noticias.py
└── docs.txt
Documento MongoDB generado (colección noticias)
La inserción a MongoDB la hace pipeline_mongolo.py (Fase 2), no el scraper directamente. El scraper solo guarda en disco.
{
"_id": ObjectId,
"archivo": "titulo-de-la-noticia.txt",
"tema": "guerra global",
"subtema": "conflictos internacionales",
"texto": "texto limpio de la noticia...",
"fecha": ISODate | null
}
Wayback Machine como fallback
def explore_wayback_machine(url, articles_folder):
api_url = f"http://archive.org/wayback/available?url={url}"
data = requests.get(api_url).json()
archive_url = data['archived_snapshots']['closest']['url']
extract_and_save_article(archive_url, articles_folder)
Si un medio está caído o bloquea el scraper, intenta obtener la versión más reciente desde archive.org.
Limitaciones conocidas
- Renderizado headless lento —
requests-htmlusa Pyppeteer/Chromium, ~2–5 seg/página. Escalar a 20.000 artículos cuesta horas. - Sin respeto a robots.txt — El scraper no verifica robots.txt. Algunos medios bloquean scraping.
- Paywall — Medios como FT, NYT, WSJ bloquean sin suscripción. El scraper solo obtiene lo que es público.
- Traducción de textos largos —
deep-translatortiene límite de ~5.000 chars por llamada. Textos largos pueden fallar silenciosamente. - Sin fecha de publicación — Se parsea el título HTML, no los metadatos
<meta property="article:published_time">. El campofechasuele quedar vacío. - Recursión sin límite de anchura — Una página con 1.000 links genera 1.000 llamadas recursivas. Puede tardar mucho en sitios grandes.
Dependencias
requests
requests-html # HTMLSession + Pyppeteer
beautifulsoup4
html2text
deep-translator # Google Translate API no oficial
PyPDF2
python-docx
openpyxl
transformers # BertTokenizer
tqdm
pymongo