Les portails de santé publics — annuaires de prestataires, bases de prix des médicaments, registres d'essais cliniques — placent un CAPTCHA devant leur moteur de recherche pour bloquer l'accès automatisé. La réponse tient en deux temps : envoyez le CAPTCHA à un service de résolution comme CaptchaAI, puis rejouez le token obtenu dans le formulaire de recherche. Ce guide montre comment monter cette collecte de données de santé proprement, en restant du bon côté du RGPD et en ne touchant qu'à des données non nominatives.
Où le CAPTCHA bloque la collecte de données de santé
Repérer le type de défi avant d'écrire une ligne de code évite de choisir la mauvaise méthode de résolution.
| Source | Type de CAPTCHA | Données visées | Cas d'usage |
|---|---|---|---|
| Annuaires de prestataires (NPI) | CAPTCHA image | Recherche médecin / établissement | Adéquation du réseau de soins |
| Portails de prix des médicaments | reCAPTCHA v2 | Tarifs des médicaments | Transparence tarifaire |
| Registres d'essais cliniques | reCAPTCHA v2 | Données d'essai, résultats | Analyse de la recherche |
| Formulaires d'assurance | reCAPTCHA v2 | Listes de couverture des médicaments | Comparaison des formulaires |
| Ordres et conseils professionnels | CAPTCHA image | Vérification d'inscription | Contrôle des habilitations |
| Indicateurs qualité des hôpitaux | Cloudflare Turnstile | Indicateurs de qualité | Analyse de performance |
Deux familles couvrent la quasi-totalité de ces portails, et CaptchaAI prend en charge les deux avec une seule intégration :
- reCAPTCHA v2 sur les formulaires de tarification et de recherche clinique ;
- CAPTCHA image (OCR) sur les annuaires plus anciens, parfois doublé d'un Cloudflare Turnstile.
Confidentialité des données de santé et périmètre RGPD
Avant d'écrire le moindre scraper, fixez le périmètre. Le RGPD range les données de santé parmi les catégories particulières de l'article 9, avec une protection renforcée. La distinction opérationnelle est simple :
- Vous pouvez collecter les données publiques et non nominatives : annuaires de médecins, tarifs de médicaments, fiches d'essais cliniques publiées.
- Vous ne collectez pas ce qui identifie un patient : avis nominatifs, dossiers individuels, toute donnée rattachée à une personne.
| Type de données | Sensibilité | Recommandation |
|---|---|---|
| Annuaires de prestataires | Faible (information publique) | Généralement sûr à collecter |
| Prix des médicaments | Faible (tarif public) | Autorisé à des fins de transparence |
| Métadonnées d'essais cliniques | Faible (registres publics) | Usage approprié pour la recherche |
| Avis de patients | Moyenne | Anonymiser avant toute analyse |
| Détails des contrats d'assurance | Faible (tarifs publiés) | Autorisé pour la comparaison |
Ne collectez jamais de données de santé à caractère personnel. Restez sur les données ouvertes et non nominatives, minimisez ce que vous conservez et vérifiez vos obligations RGPD si vous exploitez ces jeux de données au sein de l'UE.
Extraire un annuaire de prestataires
L'annuaire est le cas le plus courant : vous soumettez une spécialité et une zone géographique, puis vous lisez la liste retournée. Le collecteur ci-dessous suit toujours la même séquence :
- charger la page de recherche du portail ;
- détecter le type de défi (reCAPTCHA v2 ou CAPTCHA image) ;
- envoyer le défi à CaptchaAI et attendre le token ;
- rejouer le token dans la requête
POST; - parser la liste de prestataires retournée.
La même classe sert ensuite à la recherche de prix et au traitement par lots.
import requests
import time
import re
import base64
from bs4 import BeautifulSoup
import csv
CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"
def solve_recaptcha(sitekey, pageurl):
resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
"key": CAPTCHAAI_KEY, "method": "userrecaptcha",
"googlekey": sitekey, "pageurl": pageurl, "json": 1,
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(5)
result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
"key": CAPTCHAAI_KEY, "action": "get",
"id": task_id, "json": 1,
})
data = result.json()
if data["request"] != "CAPCHA_NOT_READY":
return data["request"]
raise TimeoutError("Timeout")
def solve_image_captcha(image_bytes):
img_b64 = base64.b64encode(image_bytes).decode()
resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
"key": CAPTCHAAI_KEY, "method": "base64",
"body": img_b64, "json": 1,
})
task_id = resp.json()["request"]
for _ in range(20):
time.sleep(3)
result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
"key": CAPTCHAAI_KEY, "action": "get",
"id": task_id, "json": 1,
})
data = result.json()
if data["request"] != "CAPCHA_NOT_READY":
return data["request"]
raise TimeoutError("Timeout")
class HealthcareDataCollector:
def __init__(self, proxy=None):
self.session = requests.Session()
if proxy:
self.session.proxies = {"http": proxy, "https": proxy}
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
})
def search_providers(self, portal_url, specialty, location, sitekey=None):
"""Search provider directory with CAPTCHA handling."""
resp = self.session.get(portal_url, timeout=30)
data = {"specialty": specialty, "location": location}
# Handle CAPTCHA
if sitekey:
token = solve_recaptcha(sitekey, portal_url)
data["g-recaptcha-response"] = token
else:
captcha_img = re.search(r'src="(/captcha[^"]+)"', resp.text)
if captcha_img:
img_url = portal_url.rstrip("/") + captcha_img.group(1)
img = self.session.get(img_url)
data["captcha"] = solve_image_captcha(img.content)
resp = self.session.post(portal_url, data=data)
return self._parse_providers(resp.text)
def lookup_drug_prices(self, pricing_url, drug_name, zip_code, sitekey):
"""Look up drug prices with CAPTCHA solving."""
# Load search page
self.session.get(pricing_url)
# Solve CAPTCHA
token = solve_recaptcha(sitekey, pricing_url)
resp = self.session.post(pricing_url, data={
"drug": drug_name,
"zip": zip_code,
"g-recaptcha-response": token,
})
if resp.status_code == 200:
return self._parse_prices(resp.text)
return []
def batch_provider_lookup(self, portal_url, specialties, locations, output_file):
"""Batch search across specialties and locations."""
all_providers = []
for specialty in specialties:
for location in locations:
try:
providers = self.search_providers(
portal_url, specialty, location,
)
for p in providers:
p["specialty_search"] = specialty
p["location_search"] = location
all_providers.extend(providers)
print(f"{specialty} / {location}: {len(providers)} providers")
time.sleep(5)
except Exception as e:
print(f"Error: {specialty} / {location}: {e}")
# Export
if all_providers:
keys = all_providers[0].keys()
with open(output_file, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
writer.writerows(all_providers)
return all_providers
def _parse_providers(self, html):
soup = BeautifulSoup(html, "html.parser")
providers = []
for card in soup.select(".provider-card, .doctor-result, tr.provider"):
providers.append({
"name": self._text(card, ".name, .provider-name"),
"specialty": self._text(card, ".specialty"),
"address": self._text(card, ".address"),
"phone": self._text(card, ".phone"),
"accepting": self._text(card, ".accepting-patients"),
})
return providers
def _parse_prices(self, html):
soup = BeautifulSoup(html, "html.parser")
prices = []
for row in soup.select(".pharmacy-row, .price-result"):
prices.append({
"pharmacy": self._text(row, ".pharmacy-name"),
"price": self._text(row, ".price, .drug-price"),
"quantity": self._text(row, ".quantity"),
})
return prices
def _text(self, el, selector):
found = el.select_one(selector)
return found.get_text(strip=True) if found else ""
# Usage
collector = HealthcareDataCollector(
proxy="http://user:pass@residential.proxy.com:5000"
)
# Provider search
providers = collector.search_providers(
portal_url="https://provider-directory.example.com/search",
specialty="Cardiology",
location="New York, NY",
)
# Drug pricing
prices = collector.lookup_drug_prices(
pricing_url="https://drug-prices.example.com/compare",
drug_name="atorvastatin",
zip_code="10001",
sitekey="6Lc_xxxxxxx",
)
Le token reCAPTCHA a une durée de vie courte : envoyez la requête POST dans les secondes qui suivent, sinon le portail le rejette. Pour couvrir plusieurs spécialités et villes, batch_provider_lookup enchaîne les recherches et exporte en CSV, avec un délai entre chaque requête pour ne pas déclencher la limitation de débit.
Récupérer les données d'essais cliniques
Les registres d'essais cliniques exposent des métadonnées publiques. La même mécanique s'applique — reCAPTCHA v2 sur le formulaire, token rejoué dans la requête — mais le parsing cible cette fois les champs propres à l'étude :
- titre de l'étude et statut de recrutement ;
- promoteur et phase clinique ;
- effectif visé et sites participants.
def collect_clinical_trials(search_url, condition, sitekey):
"""Collect clinical trial data for a medical condition."""
collector = HealthcareDataCollector(
proxy="http://user:pass@residential.proxy.com:5000"
)
token = solve_recaptcha(sitekey, search_url)
resp = collector.session.post(search_url, data={
"condition": condition,
"status": "recruiting",
"g-recaptcha-response": token,
})
if resp.status_code != 200:
return []
soup = BeautifulSoup(resp.text, "html.parser")
trials = []
for item in soup.select(".trial-item, .study-result"):
trials.append({
"title": collector._text(item, ".title, h3"),
"status": collector._text(item, ".status"),
"sponsor": collector._text(item, ".sponsor"),
"phase": collector._text(item, ".phase"),
"enrollment": collector._text(item, ".enrollment"),
"location": collector._text(item, ".location"),
})
return trials
Dépannage
La plupart des échecs viennent d'un token expiré ou d'un rythme de requêtes trop soutenu. Voici les cas les plus fréquents et leur correctif.
| Problème | Cause | Correctif |
|---|---|---|
| CAPTCHA image illisible | Image de mauvaise qualité | Relancez : une nouvelle image est générée |
| La recherche de prestataire renvoie une liste vide | Le CAPTCHA a bloqué la requête | Résolvez le CAPTCHA avant de soumettre |
| Le prix d'un médicament change selon la zone | Tarification géolocalisée | Alignez la localisation du proxy sur le code postal |
| La session expire sur plusieurs pages | Délai d'expiration du portail | Enchaînez les recherches rapidement |
| Requêtes par lots limitées en débit | Trop de requêtes | Ajoutez des délais de 5 à 10 s entre les appels |
Trois réflexes limitent la plupart de ces incidents :
- envoyez la requête
POSTdès la réception du token, sans traitement intermédiaire ; - faites tourner des proxys résidentiels alignés sur la zone géographique visée ;
- gardez un délai de 5 à 10 s entre les recherches par lots.
Questions fréquentes
Quels types de CAPTCHA rencontre-t-on sur les portails de santé ?
Surtout du reCAPTCHA v2 sur les formulaires de prix et de recherche clinique, et du CAPTCHA image (OCR) sur les annuaires plus anciens ; certains classements d'hôpitaux passent à Cloudflare Turnstile. CaptchaAI prend en charge ces trois types.
CaptchaAI prend-il en charge hCaptcha pour ces portails ?
Non — hCaptcha n'est pas pris en charge. Pour le reCAPTCHA v2, le CAPTCHA image et Turnstile, l'API répond directement.
Le RGPD s'applique-t-il à la collecte de ces données ?
Les données publiques et non nominatives — annuaires, tarifs, registres d'essais — ne sont pas des données de santé personnelles au sens de l'article 9. Dès qu'une donnée permet d'identifier un patient, ne la collectez pas.
Comment éviter le blocage lors de recherches par lots ?
Espacez les requêtes de 5 à 10 secondes, faites tourner des proxys résidentiels et alignez la géolocalisation du proxy sur le code postal recherché pour rester sous le seuil de limitation de débit.
Guides connexes
- Automatiser les portails administratifs derrière CAPTCHA
- Scraping de la recherche académique
- Rotation des proxys résidentiels pour le scraping
Passez d'une collecte fragile à un flux fiable : récupérez votre clé CaptchaAI et automatisez vos recherches de prestataires et de prix.