Files
Click/tools/proxy_checker.py
2026-05-23 00:07:44 +07:00

337 lines
12 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.
"""
Утилита проверки прокси.
- Удаляет дубликаты по ip:port
- Тестирует напрямую (HTTP и SOCKS5)
- Тестирует через локальный SOCKS5→HTTP туннель
- Сохраняет результат обратно в файл
Использование:
python tools/proxy_checker.py
python tools/proxy_checker.py --file proxies.txt --concurrency 10
python tools/proxy_checker.py --no-save # только показать, не писать файл
"""
import asyncio
import sys
import time
import argparse
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional
sys.path.insert(0, str(Path(__file__).parent.parent))
from managers.proxy_manager import Proxy, ProxyType
TEST_URL = "https://www.google.com/generate_204"
TIMEOUT = 10.0
@dataclass
class CheckResult:
proxy: Proxy
direct_ok: bool = False
direct_ms: Optional[float] = None
tunnel_ok: bool = False
tunnel_ms: Optional[float] = None
error: Optional[str] = None
@property
def ok(self) -> bool:
return self.direct_ok or self.tunnel_ok
def summary(self) -> str:
parts = []
if self.direct_ok:
parts.append(f"direct {self.direct_ms:.0f}ms")
if self.tunnel_ok:
parts.append(f"tunnel {self.tunnel_ms:.0f}ms")
if not parts:
return f"FAIL {self.error or ''}"
return "OK " + " | ".join(parts)
async def _check_direct(proxy: Proxy, timeout: float) -> tuple[bool, Optional[float]]:
"""Прямая проверка через requests в thread pool."""
import asyncio
loop = asyncio.get_running_loop()
def _sync():
import requests
t0 = time.perf_counter()
scheme = proxy.proxy_type.value
if proxy.login and proxy.password:
url = f"{scheme}://{proxy.login}:{proxy.password}@{proxy.ip}:{proxy.port}"
else:
url = f"{scheme}://{proxy.ip}:{proxy.port}"
proxies = {"http": url, "https": url}
try:
r = requests.get(TEST_URL, proxies=proxies, timeout=timeout)
ms = (time.perf_counter() - t0) * 1000
return r.status_code in (200, 204), ms
except Exception:
return False, None
return await loop.run_in_executor(None, _sync)
async def _check_tunnel(proxy: Proxy, timeout: float) -> tuple[bool, Optional[float]]:
"""Проверка через SOCKS5→HTTP туннель (только для SOCKS5/SOCKS4)."""
if proxy.proxy_type not in (ProxyType.SOCKS5, ProxyType.SOCKS4):
return False, None
from services.socks5_to_http_proxy import Socks5ToHttpProxy
import aiohttp
tunnel = Socks5ToHttpProxy(
socks5_host=proxy.ip,
socks5_port=int(proxy.port),
username=proxy.login,
password=proxy.password,
)
try:
await asyncio.wait_for(tunnel.start(), timeout=5)
t0 = time.perf_counter()
ok = await tunnel.check_connection(test_url=TEST_URL, timeout=timeout)
ms = (time.perf_counter() - t0) * 1000 if ok else None
return ok, ms
except Exception:
return False, None
finally:
try:
await asyncio.wait_for(tunnel.stop(), timeout=3)
except Exception:
pass
async def _check_direct_as_http(proxy: Proxy, timeout: float) -> tuple[bool, Optional[float]]:
"""Принудительная проверка как HTTP прокси (игнорирует настроенный тип)."""
loop = asyncio.get_running_loop()
def _sync():
import requests
t0 = time.perf_counter()
if proxy.login and proxy.password:
url = f"http://{proxy.login}:{proxy.password}@{proxy.ip}:{proxy.port}"
else:
url = f"http://{proxy.ip}:{proxy.port}"
proxies = {"http": url, "https": url}
try:
r = requests.get(TEST_URL, proxies=proxies, timeout=timeout)
ms = (time.perf_counter() - t0) * 1000
return r.status_code in (200, 204), ms
except Exception:
return False, None
return await loop.run_in_executor(None, _sync)
async def check_proxy(proxy: Proxy, timeout: float) -> CheckResult:
result = CheckResult(proxy=proxy)
try:
direct_ok, direct_ms = await asyncio.wait_for(
_check_direct(proxy, timeout), timeout=timeout + 2
)
result.direct_ok = direct_ok
result.direct_ms = direct_ms
if proxy.proxy_type in (ProxyType.SOCKS5, ProxyType.SOCKS4):
tunnel_ok, tunnel_ms = await asyncio.wait_for(
_check_tunnel(proxy, timeout), timeout=timeout + 5
)
result.tunnel_ok = tunnel_ok
result.tunnel_ms = tunnel_ms
except Exception as e:
result.error = str(e)[:60]
return result
async def retry_failed_as_http(results: list[CheckResult], timeout: float, sem: asyncio.Semaphore) -> dict[str, CheckResult]:
"""
Повторно проверяет упавшие прокси принудительно как HTTP.
Возвращает dict[ip:port -> CheckResult] только для тех что теперь прошли.
"""
failed = [r for r in results if not r.ok]
if not failed:
return {}
recovered: dict[str, CheckResult] = {}
async def _retry(r: CheckResult):
async with sem:
try:
ok, ms = await asyncio.wait_for(
_check_direct_as_http(r.proxy, timeout), timeout=timeout + 2
)
except (TimeoutError, asyncio.TimeoutError, asyncio.CancelledError, Exception):
ok, ms = False, None
if ok:
key = f"{r.proxy.ip}:{r.proxy.port}"
r.direct_ok = True
r.direct_ms = ms
r.error = None
r.proxy.proxy_type = ProxyType.HTTP
recovered[key] = r
await asyncio.gather(*[_retry(r) for r in failed], return_exceptions=True)
return recovered
def _load_proxies(file_path: str) -> tuple[list[Proxy], list[str]]:
"""Загружает прокси из файла. Возвращает (список прокси, сырые строки)."""
raw_lines = Path(file_path).read_text(encoding="utf-8").splitlines()
proxies = []
seen = set() # ip:port для дедупликации
for line in raw_lines:
stripped = line.strip()
if not stripped or stripped.startswith("#") or stripped.startswith("!"):
continue
p = Proxy.from_line(stripped)
if not p:
continue
key = f"{p.ip}:{p.port}"
if key in seen:
continue
seen.add(key)
proxies.append(p)
return proxies, raw_lines
def _save_results(file_path: str, results: list[CheckResult], raw_lines: list[str]):
"""Перезаписывает файл: рабочие без '!', нерабочие с '!'.
Использует актуальный тип прокси из results (может быть изменён retry-фазой).
"""
# ip:port → актуальный объект Proxy с возможно обновлённым типом
proxy_map: dict[str, Proxy] = {
f"{r.proxy.ip}:{r.proxy.port}": r.proxy for r in results
}
working = {f"{r.proxy.ip}:{r.proxy.port}" for r in results if r.ok}
new_lines = []
seen_keys: set[str] = set() # для удаления дубликатов
for line in raw_lines:
stripped = line.strip()
if not stripped or stripped.startswith("#"):
new_lines.append(line)
continue
clean = stripped.lstrip("!")
p = Proxy.from_line(clean)
if not p:
new_lines.append(line)
continue
key = f"{p.ip}:{p.port}"
if key in seen_keys:
continue # дубликат — пропускаем
seen_keys.add(key)
actual = proxy_map.get(key, p)
scheme = actual.proxy_type.value
if actual.login and actual.password:
formatted = f"{scheme}://{actual.ip}:{actual.port}:{actual.login}:{actual.password}"
else:
formatted = f"{scheme}://{actual.ip}:{actual.port}"
if key in working:
new_lines.append(formatted)
else:
new_lines.append(f"!{formatted}")
Path(file_path).write_text("\n".join(new_lines) + "\n", encoding="utf-8")
async def run_checker(file_path: str, concurrency: int, timeout: float, save: bool, retry_http: bool):
proxies, raw_lines = _load_proxies(file_path)
total_raw = sum(
1 for l in raw_lines
if l.strip() and not l.strip().startswith("#") and not l.strip().startswith("!")
)
duplicates = total_raw - len(proxies)
print(f"\n{'='*60}")
print(f" Файл: {file_path}")
print(f" Загружено: {total_raw} | Дубликатов: {duplicates} | К проверке: {len(proxies)}")
print(f" Параллельность: {concurrency} | Таймаут: {timeout}s")
if retry_http:
print(f" Повтор упавших через HTTP: ВКЛ")
print(f"{'='*60}\n")
sem = asyncio.Semaphore(concurrency)
results: list[CheckResult] = []
done = 0
async def _run(proxy: Proxy):
nonlocal done
async with sem:
r = await check_proxy(proxy, timeout)
results.append(r)
done += 1
status = "" if r.ok else ""
print(f" [{done:>3}/{len(proxies)}] {status} {proxy.id:<35} {r.summary()}")
await asyncio.gather(*[_run(p) for p in proxies])
# Фаза 2: повторная проверка упавших как HTTP
recovered_count = 0
if retry_http:
failed_count = sum(1 for r in results if not r.ok)
if failed_count:
print(f"\n ── Повтор {failed_count} упавших как HTTP прямую ──\n")
recovered = await retry_failed_as_http(results, timeout, sem)
recovered_count = len(recovered)
for key, r in recovered.items():
print(f" [retry] ✅ {r.proxy.ip}:{r.proxy.port} → HTTP {r.direct_ms:.0f}ms")
ok_count = sum(1 for r in results if r.ok)
fail_count = len(results) - ok_count
direct_ok = sum(1 for r in results if r.direct_ok)
tunnel_ok = sum(1 for r in results if r.tunnel_ok)
print(f"\n{'='*60}")
print(f" ИТОГО")
print(f"{'='*60}")
print(f" Рабочих : {ok_count} / {len(proxies)}")
print(f" Нерабочих : {fail_count}")
print(f" Direct OK : {direct_ok}")
print(f" Tunnel OK : {tunnel_ok}")
if recovered_count:
print(f" Спасено HTTP: {recovered_count}")
if duplicates:
print(f" Дубликатов : {duplicates} (удалены)")
if save:
_save_results(file_path, results, raw_lines)
print(f"\n Файл обновлён: {file_path}")
print(f" Нерабочие помечены '!'")
print(f"{'='*60}\n")
def main():
parser = argparse.ArgumentParser(description="Проверка прокси")
parser.add_argument("--file", default="proxies.txt", help="Файл с прокси")
parser.add_argument("--concurrency", type=int, default=5, help="Параллельность (по умолчанию: 5)")
parser.add_argument("--timeout", type=float, default=10.0, help="Таймаут секунд (по умолчанию: 10)")
parser.add_argument("--no-save", action="store_true", help="Не перезаписывать файл")
parser.add_argument("--retry-http", action="store_true", help="Повторить упавшие как HTTP напрямую")
args = parser.parse_args()
asyncio.run(run_checker(
file_path=args.file,
concurrency=args.concurrency,
timeout=args.timeout,
save=not args.no_save,
retry_http=args.retry_http,
))
if __name__ == "__main__":
main()