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.")