In this Article
سواء كنت تجري تجريف الويب أو تختبر موقع ويب، فلا غنى عن البروكسيات، لكنك تحتاج إلى عدد كبير من العناوين المختلفة لضمان وصول مستمر. وفي الوقت نفسه، لن تبدل بين البروكسيات يدويا. هنا يأتي دور مدور البروكسيات. في هذا المقال، سنبني مدور بروكسيات مخصصا باستخدام Python من الصفر.
لماذا يعد بناء مدور بروكسيات جيد أمرا أساسيا؟
يتنقل المدور تلقائيا بين بروكسيات المجموعة. ويخصص عنوان IP آخر لكل طلب أو على فاصل زمني محدد. لا يرى الخادم المستهدف أن الطلبات تأتي من المصدر نفسه، وتبدو حركة المرور وكأنها تصدر من عناوين ومواقع مختلفة، مما يساعدك على تجنب اكتشاف أنظمة مكافحة الروبوتات لك. لذلك فإن بناء مدور بروكسيات يمثل جوهر تجريف الويب أو إدارة الحسابات المتعددة. ويتولى المدور الحفاظ على تدفق مصادقة مستقر، والالتزام بإرشادات حركة مرور موقع الويب، والوصول إلى المحتوى الخاص بكل منطقة، ولا سيما في المشاريع واسعة النطاق ذات مخاطر الاتصال المتعددة.
كما توجد مزايا أخرى أقل وضوحا لاستخدام مدور البروكسيات. فهو، مثلا، يعزز خصوصيتك وإخفاء هويتك، إذ إن التغير المستمر لعنوان IP يجعل من الأصعب على مواقع الويب وأدوات التتبع إنشاء ملف تعريفي لأنشطتك. ويبتعد المخترقون عنك خطوة إضافية أيضا، لأن إخفاء عنوانك الحقيقي يصعب على المحتالين الذين يستهدفون أجهزة بعينها التعرف عليك.
إضافة إلى ذلك، تعرض مواقع ويب كثيرة نتائج مختلفة بحسب موقعك أو تفضيلاتك. وباستخدام عناوين IP متعددة، تحصل على بيانات أكثر تنوعا ودقة. كما يمنع التدوير ظهور المحتوى المخصص عندما لا تحتاج إليه.
مدور بروكسيات موثوق: مكوناته
إلى جانب استخدام عنوان IP جديد لكل طلب، ينبغي أن تضيف مزيدا من الميزات كي يفيدك مدور البروكسيات فعلا ولا يتحول إلى مصدر للمشكلات:
- منطق إعادة المحاولة: إذا لم يعمل أحد البروكسيات، يستخدم المدور بروكسيا آخر؛
- تدوير ترويسات User-Agent كي لا تبدو طلباتك صادرة من المصدر نفسه؛
- معالجة رموز حالة HTTP غير الناجحة، مثل 403 (محظور) و429 (طلبات كثيرة جدا) و500 (خطأ داخلي في الخادم)؛
- تعطيل البروكسيات التي يستمر فشلها؛
- إضافة تأخيرات قصيرة بين الطلبات لتجنب تفعيل أنظمة مكافحة الروبوتات؛
- التسجيل: كي ترى، عند إعادة المحاولة، سببها وأي بروكسي وUser-Agent استُخدما؛
- فحص السلامة قبل التجريف: لاختبار جميع البروكسيات قبل البدء والتأكد من عملها؛
- قاطع دائرة لتعطيل البروكسيات المتوقفة لفترة من الوقت؛
- جلسات ثابتة لكل مضيف، بحيث يرى النطاق عنوان IP نفسه ضمن الجلسة الواحدة عند الحاجة، مثل إدارة الحسابات المتعددة حيث تحتاج إلى الالتزام بالجلسة ذاتها.
سنضع هذه النقاط في الحسبان أثناء إنشاء مدور البروكسيات.
بناء مدور بروكسيات باستخدام Python
سننشئ مدور بروكسيات في ملف واحد. وفي مثالنا، سميناه proxy_rotator.py. ستجد أدناه جميع أجزاء الشفرة البرمجية مع شرح لما تفعله. يمكنك نسخها ولصقها. نكتب الشفرة من دون تضمين بيانات ثابتة مثل بيانات الاعتماد، لذلك لن تحتاج إلى استبدال أي شيء لاحقا.
البدء
- ثبت Python من موقعه الرسمي أو حدثه. تحتاج إلى الإصدار 3.8 أو أحدث.
- شغل الأمر التالي لتثبيت مكتبة requests ، إذ سنحتاج إليها لإجراء طلبات HTTP.
pip install requests
- احصل على قائمة البروكسيات من صفحة الخطة المطلوبة. يمكنك نسخها ولصقها يدويا في ملف .txt من لوحة تحكم DataImpulse أو تنزيلها. سمينا ملفنا proxies.txt. ويجب أن يكون بالشكل التالي:
http://login:password.host:port
https://login:password.host:port
يمكنك تغيير تنسيق البروكسي عند الحاجة.
الاستيراد والإعداد
أولا، علينا استيراد الأدوات التي تسمى وحدات والتي سنستخدمها.
import argparse
import logging
import os
import random
import threading
import time
from dataclasses import dataclass
from typing import Dict, Iterable, List, Optional, Tuple
import requests
from requests import Response
request يرسل طلبات الويب، random يختار البروكسيات والترويسات، و threading يتولى التعامل مع مسارات اتصال متعددة.
الإعدادات الافتراضية
ينفذ الجزء التالي عدة وظائف مهمة: يدور User-Agent، وينتقل إلى بروكسي آخر عند ظهور رمز خطأ، ويتحقق من عمل البروكسي.
DEFAULT_USER_AGENTS: List[str] = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15",
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
]
DEFAULT_ROTATE_ON_STATUS = {403, 407, 409, 418, 421, 425, 429, 500, 502, 503, 504}
DEFAULT_HEALTHCHECK_URL = "http://api.ipify.org"
يمكنك استبدال User-Agent أو إضافتها واستخدام موقع ويب آخر لفحص السلامة إن أردت. في حالتنا، نستخدم httpbin.org/ip. ومن الخيارات الجيدة الأخرى https://httpbin.org/get ويعرض ترويسات الطلب إلى جانب IP، https://api.ipify.org أو https://api.ipify.org?format=json لإرجاع عنوان IP الخاص بك بتنسيق JSON، https://ifconfig.me أو https://ifconfig.me/all.json ويرجع الترويسات وIP بتنسيق JSON، أو http://ip-api.com/json/ ويعرض الدولة والمدينة ومزود خدمة الإنترنت وIP.
كائن البروكسي
يحدد هذا الجزء كيفية تخزين البروكسيات.
@dataclass(frozen=True)
class Proxy:
url: str
label: str = ""
def to_requests_proxies(self) -> Dict[str, str]:
return {"http": self.url, "https": self.url}
مرة أخرى، لا تحتاج إلى استبدال أي شيء هنا.
قراءة ملف بروكسيات
يقرأ هذا الجزء البروكسيات من ملف .txt الملف.
def parse_proxy_file(path: str) -> List[Proxy]:
proxies: List[Proxy] = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
raw = line.strip()
if not raw or raw.startswith("#"):
continue
raw = raw.split(" #", 1)[0].split("\t#", 1)[0].strip()
proxies.append(Proxy(url=raw))
if not proxies:
raise ValueError(f"No proxies found in file: {path}")
return proxies
تأخير إعادة المحاولة
يصمم هذا الجزء من الشفرة منطق إعادة المحاولة. وبفضله ينتظر المدور مدة أطول كلما فشل طلب، لمنع تقييد المعدل وإيقاف الخدمة.
def _exp_backoff_sleep(base: float, attempt: int, max_sleep: float) -> None:
sleep_s = min(base * (2 ** attempt) + random.random() * base, max_sleep)
time.sleep(sleep_s)
فئة مدور البروكسيات
إنها قلب المدور. تختار بروكسيا، وتعالج الطلبات مجددا، وتتابع حالات الفشل. كما تحدد استراتيجية العمل، سواء باستخدام البروكسيات عشوائيا أو بالتنقل عبر القائمة، وتضبط المهلات والترويسات، وتستخدم البروكسي نفسه لموقع الويب. وترسل الطلبات، وتضع علامة على البروكسيات المسموح بها أو المحظورة، وتجري فحوصات السلامة.
class ProxyRotator:
def __init__(
self,
proxies: Iterable[Proxy],
strategy: str = "round_robin",
max_retries: int = 3,
timeout: Tuple[float, float] = (10.0, 30.0),
rotate_on_status: Optional[Iterable[int]] = None,
user_agents: Optional[List[str]] = None,
sticky_per_host: bool = True,
circuit_threshold: int = 2,
circuit_cooldown: float = 60.0,
backoff_base: float = 0.5,
backoff_max: float = 5.0,
healthcheck_url: Optional[str] = None,
logger: Optional[logging.Logger] = None,
) -> None:
self.proxies: List[Proxy] = list(proxies)
if not self.proxies:
raise ValueError("ProxyRotator requires at least one proxy")
if strategy not in {"round_robin", "random"}:
raise ValueError("strategy must be 'round_robin' or 'random'")
self.strategy = strategy
self.max_retries = max_retries
self.timeout = timeout
self.rotate_on_status = set(rotate_on_status or DEFAULT_ROTATE_ON_STATUS)
self.user_agents = user_agents or DEFAULT_USER_AGENTS
self.sticky_per_host = sticky_per_host
self.circuit_threshold = circuit_threshold
self.circuit_cooldown = circuit_cooldown
self.failure_counts: Dict[Proxy, int] = {p: 0 for p in self.proxies}
self.disabled_until: Dict[Proxy, float] = {p: 0.0 for p in self.proxies}
self.backoff_base = backoff_base
self.backoff_max = backoff_max
self.healthcheck_url = healthcheck_url
self._local = threading.local()
self._sticky_map: Dict[str, Proxy] = {}
self._idx = 0
self._lock = threading.Lock()
self.log = logger or logging.getLogger("proxy_rotator")
def get(self, url: str, **kwargs) -> Response:
return self.request("GET", url, **kwargs)
def post(self, url: str, **kwargs) -> Response:
return self.request("POST", url, **kwargs)
def request(self, method: str, url: str, **kwargs) -> Response:
session = getattr(self._local, "session", None)
if session is None:
session = requests.Session()
self._local.session = session
headers = kwargs.pop("headers", {}) or {}
headers.setdefault("User-Agent", random.choice(self.user_agents))
timeout = kwargs.pop("timeout", self.timeout)
last_exc: Optional[Exception] = None
last_response: Optional[Response] = None
for attempt in range(self.max_retries + 1):
proxy = self._choose_proxy(url)
if proxy is None:
raise RuntimeError("No healthy proxies currently available")
try:
self.log.debug("Using proxy %s for %s %s", proxy.url, method, url)
response = session.request(
method,
url,
headers=headers,
proxies=proxy.to_requests_proxies(),
timeout=timeout,
**kwargs,
)
last_response = response
if response.status_code in self.rotate_on_status:
self._mark_failure(proxy, f"HTTP {response.status_code}")
self._maybe_unstick_host(url, proxy)
if attempt < self.max_retries:
_exp_backoff_sleep(self.backoff_base, attempt, self.backoff_max)
continue
else:
self._mark_success(proxy)
return response
except requests.RequestException as exc:
last_exc = exc
self._mark_failure(proxy, repr(exc))
self._maybe_unstick_host(url, proxy)
if attempt < self.max_retries:
_exp_backoff_sleep(self.backoff_base, attempt, self.backoff_max)
continue
break
if last_exc:
raise last_exc
assert last_response is not None
return last_response
def health_check(self, url: Optional[str] = None, sample: Optional[int] = None) -> Dict[str, bool]:
check_url = url or self.healthcheck_url or DEFAULT_HEALTHCHECK_URL
to_test = list(self.proxies)
if sample is not None:
to_test = random.sample(to_test, k=min(sample, len(to_test)))
results: Dict[str, bool] = {}
for p in to_test:
try:
r = requests.get(check_url, proxies=p.to_requests_proxies(), timeout=(5, 10))
healthy = r.ok
except Exception:
healthy = False
results[p.url] = healthy
if healthy:
self._mark_success(p)
else:
self._mark_failure(p, "healthcheck")
return results
def _choose_proxy(self, url: str) -> Optional[Proxy]:
host = requests.utils.urlparse(url).hostname or ""
now = time.time()
with self._lock:
if self.sticky_per_host:
sticky = self._sticky_map.get(host)
if sticky and self.disabled_until.get(sticky, 0.0) <= now:
return sticky
enabled = [p for p in self.proxies if self.disabled_until.get(p, 0.0) <= now]
if not enabled:
return None
if self.strategy == "round_robin":
proxy = enabled[self._idx % len(enabled)]
self._idx = (self._idx + 1) % len(enabled)
else:
proxy = random.choice(enabled)
if self.sticky_per_host:
self._sticky_map[host] = proxy
return proxy
def _mark_failure(self, proxy: Proxy, reason: str) -> None:
with self._lock:
cnt = self.failure_counts.get(proxy, 0) + 1
self.failure_counts[proxy] = cnt
if cnt >= self.circuit_threshold:
self.disabled_until[proxy] = time.time() + self.circuit_cooldown
self.log.warning("Disabling proxy %s for %.1fs (reason: %s)", proxy.url, self.circuit_cooldown, reason)
def _mark_success(self, proxy: Proxy) -> None:
with self._lock:
self.failure_counts[proxy] = 0
self.disabled_until[proxy] = 0.0
def _maybe_unstick_host(self, url: str, proxy: Proxy) -> None:
if not self.sticky_per_host:
return
host = requests.utils.urlparse(url).hostname or ""
with self._lock:
if self._sticky_map.get(host) == proxy:
self._sticky_map.pop(host, None)
خيارات سطر الأوامر
تحتاج إلى هذا الجزء لتشغيل المدور بأمر من سطر واحد عبر الطرفية.
def build_arg_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="HTTP proxy rotator")
p.add_argument("--url", required=False, help="Target URL to request (for quick testing)")
p.add_argument("--method", default="GET", help="HTTP method to use (default: GET)")
p.add_argument(
"--proxies",
help="Path to proxies.txt file (one proxy URL per line). If omitted, reads PROXY_LIST env (comma-separated)",
)
p.add_argument("--strategy", choices=["round_robin", "random"], default="round_robin")
p.add_argument("--retries", type=int, default=3, help="Max retry attempts across proxies")
p.add_argument(
"--timeout",
type=float,
default=15.0,
help="Read timeout in seconds (connect timeout is fixed at 10s in this simple CLI)",
)
p.add_argument(
"--rotate-on",
default=",".join(str(s) for s in sorted(DEFAULT_ROTATE_ON_STATUS)),
help="Comma-separated status codes that trigger rotation",
)
p.add_argument(
"--no-sticky",
action="store_true",
help="Disable sticky-per-host behavior",
)
p.add_argument(
"--healthcheck",
action="store_true",
help="Run a healthcheck across proxies and print a summary before the request",
)
p.add_argument(
"--debug",
action="store_true",
help="Enable verbose logging",
)
return p
def _load_proxies_from_args(args: argparse.Namespace) -> List[Proxy]:
if args.proxies:
return parse_proxy_file(args.proxies)
env = os.getenv("PROXY_LIST", "").strip()
if env:
return [Proxy(url=p.strip()) for p in env.split(",") if p.strip()]
raise SystemExit("No proxies provided. Use --proxies or set PROXY_LIST env.")
def main_cli() -> None:
parser = build_arg_parser()
args = parser.parse_args()
logging.basicConfig(
level=logging.DEBUG if args.debug else logging.INFO,
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
log = logging.getLogger("proxy_rotator")
proxies = _load_proxies_from_args(args)
rotate_on_set = {int(x.strip()) for x in args.rotate_on.split(",") if x.strip()}
rotator = ProxyRotator(
proxies=proxies,
strategy=args.strategy,
max_retries=args.retries,
timeout=(10.0, args.timeout),
rotate_on_status=rotate_on_set,
user_agents=DEFAULT_USER_AGENTS,
sticky_per_host=not args.no_sticky,
circuit_threshold=2,
circuit_cooldown=60.0,
backoff_base=0.5,
backoff_max=5.0,
healthcheck_url=DEFAULT_HEALTHCHECK_URL,
logger=log,
)
if args.healthcheck:
results = rotator.health_check()
total = len(results)
healthy = sum(1 for ok in results.values() if ok)
log.info("Healthcheck: %s/%s proxies OK", healthy, total)
for url, ok in results.items():
log.info(" %-40s %s", url, "OK" if ok else "BAD")
if args.url:
try:
resp = rotator.request(args.method.upper(), args.url)
print(f"Status: {resp.status_code}")
text = resp.text
preview = text[:500].replace("\n", " ")
print(f"Body preview (first 500 chars):\n{preview}")
except Exception as e:
log.error("Request failed: %s", e)
raise
else:
log.info("No --url provided; nothing else to do. Add --url to test a request.")
نقطة الدخول
هذا هو ما يبدأ تشغيل المدور. فهو يحمل البروكسيات، ويتحقق منها عند الحاجة، ويجري الطلبات.
if __name__ == "__main__":
main_cli()
تشغيل مدور بروكسيات
يمكنك تشغيل المدور من سطر الأوامر باستخدام الأمر التالي:
python proxy_rotator.py --proxies ./proxies.txt --url http://api.ipify.org
غيّر ./proxies.txt إلى الاسم الفعلي لملف البروكسيات لديك، وغيّر – – URL إلى URL المستهدف لموقع الويب الذي تريد تجريفه أو اختباره.
كما يمكنك إضافة مزيد من العلامات:
– – strategy random كي يختار المدور بروكسيا من المجموعة عشوائيا
– – retries 5 يمكنك ضبط عدد آخر من مرات إعادة المحاولة عند الحاجة
– – healthcheck لاختبار البروكسيات مسبقا
– – no-sticky لتعطيل البروكسيات الثابتة. استخدمه فقط إذا لم تكن بحاجة إلى البقاء في الجلسة نفسها
– – debug سترى سجلات مفصلة تساعدك على استكشاف المشكلات وإصلاحها.
ملاحظة: أحيانا، إذا كان Python 3 مثبتا لديك، فقد تحتاج إلى استخدام “python3 proxy_rotator.py –-proxies ./proxies.txt –-url http://api.ipify.org” بوصفه الأمر لتشغيل المدور.
الخلاصة
بناء مدور بروكسيات ليس طويلا ولا صعبا، لكنه يساعدك على الاستفادة القصوى من مجموعة البروكسيات. ويمكن لمدور جيد يتضمن منطق إعادة المحاولة وفحص السلامة وفواصل زمنية، مقترنا ببروكسيات موثوقة، أن يرتقي بتجريف الويب أو إدارة الحسابات المتعددة إلى مستوى جديد. وللحصول على عناوين IP موثوقة ومصادرها أخلاقية، DataImpulse هنا لمساعدتك. معنا، يمكنك الحصول على أكثر من 90 مليون عنوان IP من 195 موقعا لضمان اتصال متواصل عبر المناطق. تواصل معنا على [email protected] أو اضغط زر “جرّب الآن” للبدء.
