حاسبة السعر لدى معظم الناقلات لا تعرض رقماً قبل اجتياز اختبار CAPTCHA، ولهذا يتوقف أي سكربت مقارنة عند الطلب الأول. المسار العملي ثابت: اقرأ صفحة الحاسبة، استخرج مفتاح الموقع sitekey، أرسل المهمة إلى CaptchaAI، ثم أعد إرسال النموذج ومعه الرمز في الحقل الذي ينتظره الناقل — g-recaptcha-response مع reCAPTCHA v2 وcf-turnstile-response مع Cloudflare Turnstile.
الدليل موجّه لفرق البيانات في وكالات الشحن ومنصات المقارنة التي تجمع أسعاراً معلنة على صفحات عامة أو حسابات تملك صلاحية الوصول إليها: خريطة الاختبارات، نمطان جاهزان بـ Python، جدول وتيرة، وحساب عدد الـ Threads.
لماذا تُحمى حاسبات الأسعار وصفحات التتبع؟
استعلام السعر ليس صفحة ثابتة، بل عملية تسعير حية تحسب الوزن الحجمي ورسوم الوقود والمنطقة الجغرافية. وهي مكلفة على الخادم، فتُوضع خلف اختبار تحقق يقلّل الطلبات الآلية غير المنضبطة، ومثلها صفحات التتبع المستدعاة آلاف المرات يومياً.
ما ينتج عن ذلك عملياً:
- نموذج السعر يرجع صفحة فارغة بدل جدول الأسعار حين يُرسَل بلا رمز تحقق صالح.
- وسطاء الشحن خلف Cloudflare يعرضون Turnstile قبل قبول أي طلب عرض سعر.
- بوابات التتبع البحري ما زالت تعتمد اختبارات صورية تُقرأ عبر OCR.
- تكرار الطلبات من عنوان IP واحد يرفع حساسية الحماية، فيظهر الاختبار أسرع لاحقاً.
خريطة اختبارات CAPTCHA حسب نوع الناقل
| نوع الناقل | نوع الاختبار | الصفحات المحمية | هدف الجمع |
|---|---|---|---|
| ناقلات الطرود: UPS وFedEx | reCAPTCHA v2 | حاسبة الأسعار والتتبع | مقارنة أسعار الطرود |
| وسطاء الشحن البري | Cloudflare Turnstile | نماذج طلب عرض السعر | تسعير المسارات |
| الناقلات البحرية | CAPTCHA صوري | تتبع السفن والحاويات | متابعة الحاويات |
| البوابات الجمركية | reCAPTCHA v2 | حاسبات الرسوم الجمركية | تقدير الرسوم مسبقاً |
| شركات الميل الأخير | reCAPTCHA v2 | صفحات تتبع التسليم | مراقبة الأداء |
| البريد الحكومي | CAPTCHA صوري | حاسبات الأسعار الدولية | تسعير الشحن الدولي |
سيناريو من السوق: وكالة شحن في الرياض تبني جدول أسعار أسبوعياً
فريق تسعير في وكالة شحن بالرياض يخدم متاجر إلكترونية تشحن من مستودع واحد إلى جدة والدمام والكويت والدوحة، ويحتاج كل يوم أحد إلى صورة سعرية محدثة قبل تثبيت أسعار الشحن المعروضة للعملاء.
قبل الأتمتة: موظفان يفتحان ست حاسبات يدوياً ويعيدان إدخال الوزن والأبعاد والرمز البريدي لكل مسار. أربعون مساراً تعني 240 استعلاماً يدوياً وأخطاء إدخال لا تُكتشف إلا بعد نشر السعر.
بعد الأتمتة: سكربت واحد يمر على الناقلات الست بنفس بيانات الشحنة، يحل الاختبار عند ظهوره، ويكتب النتيجة في ملف واحد مع وقت القراءة. والمكسب الحقيقي ليس السرعة بل الاتساق: كل الأسعار مقروءة داخل نفس النافذة الزمنية، فالمقارنة عادلة.
شرطان يُبقيان السيناريو في المنطقة الآمنة: اجمع ما هو معروض علناً أو ما تملك اتفاقاً على قراءته، والتزم بالوتيرة أدناه.
النمط الأول: مقارنة الأسعار عبر عدة ناقلات
فكرة الكود التالي مباشرة: دالة حل مستقلة لكل نوع اختبار، وفئة تدير الجلسة والخادم الوسيط، ثم حلقة تمر على الناقلات وترتب النتائج من الأرخص. لاحظ أن اسم حقل الرمز يتغير بحسب نوع الاختبار، وأن السكربت يترك ثلاث ثوانٍ بين ناقل وآخر.
import requests
import time
import re
import base64
from bs4 import BeautifulSoup
CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"
def solve_recaptcha(sitekey, pageurl):
resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
"key": CAPTCHAAI_KEY, "method": "userrecaptcha",
"googlekey": sitekey, "pageurl": pageurl, "json": 1,
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(5)
result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
"key": CAPTCHAAI_KEY, "action": "get",
"id": task_id, "json": 1,
})
data = result.json()
if data["request"] != "CAPCHA_NOT_READY":
return data["request"]
raise TimeoutError("Timeout")
def solve_turnstile(sitekey, pageurl):
resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
"key": CAPTCHAAI_KEY, "method": "turnstile",
"sitekey": sitekey, "pageurl": pageurl, "json": 1,
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(5)
result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
"key": CAPTCHAAI_KEY, "action": "get",
"id": task_id, "json": 1,
})
data = result.json()
if data["request"] != "CAPCHA_NOT_READY":
return data["request"]
raise TimeoutError("Timeout")
class ShippingRateScraper:
def __init__(self, proxy=None):
self.session = requests.Session()
if proxy:
self.session.proxies = {"http": proxy, "https": proxy}
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
})
def get_rate(self, carrier, shipment):
"""Get shipping rate from a single carrier."""
url = carrier["rate_url"]
resp = self.session.get(url, timeout=30)
# Handle CAPTCHA
sitekey = self._extract_sitekey(resp.text)
if sitekey:
if carrier.get("captcha_type") == "turnstile":
token = solve_turnstile(sitekey, url)
field = "cf-turnstile-response"
else:
token = solve_recaptcha(sitekey, url)
field = "g-recaptcha-response"
resp = self.session.post(url, data={
**shipment,
field: token,
})
else:
resp = self.session.post(url, data=shipment)
if resp.status_code == 200:
return self._parse_rates(resp.text)
return None
def compare_rates(self, carriers, shipment):
"""Compare rates across multiple carriers."""
results = []
for carrier in carriers:
try:
rates = self.get_rate(carrier, shipment)
results.append({
"carrier": carrier["name"],
"status": "success",
"rates": rates,
})
except Exception as e:
results.append({
"carrier": carrier["name"],
"status": "error",
"error": str(e),
})
time.sleep(3)
# Sort by cheapest
successful = [r for r in results if r["rates"]]
if successful:
successful.sort(
key=lambda x: float(
re.sub(r'[^\d.]', '', x["rates"][0].get("price", "9999"))
)
)
return {
"shipment": shipment,
"results": results,
"cheapest": successful[0] if successful else None,
}
def _extract_sitekey(self, html):
match = re.search(r'data-sitekey="([^"]+)"', html)
return match.group(1) if match else None
def _parse_rates(self, html):
soup = BeautifulSoup(html, "html.parser")
rates = []
for row in soup.select(".rate-option, .shipping-option, tr.rate"):
service = row.select_one(".service-name, td:first-child")
price = row.select_one(".price, .rate-amount, td:nth-child(2)")
eta = row.select_one(".delivery-time, .eta, td:nth-child(3)")
if service:
rates.append({
"service": service.get_text(strip=True),
"price": price.get_text(strip=True) if price else "",
"eta": eta.get_text(strip=True) if eta else "",
})
return rates
# Usage
scraper = ShippingRateScraper(
proxy="http://user:[email protected]:5000"
)
carriers = [
{"name": "Carrier A", "rate_url": "https://carrier-a.example.com/rates", "captcha_type": "recaptcha"},
{"name": "Carrier B", "rate_url": "https://carrier-b.example.com/rates", "captcha_type": "turnstile"},
{"name": "Carrier C", "rate_url": "https://carrier-c.example.com/rates", "captcha_type": "recaptcha"},
]
shipment = {
"origin_zip": "10001",
"dest_zip": "90210",
"weight": "10",
"length": "12",
"width": "8",
"height": "6",
}
comparison = scraper.compare_rates(carriers, shipment)
print(f"Cheapest: {comparison['cheapest']}")
ثلاث نقاط تستحق الانتباه:
- استخراج sitekey يتم من HTML الصفحة لا من ملف إعدادات، لأن الناقلات تغيّره دون إشعار.
- الجلسة
requests.Sessionتحمل الكوكيز بين القراءة والإرسال؛ فقدانها يعني وصول رمز محلول إلى جلسة أخرى. - كل ناقل يفشل بمعزل عن غيره: الخطأ يُسجَّل داخل النتيجة ولا يوقف بقية المقارنة.
النمط الثاني: متابعة الشحنات بالجملة
بعد تثبيت الأسعار يأتي الجزء الأطول عمراً: متابعة الشحنات الجارية. المنطق نفسه بفارق واحد — رقم التتبع هو المتغير والصفحة ثابتة، فتمر الجلسة نفسها على دفعة أرقام بفاصل زمني بينها.
class ShipmentTracker:
def __init__(self, proxy=None):
self.session = requests.Session()
if proxy:
self.session.proxies = {"http": proxy, "https": proxy}
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
})
def track(self, carrier_url, tracking_number, sitekey=None):
"""Track a single shipment."""
resp = self.session.get(carrier_url, timeout=30)
data = {"tracking_number": tracking_number}
if sitekey:
token = solve_recaptcha(sitekey, carrier_url)
data["g-recaptcha-response"] = token
resp = self.session.post(carrier_url, data=data)
if resp.status_code == 200:
soup = BeautifulSoup(resp.text, "html.parser")
return {
"tracking": tracking_number,
"status": self._text(soup, ".status, .tracking-status"),
"location": self._text(soup, ".location, .current-location"),
"eta": self._text(soup, ".eta, .estimated-delivery"),
"events": self._parse_events(soup),
}
return None
def bulk_track(self, carrier_url, tracking_numbers, sitekey=None):
"""Track multiple shipments."""
results = []
for tn in tracking_numbers:
try:
info = self.track(carrier_url, tn, sitekey)
results.append(info or {"tracking": tn, "status": "not found"})
except Exception as e:
results.append({"tracking": tn, "error": str(e)})
time.sleep(3)
return results
def _text(self, soup, selector):
el = soup.select_one(selector)
return el.get_text(strip=True) if el else ""
def _parse_events(self, soup):
events = []
for event in soup.select(".tracking-event, .event-row"):
events.append({
"date": self._text(event, ".date, .event-date"),
"description": self._text(event, ".desc, .event-desc"),
"location": self._text(event, ".loc, .event-location"),
})
return events
جدول الوتيرة: ماذا تجمع ومتى
| نوع البيانات | الوتيرة المناسبة | الحجم التقريبي |
|---|---|---|
| أسعار الطرود المحلية | أسبوعياً | 50-100 مسار |
| الأسعار الدولية | شهرياً | 20-50 وجهة |
| الشحن البري الجزئي LTL | يومياً | 10-30 مسار |
| الشحن البحري | أسبوعياً | 10-20 زوج موانئ |
| تتبع الشحنات النشطة | كل 4 ساعات | لكل شحنة قيد التسليم |
الوتيرة ليست تفصيلاً ثانوياً: رفعها بلا سبب يزيد الاختبارات الظاهرة دون معلومة جديدة، لأن أسعار المسارات المحلية نادراً ما تتحرك أكثر من مرة أسبوعياً. التتبع وحده يستحق فحصاً كل بضع ساعات.
ضبط الطلبات والخوادم الوسيطة
- ابقَ في حدود 20-30 استعلام سعر بالساعة من عنوان IP واحد، ووزّعها بدل إرسالها متتالية.
- استخدم جلسة ثابتة sticky في النماذج متعددة الخطوات؛ تغيير العنوان في منتصف التدفق يبطل الجلسة قبل وصول النتيجة.
- خزّن كل استجابة خام مع طابعها الزمني؛ عند اختلاف السعر لاحقاً، الملف الخام وحده يحسم السبب.
- افصل مفتاح الـ API عن الكود واقرأه من متغير بيئة، خصوصاً في المهام المجدولة.
كم Thread تحتاج فعلاً؟
يحتسب CaptchaAI الاشتراك على عدد الـ Threads المتزامنة — الاختبارات قيد الحل في اللحظة نفسها — لا على عدد عمليات الحل، وكل خطة تشمل حلولاً غير محدودة خلال الشهر. خطة BASIC بسعر 15 دولاراً شهرياً تمنح 5 Threads، وSTANDARD بسعر 30 دولاراً تمنح 15 Thread، وADVANCE بسعر 90 دولاراً تمنح 50 Thread.
عملياً في سيناريو الوكالة أعلاه: ست ناقلات بالتوازي لا تزيد على ستة اختبارات متزامنة، وهو ما تغطيه STANDARD مع هامش لإعادة المحاولة. أما متابعة آلاف الشحنات كل أربع ساعات فتستفيد من ADVANCE لتداخل الدفعات.
على مستوى الأنواع: تُحَل اختبارات reCAPTCHA v2 وv3 وCloudflare Turnstile وCloudflare Challenge وGeeTest v3 والاختبارات الصورية وشبكات الصور وBLS. في المقابل، لا تُدعَم hCaptcha ولا FunCaptcha، ودعم GeeTest v4 معلن كقادم قريباً وغير متاح بعد؛ فإن اعتمدت بوابة ناقل أحد هذه الأنواع، فالمسار البديل هو واجهة الناقل الرسمية.
أخطاء شائعة أثناء التشغيل
| العَرَض | السبب الأرجح | المعالجة |
|---|---|---|
| حاسبة السعر ترجع صفحة فارغة | إرسال النموذج قبل إدراج رمز الحل | احلّ الاختبار أولاً ثم أرسل النموذج بالحقل الصحيح |
| صفحة التتبع تقول «غير موجود» | تنسيق خاطئ للرقم أو مسح لم يُسجَّل بعد | راجع التنسيق وأعد المحاولة لاحقاً |
| السعر يختلف عن الموقع | رسوم إضافية غير محسوبة مثل رسوم الوقود | اقرأ حقول الرسوم الإضافية وأضفها للإجمالي |
| انتهاء مهلة الجلسة في نموذج السعر | تدفق متعدد الخطوات استغرق أطول من المهلة | ثبّت الجلسة عبر خادم وسيط ثابت وقلّل زمن الحل |
| الناقل يحجب عنوان IP | استعلامات كثيرة في نافذة قصيرة | خفّض الوتيرة ووزّع الطلبات ونوّع مصدر الخروج |
الأسئلة الشائعة
هل جمع أسعار الشحن من مواقع الناقلات مسموح؟
البيانات المعروضة علناً على الحاسبة شيء، وما يقع خلف تسجيل الدخول شيء آخر. اقتصر على الصفحات العامة أو حسابات تملكها باتفاق مع الناقل، والتزم بشروط الاستخدام وبوتيرة معقولة.
متى أستخدم واجهة الناقل الرسمية بدل قراءة الحاسبة؟
كلما كانت متاحة: فهي أدق وأسرع ولا تعرض اختبار تحقق أصلاً. تبقى قراءة الحاسبة مفيدة حين لا يوفر الناقل واجهة عامة، أو حين تحتاج السعر المعروض للعميل النهائي لا التعاقدي.
لماذا يظهر الاختبار أحياناً ويختفي أحياناً على الصفحة نفسها؟
لأن القرار سلوكي: سمعة عنوان IP، وعدد الطلبات الأخيرة، وكوكيز جلسة سابقة. لهذا يفحص السكربت وجود sitekey في كل مرة بدل افتراض حالة ثابتة.
كيف أتعامل مع اختلاف السعر بين قراءتين متتاليتين؟
سجّل الطابع الزمني والوزن الحجمي والرسوم الإضافية مع كل قراءة، وقارن قراءات من نفس النافذة الزمنية فقط. الأسعار الدولية تتحرك مع رسوم الوقود والموسم، فالمقارنة المتباعدة تضلّل.
اقرأ أيضاً
افتح حساب CaptchaAI وشغّل أول مقارنة أسعار بين الناقلات اليوم.