almeira-web/scripts/sanitize_vps_persisted_products.py

154 lines
7.3 KiB
Python
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.

import paramiko
import json
import re
import unicodedata
import io
import sys
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
HOST = "161.97.93.156"
USER = "carlos"
PASSWORD = "Innova1978+*?"
def clean_title(title, brand='Sneakers'):
c = title or ''
# 0. Normalizar fuentes estilizadas (Unicode Mathematical / Bold fonts)
c = unicodedata.normalize('NFKD', c).strip()
# 1. Quitar emojis y pictogramas de todo tipo
c = re.sub(r'[\U00010000-\U0010ffff]|[\u2600-\u27bf]|[\u2300-\u23ff]|[\u200d\ufe0f]|\*|⭐|🌹|⚡|💥|👖|👩🏻|👫|👻|☀️|🔝|🙋🏻‍♀️|😍|🎩|💕|⏰|🌎', ' ', c)
# 2. Quitar códigos de proveedor/bodega: Cod: 734-732 Bbo, Cod: 2236, Cod: 668/833 Bbo, etc.
c = re.sub(r'\bcod(?:igo)?\s*[:\-–—]?\s*[\d\s\-\/]+(?:\s*bbo)?\b', ' ', c, flags=re.I)
# 3. Quitar paréntesis o corchetes con tallas o números
c = re.sub(r'\(\s*(?:tallas?|euro|eur|talla|disponibles?|del)?[^\)]*\b(?:3[2-9]|4[0-9]|5[0-6])\b[^\)]*\)', ' ', c, flags=re.I)
c = re.sub(r'\([^\)]*\b(?:euro|eur|talla|tallas|mil)\b[^\)]*\)', ' ', c, flags=re.I)
c = re.sub(r'\(\s*(?:vienen?|viene)?\s*reducid[oa]s?\s*\)', ' ', c, flags=re.I)
c = re.sub(r'\(\s*\)|\[\s*\]', ' ', c)
# 4. Quitar rangos y expresiones de tallas
c = re.sub(r'\bdel\s+(?:3[0-9]|4[0-9])\s+al\s+(?:3[0-9]|4[0-9])(?:\s*(?:eur|eu|euro))?\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:euro|eur|eu)\s*[:\-–—\/\s]+(?:3[0-9]|4[0-9])(?:\s*(?:-|–|—|a|al|\/)\s*(?:3[0-9]|4[0-9]))*', ' ', c, flags=re.I)
c = re.sub(r'\b(?:3[0-9]|4[0-9])\s+al\s+(?:3[0-9]|4[0-9])(?:\s*(?:eur|eu|euro))\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:3[2-9]|4[0-9]|5[0-6])\s*(?:&|y|\/|\-)\s*(?:hombre\s+)?(?:3[2-9]|4[0-9]|5[0-6])(?:\s*(?:eur|eu|euro))?\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:3[2-9]|4[0-9]|5[0-6])(?:\s*[-–—\/]\s*(?:3[2-9]|4[0-9]|5[0-6]))+\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:3[4-9]|4[0-8])\s*(?:-|–|—|a|al|\/)\s*(?:3[4-9]|4[0-8])\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:talla|tallas|eur|euro|eu)\s+(?:3[2-9]|4[0-8])\b', ' ', c, flags=re.I)
c = re.sub(r'\bhasta\s+(?:talla\s+)?(?:3[2-9]|4[0-9]|5[0-6])(?:\s*(?:eur|eu|euro))?\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:solo\s+)?tallas?\s*(?:[:\-–—\s]+(?:hasta|desde|de|disponibles?|al\s+\d+))?\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:al|a)\s+(?:3[2-9]|4[0-8])\b', ' ', c, flags=re.I)
c = re.sub(r'\b(?:hasta|desde|tallas?|talla|disponibles?)\b', ' ', c, flags=re.I)
# 5. Quitar .000 residual y precios monetarios
c = re.sub(r'(?:^|[\s\(\[\.,\-_])(?:[1-9]\d{0,2})?\.000\b', ' ', c, flags=re.I)
c = re.sub(r'\b\d{2,3}[\.,]\d{3}\b|\b\d{2,3}k\b|\b\d{2,3}\s*mil\b|\$\s*\d+', ' ', c, flags=re.I)
c = re.sub(r'\beu\s*\d+\b', ' ', c, flags=re.I)
c = re.sub(r'\.000\b', ' ', c, flags=re.I)
# 6. Quitar palabras residuales
c = re.sub(r'dama\s*y\s*caballero|recarga|nueva\s*mercanc[ií]a|[uú]ltima\s*unidad', ' ', c, flags=re.I)
c = re.sub(r'\b(costo|precio|valor|al\s*por\s*mayor|mayorista)\b', ' ', c, flags=re.I)
c = re.sub(r'\b(hombre|dama|caballero)\b', ' ', c, flags=re.I)
c = re.sub(r'\b(eur|euro|eu|mil|talla|tallas)\b', ' ', c, flags=re.I)
# 7. Quitar secuencias de tallas al final precedidas de guion o slash (ej: Diesel - 41 42 43, Jordan R3 - 42, Puma Tabla / 36)
if not re.search(r'(?:pegasus\s*40|balance\s*740)', c, re.I):
c = re.sub(r'[\s\-–—\/:]+(?:t:?\s*)?(?:3[2-9]|4[0-8])(?:\s*[\-–—\/\s]\s*(?:3[2-9]|4[0-8]))*\s*(?:eu\d+)?\s*$', '', c, flags=re.I)
# 8. Limpiar barras residuales, preposiciones flotantes al final y símbolos
c = re.sub(r'\s*\/+\s*(?:\d+)?', ' ', c)
c = re.sub(r'\s+(?:de|y|al|a|en|con|del|para)\s*$', '', c, flags=re.I)
c = re.sub(r'\s+&\s*$', '', c)
c = re.sub(r'^\s*&\s+', '', c)
c = re.sub(r'^[#*_\-•—–~:;,\s\.\/&]+|[#*_\-•—–~:;,\s\.\/&]+$', '', c)
c = re.sub(r'\s+', ' ', c).strip()
# 8. Fallback si quedó genérico o vacío
clean_lower = c.lower()
is_generic = (
len(c) < 3 or
re.match(r'^[\d\s\-.,/&]+$', c) or
clean_lower in ('euro', 'eur', 'zapatillas', 'sneakers', 'calzado', 'estilo', 'moda', 'solo', 'sin producto') or
clean_lower.startswith('solo ') or
clean_lower.startswith('euro ') or
clean_lower.startswith('sin producto')
)
if is_generic:
if brand and brand not in ('Urbano / Variado', 'Desconocida', 'Generica'):
c = f'{brand} Urban Edition'
else:
c = 'Sneakers Urban Classic'
# 9. Capital Case limpio conservando acrónimos
words = c.split()
formatted_words = []
for w in words:
if re.match(r'^(OG|SE|SB|TN|CL|GT|RS-X|BB|LV|NB|XT-6|CR7|V2K|V5K|9060|550|530|1906R|2002R)$', w, re.I):
formatted_words.append(w.upper())
elif re.match(r'^[A-Z0-9]+$', w) and len(w) <= 4 and not re.search(r'[aeiou]', w, re.I):
formatted_words.append(w.upper())
else:
formatted_words.append(w.capitalize())
return ' '.join(formatted_words)
print("[1] Conectando a VPS...")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=30)
stdin, stdout, stderr = client.exec_command("docker ps --format '{{.ID}} {{.Names}}'")
containers = stdout.read().decode('utf-8').splitlines()
drip_container = None
for c in containers:
if 'bfmno' in c.lower() or 'drip' in c.lower():
drip_container = c.split()[0]
break
if not drip_container:
print("Error: No se encontró el contenedor de DripZone")
sys.exit(1)
print(f"[2] Extrayendo persisted_products.json del contenedor {drip_container}...")
stdin, stdout, stderr = client.exec_command(f"docker exec {drip_container} cat /app/data/persisted_products.json")
raw_json = stdout.read().decode('utf-8')
products = json.loads(raw_json)
print(f" -> Total productos cargados: {len(products)}")
cleaned_count = 0
for p in products:
old_name = p.get('name', '')
brand = p.get('brand', 'Sneakers')
new_name = clean_title(old_name, brand)
if old_name != new_name:
p['name'] = new_name
p['modelName'] = new_name
cleaned_count += 1
print(f" [CLEANED] \"{old_name}\" -> \"{new_name}\"")
print(f"\n[3] Productos corregidos: {cleaned_count}. Guardando en el contenedor...")
updated_json = json.dumps(products, ensure_ascii=False, indent=2)
# Escribir a archivo temporal en VPS y copiar al contenedor
sftp = client.open_sftp()
with sftp.open('/home/carlos/persisted_products_clean.json', 'w') as f:
f.write(updated_json)
sftp.close()
stdin, stdout, stderr = client.exec_command(f"docker cp /home/carlos/persisted_products_clean.json {drip_container}:/app/data/persisted_products.json")
stdout.channel.recv_exit_status()
print(" [OK] Archivo copiado al contenedor.")
# También reiniciar el contenedor de Coolify para que recargue en RAM
print("[4] Reiniciando contenedor Coolify para cargar catálogo limpio en RAM...")
stdin, stdout, stderr = client.exec_command(f"docker restart {drip_container}")
stdout.channel.recv_exit_status()
print(" [OK] Contenedor reiniciado con éxito.")
client.close()
print("🎉 Proceso finalizado.")