Портал окружного асессора вместо координат участка возвращает картинку с шестью искажёнными символами? Это не сбой, а Image/OCR CAPTCHA — стандартная защита старых государственных ГИС-систем. Дальше два варианта: решать капчу вручную при каждом запросе или передавать её в CaptchaAI API и получать готовый текст за секунды, не останавливая скрипт сбора данных.
Кадастровые системы округов, порталы зонирования, геологические и экологические базы годами работают на устаревшем стеке — длинные циклы закупок и ограниченные бюджеты не оставляют времени на замену капчи чем-то современным. На входе чаще всего встречается CAPTCHA-изображение с искажённым текстом, реже — reCAPTCHA v2 или математическая капча. Для сбора границ участков, данных о зонировании, оценке недвижимости и зонах затопления это единственная преграда между HTTP-запросом и нужными данными.
Где на ГИС-порталах встречается CAPTCHA
Тип капчи и повод её показать зависят от конкретного источника данных — вот типичная картина по шести распространённым типам порталов:
| Тип портала | Тип CAPTCHA | Когда запрашивается |
|---|---|---|
| ГИС округа / система оценки | CAPTCHA-изображение с текстом | Поиск участка по кадастровому номеру |
| Геопространственные порталы штата | Пользовательская CAPTCHA | Запрос на скачивание данных |
| Порталы Геологической службы США (USGS) | reCAPTCHA v2 | Массовая выгрузка данных |
| Карты муниципального зонирования | CAPTCHA-изображение | Повторные запросы по одному объекту |
| Экологические базы данных | Математическая CAPTCHA | Формирование отчёта |
| Поиск зон затопления | CAPTCHA-изображение с текстом | Запрос по адресу |
Экстрактор данных ГИС на Python
Ниже — рабочий класс на requests: он делает обычный GET-запрос, ищет признаки капчи в HTML по регулярному выражению, скачивает изображение, отправляет его в CaptchaAI через base64 и опрашивает res.php, пока не получит решённый текст. Перед повторной отправкой формы важно собрать все скрытые поля (input[type='hidden']) — большинство ГИС-систем используют их для CSRF-токенов и идентификаторов сессии, и без них портал молча отклонит запрос.
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"]
)
Метод bulk_extract добавляет паузу между запросами — это не только вежливость по отношению к серверу округа, но и способ не спровоцировать более агрессивную защиту на самом портале.
Извлечение геоданных по координатам на JavaScript
Для команд на Node.js — тот же принцип, только вместо ID участка на вход идут широта и долгота, а extractRegion проходит сеткой по прямоугольной области и на каждой точке проверяет, нужна ли капча:
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
});
Шаг сетки (gridSize) напрямую влияет на нагрузку по капчам: более мелкая сетка даёт точнее покрытие региона, но и кратно больше запросов — а значит, и больше решений CAPTCHA за проход. Закладывайте это в расчёт потоков заранее, а не после того как выгрузка региона займёт полдня.
Параметры API CaptchaAI для капч на ГИС-порталах
| Параметр | Значение | Когда использовать |
|---|---|---|
method |
base64 |
Стандартная CAPTCHA-изображение |
numeric |
1 |
Только цифровые CAPTCHA |
min_len |
4 |
Известно минимальное число символов |
max_len |
6 |
Известно максимальное число символов |
language |
0 |
Английский/Latin символы |
textinstructions |
Свой текст | Математические CAPTCHA или капчи с нестандартным форматом |
textinstructions стоит использовать сразу, как только видите, что округ применяет нетиповую капчу: одна строка вроде «5 заглавных латинских букв» или «решить пример» экономит часы на подборе min_len/max_len вручную.
Типичные проблемы при обработке CAPTCHA на ГИС-порталах
| Проблема | Причина | Решение |
|---|---|---|
| Картинка CAPTCHA загружается битой | Нужен сессионный cookie | Сначала загрузите страницу поиска |
| Решённый текст портал отклоняет | Чувствительность к регистру | Добавьте параметр case_sensitive=1 |
| Портал каждый раз присылает новую капчу | CAPTCHA привязана к сессии | Скачивайте и решайте в рамках одной сессии, не открывайте новую на каждый повтор |
| После капчи нет данных по участку | Не отправлены скрытые поля формы | Извлеките все скрытые поля перед отправкой |
Чаще всего время теряют именно на третьей строке — скрипт открывает новую сессию на каждый повтор запроса, портал в ответ выдаёт новую капчу, и решение из предыдущего шага становится бесполезным ещё до того, как форма успела отправиться.
Сколько это стоит на практике
Допустим, вы собираете данные с двадцати окружных ГИС-порталов США по заказу риелторского агентства и получаете в среднем 300–400 капч в сутки. При таком объёме план STANDARD ($30/мес, 15 потоков) обычно закрывает нагрузку без роста времени ответа; для более плотного графика или параллельного сканирования нескольких округов разумнее сразу взять ADVANCE ($90/мес, 50 потоков). Тарификация по потокам, а не по числу решений, здесь удобна именно потому, что число капч на регион заранее не предсказать — сегодня двадцать, завтра двести после смены защиты на портале.
Чек-лист перед массовым извлечением
- Прежде чем запускать большую выгрузку, проверьте область просмотра карты, фильтр региона и элементы управления пагинацией — ошибка в границах обходится дороже, чем кажется на старте.
- Сохраняйте нормализованные координаты и необработанный ответ портала, чтобы ошибки извлечения оставались отлаживаемыми постфактум.
- Ставьте пакет на паузу, когда плотность капч резко растёт, вместо того чтобы прятать это за повторными попытками — резкий рост обычно означает, что на портале что-то изменили.
- Логируйте
task_idкаждого решения CaptchaAI рядом с координатами — при разборе инцидента это быстрее, чем воспроизводить запрос заново.
Часто задаваемые вопросы
Сколько времени скрипт ждёт решения одной капчи?
В примере выше опрос res.php идёт с интервалом 3 секунды и до 30 попыток — то есть до полутора минут на одну капчу, прежде чем сработает TimeoutError. На практике решение обычно приходит намного раньше этого потолка; сам потолок — это защита от зависшего цикла, а не ожидаемое время ответа.
Портал присылает новую капчу при каждом повторном запросе — что делать?
Это означает, что капча привязана к конкретной сессии, а не к запросу. Скачивайте изображение и отправляйте решённый текст в рамках одной и той же сессии (requests.Session() в примере выше уже это обеспечивает) — открывать новое соединение на каждую попытку бессмысленно.
Как обрабатывать нестандартные форматы капч у разных округов?
Единого формата нет — один округ просит пять заглавных букв, другой присылает математический пример. Опишите конкретный формат через параметр textinstructions CaptchaAI вместо того, чтобы подбирать min_len/max_len наугад под каждый портал.
Получится скачать shapefile или GeoJSON, если файл спрятан за капчей?
Да — если портал отдаёт пространственные данные по прямой ссылке после капчи, решите капчу через CaptchaAI, чтобы получить доступ к ссылке, а сам файл скачивайте обычным HTTP-запросом отдельно.
Стоит ли выбирать Python или JavaScript для парсинга ГИС-порталов?
Технической разницы нет — оба примера выше решают одну и ту же задачу. Берите тот стек, на котором уже стоит остальной пайплайн: Python — если данные потом уходят в pandas/GeoPandas, Node.js — если рядом уже крутится Puppeteer или другой браузерный слой.
Итог
Надёжное извлечение данных ГИС не требует ручного разбора капч на каждом округе. Получите API-ключ CaptchaAI, подключите его к парсеру и обрабатывайте капчи государственных порталов автоматически.