FLUJOS/INFO/DOCS/CONTEXT/SCRAPER_NOTICIAS.md
CAPITANSITO 954f47996f refactor: reorganizar docs y pocs bajo INFO/
- 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>
2026-04-21 23:49:33 +02:00

270 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
1. Visita ~90 URLs de medios de comunicación internacionales
2. Explora sus páginas recursivamente hasta profundidad 6
3. Descarga artículos (texto) y ficheros adjuntos (PDF, CSV, DOCX, XLSX, ZIP)
4. Traduce a español si el contenido está en otro idioma
5. Limpia y tokeniza con BERT
6. Guarda en disco y MongoDB (`noticias`)
---
## Lista de fuentes (90 medios)
```python
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
```python
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.
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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
```python
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.
```json
{
"_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
```python
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
1. **Renderizado headless lento**`requests-html` usa Pyppeteer/Chromium, ~25 seg/página. Escalar a 20.000 artículos cuesta horas.
2. **Sin respeto a robots.txt** — El scraper no verifica robots.txt. Algunos medios bloquean scraping.
3. **Paywall** — Medios como FT, NYT, WSJ bloquean sin suscripción. El scraper solo obtiene lo que es público.
4. **Traducción de textos largos**`deep-translator` tiene límite de ~5.000 chars por llamada. Textos largos pueden fallar silenciosamente.
5. **Sin fecha de publicación** — Se parsea el título HTML, no los metadatos `<meta property="article:published_time">`. El campo `fecha` suele quedar vacío.
6. **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
```