Skip to content

Commit cff4341

Browse files
committed
fix(clash): add spanish translation for proxy management guide
1 parent 00c7d09 commit cff4341

File tree

4 files changed

+935
-2
lines changed

4 files changed

+935
-2
lines changed

_posts/es/2025-06-08-clash-es.md

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
---
2+
audio: false
3+
generated: false
4+
lang: es
5+
layout: post
6+
title: Automatización de la Gestión de Proxies en Clash
7+
translated: true
8+
---
9+
10+
Esta publicación detalla un script de Python, `clash.py`, diseñado para **automatizar la gestión de tu configuración de proxy Clash**. Maneja todo, desde **descargar periódicamente configuraciones de proxy actualizadas** y **reiniciar el servicio Clash** hasta **seleccionar y cambiar inteligentemente al proxy disponible más rápido** dentro de un grupo designado. Complementando a `clash.py`, el módulo `speed.py` facilita **pruebas de latencia concurrentes de proxies individuales de Clash**, asegurando que tu conexión siempre se enrute a través del servidor óptimo.
11+
12+
## clash.py
13+
14+
```python
15+
import os
16+
import subprocess
17+
import time
18+
import shutil
19+
import argparse
20+
import logging
21+
import requests
22+
import json
23+
import urllib.parse
24+
25+
# Asumiendo que speed.py está en el mismo directorio o accesible en PYTHONPATH
26+
from speed import get_top_proxies
27+
28+
# --- Configuración ---
29+
CLASH_CONTROLLER_HOST = "127.0.0.1"
30+
CLASH_CONTROLLER_PORT = 9090
31+
CLASH_API_BASE_URL = f"http://{CLASH_CONTROLLER_HOST}:{CLASH_CONTROLLER_PORT}"
32+
# El nombre del grupo de proxy al que se asignará el mejor proxy individual.
33+
# Asegúrate de que este grupo exista en tu configuración de Clash.
34+
TARGET_PROXY_GROUP = "🚧Proxy"
35+
36+
def setup_logging():
37+
"""Configura el registro básico para el script."""
38+
logging.basicConfig(
39+
filename='clash.log',
40+
level=logging.INFO,
41+
format='%(asctime)s - %(message)s',
42+
datefmt='%Y-%m-%d %H:%M:%S'
43+
)
44+
45+
def start_system_proxy(global_proxy_address):
46+
"""Establece variables de entorno de proxy a nivel del sistema."""
47+
os.environ["GLOBAL_PROXY"] = global_proxy_address # Establece para consistencia si se necesita en otro lugar
48+
os.environ["HTTP_PROXY"] = f"http://{global_proxy_address}"
49+
os.environ["HTTPS_PROXY"] = f"http://{global_proxy_address}"
50+
os.environ["http_proxy"] = f"http://{global_proxy_address}"
51+
os.environ["https_proxy"] = f"http://{global_proxy_address}"
52+
# Estos normalmente no necesitan ser explícitamente establecidos como "false" con herramientas modernas,
53+
# pero se mantienen para compatibilidad con la intención original de tu script.
54+
os.environ["HTTP_PROXY_REQUEST_FULLURI"] = "false"
55+
os.environ["HTTPS_PROXY_REQUEST_FULLURI"] = "false"
56+
os.environ["ALL_PROXY"] = os.environ["http_proxy"]
57+
logging.info(f"Proxy a nivel del sistema establecido en: {global_proxy_address}")
58+
59+
def stop_system_proxy():
60+
"""Borra las variables de entorno de proxy a nivel del sistema."""
61+
os.environ["http_proxy"] = ""
62+
os.environ["HTTP_PROXY"] = ""
63+
os.environ["https_proxy"] = ""
64+
os.environ["HTTPS_PROXY"] = ""
65+
os.environ["HTTP_PROXY_REQUEST_FULLURI"] = "true" # Revertir a predeterminado
66+
os.environ["HTTPS_PROXY_REQUEST_FULLURI"] = "true"
67+
os.environ["ALL_PROXY"] = ""
68+
logging.info("Proxy a nivel del sistema detenido (variables de entorno borradas).")
69+
70+
def switch_clash_proxy_group(group_name, proxy_name):
71+
"""
72+
Cambia el proxy activo en un grupo de proxy de Clash especificado a un nuevo proxy.
73+
"""
74+
encoded_group_name = urllib.parse.quote(group_name)
75+
url = f"{CLASH_API_BASE_URL}/proxies/{encoded_group_name}"
76+
headers = {"Content-Type": "application/json"}
77+
payload = {"name": proxy_name}
78+
79+
try:
80+
response = requests.put(url, headers=headers, data=json.dumps(payload), timeout=5)
81+
response.raise_for_status()
82+
logging.info(f"Se cambió exitosamente '{group_name}' a '{proxy_name}'.")
83+
return True
84+
except requests.exceptions.ConnectionError:
85+
logging.error(f"Error: No se pudo conectar a la API de Clash en {CLASH_API_BASE_URL} para cambiar el proxy.")
86+
logging.error("Asegúrate de que Clash esté ejecutándose y su external-controller esté configurado.")
87+
return False
88+
except requests.exceptions.Timeout:
89+
logging.error(f"Error: La conexión a la API de Clash se agotó mientras se cambiaba el proxy para '{group_name}'.")
90+
return False
91+
except requests.exceptions.RequestException as e:
92+
logging.error(f"Ocurrió un error inesperado al cambiar el proxy para '{group_name}': {e}")
93+
return False
94+
95+
def main():
96+
"""Función principal para gestionar la configuración de Clash, reiniciar y seleccionar el mejor proxy."""
97+
setup_logging()
98+
99+
parser = argparse.ArgumentParser(description="Script de configuración y gestión de Clash.")
100+
parser.add_argument("--minutes", type=int, default=10, help="Minutos entre actualizaciones (predeterminado: 10)")
101+
parser.add_argument("--iterations", type=int, default=1000, help="Número de iteraciones (predeterminado: 1000)")
102+
parser.add_argument(
103+
"--config-url",
104+
type=str,
105+
default=os.getenv("CLASH_DOWNLOAD_URL"),
106+
help="URL para descargar la configuración de Clash. Por defecto, usa la variable de entorno CLASH_DOWNLOAD_URL si está configurada, de lo contrario, una URL codificada."
107+
)
108+
args = parser.parse_args()
109+
110+
ITERATIONS = args.iterations
111+
SLEEP_SECONDS = args.minutes * 60
112+
config_download_url = args.config_url
113+
114+
if not config_download_url:
115+
logging.critical("Error: No se proporcionó URL de descarga de configuración. Por favor, configura la variable de entorno CLASH_DOWNLOAD_URL o usa el argumento --config-url.")
116+
return # Salir si no hay URL disponible
117+
118+
clash_executable_path = "/home/lzw/clash-linux-386-v1.17.0/clash-linux-386"
119+
clash_config_dir = os.path.expanduser("~/.config/clash")
120+
clash_config_path = os.path.join(clash_config_dir, "config.yaml")
121+
122+
for i in range(1, ITERATIONS + 1):
123+
logging.info(f"--- Iniciando Iteración {i} de {ITERATIONS} ---")
124+
125+
# Paso 1: Detener cualquier configuración de proxy del sistema existente
126+
stop_system_proxy()
127+
128+
# Paso 2: Descargar y actualizar la configuración de Clash
129+
try:
130+
logging.info(f"Descargando nueva configuración desde: {config_download_url}")
131+
subprocess.run(["wget", config_download_url, "-O", "zhs4.yaml"], check=True, capture_output=True)
132+
os.makedirs(clash_config_dir, exist_ok=True)
133+
shutil.move("zhs4.yaml", clash_config_path)
134+
logging.info("¡Configuración de Clash actualizada exitosamente!")
135+
except subprocess.CalledProcessError as e:
136+
logging.error(f"Fallo al descargar o mover el archivo de configuración: {e.stderr.decode().strip()}")
137+
logging.error("Saltando a la siguiente iteración.")
138+
time.sleep(10) # Esperar un poco antes de reintentar
139+
continue
140+
except Exception as e:
141+
logging.error(f"Ocurrió un error inesperado durante la actualización de configuración: {e}")
142+
logging.error("Saltando a la siguiente iteración.")
143+
time.sleep(10)
144+
continue
145+
146+
# Paso 3: Iniciar Clash en segundo plano
147+
clash_process = None
148+
try:
149+
# Es crucial que Clash inicie con el external-controller habilitado y accesible
150+
# Esto normalmente se configura dentro del config.yaml mismo.
151+
clash_process = subprocess.Popen([clash_executable_path],
152+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
153+
logging.info(f"Clash iniciado con PID {clash_process.pid}")
154+
# Darle a Clash un momento para inicializar completamente y abrir su puerto API
155+
time.sleep(5)
156+
except FileNotFoundError:
157+
logging.critical(f"Ejecutable de Clash no encontrado en: {clash_executable_path}")
158+
logging.critical("Por favor, asegúrate de que la ruta sea correcta y Clash esté instalado.")
159+
return # Error crítico, salir del script
160+
except Exception as e:
161+
logging.error(f"Fallo al iniciar Clash: {e}")
162+
logging.error("Saltando a la siguiente iteración.")
163+
if clash_process: clash_process.terminate()
164+
time.sleep(10)
165+
continue
166+
167+
# Paso 4: Probar velocidades de proxy y seleccionar el mejor
168+
best_proxy_name = None
169+
try:
170+
logging.info("Probando velocidades de proxy para encontrar el mejor...")
171+
top_proxies = get_top_proxies(num_results=1) # Obtener solo el mejor proxy
172+
if top_proxies:
173+
best_proxy_name = top_proxies[0]['name']
174+
logging.info(f"Proxy mejor identificado: '{best_proxy_name}' con latencia {top_proxies[0]['latency']}ms")
175+
else:
176+
logging.warning("No hubo pruebas de proxy exitosas. No se puede seleccionar un mejor proxy para esta iteración.")
177+
except Exception as e:
178+
logging.error(f"Error durante la prueba de velocidad de proxy: {e}")
179+
180+
# Paso 5: Cambiar el grupo de proxy de Clash al mejor proxy (si se encontró)
181+
if best_proxy_name:
182+
# Antes de configurar el proxy del sistema, asegurarse de que Clash esté configurado correctamente.
183+
# Establecer el proxy del sistema para apuntar al proxy HTTP local de Clash.
184+
# Clash normalmente ejecuta su proxy HTTP en el puerto 7890 (o similar, verifica tu configuración).
185+
clash_local_proxy_address = f"{CLASH_CONTROLLER_HOST}:7890" # Ajustar si tu puerto HTTP de Clash es diferente
186+
start_system_proxy(clash_local_proxy_address)
187+
188+
if not switch_clash_proxy_group(TARGET_PROXY_GROUP, best_proxy_name):
189+
logging.error(f"Fallo al cambiar el grupo de Clash '{TARGET_PROXY_GROUP}' a '{best_proxy_name}'.")
190+
else:
191+
logging.warning("No se encontró mejor proxy, omitiendo el cambio de grupo de proxy y configuración de proxy del sistema para esta iteración.")
192+
193+
# Paso 6: Esperar la duración especificada
194+
logging.info(f"Esperando {SLEEP_SECONDS / 60} minutos antes de la siguiente iteración...")
195+
time.sleep(SLEEP_SECONDS)
196+
197+
# Paso 7: Detener el proceso de Clash
198+
if clash_process:
199+
logging.info("Terminando proceso de Clash...")
200+
clash_process.terminate()
201+
try:
202+
clash_process.wait(timeout=10) # Darle a Clash un poco más de tiempo para apagarse correctamente
203+
logging.info("Clash detenido exitosamente.")
204+
except subprocess.TimeoutExpired:
205+
logging.warning("Clash no terminó correctamente, matando proceso.")
206+
clash_process.kill()
207+
clash_process.wait() # Asegurarse de que el proceso esté completamente terminado
208+
except Exception as e:
209+
logging.error(f"Error mientras se esperaba que Clash se detuviera: {e}")
210+
211+
logging.info(f"--- Iteración {i} completada ---")
212+
213+
logging.info(f"Completadas {ITERATIONS} iteraciones. Script finalizado.")
214+
215+
if __name__ == "__main__":
216+
main()
217+
```
218+
219+
## speed.py
220+
221+
```python
222+
import requests
223+
import json
224+
import urllib.parse
225+
import time
226+
from concurrent.futures import ThreadPoolExecutor, as_completed
227+
import logging # Importar el módulo de registro
228+
229+
# --- Configuración ---
230+
CLASH_CONTROLLER_HOST = "127.0.0.1" # Usar 127.0.0.1 ya que el controlador está en la misma máquina
231+
CLASH_CONTROLLER_PORT = 9090
232+
CLASH_API_BASE_URL = f"http://{CLASH_CONTROLLER_HOST}:{CLASH_CONTROLLER_PORT}"
233+
LATENCY_TEST_URL = "https://github.com" # URL de prueba actualizada
234+
LATENCY_TEST_TIMEOUT_MS = 5000 # Milisegundos
235+
CONCURRENT_CONNECTIONS = 10 # Número de pruebas concurrentes
236+
237+
# Lista de nombres de grupos de proxy conocidos para excluir de las pruebas de velocidad
238+
# Estos normalmente no son nodos individuales sino grupos de políticas o proxies especiales.
239+
EXCLUDE_PROXY_GROUPS = [
240+
"DIRECT",
241+
"REJECT",
242+
"GLOBAL", # Ya excluido por defecto en la API
243+
"🇨🇳国内网站或资源",
244+
"🌵其它规则外",
245+
"🎬Netflix等国外流媒体",
246+
"📦ChatGPT",
247+
"📹Youtube",
248+
"📺爱奇艺等国内流媒体",
249+
"🚧Proxy",
250+
# Agregar cualquier otro nombre de grupo que quieras excluir aquí
251+
]
252+
253+
# --- Configuración de Registro para speed.py ---
254+
# Configurar el registro para este script específico
255+
# Esto asegura que cuando speed.py sea importado y sus funciones sean llamadas,
256+
# su salida vaya a speed.log, separada de clash_manager.log.
257+
logging.basicConfig(
258+
filename='clash.log',
259+
level=logging.INFO,
260+
format='%(asctime)s - %(levelname)s - %(message)s',
261+
datefmt='%Y-%m-%d %H:%M:%S'
262+
)
263+
# Opcionalmente, si también quieres ver la salida en la consola, agrega un StreamHandler:
264+
# console_handler = logging.StreamHandler()
265+
# console_handler.setLevel(logging.INFO)
266+
# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
267+
# console_handler.setFormatter(formatter)
268+
# logging.getLogger().addHandler(console_handler)
269+
270+
# --- Lógica del Script ---
271+
272+
def get_all_proxy_names():
273+
"""Obtiene todos los nombres de proxy de la API de Clash, excluyendo grupos conocidos."""
274+
try:
275+
response = requests.get(f"{CLASH_API_BASE_URL}/proxies", timeout=5)
276+
response.raise_for_status() # Lanzar una excepción para errores HTTP (4xx o 5xx)
277+
proxies_data = response.json()
278+
279+
all_names = proxies_data.get("proxies", {}).keys()
280+
281+
# Filtrar los nombres de grupo
282+
filtered_names = [name for name in all_names if name not in EXCLUDE_PROXY_GROUPS]
283+
284+
logging.info(f"Se obtuvieron exitosamente {len(filtered_names)} nombres de proxy probables.")
285+
return filtered_names
286+
except requests.exceptions.ConnectionError:
287+
logging.error(f"No se pudo conectar a la API de Clash en {CLASH_API_BASE_URL}. Asegúrate de que Clash esté ejecutándose.")
288+
return []
289+
except requests.exceptions.Timeout:
290+
logging.error(f"La conexión a la API de Clash se agotó después de 5 segundos.")
291+
return []
292+
except requests.exceptions.RequestException as e:
293+
logging.error(f"Ocurrió un error inesperado al obtener los nombres de proxy: {e}")
294+
return []
295+
296+
def test_proxy_latency(proxy_name):
297+
"""Prueba la latencia de un solo proxy usando la API de Clash.
298+
Retorna una tupla (proxy_name, latency) o (proxy_name, None) en caso de fallo.
299+
"""
300+
encoded_proxy_name = urllib.parse.quote(proxy_name)
301+
url = f"{CLASH_API_BASE_URL}/proxies/{encoded_proxy_name}/delay"
302+
params = {
303+
"url": LATENCY_TEST_URL,
304+
"timeout": LATENCY_TEST_TIMEOUT_MS
305+
}
306+
try:
307+
# el timeout de requests está en segundos, convertir milisegundos
308+
response = requests.get(url, params=params, timeout=(LATENCY_TEST_TIMEOUT_MS / 1000) + 1)
309+
response.raise_for_status()
310+
latency_data = response.json()
311+
latency = latency_data.get("delay")
312+
logging.info(f"Proxy: {proxy_name} - Latencia: {latency}ms")
313+
return proxy_name, latency
314+
except requests.exceptions.RequestException as e:
315+
logging.warning(f"Error probando '{proxy_name}': {e}")
316+
return proxy_name, None
317+
318+
def get_top_proxies(num_results=5):
319+
"""
320+
Prueba las velocidades de los proxies de Clash concurrentemente y retorna los N proxies individuales más rápidos.
321+
322+
Retorna:
323+
list: Una lista de diccionarios, cada uno contiene 'name' y 'latency' para los mejores proxies.
324+
Retorna una lista vacía si no se encuentran proxies probables o ocurre un error.
325+
"""
326+
logging.info("Iniciando prueba de velocidad de proxies de Clash vía API Externa (concurrentemente)...")
327+
logging.info(f"Usando URL de prueba: {LATENCY_TEST_URL}")
328+
logging.info(f"Ejecutando {CONCURRENT_CONNECTIONS} pruebas a la vez. Esto puede tomar un momento...")
329+
330+
proxy_names_to_test = get_all_proxy_names()
331+
if not proxy_names_to_test:
332+
logging.warning("No se encontraron proxies probables o ocurrió un error durante la obtención de nombres de proxy.")
333+
return []
334+
335+
logging.info(f"Se encontraron {len(proxy_names_to_test)} proxies individuales para probar.")
336+
337+
proxy_latencies = {}
338+
339+
with ThreadPoolExecutor(max_workers=CONCURRENT_CONNECTIONS) as executor:
340+
future_to_proxy = {executor.submit(test_proxy_latency, name): name for name

0 commit comments

Comments
 (0)