حالات الاستخدام

حل كابتشا بوابات GIS واستخراج بيانات الخرائط والطرود

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

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

متى تظهر الكابتشا على بوابات GIS

قبل كتابة أي كود، من المفيد معرفة أي نوع تحقق يستخدمه كل نوع بوابة وما الذي يستدعيه، لأن ذلك يحدد المعلمات التي ستمررها لاحقًا:

نوع البوابة نوع التحقق ما يستدعيه
مقاطعة GIS/assessor صورة نص CAPTCHA استعلامات البحث عن الطرود
البوابات الجغرافية المكانية للدولة اختبار CAPTCHA المخصص طلبات تنزيل البيانات
بوابات بيانات هيئة المسح الجيولوجي الأمريكية reCAPTCHA v2 الوصول إلى البيانات بالجملة
خرائط تقسيم البلديات صورة التحقق عمليات البحث المتكررة عن الممتلكات
قواعد البيانات البيئية الرياضيات كابتشا توليد التقرير
بحث منطقة الفيضانات صورة نص CAPTCHA استعلامات العنوان

الغالبية العظمى من هذه البوابات تعتمد كابتشا صور بسيطة، وهو بالضبط ما تحلّه واجهة OCR في CaptchaAI عبر method=base64.

سيناريو عملي: بيانات الطرود لتحليل عقاري

لنفترض أن فريق proptech في المنطقة يبني نموذجًا لتقييم العقارات ويحتاج إلى حدود القطع وتصنيف المناطق من بوابة مقاطعة أمريكية تفرض كابتشا صور على كل عملية بحث. بدل إدخال آلاف الطرود يدويًا، يمرّر الفريق قائمة معرّفات القطع عبر دالة bulk_extract، ويتكفّل CaptchaAI بحل صورة الكابتشا في كل طلب، مع مهلة قصيرة بين الطلبات تحافظ على استقرار الجلسة. النتيجة مسار جمع موثّق يعمل ضمن البيانات العامة المصرّح بها فقط، وقابل للتكرار كلما احتاج الفريق إلى تحديث النموذج.

مستخرج بيانات GIS بلغة Python

يبدأ المسار بجلسة requests تحمل معرّف القطعة أو العنوان، وتكتشف وجود صورة كابتشا، وتحلّها عبر CaptchaAI، ثم تعيد إرسال النموذج مع النص الناتج والحقول المخفية:

import requests
import base64
import time
import re

class GISDataExtractor:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def lookup_parcel(self, portal_url, parcel_id):
        """Look up parcel data by ID, solving CAPTCHAs as needed."""
        response = self.session.get(
            f"{portal_url}/parcel", params={"id": parcel_id}
        )

        if self._has_image_captcha(response.text):
            captcha_url = self._extract_captcha_url(response.text, portal_url)
            captcha_text = self._solve_captcha(captcha_url)

            # Re-submit with solved CAPTCHA
            response = self.session.post(f"{portal_url}/parcel", data={
                "id": parcel_id,
                "captcha": captcha_text,
                **self._extract_hidden_fields(response.text)
            })

        return self._parse_parcel_data(response.text)

    def search_by_address(self, portal_url, address):
        """Search GIS records by street address."""
        response = self.session.get(
            f"{portal_url}/search", params={"address": address}
        )

        if self._has_image_captcha(response.text):
            captcha_url = self._extract_captcha_url(response.text, portal_url)
            captcha_text = self._solve_captcha(captcha_url)

            response = self.session.post(f"{portal_url}/search", data={
                "address": address,
                "captcha": captcha_text,
                **self._extract_hidden_fields(response.text)
            })

        return self._parse_search_results(response.text)

    def bulk_extract(self, portal_url, parcel_ids, delay=3):
        """Extract data for multiple parcels with rate limiting."""
        results = {}

        for parcel_id in parcel_ids:
            try:
                results[parcel_id] = self.lookup_parcel(portal_url, parcel_id)
            except Exception as e:
                results[parcel_id] = {"error": str(e)}
            time.sleep(delay)

        return results

    def _has_image_captcha(self, html):
        return bool(re.search(
            r'captcha|verification.?image|security.?code',
            html, re.IGNORECASE
        ))

    def _extract_captcha_url(self, html, base_url):
        from bs4 import BeautifulSoup
        from urllib.parse import urljoin
        soup = BeautifulSoup(html, "html.parser")

        img = (
            soup.find("img", attrs={"src": lambda s: s and "captcha" in s.lower()}) or
            soup.find("img", {"id": re.compile(r"captcha", re.I)}) or
            soup.find("img", {"class": re.compile(r"captcha", re.I)})
        )

        if img and img.get("src"):
            return urljoin(base_url, img["src"])
        raise ValueError("CAPTCHA image not found")

    def _solve_captcha(self, captcha_url):
        """Download and solve image CAPTCHA."""
        img_response = self.session.get(captcha_url)
        img_base64 = base64.b64encode(img_response.content).decode("utf-8")

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "base64",
            "body": img_base64,
            "json": 1
        })
        task_id = resp.json()["request"]

        for _ in range(30):
            time.sleep(3)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = result.json()
            if data["status"] == 1:
                return data["request"]

        raise TimeoutError("CAPTCHA solve timed out")

    def _extract_hidden_fields(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        fields = {}
        for inp in soup.select("input[type='hidden']"):
            name = inp.get("name")
            if name:
                fields[name] = inp.get("value", "")
        return fields

    def _parse_parcel_data(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_of(*selectors):
            for selector in selectors:
                element = soup.select_one(selector)
                if element:
                    return element.get_text(strip=True)
            return ""

        return {
            "parcel_id": text_of(".parcel-id", "#parcelId"),
            "owner": text_of(".owner", ".owner-name"),
            "address": text_of(".address", ".situs"),
            "zoning": text_of(".zoning", ".zone-code"),
            "acreage": text_of(".acreage", ".area"),
            "assessed_value": text_of(".assessed", ".value"),
            "land_use": text_of(".land-use", ".use-code"),
        }

    def _parse_search_results(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        results = []
        for row in soup.select(".result-row, tr.parcel"):
            def row_text(*selectors):
                for selector in selectors:
                    element = row.select_one(selector)
                    if element:
                        return element.get_text(strip=True)
                return ""

            results.append({
                "parcel_id": row_text(".parcel-id"),
                "address": row_text(".address"),
                "owner": row_text(".owner"),
            })
        return results


# Usage
extractor = GISDataExtractor("YOUR_API_KEY")

# Single parcel lookup
parcel = extractor.lookup_parcel(
    "https://gis.county.example.gov",
    "12-34-567-890"
)
print(f"Owner: {parcel['owner']}, Zoning: {parcel['zoning']}")

# Bulk extraction
parcels = extractor.bulk_extract(
    "https://gis.county.example.gov",
    ["12-34-567-890", "12-34-567-891", "12-34-567-892"]
)

الاستخراج حسب الإحداثيات بلغة JavaScript

عندما تكون البوابة قائمة على الخريطة بدل البحث النصي، تستعلم عن نقطة lat/lng مباشرة، أو تمسح مستطيلًا كاملًا بحدود معدل بين كل نقطة والأخرى:

class GISExtractor {
  constructor(apiKey) {
    this.apiKey = apiKey;
  }

  async extractByCoordinates(portalUrl, lat, lng) {
    const url = `${portalUrl}/identify?lat=${lat}&lng=${lng}`;
    const response = await fetch(url);
    const html = await response.text();

    if (this.hasCaptcha(html)) {
      return this.solveAndExtract(portalUrl, html, { lat, lng });
    }

    return this.parseGISData(html);
  }

  async extractRegion(portalUrl, bounds, gridSize = 0.01) {
    const results = [];
    const { north, south, east, west } = bounds;

    for (let lat = south; lat <= north; lat += gridSize) {
      for (let lng = west; lng <= east; lng += gridSize) {
        try {
          const data = await this.extractByCoordinates(portalUrl, lat, lng);
          if (data.parcelId) results.push(data);
        } catch (error) {
          console.error(`Failed at ${lat},${lng}: ${error.message}`);
        }
        // Rate limit
        await new Promise(r => setTimeout(r, 2000));
      }
    }

    return results;
  }

  hasCaptcha(html) {
    return /captcha|verification.?image|security.?code/i.test(html);
  }

  async solveAndExtract(portalUrl, html, params) {
    const imgMatch = html.match(/src="([^"]*captcha[^"]*)"/i);
    if (!imgMatch) throw new Error('CAPTCHA image not found');

    const imgUrl = new URL(imgMatch[1], portalUrl).href;
    const imgResp = await fetch(imgUrl);
    const buffer = await imgResp.arrayBuffer();
    const base64 = Buffer.from(buffer).toString('base64');

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'base64',
        body: base64,
        json: '1'
      })
    });
    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 30; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const result = await fetch(
        `https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const data = await result.json();
      if (data.status === 1) {
        const response = await fetch(portalUrl, {
          method: 'POST',
          body: new URLSearchParams({
            ...params,
            captcha: data.request
          })
        });
        return this.parseGISData(await response.text());
      }
    }
    throw new Error('CAPTCHA solve timed out');
  }

  parseGISData(html) {
    return {
      parcelId: html.match(/parcel.?id[^>]*>([^<]+)/i)?.[1]?.trim(),
      zoning: html.match(/zon(?:e|ing)[^>]*>([^<]+)/i)?.[1]?.trim(),
      acreage: html.match(/acreage|area[^>]*>([^<]+)/i)?.[1]?.trim(),
      landUse: html.match(/land.?use[^>]*>([^<]+)/i)?.[1]?.trim()
    };
  }
}

// Usage
const gis = new GISExtractor('YOUR_API_KEY');

// Single coordinate lookup
const data = await gis.extractByCoordinates(
  'https://gis.county.example.gov',
  34.0522, -118.2437
);

// Extract entire region
const region = await gis.extractRegion('https://gis.county.example.gov', {
  north: 34.10, south: 34.00, east: -118.20, west: -118.30
});

ضبط معلمات الكابتشا لبوابات GIS

دقة النص الناتج تتحسّن كثيرًا عندما تخبر الخدمة بما تتوقعه من الصورة: عدد الأحرف، هل هي أرقام فقط، وما تعليمات التنسيق. هذه أكثر المعلمات فائدة على بوابات GIS:

المعلمة القيمة حالة الاستخدام
method base64 الصورة القياسية CAPTCHA
numeric 1 اختبارات CAPTCHA الرقمية فقط
min_len 4 عند معرفة عدد الأحرف
max_len 6 عند معرفة عدد الأحرف
language 0 أحرف /Latin الإنجليزية
textinstructions مخصص Math CAPTCHAs أو الرموز المنسقة

حل المشكلات الشائعة

معظم حالات الفشل ليست في الحل نفسه بل في إدارة الجلسة والحقول المخفية. هذا الجدول يربط كل عرض بسببه وإجرائه:

المشكلة السبب الإجراء
تحميل صورة CAPTCHA معطل مطلوب ملف تعريف الارتباط للجلسة حمّل صفحة البحث أولاً
تم رفض النص الذي تم حله حساسية الحالة أضف معلمة case_sensitive=1
تعيد البوابة اختبار CAPTCHA مختلفًا اختبار CAPTCHA خاص بالجلسة نزّل الصورة واحلها في الجلسة نفسها
لا تظهر بيانات الطرود بعد CAPTCHA حقول النموذج المخفية مفقودة استخرج كل المدخلات المخفية قبل الإرسال

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

هل يحل CaptchaAI reCAPTCHA v2 الذي تستخدمه بعض بوابات USGS؟

نعم. reCAPTCHA v2 من الأنواع المدعومة بالكامل في CaptchaAI، وله مسار حل مستقل عن كابتشا الصور. عندما تصادف بوابة تعتمد reCAPTCHA v2 بدل صورة نصية، استخدم مسار reCAPTCHA المخصّص بدل مسار OCR الموضّح هنا.

كم عدد الـ threads الذي أحتاجه لاستخراج دفعات كبيرة من الطرود؟

فوترة CaptchaAI قائمة على الـ threads المتزامنة لا على عدد عمليات الحل، مع حلول غير محدودة لكل thread خلال الشهر. تبدأ الخطط من BASIC بسعر ‎$15 شهريًا و5 threads، وترتفع أعداد الـ threads مع الخطط الأعلى. للاستخراج الدفعي المتسلسل مع مهلة بين الطلبات، يكفي عدد صغير من الـ threads؛ ووجود threads أكثر يسمح بمعالجة عدة طرود بالتوازي.

كيف أتجنّب حظر الجلسة أثناء المسح الجغرافي التدريجي؟

اجعل حدود المعدل محافظة: أبقِ المهلة بين الطلبات (delay في Python وsetTimeout في JavaScript)، ووسّع نطاق gridSize عند مسح المستطيلات الكبيرة، وأعِد استخدام الجلسة نفسها بدل فتح اتصال جديد لكل طلب. الهدف أن يبدو نمط الطلبات قريبًا من استخدام بشري معقول، فلا يصبح الجمع نفسه سببًا دائمًا لظهور الكابتشا.

هل يتعامل CaptchaAI مع الكابتشا الرقمية والمعادلات الرياضية على هذه البوابات؟

نعم. للكابتشا الرقمية فقط مرّر numeric=1، ولكابتشا الرياضيات استخدم textinstructions لوصف المطلوب — مثل «احسب ناتج المعادلة» — فتعيد الخدمة القيمة النهائية جاهزة للإرسال بدل النص الحرفي.

يمكنك عبر CaptchaAI بناء مسار جمع GIS محافظ يبدأ بطلبات فردية موثّقة ثم يتوسّع تدريجيًا إلى دفعات أكبر من دون التضحية بجلسات البوابة أو دقة النتائج.


الخطوات التالية

أدلة ذات صلة

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