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
- Início rápido do CaptchaAI: sua primeira resolução de CAPTCHA em 5 minutos
- Como resolver reCAPTCHA v2 com a API: guia passo a passo
- Como resolver Cloudflare Turnstile pela API
- Como resolver GeeTest v3 usando API