Casos de Uso

Manipulação de CAPTCHA para extração de dados GIS e mapeamento

Portais GIS governamentais, sistemas de avaliação de condados e plataformas de mapeamento protegem consultas geoespaciais com CAPTCHAs de imagem e OCR. Esses portais servem limites de parcelas, designações de zoneamento, zonas de inundação e avaliações de propriedades – dados que são valiosos para análise imobiliária, planejamento urbano e pesquisa ambiental. Veja como lidar com os CAPTCHAs.

Padrões CAPTCHA em portais GIS

Tipo de portal Tipo CAPTCHA Gatilho
Condado GIS/assessor Texto da imagem CAPTCHA Consultas de pesquisa de pacotes
Portais geoespaciais estaduais CAPTCHA personalizado Solicitações de download de dados
Portais de dados do USGS reCAPTCHA v2 Acesso a dados em massa
Mapas de zoneamento municipal Imagem CAPTCHA Pesquisas repetidas de propriedades
Bancos de dados ambientais Matemática CAPTCHA Geração de relatórios
Pesquisa de zona de inundação Texto da imagem CAPTCHA Consultas de endereço

Extrator de dados GIS

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_or_none(node):
            return node.text.strip() if node and node.text else None

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

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

        def text_or_none(node):
            return node.text.strip() if node and node.text else None

        results = []
        for row in soup.select(".result-row, tr.parcel"):
            results.append({
                "parcel_id": text_or_none(row.select_one(".parcel-id")),
                "address": text_or_none(row.select_one(".address")),
                "owner": text_or_none(row.select_one(".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"]
)

Extração baseada em coordenadas (JavaScript)

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
});

Parâmetros CAPTCHA para portais GIS

Parâmetro Valor Caso de uso
method base64 Imagem padrão CAPTCHA
numeric 1 CAPTCHAs somente numéricos
min_len 4 Quando a contagem de caracteres é conhecida
max_len 6 Quando a contagem de caracteres é conhecida
language 0 Caracteres Portuguese/Latin
textinstructions Personalizado CAPTCHAs matemáticos ou códigos formatados

Lista de verificação de extração pré-lote

  • Verifique a janela de visualização do mapa, o filtro de região e os controles de paginação antes de iniciar uma grande execução de coleta.
  • Armazene a carga útil das coordenadas normalizadas e a resposta do alvo bruto para que os bugs de extração permaneçam depuráveis.
  • Pause o lote quando a densidade do CAPTCHA aumentar, em vez de permitir que novas tentativas ocultem uma mudança de comportamento do alvo.

Solução de problemas

Problema Causa Correção
Carregamento de imagem CAPTCHA quebrado Cookie de sessão obrigatório Carregue a página de pesquisa primeiro
Texto resolvido rejeitado Sensibilidade a maiúsculas e minúsculas Adicionar parâmetro case_sensitive=1
Portal retorna CAPTCHA diferente CAPTCHA específico da sessão Baixe e resolva na mesma sessão
Nenhum dado de pacote após CAPTCHA Campos de formulário ocultos ausentes Extraia todas as entradas ocultas antes de enviar

Perguntas frequentes

Por que os portais GIS usam CAPTCHAs de imagem antigos?

Os sistemas GIS governamentais são frequentemente construídos em plataformas legadas que são anteriores aos serviços CAPTCHA modernos. As restrições orçamentais e os longos ciclos de aquisição significam que estes CAPTCHAs mais antigos persistem.

Como devo lidar com formatos CAPTCHA específicos do condado?

Cada condado pode usar diferentes implementações de CAPTCHA. Use o parâmetro textinstructions de CaptchaAI para descrever o formato específico - por exemplo, "5 letras maiúsculas" ou "resolver a equação matemática".

Posso extrair dados shapefile ou GeoJSON por trás de CAPTCHAs?

Caso o portal ofereça dados espaciais para download por trás de um CAPTCHA, resolva o CAPTCHA para acessar o link de download. CaptchaAI lida com o CAPTCHA; em seguida, baixe o arquivo normalmente.

Próximas etapas

Extraia dados GIS de forma confiável -obtenha sua chave API CaptchaAIe lidar automaticamente com CAPTCHAs de portais governamentais.


Próximos passos

Os comentários estão desativados para este artigo.