Un portail SIG affiche un CAPTCHA image juste avant de livrer un plan de parcelle, un zonage ou une zone inondable : c'est presque toujours le seul point de friction qui bloque l'automatisation d'une extraction géospatiale. CaptchaAI résout ces CAPTCHA image et OCR via son API, ce qui vous laisse récupérer limites cadastrales, désignations de zonage et évaluations foncières sans intervention manuelle. Voici comment brancher cette résolution sur un extracteur Python ou JavaScript, portail par portail.
Où les CAPTCHA apparaissent sur les portails SIG
Que vous interrogiez un cadastre de comté nord-américain, un Géoportail de l'urbanisme ou une base régionale, le déclencheur du CAPTCHA suit presque toujours les mêmes schémas. Les données concernées alimentent des usages à forte valeur :
- limites de parcelles et références cadastrales, pour l'analyse immobilière ;
- désignations de zonage et PLU, pour l'urbanisme et l'instruction de permis ;
- zones inondables et bases environnementales, pour l'évaluation des risques.
| Type de portail | Type de CAPTCHA | Déclencheur |
|---|---|---|
| SIG / cadastre de comté | CAPTCHA image texte | Requêtes de recherche de parcelles |
| Portails géospatiaux régionaux | CAPTCHA personnalisé | Demandes de téléchargement de données |
| Portails de données USGS | reCAPTCHA v2 | Accès aux données en masse |
| Cartes de zonage municipal (PLU) | CAPTCHA image | Consultations répétées de propriétés |
| Bases de données environnementales | CAPTCHA mathématique | Génération de rapports |
| Recherche de zone inondable | CAPTCHA image texte | Requêtes par adresse |
Côté conformité, gardez le réflexe RGPD : minimisez les données personnelles extraites (noms de propriétaires, adresses nominatives) et vérifiez vos obligations avant de constituer un jeu de données réutilisable.
Régler les paramètres OCR pour les CAPTCHA SIG
Avant d'écrire l'extracteur, cadrez ce que l'OCR doit lire. Les CAPTCHA des portails SIG sont rarement homogènes : chiffres seuls, codes à longueur fixe, ou petites équations mathématiques. Ces paramètres, passés à l'endpoint in.php, augmentent nettement le taux de réussite.
| Paramètre | Valeur | Cas d'usage |
|---|---|---|
method |
base64 |
CAPTCHA image standard |
numeric |
1 |
CAPTCHA composés uniquement de chiffres |
min_len |
4 |
Quand le nombre de caractères est connu |
max_len |
6 |
Quand le nombre de caractères est connu |
language |
0 |
Caractères latins / anglais |
textinstructions |
Personnalisé | CAPTCHA mathématiques ou codes au format particulier |
Extraire les parcelles en Python
L'extracteur ci-dessous enchaîne quatre étapes à chaque requête de parcelle :
- il récupère la page de recherche et détecte la présence d'un CAPTCHA image ;
- il télécharge l'image et l'envoie à CaptchaAI via
in.php; - il interroge
res.phpjusqu'à obtenir le texte résolu ; - il renvoie le formulaire avec le texte résolu et les champs masqués du portail.
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(node):
return node.get_text(strip=True) if node else None
return {
"parcel_id": text_of(soup.select_one(".parcel-id, #parcelId")),
"owner": text_of(soup.select_one(".owner, .owner-name")),
"address": text_of(soup.select_one(".address, .situs")),
"zoning": text_of(soup.select_one(".zoning, .zone-code")),
"acreage": text_of(soup.select_one(".acreage, .area")),
"assessed_value": text_of(soup.select_one(".assessed, .value")),
"land_use": text_of(soup.select_one(".land-use, .use-code")),
}
def _parse_search_results(self, html):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_of(node):
return node.get_text(strip=True) if node else None
results = []
for row in soup.select(".result-row, tr.parcel"):
results.append({
"parcel_id": text_of(row.select_one(".parcel-id")),
"address": text_of(row.select_one(".address")),
"owner": text_of(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"]
)
Notez le time.sleep(delay) dans bulk_extract : une extraction de masse doit rester polie envers le portail, aussi bien pour éviter un blocage que pour respecter ses conditions d'utilisation.
Extraction par coordonnées géographiques en JavaScript
Certains portails cartographiques n'exposent pas de recherche par identifiant de parcelle mais un point d'interrogation par latitude/longitude. La classe suivante balaie une emprise géographique en grille et résout le CAPTCHA à la volée quand il apparaît. Cas concret : vous cartographiez les zones inondables d'un secteur en parcourant une grille de coordonnées, et le portail intercale un CAPTCHA image tous les quelques appels.
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
});
Questions fréquentes
CaptchaAI résout-il aussi le reCAPTCHA v2 de certains portails USGS ?
Oui. Au-delà des CAPTCHA image et OCR, CaptchaAI prend en charge reCAPTCHA v2 (méthode userrecaptcha). Sur un portail d'accès en masse protégé par reCAPTCHA v2, vous soumettez le sitekey et l'URL de la page, puis injectez le token g-recaptcha-response renvoyé — la même logique que pour les CAPTCHA image, avec un endpoint et des paramètres différents.
Extraire des données cadastrales derrière un CAPTCHA est-il conforme au RGPD ?
La résolution du CAPTCHA ne modifie en rien vos obligations : c'est l'usage des données extraites qui compte. Si votre jeu de données contient des noms de propriétaires ou des adresses nominatives, minimisez la collecte, documentez votre base légale et vérifiez vos obligations RGPD avant toute réutilisation. CaptchaAI traite l'image du CAPTCHA, pas vos données géospatiales.
Combien coûte la résolution des CAPTCHA pour un gros volume de parcelles ?
CaptchaAI facture par thread concurrent, pas à la résolution. Le plan BASIC ($15/mois, 5 threads) inclut un nombre illimité de résolutions par thread : pour extraire des milliers de parcelles, c'est votre parallélisme — le nombre de threads — qui borne le débit, jamais un coût unitaire par CAPTCHA.
Puis-je lancer l'extraction sur une région entière en une seule passe ?
Oui, c'est exactement le rôle de extractRegion : elle balaie une emprise en grille et résout chaque CAPTCHA rencontré. Gardez le délai entre requêtes (ici 2 s) pour rester dans les limites du portail et respecter ses conditions d'utilisation, surtout sur une grille dense couvrant un large secteur.
Dépannage des CAPTCHA sur portails cartographiques
| Problème | Cause | Correctif |
|---|---|---|
| L'image du CAPTCHA se charge cassée ou vide | Cookie de session requis | Chargez d'abord la page de recherche pour obtenir le cookie de session |
| Le texte résolu est refusé | Sensibilité à la casse | Ajoutez le paramètre case_sensitive=1 |
| Le portail renvoie un CAPTCHA différent à chaque essai | CAPTCHA lié à la session | Téléchargez et résolvez le CAPTCHA dans la même session |
| Aucune donnée de parcelle après résolution | Champs de formulaire masqués manquants | Extrayez toutes les entrées hidden avant d'envoyer le formulaire |
Automatisez vos extractions SIG
Extrayez vos données cartographiques sans interruption : créez votre clé API CaptchaAI et laissez le portail gouvernemental résoudre ses CAPTCHA image de lui-même.