
Uno de los atajos más eficaces para pasar del texto bruto al valor semántico consiste en detectar automáticamente las entidades (personas, lugares, organizaciones, productos…) que contiene cada página. Google alimenta su Knowledge Graph con esos mismos nodos y recompensa los contenidos que los mencionan de forma coherente; por eso un auditor SEO que ignore las entidades vuela a ciegas.
¿Por qué las entidades son gasolina para el SEO semántico?
Google declaró hace años que sus sistemas de ranking están basados en entender entidades y sus relaciones, más que en simples palabras clave . Estudios prácticos muestran que conectar tu contenido con el Knowledge Graph aumenta la visibilidad de marca y la relevancia temática . Además, aprovechar esas entidades para crear enlaces internos refuerza la autoridad de cada clúster de contenido .
En términos operativos, un flujo de auditoría típico incluye:
- Rastrear o cargar las URLs objeto de estudio.
- Aplicar NER (Reconocimiento de Entidades Nombradas) con un modelo estadístico o reglas.
- Comparar listas de entidades propias con las de competidores o con un “golden set” sectorial.
- Detectar huecos y proponer mejoras de copy o de enlazado interno.
Prompt #1 — Auditor SEO de entidades con spaCy
Prompt:
Eres un Ingeniero de Contenidos especializado en procesamiento de texto y NLP en español. Genera un único script Python que cumpla los siguientes requisitos:
1. **Entrada de datos**
- Reciba como argumento de línea de comandos la ruta a un fichero de texto (`urls.txt`) que contenga una URL por línea.
- Compruebe que el fichero existe y sea legible; en caso contrario, muestre un mensaje de error claro y termine la ejecución.
2. **Descarga y extracción de texto**
- Para cada URL, haga una petición HTTP segura (con tiempo de espera configurable y gestión de errores de conexión o código de estado HTTP distinto de 200).
- Analice el HTML con **BeautifulSoup** y extraiga todo el texto de los elementos `<p>`.
- Ignore contenidos duplicados y vacíos.
3. **Análisis con spaCy**
- Cargue el modelo `es_core_news_md`.
- Procese en batch los textos extraídos para mejorar rendimiento.
- Detecte entidades de los tipos **PERSON**, **ORG**, **GPE**, **PRODUCT** y **DATE**.
4. **Cálculo de frecuencias**
- Cuente cuántas veces aparece cada entidad (texto normalizado, sin distinguir mayúsculas/minúsculas).
- Genere una lista ordenada (de mayor a menor frecuencia) agrupada por tipo de entidad.
5. **Exportación de resultados**
- Cree un fichero `entities.csv` con las columnas:
```
entity,type,frequency
```
- Asegúrese de que el CSV sea compatible con Excel/Udemy (codificación UTF-8, sin índice).
6. **Calidad de código**
- Incluya comentarios y docstrings en formato numpydoc para funciones y clases.
- Use manejo de excepciones para todos los puntos de fallo (I/O, red, NLP).
- Aplique buenas prácticas de rendimiento (uso de sesiones HTTP de `requests`, procesamiento en lote de spaCy).
- Añada un bloque `if __name__ == "__main__":` para la ejecución como script.
- Incluya al inicio un bloque de importaciones claro y, opcionalmente, un fichero `requirements.txt` comentado.
Entrega solo el contenido del script Python (un único archivo) listo para ejecutarse en un entorno con Python 3.8+.
Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de línea de comandos para extraer entidades nombradas de textos en URLs.
Requisitos:
1. Recibe como argumento la ruta a un fichero de texto con una URL por línea.
2. Comprueba existencia y legibilidad del fichero.
3. Descarga seguro de cada URL y extracción de texto de <p> con BeautifulSoup.
4. Procesamiento en batch con spaCy (es_core_news_md) para detectar PERSON, ORG, GPE, PRODUCT, DATE.
5. Conteo de frecuencias, normalización a minúsculas.
6. Exportación a entidades.csv (UTF-8, sin índice).
7. Buenas prácticas: sesiones HTTP, manejo de excepciones, comentarios y docstrings en numpydoc.
"""
import sys
import os
import logging
import csv
from pathlib import Path
from typing import List, Dict, Tuple
import requests
from bs4 import BeautifulSoup
import spacy
from spacy.tokens import Doc
# Configuración de logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
def load_urls(file_path: str) -> List[str]:
"""
Carga las URLs desde un fichero de texto.
Parameters
----------
file_path : str
Ruta al fichero de texto que contiene una URL por línea.
Returns
-------
List[str]
Lista de URLs limpias.
Raises
------
FileNotFoundError
Si el fichero no existe.
PermissionError
Si el fichero no es legible.
"""
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"El fichero no existe: {file_path}")
if not os.access(path, os.R_OK):
raise PermissionError(f"No se puede leer el fichero: {file_path}")
with path.open("r", encoding="utf-8") as f:
urls = [line.strip() for line in f if line.strip()]
return urls
def fetch_text_from_url(
url: str,
session: requests.Session,
timeout: Tuple[int, int] = (5, 10)
) -> str:
"""
Descarga el contenido de una URL y extrae el texto de los elementos <p>.
Parameters
----------
url : str
URL a procesar.
session : requests.Session
Sesión HTTP para mejorar rendimiento.
timeout : Tuple[int, int]
Timeout para la conexión y la lectura en segundos.
Returns
-------
str
Texto concatenado de todos los párrafos.
Raises
------
requests.RequestException
En caso de errores de conexión o HTTP status != 200.
"""
try:
response = session.get(url, timeout=timeout)
response.raise_for_status()
except requests.RequestException as e:
logging.error(f"Error al descargar {url}: {e}")
return ""
soup = BeautifulSoup(response.text, "html.parser")
paragraphs = soup.find_all("p")
texts = [p.get_text(strip=True) for p in paragraphs if p.get_text(strip=True)]
# Eliminar duplicados manteniendo orden
seen = set()
unique_texts = []
for txt in texts:
if txt not in seen:
seen.add(txt)
unique_texts.append(txt)
return "\n".join(unique_texts)
def extract_entities(
texts: List[str],
nlp_model: spacy.language.Language,
batch_size: int = 20
) -> List[spacy.tokens.Span]:
"""
Procesa en batch los textos y extrae entidades nombradas del tipo
PERSON, ORG, GPE, PRODUCT y DATE.
Parameters
----------
texts : List[str]
Lista de textos a procesar.
nlp_model : spacy.language.Language
Modelo spaCy cargado.
batch_size : int
Tamaño de lote para procesar.
Returns
-------
List[spacy.tokens.Span]
Lista de spans con las entidades encontradas.
"""
entities = []
for doc in nlp_model.pipe(texts, batch_size=batch_size):
for ent in doc.ents:
if ent.label_ in {"PERSON", "ORG", "GPE", "PRODUCT", "DATE"}:
entities.append(ent)
return entities
def count_entity_frequencies(
entities: List[spacy.tokens.Span]
) -> List[Tuple[str, str, int]]:
"""
Cuenta la frecuencia de cada entidad, normalizando a minúsculas.
Parameters
----------
entities : List[spacy.tokens.Span]
Lista de spans con entidades.
Returns
-------
List[Tuple[str, str, int]]
Lista de tuplas (entidad, tipo, frecuencia) ordenada por frecuencia descendente.
"""
freq: Dict[Tuple[str, str], int] = {}
for ent in entities:
key = (ent.text.lower(), ent.label_)
freq[key] = freq.get(key, 0) + 1
# Ordenar por frecuencia descendente
sorted_items = sorted(
[(text, label, count) for (text, label), count in freq.items()],
key=lambda x: x[2],
reverse=True
)
return sorted_items
def save_to_csv(
data: List[Tuple[str, str, int]],
output_file: str = "entities.csv"
) -> None:
"""
Guarda las entidades y sus frecuencias en un CSV compatible con Excel/Udemy.
Parameters
----------
data : List[Tuple[str, str, int]]
Lista de tuplas (entidad, tipo, frecuencia).
output_file : str
Nombre del fichero de salida CSV.
"""
try:
with open(output_file, "w", encoding="utf-8", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["entity", "type", "frequency"])
for entity, ent_type, freq in data:
writer.writerow([entity, ent_type, freq])
logging.info(f"Resultados guardados en {output_file}")
except Exception as e:
logging.error(f"No se pudo escribir el fichero CSV: {e}")
def main():
"""
Función principal del script.
"""
if len(sys.argv) != 2:
logging.error("Uso: python script.py <ruta_urls.txt>")
sys.exit(1)
urls_file = sys.argv[1]
try:
urls = load_urls(urls_file)
except (FileNotFoundError, PermissionError) as e:
logging.error(e)
sys.exit(1)
# Inicializar sesión HTTP y modelo spaCy
session = requests.Session()
try:
nlp = spacy.load("es_core_news_md")
except Exception as e:
logging.error(f"Error cargando modelo spaCy: {e}")
sys.exit(1)
# Descargar y extraer textos
all_texts = []
for url in urls:
text = fetch_text_from_url(url, session)
if text:
all_texts.append(text)
if not all_texts:
logging.warning("No se extrajo texto de ninguna URL válida.")
sys.exit(0)
# Extraer entidades y contar frecuencias
entities = extract_entities(all_texts, nlp, batch_size=20)
freq_data = count_entity_frequencies(entities)
# Guardar resultados
save_to_csv(freq_data, output_file="entities.csv")
if __name__ == "__main__":
main()
# requirements.txt (opcional):
# ----------------------------
# beautifulsoup4>=4.9.3
# requests>=2.24.0
# spacy>=3.0.0
# es-core-news-md @ https://github.com/explosion/spacy-models/releases/download/es_core_news_md-3.3.0/es_core_news_md-3.3.0-py3-none-any.whl
Cómo usarlo
- Instala dependencias: pip install spacy beautifulsoup4 requests && python -m spacy download es_core_news_md.
- El modelo español de spaCy alcanza ≈ 88 F-score en CoNLL-03 tras su última revisión de 2024 .
- El CSV resultante permite filtrar rápidamente páginas que no mencionan entidades clave de tu topic cluster.
Prompt #2 — Auditor comparativo con Transformers (Hugging Face)
Prompt:
Eres un analista de competencia y tu tarea es generar un script Python de un solo fichero, llamado `analisis_competencia.py`, que cumpla estos requisitos:
1. **Entorno y dependencias**
- Python 3.8+
- Paquetes: `transformers`, `torch` (o `tensorflow`), `tqdm`, `click`, `logging`, `pandas`
- Incluye instrucciones de instalación en el encabezado o en un bloque `requirements.txt`
2. **Carga del modelo**
- Utiliza `transformers.pipeline` para cargar `Babelscape/wikineural-multilingual-ner`
- Carga el modelo una sola vez al inicio y reutilízalo
3. **Interfaz de línea de comandos**
- Emplea `click` para aceptar dos parámetros obligatorios:
- `--mios`: ruta al directorio de tus textos
- `--competencia`: ruta al directorio de los textos de la competencia
- Opción opcional `--salida`: ruta de un fichero CSV/JSON de resultados (por defecto `brecha_entidades.csv`)
4. **Procesamiento de archivos**
- Recorre recursivamente ambos directorios buscando archivos de texto plano (`.txt`)
- Lee cada archivo con control de codificación y captura errores de lectura
5. **Extracción y conteo de entidades**
- Para cada conjunto (`mios` y `competencia`) extrae entidades con el pipeline NER
- Normaliza nombres de entidades (por ejemplo, convertir a minúsculas y strip)
- Cuenta frecuencias por entidad usando `collections.Counter` o `pandas.Series`
6. **Cálculo de la brecha**
- Identifica entidades que aparecen **al menos 3 veces** en `competencia` y **0 veces** en `mios`
- Guarda la lista de estas entidades junto con sus frecuencias en competencia
7. **Salida de resultados**
- Genera un fichero CSV (o JSON si se solicita) con columnas: `entidad`, `freq_competencia`
- Imprime por pantalla un resumen: total de entidades en cada grupo y número de brechas encontradas
8. **Calidad del código**
- Añade docstrings (funciones y módulo) y typing (`def foo(...) -> dict:`)
- Estructura modular: funciones como `cargar_modelo()`, `procesar_directorio()`, `calcular_brecha()`
- Usa `if __name__ == "__main__": main()`
- Implementa logging con distintos niveles (`INFO`, `ERROR`) y barra de progreso con `tqdm`
9. **Robustez y buenas prácticas**
- Validación de rutas de directorio y existencia de archivos
- Manejo de excepciones para fallos en el modelo y en lectura de ficheros
- Cierre seguro de recursos
- Comentarios breves donde aporte claridad a secciones críticas
Devuelve **solo el código** completo de `analisis_competencia.py`, sin explicaciones adicionales.
Script:
#!/usr/bin/env python3
"""
analisis_competencia.py
Requisitos de instalación:
# Crear virtualenv e instalar paquetes:
pip install transformers torch tqdm click pandas
O usar un fichero requirements.txt:
transformers
torch
tqdm
click
pandas
"""
import logging
from pathlib import Path
from typing import List, Tuple
import click
from transformers import pipeline, Pipeline
from tqdm import tqdm
import pandas as pd
from collections import Counter
def cargar_modelo() -> Pipeline:
"""Carga el modelo NER Babelscape/wikineural-multilingual-ner."""
try:
ner_pipeline = pipeline(
"ner",
model="Babelscape/wikineural-multilingual-ner",
tokenizer="Babelscape/wikineural-multilingual-ner",
grouped_entities=True
)
logging.info("Modelo NER cargado correctamente")
return ner_pipeline
except Exception as e:
logging.error(f"Error al cargar el modelo: {e}")
raise
def procesar_directorio(dir_path: Path, ner_pipeline: Pipeline) -> Counter:
"""Procesa archivos .txt en un directorio y retorna contador de entidades."""
entity_counter: Counter = Counter()
if not dir_path.is_dir():
logging.error(f"Directorio no existe: {dir_path}")
return entity_counter
txt_files = list(dir_path.rglob("*.txt"))
for file in tqdm(txt_files, desc=f"Procesando {dir_path}"):
try:
text = file.read_text(encoding="utf-8")
except UnicodeDecodeError:
try:
text = file.read_text(encoding="latin-1")
except Exception as e:
logging.error(f"No se pudo leer {file}: {e}")
continue
try:
ner_results = ner_pipeline(text)
for ent in ner_results:
name = ent["word"].lower().strip()
entity_counter[name] += 1
except Exception as e:
logging.error(f"Error NER en {file}: {e}")
continue
return entity_counter
def calcular_brecha(
mios: Counter,
competencia: Counter,
umbral: int = 3
) -> List[Tuple[str, int]]:
"""Calcula brecha: freq>=umbral en competencia y 0 en mios."""
brechas: List[Tuple[str, int]] = []
for ent, freq in competencia.items():
if freq >= umbral and mios.get(ent, 0) == 0:
brechas.append((ent, freq))
return brechas
@click.command()
@click.option(
"--mios",
required=True,
type=click.Path(exists=True, file_okay=False),
help="Directorio de mis textos"
)
@click.option(
"--competencia",
required=True,
type=click.Path(exists=True, file_okay=False),
help="Directorio de textos de la competencia"
)
@click.option(
"--salida",
default="brecha_entidades.csv",
help="Fichero de salida CSV o JSON"
)
def main(mios: str, competencia: str, salida: str) -> None:
"""Punto de entrada del script."""
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.info("Iniciando análisis de competencia")
ner = cargar_modelo()
mios_counter = procesar_directorio(Path(mios), ner)
comp_counter = procesar_directorio(Path(competencia), ner)
brechas = calcular_brecha(mios_counter, comp_counter)
df = pd.DataFrame(brechas, columns=["entidad", "freq_competencia"])
try:
if salida.lower().endswith(".json"):
df.to_json(salida, orient="records", force_ascii=False, indent=2)
else:
df.to_csv(salida, index=False)
logging.info(f"Resultados guardados en {salida}")
except Exception as e:
logging.error(f"Error al guardar resultados: {e}")
# Resumen por pantalla
print(f"Total entidades únicas en mis textos: {len(mios_counter)}")
print(f"Total entidades únicas en competencia: {len(comp_counter)}")
print(f"Entidades brecha encontradas: {len(brechas)}")
if __name__ == "__main__":
main()
Claves técnicas
- El modelo WikiNeural combina datos automáticos + humanos y soporta 9 idiomas, incluido español .
- La tubería aggregation_strategy=»simple» une sub-tokens y devuelve spans listos para contar .
- El archivo gap.json revela entidades que tus rivales cubren y tú no, orientación directa para nuevos artículos o enlaces internos.
Otras opciones
Solución | Tipo | Complejidad | Coste aprox.* | Ventajas | Limitaciones |
---|---|---|---|---|---|
spaCy NER | OSS local | Media | Gratis | Rápido, personalizable; EntityRuler combina reglas + modelo | RAM >500 MB en modelos md/lg |
Stanza | OSS local | Media | Gratis | 80 idiomas; buenos resultados en español | Modelos pesados, +CPU |
Flair | OSS local | Media | Gratis | SOTA, dominio biomédico opcional | Documentación dispersa |
Transformers pipeline | OSS + cloud opcional | Media | Gratis local / 0,06 $·1k tok cloud | Model zoo enorme | Requiere GPU para lotes grandes |
Google Cloud NL API | SaaS | Baja | 1 $·1k chars entidad → 0,0005 $ escalado | SLA, 30 k chars gratis/mes | Límite 1 000 chars/llamada |
AWS Comprehend | SaaS | Baja | 1 $·10k chars (aprox.) | Modelos médicos y personalizados | Pre-procesado 100 chars mín. |
OpenAI GPT NER (func-call) | SaaS | Baja | 0,0003 $·1k tok (gpt-4o) | Alta precisión, multilingüe | Datos viajan a la nube |
EntityRuler (spaCy) | Reglas | Baja | Gratis | Cobertura exacta de listas de productos o marcas | No generaliza |
Relik + Neo4j (LlamaIndex) | OSS | Media-Alta | Gratis local | Une NER y entity linking para grafos | Stack más complejo |
*Precios consultados en mayo 2025; pueden variar.
Las Entidades son pieza clave del SEO Semántico
Las entidades son la moneda semántica que Google entiende y premia. Con spaCy obtienes un auditor local rápido; con Transformers añades modelos multilingües de última generación. Las APIs cloud (Google, AWS, OpenAI) simplifican el despliegue en producción sin mantener infraestructura, pero implican coste y envío de datos. Combina NER con enlaces internos por entidad para reforzar tu topical authority —práctica respaldada por estudios recientes — y vigila las brechas frente a la competencia para decidir tu próximo artículo o actualización.
Con los prompts y la comparativa anterior, tienes todo para convertir tus textos (y los ajenos) en insights accionables basados en entidades. ¡A mapear tu propio Knowledge Graph!