دروس API

أفضل ممارسات ترميز الصور CAPTCHA Base64

معظم حالات فشل حل صور CAPTCHA لا تأتي من الخدمة نفسها، بل من سلسلة base64 مُشوّهة قبل أن تصل إليها. بادئة زائدة أو ترميز مزدوج أو بايت واحد مفقود يكفي لإرجاع رمز خطأ أو إجابة غير صحيحة. الحل بسيط: رمّز الصورة مرة واحدة بالطريقة الصحيحة، وتحقّق من الحمولة قبل إرسالها إلى CaptchaAI عبر المعلمة method=base64. يشرح هذا الدليل هذه الخطوات في Python مع أمثلة جاهزة للنسخ.

تخيّل مطوّراً في القاهرة يبني أداة لمراقبة أسعار متجر خليجي يعرض في صفحة تسجيل الدخول صورة CAPTCHA نصية. لو أرسل السلسلة كما نسخها من المتصفح — مع بادئة data:image/png;base64, — فسيحصل على أخطاء متكررة رغم أن الصورة سليمة تماماً. الفرق بين تدفّق مستقر وآخر مليء بالأخطاء هو غالباً بضعة أسطر في مرحلة الترميز، لا في مرحلة الحل.


كيف يستقبل CaptchaAI صور CAPTCHA بصيغة Base64

يستقبل CaptchaAI صور CAPTCHA عبر نقطة النهاية in.php عندما تضبط المعلمة method=base64، فتُرسل محتوى الصورة كسلسلة نصية داخل الحقل body بدلاً من رفع ملف. هذا الأسلوب يناسب لقطات الشاشة والصور المُولّدة في الذاكرة دون كتابتها على القرص أولاً. النقطة الجوهرية أن يحتوي الحقل body على بايتات الصورة المُرمّزة بـ base64 فقط، فتجنّب إرسال:

  • بادئة data:image/...;base64, قبل البيانات.
  • أي مسافات أو أسطر جديدة داخل السلسلة.
  • سلسلة مُرمّزة مرتين أو نصاً غير مُرمّز أصلاً.
import requests
import base64
import os


def submit_image_captcha(image_base64):
    """Submit base64-encoded image to CaptchaAI."""
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": os.environ["CAPTCHAAI_API_KEY"],
        "method": "base64",
        "body": image_base64,
        "json": 1,
    }, timeout=30)
    return resp.json()

ترميز صورة محفوظة على القرص

هذه أبسط حالة: صورة CAPTCHA نزّلتها أو حفظها السكربت مسبقاً. القاعدة الوحيدة التي يجب الالتزام بها هي فتح الملف في الوضع الثنائي (rb) لا الوضع النصي، لأن قراءة بيانات ثنائية كنص تُفسدها قبل الترميز. بعد القراءة، يحوّل base64.b64encode البايتات إلى سلسلة، ونستخدم decode("ascii") لتحويلها من نوع bytes إلى نص عادي جاهز للإرسال:

# from_file.py
import base64


def encode_from_file(filepath):
    """Read an image file and return base64 string."""
    with open(filepath, "rb") as f:
        raw = f.read()
    return base64.b64encode(raw).decode("ascii")


# Usage
b64 = encode_from_file("captcha.png")
print(f"Encoded length: {len(b64)} chars")

ترميز صورة من رابط مباشر

كثير من المواقع تعرض صورة CAPTCHA على رابط مستقل يمكن تنزيله مباشرة. قبل الترميز، تحقّق من ترويسة Content-Type للتأكد أن ما نزّلته صورة فعلاً وليس صفحة خطأ HTML أو استجابة إعادة توجيه — فتمرير محتوى غير صورة إلى الخدمة يهدر رصيدك ويعيد أخطاء يصعب تفسيرها. الدالة التالية تُنزّل الصورة، تتحقق من نوعها، ثم تُعيد سلسلة base64 نظيفة:

# from_url.py
import requests
import base64


def encode_from_url(image_url):
    """Download image and return base64 string."""
    resp = requests.get(image_url, timeout=15)
    resp.raise_for_status()

    # Verify it's actually an image
    content_type = resp.headers.get("Content-Type", "")
    if not content_type.startswith("image/"):
        raise ValueError(f"Not an image: {content_type}")

    return base64.b64encode(resp.content).decode("ascii")


# Usage
b64 = encode_from_url("https://example.com/captcha.png")

ترميز لقطة شاشة من Selenium

أحياناً لا يكون للصورة رابط مباشر، بل تُرسم داخل الصفحة كعنصر canvas أو صورة مُضمّنة. في هذه الحالة نلتقط لقطة شاشة للعنصر نفسه. الطريقة الأولى تستخدم الخاصية screenshot_as_base64 التي تُعيد السلسلة جاهزة دون أي معالجة إضافية. الطريقة الثانية تلتقط لقطة للصفحة كاملة ثم تقتصّ حدود العنصر باستخدام Pillow — وهي مفيدة حين لا يدعم المتصفح لقطة العنصر المفردة أو حين تحتاج إلى هامش حول الصورة:

# from_selenium.py
import base64
from selenium.webdriver.common.by import By


def encode_from_element(driver, selector):
    """Screenshot a specific element and return base64."""
    element = driver.find_element(By.CSS_SELECTOR, selector)
    screenshot_b64 = element.screenshot_as_base64
    return screenshot_b64


def encode_from_page_crop(driver, selector):
    """Crop a specific region from the page screenshot."""
    from PIL import Image
    import io

    element = driver.find_element(By.CSS_SELECTOR, selector)
    location = element.location
    size = element.size

    # Full page screenshot
    png = driver.get_screenshot_as_png()
    img = Image.open(io.BytesIO(png))

    # Crop to element bounds
    left = location["x"]
    top = location["y"]
    right = left + size["width"]
    bottom = top + size["height"]
    cropped = img.crop((left, top, right, bottom))

    # Encode
    buffer = io.BytesIO()
    cropped.save(buffer, format="PNG")
    return base64.b64encode(buffer.getvalue()).decode("ascii")

ثلاثة أخطاء ترميز تُفشل الإرسال

الأخطاء الثلاثة التالية مسؤولة عن الغالبية العظمى من رسائل ERROR_WRONG_FILE_EXTENSION والإجابات الخاطئة. جميعها تحدث في مرحلة الترميز، أي قبل أن تلمس الخدمة الصورة إطلاقاً، ولهذا يصعب تشخيصها إن لم تعرف أين تبحث.

الخطأ الأول: ترك بادئة data URI

حين تنسخ صورة من المتصفح أو من عنصر canvas، تأتي غالباً على شكل data:image/png;base64,.... الجزء الذي يسبق الفاصلة ليس جزءاً من بيانات الصورة، ويجب حذفه قبل الإرسال:

# WRONG — includes data URI prefix
bad = "data:image/png;base64,iVBORw0KGgo..."

# RIGHT — raw base64 only
good = "iVBORw0KGgo..."

# Fix: Strip the prefix
def clean_base64(b64_string):
    if "," in b64_string:
        return b64_string.split(",", 1)[1]
    return b64_string

الخطأ الثاني: الترميز المزدوج

بعض الدوال، مثل screenshot_as_base64، تُعيد السلسلة مُرمّزة بالفعل. تمريرها مرة أخرى إلى b64encode يُنتج سلسلة مزدوجة الترميز لا تُمثّل أي صورة صالحة:

# WRONG — encoding an already-encoded string
already_b64 = element.screenshot_as_base64
double_encoded = base64.b64encode(already_b64.encode()).decode()  # BAD

# RIGHT — use as-is
correct = element.screenshot_as_base64  # Already base64

الخطأ الثالث: قراءة الملف كنص بدل بايتات

فتح ملف صورة في الوضع النصي (r) يُفسد البايتات الثنائية بسبب محاولة فك ترميز المحارف. استخدم دائماً الوضع الثنائي (rb) عند قراءة الصور:

# WRONG — reading as text
with open("captcha.png", "r") as f:  # Text mode
    content = f.read()  # Corrupted binary data

# RIGHT — reading as bytes
with open("captcha.png", "rb") as f:  # Binary mode
    content = f.read()
encoded = base64.b64encode(content).decode("ascii")

تحقّق من الصورة قبل الإرسال

بدل انتظار رمز خطأ من الخادم، افحص السلسلة محلياً أولاً. الدالة التالية تكشف بادئة data URI، تحاول فك الترميز للتأكد من صلاحيته، تقيس الحجم، وتتعرّف على تنسيق الصورة من بايتاتها الأولى (التوقيع السحري). دمج هذا الفحص في مسار الإرسال يوفّر رصيدك ويحوّل الأخطاء الغامضة إلى رسائل واضحة قابلة للتصحيح:

# validate.py
import base64
import io


def validate_captcha_image(b64_string):
    """Validate base64 image before submitting to CaptchaAI."""
    errors = []

    # Check for data URI prefix
    if b64_string.startswith("data:"):
        errors.append("Contains data URI prefix — strip it")
        b64_string = b64_string.split(",", 1)[1]

    # Try decoding
    try:
        decoded = base64.b64decode(b64_string)
    except Exception as e:
        return {"valid": False, "errors": [f"Invalid base64: {e}"]}

    # Check size
    size_kb = len(decoded) / 1024
    if size_kb < 1:
        errors.append(f"Image too small ({size_kb:.1f} KB) — likely corrupt")
    if size_kb > 500:
        errors.append(f"Image large ({size_kb:.1f} KB) — consider resizing")

    # Check image format
    if decoded[:8] == b'\x89PNG\r\n\x1a\n':
        fmt = "PNG"
    elif decoded[:3] == b'\xff\xd8\xff':
        fmt = "JPEG"
    elif decoded[:4] == b'GIF8':
        fmt = "GIF"
    elif decoded[:4] == b'RIFF':
        fmt = "WEBP"
    else:
        errors.append("Unknown image format")
        fmt = "unknown"

    return {
        "valid": len(errors) == 0,
        "format": fmt,
        "size_kb": round(size_kb, 1),
        "errors": errors,
    }


# Usage
result = validate_captcha_image(b64_string)
if not result["valid"]:
    print(f"Issues: {result['errors']}")
else:
    print(f"Valid {result['format']}, {result['size_kb']} KB")

اختيار تنسيق الصورة المناسب

التنسيق الذي تختاره يؤثر مباشرة في وضوح الأحرف، وبالتالي في دقة الحل. الجدول التالي يلخّص متى تستخدم كل تنسيق:

التنسيق الأنسب لـ الحجم الجودة
PNG اختبارات CAPTCHA النصية ولقطات الشاشة أكبر بدون فقدان
JPEG اختبارات CAPTCHA المبنية على صور فوتوغرافية أصغر بفقدان (استخدم جودة ≥ 85)
GIF رسوم التحقق المتحركة متغيّر ألوان محدودة
WEBP المتصفحات الحديثة الأصغر جودة جيدة

التوصية: استخدم PNG لاختبارات CAPTCHA النصية. يحافظ الضغط بدون فقدان على حواف الأحرف الدقيقة، وهو ما يرفع دقة الحل. احتفظ بـ JPEG للصور الفوتوغرافية حيث يكون فرق الحجم كبيراً وتأثير الفقدان محدوداً.

نصيحة: اجعل validate_captcha_image() بوابة إلزامية قبل كل إرسال؛ فهو يحوّل أخطاء الخادم الغامضة إلى رسائل محلية واضحة توفّر عليك رصيداً ووقتاً.


معالجة رموز الأخطاء الشائعة

عندما يرفض الخادم الحمولة، يعيد رمز خطأ محدداً يشير مباشرة إلى السبب. هذا الجدول يربط كل رمز بمصدره وحلّه:

المشكلة السبب الإجراء
ERROR_WRONG_FILE_EXTENSION بيانات base64 غير صالحة تحقّق منها عبر validate_captcha_image()
ERROR_TOO_BIG_CAPTCHA_FILESIZE حجم الصورة أكبر من 600 كيلوبايت غيّر الحجم أو اضغط قبل الترميز
ERROR_ZERO_CAPTCHA_FILESIZE صورة فارغة أو تالفة تأكّد من نجاح التنزيل قبل الترميز
نتيجة حل خاطئة JPEG مضغوط بإفراط استخدم PNG أو جودة JPEG ≥ 85

سيناريو عملي: إرسال دفعة صور في وقت واحد

عند مراقبة عدة صفحات في آنٍ واحد، لن ترسل صورة واحدة بل عشرات. هنا يظهر دور نموذج تسعير CaptchaAI القائم على الـ Thread: كل thread يعالج اختبار CAPTCHA واحداً في اللحظة نفسها، وبمجرد انتهاء الحل يتحرّر ليأخذ التالي. خطة BASIC ‏($15 شهرياً، 5 threads) تسمح بخمس عمليات حل متزامنة مع حلول غير محدودة خلال الشهر، بينما تمنحك خطة ADVANCE ‏($90 شهرياً، 50 thread) هامشاً أوسع للحملات الكبيرة.

عملياً، اتبع هذا الترتيب:

  1. رمّز كل صورة عبر إحدى الدوال أعلاه.
  2. مرّرها على validate_captcha_image() واحتفظ بالصالحة فقط.
  3. أرسل الصور عبر عمّال متوازين بعدد لا يتجاوز عدد الـ threads في خطتك.

بهذا تتجنّب هدر الرصيد على صور تالفة وتحافظ على ثبات معدل الإرسال.


الأسئلة الشائعة

لماذا يعيد CaptchaAI إجابة خاطئة رغم أن الصورة تبدو صحيحة؟

غالباً يكون السبب في السلسلة لا في الصورة: بادئة data: لم تُحذف، أو السلسلة مُرمّزة مرتين. مرّر الحمولة على validate_captcha_image() أولاً، وتأكّد أنك لا تُرمّز ناتجاً مُرمّزاً بالفعل مثل screenshot_as_base64.

هل أحتاج إلى ضغط الصورة أو تغيير حجمها قبل الترميز؟

فقط إذا تجاوزت 600 كيلوبايت بعد فك الترميز، أو إذا كانت لقطة الشاشة أكبر بكثير من منطقة الـ CAPTCHA. اقتصّ العنصر المطلوب وأبقِ الصورة صغيرة وواضحة؛ الحجم الكبير لا يعني دقة أعلى.

كيف أُرسل عدة صور CAPTCHA في الوقت نفسه؟

شغّل عمّالاً متوازين بعدد لا يتجاوز عدد الـ threads في خطتك — خمسة في خطة BASIC ‏($15 شهرياً). كل thread يعالج صورة واحدة في اللحظة، والحلول غير محدودة داخل الخطة.

هل يمكنني إرسال صور بصيغة SVG مباشرة؟

لا. حوّل SVG إلى PNG أولاً عبر مكتبة مثل Pillow أو cairosvg، ثم رمّز الناتج بـ base64.

ماذا يعني الخطأ ERROR_ZERO_CAPTCHA_FILESIZE وكيف أُصلحه؟

يعني أن الحمولة المُرسلة فارغة أو تالفة، وغالباً بسبب فشل التنزيل أو ملف بحجم صفر. تحقّق من نجاح خطوة التنزيل ومن أن len(decoded) أكبر من صفر قبل الإرسال.


أدلة ذات صلة


رمّز صور CAPTCHA بالشكل الصحيح وأرسلها بثقة — ابدأ مع CaptchaAI.

التعليقات غير مفعّلة لهذا المقال.