337 lines
12 KiB
Python
337 lines
12 KiB
Python
"""
|
||
Утилита проверки прокси.
|
||
|
||
- Удаляет дубликаты по 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()
|