Geodaten aus behördlichen GIS-Portalen lassen sich automatisiert abrufen – auch dann, wenn vor der Parzellensuche ein Bild-CAPTCHA steht. Der Ablauf ist bei nahezu allen Portalen derselbe:
- Suchseite laden und das Session-Cookie übernehmen.
- CAPTCHA-Bild extrahieren und an CaptchaAI zur OCR-Lösung übergeben.
- Zurückgegebenen Text in das passende Formularfeld eintragen.
- Abfrage erneut absenden – zusammen mit allen versteckten Formularfeldern.
- Parzellengrenzen, Zonierung, Überschwemmungsgebiete oder Bewertungen aus der HTML-Antwort parsen.
Was die Stabilität am stärksten beeinflusst, ist selten das CAPTCHA selbst, sondern das Abrufmuster. Trennen Sie die Arbeit sauber nach Modus: einzelne Parzellen-Lookups, kurze Adresslisten und langsame Batch-Läufe mit konservativer Rate. So bleiben öffentliche Geoportale reaktionsfähig, und Ihre Kosten bleiben planbar – gerade weil viele dieser Systeme nicht auf schnelle Massenabfragen ausgelegt sind. Dieser Leitfaden zeigt die konkrete Umsetzung in Python und Node.js, die passenden CAPTCHA-Parameter und die häufigsten Fehlerquellen.
Welche CAPTCHAs auf GIS- und Geoportalen auftauchen
| Portaltyp | CAPTCHA-Typ | Auslöser |
|---|---|---|
| Kataster- und Landkreis-GIS | Bildtext-CAPTCHA | Parzellensuche |
| Landes-Geoportale | Individuelles CAPTCHA | Daten-Downloads |
| USGS-Datenportale | reCAPTCHA v2 | Massendatenzugriff |
| Kommunale Bebauungspläne | Bild-CAPTCHA | Wiederholte Objektsuche |
| Umweltdatenbanken | Mathe-CAPTCHA | Berichterstellung |
| Überschwemmungsgebiete | Bildtext-CAPTCHA | Adressabfragen |
CaptchaAI löst die relevanten Typen dieser Liste: Bild- und OCR-CAPTCHAs über den post-Endpoint sowie reCAPTCHA v2, wo Portale darauf umgestellt haben. Für individuelle oder mathematische Varianten steuern Sie das Verhalten über die weiter unten beschriebenen Parameter.
Geodaten im DACH-Raum: ALKIS, Geoportale und DSGVO
Im deutschsprachigen Raum liegen die interessanten Bestände selten bei einem einzelnen Landkreis, sondern verteilt über mehrere Ebenen:
- Liegenschaftskataster (ALKIS) der Länder – Parzellen, Flurstücke und Eigentümerangaben.
- INSPIRE-Dienste und zentrale Einstiegspunkte wie Geoportal.de für harmonisierte Geodaten.
- Landes- und Kommunal-Geoportale mit Bebauungsplänen, Umwelt- und Überschwemmungsdaten.
Viele davon liefern ihre Daten offen über WMS/WFS aus. Sobald aber eine Weboberfläche mit Einzelabfragen dazwischenliegt, taucht regelmäßig ein Bild-CAPTCHA auf – genau wie bei den US-County-Systemen im Code-Beispiel weiter unten.
Zwei Punkte sind dabei DACH-spezifisch wichtig. Der erste ist die Rechtslage:
Hinweis: Eigentümernamen und Adressen aus Katasterauszügen sind personenbezogene Daten im Sinne der DSGVO. Prüfen Sie Nutzungsbedingungen und Rechtsgrundlage, bevor Sie solche Felder dauerhaft speichern oder weiterverarbeiten – ein CAPTCHA-Lösungsdienst nimmt Ihnen diese Sorgfaltspflicht nicht ab.
Der zweite Punkt ist die Last: Behördenportale sind oft auf geringe Nutzerzahlen ausgelegt. Eine konservative Rate von etwa einer Abfrage alle 2–3 Sekunden und klar abgegrenzte Batch-Fenster sind hier keine Höflichkeit, sondern die Voraussetzung dafür, dass Ihre Zugriffe stabil durchlaufen.
GIS-Daten mit Python extrahieren
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"]
)
Koordinatenbasierte Extraktion mit Node.js
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
});
CAPTCHA-Parameter für Geoportale richtig setzen
Je präziser Sie das erwartete Format beschreiben, desto zuverlässiger fällt die OCR-Lösung aus. Diese Parameter gehören mit in die Anfrage an in.php:
| Parameter | Wert | Anwendungsfall |
|---|---|---|
method |
base64 |
Standard-Bild-CAPTCHA |
numeric |
1 |
Rein numerische CAPTCHAs |
min_len |
4 |
Wenn die Zeichenanzahl bekannt ist |
max_len |
6 |
Wenn die Zeichenanzahl bekannt ist |
language |
0 |
Lateinische Zeichen |
textinstructions |
individuell | Mathe-CAPTCHAs oder formatierte Codes |
Typische Fehler und ihre Lösung
| Problem | Ursache | Lösung |
|---|---|---|
| CAPTCHA-Bild lädt fehlerhaft | Sitzungscookie erforderlich | Zuerst die Suchseite laden |
| Gelöster Text wird abgelehnt | Groß-/Kleinschreibung relevant | Parameter case_sensitive=1 ergänzen |
| Portal liefert ein anderes CAPTCHA | Sitzungsspezifisches CAPTCHA | In derselben Sitzung herunterladen und lösen |
| Keine Parzellendaten nach dem CAPTCHA | Versteckte Formularfelder fehlen | Alle Hidden-Inputs vor dem Absenden auslesen |
Bewährtes Vorgehen bei größeren Extraktionen
Für wiederkehrende Läufe über viele Parzellen zahlt sich ein paar Vorsichtsmaßnahmen aus:
- Session zuerst aufbauen: Laden Sie die Suchseite, bevor Sie das CAPTCHA anfordern – viele Portale binden das CAPTCHA an ein gültiges Sitzungscookie.
- Rate konservativ halten: Eine feste Verzögerung pro Abfrage schützt vor Sperren und hält das Portal reaktionsfähig.
- Idempotent zwischenspeichern: Persistieren Sie jede erfolgreiche Parzelle sofort, damit ein Abbruch keinen kompletten Neustart erzwingt.
- Fehler pro Datensatz isolieren: Fangen Sie Ausnahmen je Parzelle ab, statt den gesamten Batch scheitern zu lassen.
Häufige Fragen
Ist das automatisierte Abrufen öffentlicher Geodaten in Deutschland zulässig?
Öffentlich bereitgestellte Geodaten dürfen in der Regel genutzt werden, personenbezogene Felder wie Eigentümernamen unterliegen jedoch der DSGVO. Prüfen Sie die Nutzungsbedingungen des Portals und Ihre Rechtsgrundlage, bevor Sie solche Angaben speichern. Ein CAPTCHA-Lösungsdienst ändert an dieser Sorgfaltspflicht nichts – er verarbeitet nur die CAPTCHA-Abfrage.
Wie viele Parzellen kann ich parallel abfragen?
Die Parallelität richtet sich nach Ihrer Thread-Zahl, nicht nach der Zahl der Lösungen. CaptchaAI rechnet pro gleichzeitigem Thread ab: BASIC (15 $/Monat, 5 Threads) erlaubt fünf parallele Abfragen mit unbegrenzten Lösungen, ADVANCE (90 $/Monat, 50 Threads) entsprechend mehr. In der Praxis ist ohnehin die konservative Rate des Zielportals der begrenzende Faktor, nicht Ihr Plan.
Was passiert, wenn ein Geoportal reCAPTCHA v2 statt eines Bild-CAPTCHAs zeigt?
Dann wechseln Sie den Solver-Typ, nicht den Ansatz. CaptchaAI löst reCAPTCHA v2 ebenfalls per API und liefert ein g-recaptcha-response-Token zurück, das Sie in das Formular eintragen. Details dazu finden Sie im verlinkten Leitfaden weiter unten.
Lohnt sich das für einmalige Recherchen oder nur für große Batch-Läufe?
Beides. Für eine einzelne Adressrecherche genügt ein Parzellen-Lookup mit einem gelösten CAPTCHA. Der wirtschaftliche Vorteil der Thread-basierten Abrechnung zeigt sich aber bei wiederkehrenden Batch-Läufen über tausende Parzellen, weil dann keine Kosten pro Lösung anfallen.
Nächste Schritte
Extrahieren Sie Geodaten, ohne dass Bild-CAPTCHAs Ihre Läufe stoppen – holen Sie sich Ihren CaptchaAI-API-Schlüssel und automatisieren Sie die CAPTCHA-Verarbeitung von Behördenportalen.
Verwandte Leitfäden
- CAPTCHAs auf Behördenportalen automatisch lösen
- Geschützte Websites zuverlässig scrapen
- Bild-CAPTCHAs mit Node.js lösen