diff --git a/categorias.csv b/categorias.csv deleted file mode 100644 index a31ef80..0000000 --- a/categorias.csv +++ /dev/null @@ -1,16 +0,0 @@ -id,nombre -1,Ciencia -2,Cultura -3,Deportes -4,Economía -5,Educación -6,Entretenimiento -7,Internacional -8,Medio Ambiente -9,Moda -10,Opinión -11,Política -12,Salud -13,Sociedad -14,Tecnología -15,Viajes diff --git a/cluster_worker.py b/cluster_worker.py index 98438ff..6facadf 100644 --- a/cluster_worker.py +++ b/cluster_worker.py @@ -31,30 +31,41 @@ EVENT_BATCH_IDS = int(os.environ.get("EVENT_BATCH_IDS", "200")) EVENT_SLEEP_IDLE = float(os.environ.get("EVENT_SLEEP_IDLE", "5.0")) EVENT_DIST_THRESHOLD = float(os.environ.get("EVENT_DIST_THRESHOLD", "0.25")) +EMB_MODEL = os.environ.get( + "EMB_MODEL", + "sentence-transformers/paraphrase-multilingual-mpnet-base-v2", +) + def get_conn(): return psycopg2.connect(**DB) def ensure_schema(conn): + """ + Asegura que la tabla de eventos y las columnas necesarias existen. + Aquí se asume el esquema original de eventos con centroid JSONB. + """ with conn.cursor() as cur: cur.execute( """ CREATE TABLE IF NOT EXISTS eventos ( id SERIAL PRIMARY KEY, - creado_en TIMESTAMP NOT NULL DEFAULT NOW(), + creado_en TIMESTAMP NOT NULL DEFAULT NOW(), actualizado_en TIMESTAMP NOT NULL DEFAULT NOW(), - centroid JSONB NOT NULL, + centroid JSONB NOT NULL, total_traducciones INTEGER NOT NULL DEFAULT 1 ); """ ) + cur.execute( """ ALTER TABLE traducciones ADD COLUMN IF NOT EXISTS evento_id INTEGER REFERENCES eventos(id); """ ) + cur.execute( """ CREATE INDEX IF NOT EXISTS idx_traducciones_evento @@ -67,6 +78,7 @@ def ensure_schema(conn): ON traducciones(evento_id, noticia_id); """ ) + cur.execute( """ CREATE OR REPLACE FUNCTION actualizar_evento_modificado() @@ -91,43 +103,55 @@ def ensure_schema(conn): def fetch_pending_traducciones(conn) -> List[int]: + """ + Traducciones con status 'done', sin evento asignado + y que ya tienen embedding en traduccion_embeddings para EMB_MODEL. + """ with conn.cursor() as cur: cur.execute( """ SELECT t.id FROM traducciones t - JOIN embeddings e ON e.traduccion_id = t.id + JOIN traduccion_embeddings e + ON e.traduccion_id = t.id + AND e.model = %s WHERE t.status = 'done' AND t.evento_id IS NULL AND t.lang_to = ANY(%s) ORDER BY t.id DESC LIMIT %s; """, - (EVENT_LANGS, EVENT_BATCH_IDS), + (EMB_MODEL, EVENT_LANGS, EVENT_BATCH_IDS), ) rows = cur.fetchall() return [r[0] for r in rows] def fetch_embeddings_for(conn, tr_ids: List[int]) -> Dict[int, np.ndarray]: + """ + Devuelve un diccionario {traduccion_id: vector_numpy} + leyendo de traduccion_embeddings.embedding para el EMB_MODEL. + """ if not tr_ids: return {} + with conn.cursor() as cur: cur.execute( """ - SELECT traduccion_id, vec - FROM embeddings - WHERE traduccion_id = ANY(%s); + SELECT traduccion_id, embedding + FROM traduccion_embeddings + WHERE traduccion_id = ANY(%s) + AND model = %s; """, - (tr_ids,), + (tr_ids, EMB_MODEL), ) rows = cur.fetchall() out: Dict[int, np.ndarray] = {} - for tr_id, vec in rows: - if not vec: + for tr_id, emb in rows: + if not emb: continue - arr = np.array([float(x or 0.0) for x in vec], dtype="float32") + arr = np.array([float(x or 0.0) for x in emb], dtype="float32") if arr.size == 0: continue out[int(tr_id)] = arr @@ -135,6 +159,9 @@ def fetch_embeddings_for(conn, tr_ids: List[int]) -> Dict[int, np.ndarray]: def fetch_centroids(conn) -> List[Dict[str, Any]]: + """ + Carga todos los centroides actuales desde eventos. + """ with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: cur.execute( """ @@ -180,6 +207,10 @@ def assign_to_event( vec: np.ndarray, centroids: List[Dict[str, Any]], ) -> None: + """ + Asigna una traducción a un evento existente (si distancia <= umbral) + o crea un evento nuevo con este vector como centroide. + """ from psycopg2.extras import Json if vec is None or vec.size == 0: @@ -260,11 +291,12 @@ def assign_to_event( def main(): log.info( "Iniciando cluster_worker eventos " - "(EVENT_LANGS=%s, BATCH_IDS=%s, DIST_THRESHOLD=%.3f, SLEEP=%.1fs)", + "(EVENT_LANGS=%s, BATCH_IDS=%s, DIST_THRESHOLD=%.3f, SLEEP=%.1fs, EMB_MODEL=%s)", ",".join(EVENT_LANGS), EVENT_BATCH_IDS, EVENT_DIST_THRESHOLD, EVENT_SLEEP_IDLE, + EMB_MODEL, ) while True: diff --git a/continentes.csv b/continentes.csv deleted file mode 100644 index 444d792..0000000 --- a/continentes.csv +++ /dev/null @@ -1,7 +0,0 @@ -id,nombre -1,África -2,América -3,Asia -4,Europa -5,Oceanía -6,Antártida diff --git a/download_models.py b/download_models.py index 37ff5b2..2e2e31b 100644 --- a/download_models.py +++ b/download_models.py @@ -2,7 +2,6 @@ import nltk import logging import ssl -# Soluciona problemas de certificado SSL en algunas configuraciones de sistema al descargar try: _create_unverified_https_context = ssl._create_unverified_context except AttributeError: @@ -12,31 +11,22 @@ else: logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s') -# Lista de paquetes de NLTK que newspaper3k puede necesitar. -# 'punkt' y 'punkt_tab' son para tokenización, 'stopwords' para el resumen. PACKAGES = ['punkt', 'punkt_tab', 'stopwords'] def download_nltk_data(): - """ - Descarga los paquetes de NLTK necesarios para newspaper3k. - """ for package in PACKAGES: try: logging.info(f"Verificando si el paquete '{package}' de NLTK está disponible...") - # Determina la ruta correcta para la verificación if package.startswith('punkt'): path = f'tokenizers/{package}' else: path = f'corpora/{package}' - nltk.data.find(path) logging.info(f"El paquete '{package}' ya está descargado.") - except LookupError: logging.info(f"El paquete '{package}' no se encontró. Iniciando descarga...") try: - # El parámetro quiet=True evita el diálogo interactivo nltk.download(package, quiet=True) logging.info(f"Paquete '{package}' descargado con éxito.") except Exception as e: @@ -44,6 +34,7 @@ def download_nltk_data(): import sys sys.exit(1) + if __name__ == '__main__': download_nltk_data() diff --git a/embeddings_worker.py b/embeddings_worker.py index 405241f..105050e 100644 --- a/embeddings_worker.py +++ b/embeddings_worker.py @@ -13,7 +13,6 @@ import torch logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(levelname)s: %(message)s') log = logging.getLogger(__name__) -# ---------- Configuración DB ---------- DB = dict( host=os.environ.get("DB_HOST", "localhost"), port=int(os.environ.get("DB_PORT", 5432)), @@ -22,31 +21,22 @@ DB = dict( password=os.environ.get("DB_PASS", "x"), ) -# ---------- Parámetros de worker ---------- -# Modelo por defecto: multilingüe, bueno para muchas lenguas EMB_MODEL = os.environ.get( "EMB_MODEL", "sentence-transformers/paraphrase-multilingual-mpnet-base-v2", ) EMB_BATCH = int(os.environ.get("EMB_BATCH", "128")) SLEEP_IDLE = float(os.environ.get("EMB_SLEEP_IDLE", "5.0")) -# Filtrado por idiomas destino (coma-separado). Por defecto sólo 'es' EMB_LANGS = [s.strip() for s in os.environ.get("EMB_LANGS", "es").split(",") if s.strip()] - -# DEVICE_ENV: 'auto' | 'cpu' | 'cuda' DEVICE_ENV = os.environ.get("DEVICE", "auto").lower() - -# Límite por iteración (para no tragar toda la tabla de golpe) EMB_LIMIT = int(os.environ.get("EMB_LIMIT", "1000")) -# ---------- Utilidades ---------- def get_conn(): return psycopg2.connect(**DB) def ensure_schema(conn): - """Crea la tabla de embeddings para traducciones si no existe.""" with conn.cursor() as cur: cur.execute( """ @@ -61,21 +51,12 @@ def ensure_schema(conn): ); """ ) - # Alineado con init-db/08-embeddings.sql - cur.execute( - "CREATE INDEX IF NOT EXISTS idx_tr_emb_model ON traduccion_embeddings(model);" - ) - cur.execute( - "CREATE INDEX IF NOT EXISTS idx_tr_emb_traduccion_id ON traduccion_embeddings(traduccion_id);" - ) + cur.execute("CREATE INDEX IF NOT EXISTS idx_tr_emb_model ON traduccion_embeddings(model);") + cur.execute("CREATE INDEX IF NOT EXISTS idx_tr_emb_traduccion_id ON traduccion_embeddings(traduccion_id);") conn.commit() def fetch_batch_pending(conn) -> List[psycopg2.extras.DictRow]: - """ - Devuelve un lote de traducciones 'done' del/los idioma(s) objetivo - que no tienen embedding aún para el EMB_MODEL indicado. - """ with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: cur.execute( """ @@ -96,15 +77,10 @@ def fetch_batch_pending(conn) -> List[psycopg2.extras.DictRow]: """, (EMB_MODEL, EMB_LANGS, EMB_LIMIT), ) - rows = cur.fetchall() - return rows + return cur.fetchall() def texts_from_rows(rows: List[psycopg2.extras.DictRow]) -> List[str]: - """ - Compone el texto a vectorizar por cada traducción: - 'titulo_trad' + '\n' + 'resumen_trad'. Si alguno falta, usa lo disponible. - """ texts: List[str] = [] for r in rows: title = (r["titulo_trad"] or "").strip() @@ -117,15 +93,11 @@ def texts_from_rows(rows: List[psycopg2.extras.DictRow]) -> List[str]: def upsert_embeddings(conn, rows, embs: np.ndarray, model_name: str): - """ - Inserta/actualiza embeddings por traducción en lote (batch insert). - """ if embs.size == 0 or not rows: return dim = int(embs.shape[1]) - # Preparamos los datos para execute_values data = [ ( int(r["traduccion_id"]), @@ -152,7 +124,6 @@ def upsert_embeddings(conn, rows, embs: np.ndarray, model_name: str): conn.commit() -# ---------- Main loop ---------- def main(): log.info("Arrancando embeddings_worker para TRADUCCIONES") log.info( diff --git a/install.sh b/install.sh deleted file mode 100644 index 00e3af1..0000000 --- a/install.sh +++ /dev/null @@ -1,223 +0,0 @@ -#!/bin/bash - -set -e - -APP_NAME="rss" -DB_NAME="rss" -DB_USER="rss" -APP_USER="x" -APP_DIR=$(pwd) -PYTHON_ENV="$APP_DIR/venv" -WSGI_APP_ENTRY="app:app" -WEB_PORT=8000 - -echo "🟢 Paso 0: Verificaciones y confirmación de seguridad" -if [[ $EUID -ne 0 ]]; then - echo "❌ Este script debe ser ejecutado como root (usa sudo)." - exit 1 -fi - -echo "------------------------------------------------------------------" -echo "⚠️ ADVERTENCIA: Este script realizará las siguientes acciones DESTRUCTIVAS:" -echo " - Eliminará TODOS los servicios systemd que empiecen por '$APP_NAME'." -echo " - Eliminará PERMANENTEMENTE la base de datos '$DB_NAME'." -echo " - Eliminará PERMANENTEMENTE el usuario de base de datos '$DB_USER'." -echo "------------------------------------------------------------------" -read -p "Estás seguro de que quieres continuar? (escribe 'si' para confirmar): " CONFIRM -if [ "$CONFIRM" != "si" ]; then - echo "Operación cancelada por el usuario." - exit 0 -fi - -read -sp "🔑 Introduce la contraseña para el usuario de la base de datos '$DB_USER' (se creará de nuevo): " DB_PASS -echo -if [ -z "$DB_PASS" ]; then - echo "❌ La contraseña no puede estar vacía. Abortando." - exit 1 -fi - -echo "🧹 Paso 0.5: Limpiando instalación anterior..." -echo " -> Buscando y eliminando servicios systemd antiguos..." -for service in $(systemctl list-unit-files | grep "^$APP_NAME" | cut -d' ' -f1); do - echo " -> Deteniendo y deshabilitando $service" - systemctl stop "$service" || true - systemctl disable "$service" || true -done -rm -f /etc/systemd/system/$APP_NAME* -systemctl daemon-reload -echo " -> Servicios systemd limpiados." - -echo "🟢 Paso 1: Instalando dependencias del sistema..." -apt-get update -apt-get install -y wget ca-certificates postgresql postgresql-contrib python3-venv python3-pip python3-dev libpq-dev gunicorn - -echo "🔥 Paso 2: Eliminando y recreando la base de datos y el usuario..." -## CORRECCIÓN: Se exporta PGPASSWORD para evitar exponer la contraseña en la línea de comandos -## y se agrupa con la del paso 4 para mayor eficiencia. -export PGPASSWORD="$DB_PASS" - -sudo -u postgres psql -c "DROP DATABASE IF EXISTS $DB_NAME;" -sudo -u postgres psql -c "DROP USER IF EXISTS $DB_USER;" -echo " -> Entidades de BD anteriores eliminadas." -sudo -u postgres psql -c "CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';" -sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" -echo "✅ Base de datos y usuario recreados con éxito." - -echo "🐍 Paso 3: Configurando el entorno de la aplicación..." -if ! id "$APP_USER" &>/dev/null; then - echo "👤 Creando usuario del sistema '$APP_USER'..." - useradd -m -s /bin/bash "$APP_USER" -else - echo "✅ Usuario del sistema '$APP_USER' ya existe." -fi -chown -R "$APP_USER":"$APP_USER" "$APP_DIR" - -sudo -u "$APP_USER" bash < Creando entorno virtual en $PYTHON_ENV" -rm -rf "$PYTHON_ENV" -python3 -m venv "$PYTHON_ENV" -echo " -> Instalando dependencias desde requirements.txt..." -"$PYTHON_ENV/bin/python" -m pip install --upgrade pip -if [ -f "requirements.txt" ]; then - "$PYTHON_ENV/bin/python" -m pip install -r "requirements.txt" -else - echo "⚠️ ADVERTENCIA: No se encontró requirements.txt." -fi -EOF - -echo "🧠 Paso 3.5: Descargando modelos de lenguaje para Newspaper3k..." -if [ -f "download_models.py" ]; then - sudo -u "$APP_USER" "$PYTHON_ENV/bin/python" "$APP_DIR/download_models.py" - echo "✅ Modelos NLP verificados/descargados." -else - echo "⚠️ ADVERTENCIA: No se encontró download_models.py. El scraping de URLs puede fallar." -fi - -echo "📐 Paso 4: Creando esquema de BD y sembrando datos..." -# La variable PGPASSWORD ya está exportada desde el paso 2 - -psql -U "$DB_USER" -h localhost -d "$DB_NAME" < Buscando archivos .sql para sembrar datos..." -if [ -f "continentes.sql" ]; then - echo " -> Cargando continentes.sql..." - psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "continentes.sql" -fi -if [ -f "categorias.sql" ]; then - echo " -> Cargando categorias.sql..." - psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "categorias.sql" -fi -if [ -f "paises.sql" ]; then - echo " -> Cargando paises.sql..." - psql -U "$DB_USER" -h localhost -d "$DB_NAME" -f "paises.sql" -fi - -echo " -> Actualizando contadores de secuencias de la base de datos..." -psql -U "$DB_USER" -h localhost -d "$DB_NAME" < /etc/systemd/system/$APP_NAME.service -[Unit] -Description=Gunicorn instance to serve $APP_NAME -After=network.target - -[Service] -User=$APP_USER -Group=$APP_USER -WorkingDirectory=$APP_DIR -Environment="PATH=$PYTHON_ENV/bin" -Environment="SECRET_KEY=$(python3 -c 'import os; print(os.urandom(24).hex())')" -Environment="DB_HOST=localhost" -Environment="DB_PORT=5432" -Environment="DB_NAME=$DB_NAME" -Environment="DB_USER=$DB_USER" -Environment="DB_PASS=$DB_PASS" -ExecStart=$PYTHON_ENV/bin/gunicorn --workers 3 --bind 0.0.0.0:$WEB_PORT --timeout 120 $WSGI_APP_ENTRY -Restart=always - -[Install] -WantedBy=multi-user.target -EOF - -## CORRECCIÓN: Se elimina la creación de rss-worker.service y rss-worker.timer. -## Se añade el nuevo servicio para el planificador persistente scheduler.py. -cat < /etc/systemd/system/$APP_NAME-scheduler.service -[Unit] -Description=$APP_NAME Scheduler Worker -After=postgresql.service - -[Service] -Type=simple -User=$APP_USER -Group=$APP_USER -WorkingDirectory=$APP_DIR -Environment="PATH=$PYTHON_ENV/bin" -Environment="SECRET_KEY=$(python3 -c 'import os; print(os.urandom(24).hex())')" -Environment="DB_HOST=localhost" -Environment="DB_PORT=5432" -Environment="DB_NAME=$DB_NAME" -Environment="DB_USER=$DB_USER" -Environment="DB_PASS=$DB_PASS" -ExecStart=$PYTHON_ENV/bin/python $APP_DIR/scheduler.py -Restart=always - -[Install] -WantedBy=multi-user.target -EOF - -echo "✅ Archivos de servicio creados." - -echo "🚀 Paso 7: Recargando, habilitando, arrancando servicios y configurando firewall..." -systemctl daemon-reload -systemctl enable $APP_NAME.service -systemctl start $APP_NAME.service - -## CORRECCIÓN: Se habilitan e inician el nuevo servicio del planificador. -systemctl enable $APP_NAME-scheduler.service -systemctl start $APP_NAME-scheduler.service - -if command -v ufw &> /dev/null && ufw status | grep -q 'Status: active'; then - echo " -> Firewall UFW detectado. Abriendo puerto $WEB_PORT..." - ufw allow $WEB_PORT/tcp - ufw reload -fi - -echo "" -echo "🎉 ¡REINSTALACIÓN COMPLETADA!" -echo "--------------------------------" -echo "" -echo "✅ La aplicación web está ahora accesible en:" -echo " http://:$WEB_PORT" -echo "" -echo "Puedes verificar el estado de los servicios con:" -echo "sudo systemctl status $APP_NAME.service" -## CORRECCIÓN: Se actualiza el mensaje para reflejar el nuevo nombre del servicio del worker. -echo "sudo systemctl status $APP_NAME-scheduler.service" -echo "" -echo "Para ver los logs de la aplicación web:" -echo "sudo journalctl -u $APP_NAME.service -f" -echo "Para ver los logs del planificador de noticias:" -echo "sudo journalctl -u $APP_NAME-scheduler.service -f" diff --git a/paises.csv b/paises.csv deleted file mode 100644 index ea90890..0000000 --- a/paises.csv +++ /dev/null @@ -1,196 +0,0 @@ -id,nombre,continente_id -1,Afganistán,3 -2,Albania,4 -3,Alemania,4 -4,Andorra,4 -5,Angola,1 -6,Antigua y Barbuda,2 -7,Arabia Saudita,3 -8,Argelia,1 -9,Argentina,2 -10,Armenia,3 -11,Australia,5 -12,Austria,4 -13,Azerbaiyán,3 -14,Bahamas,2 -15,Bangladés,3 -16,Barbados,2 -17,Baréin,3 -19,Belice,2 -20,Benín,1 -21,Bielorrusia,4 -22,Birmania,3 -23,Bolivia,2 -24,Bosnia y Herzegovina,4 -25,Botsuana,1 -26,Brasil,2 -27,Brunéi,3 -28,Bulgaria,4 -29,Burkina Faso,1 -30,Burundi,1 -31,Bután,3 -18,Bélgica,4 -32,Cabo Verde,1 -33,Camboya,3 -34,Camerún,1 -35,Canadá,2 -36,Catar,3 -37,Chad,1 -38,Chile,2 -39,China,3 -40,Chipre,3 -41,Colombia,2 -42,Comoras,1 -43,Corea del Norte,3 -44,Corea del Sur,3 -46,Costa Rica,2 -45,Costa de Marfil,1 -47,Croacia,4 -48,Cuba,2 -49,Dinamarca,4 -50,Dominica,2 -51,Ecuador,2 -52,Egipto,1 -53,El Salvador,2 -54,Emiratos Árabes Unidos,3 -55,Eritrea,1 -56,Eslovaquia,4 -57,Eslovenia,4 -58,España,4 -59,Estados Unidos,2 -60,Estonia,4 -61,Esuatini,1 -62,Etiopía,1 -63,Filipinas,3 -64,Finlandia,4 -65,Fiyi,5 -66,Francia,4 -67,Gabón,1 -68,Gambia,1 -69,Georgia,3 -70,Ghana,1 -71,Granada,2 -72,Grecia,4 -73,Guatemala,2 -74,Guinea,1 -76,Guinea Ecuatorial,1 -75,Guinea-Bisáu,1 -77,Guyana,2 -78,Haití,2 -79,Honduras,2 -80,Hungría,4 -81,India,3 -82,Indonesia,3 -83,Irak,3 -85,Irlanda,4 -84,Irán,3 -86,Islandia,4 -87,Islas Marshall,5 -88,Islas Salomón,5 -89,Israel,3 -90,Italia,4 -91,Jamaica,2 -92,Japón,3 -93,Jordania,3 -94,Kazajistán,3 -95,Kenia,1 -96,Kirguistán,3 -97,Kiribati,5 -98,Kuwait,3 -99,Laos,3 -100,Lesoto,1 -101,Letonia,4 -103,Liberia,1 -104,Libia,1 -105,Liechtenstein,4 -106,Lituania,4 -107,Luxemburgo,4 -102,Líbano,3 -108,Macedonia del Norte,4 -109,Madagascar,1 -110,Malasia,3 -111,Malaui,1 -112,Maldivas,3 -114,Malta,4 -113,Malí,1 -115,Marruecos,1 -116,Mauricio,1 -117,Mauritania,1 -119,Micronesia,5 -120,Moldavia,4 -122,Mongolia,3 -123,Montenegro,4 -124,Mozambique,1 -118,México,2 -121,Mónaco,4 -125,Namibia,1 -126,Nauru,5 -127,Nepal,3 -128,Nicaragua,2 -130,Nigeria,1 -131,Noruega,4 -132,Nueva Zelanda,5 -129,Níger,1 -133,Omán,3 -135,Pakistán,3 -136,Palaos,5 -137,Palestina,3 -138,Panamá,2 -139,Papúa Nueva Guinea,5 -140,Paraguay,2 -134,Países Bajos,4 -141,Perú,2 -142,Polonia,4 -143,Portugal,4 -144,Reino Unido,4 -145,República Centroafricana,1 -146,República Checa,4 -148,República Democrática del Congo,1 -149,República Dominicana,2 -147,República del Congo,1 -150,Ruanda,1 -151,Rumanía,4 -152,Rusia,3 -153,Samoa,5 -154,San Cristóbal y Nieves,2 -155,San Marino,4 -156,San Vicente y las Granadinas,2 -157,Santa Lucía,2 -158,Santo Tomé y Príncipe,1 -159,Senegal,1 -160,Serbia,4 -161,Seychelles,1 -162,Sierra Leona,1 -163,Singapur,3 -164,Siria,3 -165,Somalia,1 -166,Sri Lanka,3 -167,Sudáfrica,1 -168,Sudán,1 -169,Sudán del Sur,1 -170,Suecia,4 -171,Suiza,4 -172,Surinam,2 -173,Tailandia,3 -174,Tanzania,1 -175,Tayikistán,3 -176,Timor Oriental,3 -177,Togo,1 -178,Tonga,5 -179,Trinidad y Tobago,2 -181,Turkmenistán,3 -182,Turquía,3 -183,Tuvalu,5 -180,Túnez,1 -184,Ucrania,4 -185,Uganda,1 -186,Uruguay,2 -187,Uzbekistán,3 -188,Vanuatu,5 -189,Vaticano,4 -190,Venezuela,2 -191,Vietnam,3 -192,Yemen,3 -193,Yibuti,1 -194,Zambia,1 -195,Zimbabue,1 diff --git a/total_pages b/total_pages deleted file mode 100644 index e69de29..0000000