Un comparateur de prix de médicaments bute rarement sur le parsing : il bute sur le défi CAPTCHA placé entre le formulaire de recherche et la grille de prix. Ce qui le débloque n'a rien d'exotique — une session HTTP qui garde ses cookies, un token reCAPTCHA v2 obtenu par API, et une cadence assez lente pour ne pas rallumer le défi à chaque appel.
Périmètre, RGPD et données de santé : à cadrer avant le premier CAPTCHA
Trois questions se règlent hors du code.
- Quelles pages ? Uniquement des tarifs publics. Lisez les CGU et le
robots.txt, et n'automatisez jamais un espace assuré ou un compte patient qui ne vous appartient pas. - Quelles données ? Un code postal ou une molécule rattachés à un utilisateur identifiable relèvent du RGPD, qui range les données de santé parmi les catégories sensibles.
- Pour quelle finalité ? Documentez finalité, durée de conservation et sous-traitants, puis faites valider le périmètre par un juriste ou votre DPO.
Où le défi CAPTCHA surgit dans un portail de pharmacie
Le défi répond à un comportement, pas à une URL : deux collecteurs qui visitent les mêmes pages n'en rencontrent pas le même nombre.
| Type de page | Défi rencontré | Ce qui le déclenche |
|---|---|---|
| Recherche de médicament | reCAPTCHA v2 | Chaque soumission du formulaire |
| Résultats de prix | Cloudflare Turnstile | Accès automatisé suspecté |
| Localisateur d'officine | reCAPTCHA v2 | Requêtes géolocalisées répétées |
| Recherche de coupons | CAPTCHA image | Avant l'affichage des réductions |
Le socle : session persistante et token reCAPTCHA v2
Le script charge la page avec une session requests qui garde ses cookies, extrait le sitekey, envoie la tâche à in.php, puis interroge res.php toutes les trois secondes jusqu'au token — les plafonds publiés situent la résolution d'un reCAPTCHA v2 sous les 60 secondes.
import requests
import time
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def solve_recaptcha(site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": "YOUR_API_KEY",
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": "YOUR_API_KEY",
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("Solve timed out")
def search_drug_prices(drug_name, zipcode):
search_url = "https://pharmacy.example.com/search"
# Load the search page to get the site key
page = session.get(search_url)
# Extract reCAPTCHA site key
import re
match = re.search(r'data-sitekey="([^"]+)"', page.text)
site_key = match.group(1)
# Solve the CAPTCHA
token = solve_recaptcha(site_key, search_url)
# Submit the search with token
results = session.post(search_url, data={
"drug": drug_name,
"zip": zipcode,
"g-recaptcha-response": token
})
return parse_prices(results.text)
Interroger plusieurs officines dans une même comparaison
Déclarez vos sources comme des données plutôt que comme du code : chaque entrée porte son URL et le type de défi attendu, et la boucle renvoie une ligne par source, échecs compris :
PHARMACY_SOURCES = [
{
"name": "PharmacyA",
"url": "https://pharmacya.example.com/pricing",
"captcha_type": "recaptcha_v2"
},
{
"name": "PharmacyB",
"url": "https://pharmacyb.example.com/drugs",
"captcha_type": "turnstile"
},
{
"name": "PharmacyC",
"url": "https://pharmacyc.example.com/search",
"captcha_type": "image"
}
]
def compare_drug_price(drug_name, zipcode):
results = []
for source in PHARMACY_SOURCES:
try:
prices = fetch_prices(
source["url"],
source["captcha_type"],
drug_name,
zipcode
)
results.append({
"source": source["name"],
"prices": prices,
"status": "success"
})
except Exception as e:
results.append({
"source": source["name"],
"error": str(e),
"status": "failed"
})
return sorted(results, key=lambda r: r.get("prices", {}).get("lowest", float("inf")))
Une officine en échec reste dans le résultat : trois jours d'affilée, c'est un changement de widget.
Un aiguillage unique par type de défi
Chaque famille attend son champ : g-recaptcha-response pour reCAPTCHA v2, cf-turnstile-response pour Turnstile, un champ maison pour un CAPTCHA image. Centralisez cet aiguillage : le jour où une officine change de widget, vous ajoutez une branche, pas un script.
def fetch_prices(url, captcha_type, drug_name, zipcode):
page = session.get(url)
if captcha_type == "recaptcha_v2":
import re
match = re.search(r'data-sitekey="([^"]+)"', page.text)
token = solve_recaptcha(match.group(1), url)
field_name = "g-recaptcha-response"
elif captcha_type == "turnstile":
import re
match = re.search(r'data-sitekey="(0x[^"]+)"', page.text)
token = solve_turnstile(match.group(1), url)
field_name = "cf-turnstile-response"
elif captcha_type == "image":
img_data = extract_captcha_image(page.text)
token = solve_image_captcha(img_data)
field_name = "captcha"
return session.post(url, data={
"drug": drug_name,
"zip": zipcode,
field_name: token
})
Résoudre moins souvent grâce à la session
Chaque résolution occupe un thread quelques secondes. La marge n'est pas dans la vitesse mais dans le nombre de défis déclenchés : ne résolvez que si la réponse contient réellement un widget.
class PharmacySession:
def __init__(self, base_url):
self.session = requests.Session()
self.base_url = base_url
self.searches_since_captcha = 0
def search(self, drug_name, zipcode):
result = self.session.post(f"{self.base_url}/search", data={
"drug": drug_name,
"zip": zipcode
})
if self.is_captcha_page(result.text):
token = self.solve_page_captcha(result.text)
result = self.session.post(f"{self.base_url}/search", data={
"drug": drug_name,
"zip": zipcode,
"g-recaptcha-response": token
})
self.searches_since_captcha = 0
else:
self.searches_since_captcha += 1
return result
def is_captcha_page(self, html):
return "g-recaptcha" in html or "cf-turnstile" in html
def solve_page_captcha(self, html):
import re
match = re.search(r'data-sitekey="([^"]+)"', html)
return solve_recaptcha(match.group(1), self.base_url)
La détection reste naïve. Si le widget est injecté par un script, inspectez la réponse XHR, pas la page initiale.
Côté JavaScript : paralléliser les sources
Sous Node.js, la même logique s'écrit avec Promise.all : chaque source est traitée de son côté et le tableau final ne garde que les réponses exploitables.
async function comparePharmacyPrices(drugName, zipCode, sources) {
const results = await Promise.all(
sources.map(async (source) => {
try {
const price = await fetchDrugPrice(source, drugName, zipCode);
return { source: source.name, price, status: 'success' };
} catch (error) {
return { source: source.name, error: error.message, status: 'failed' };
}
})
);
return results
.filter(r => r.status === 'success')
.sort((a, b) => a.price - b.price);
}
async function fetchDrugPrice(source, drugName, zipCode) {
// Solve CAPTCHA for this source
const token = await solveCaptcha(source.siteKey, source.url);
const response = await fetch(source.url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
drug: drugName,
zip: zipCode,
'g-recaptcha-response': token
})
});
return parsePrice(await response.text());
}
Limitez malgré tout la concurrence par domaine : une rafale se lit comme un signal d'automatisation.
Rythme des requêtes et rotation des proxys
Les portails surveillent moins l'identité du client que son rythme. Quatre leviers :
- Laisser 5 à 10 secondes entre deux recherches.
- Repartir sur une nouvelle session toutes les 20 à 30 requêtes.
- Varier les recherches plutôt que boucler sur la même molécule.
- Réserver les proxys résidentiels aux portails les plus stricts : les IP de datacenter déclenchent plus de défis.
Exemple chiffré : douze officines, deux relevés par jour
Un comparateur francophone suit 12 officines en France, en Belgique et en Suisse romande, sur 40 molécules, deux fois par jour : 12 × 40 × 2 = 960 recherches quotidiennes, dont une fraction seulement déclenche un défi grâce aux sessions réutilisées. Un worker chez OVHcloud, Scaleway ou en région AWS eu-west-3 (Paris) reste à quelques dizaines de millisecondes des portails européens. La facturation CaptchaAI porte sur des threads simultanés, en dollars US : BASIC ($15/mois, 5 threads) couvre cette collecte en séquentiel, STANDARD ($30/mois, 15 threads) permet de relever les 12 sources en parallèle.
Ce que CaptchaAI résout sur ces portails, et ce qu'il ne résout pas
Une officine qui bascule sur hCaptcha sort du comparateur : ce type n'est pas pris en charge.
| Défi rencontré | Prise en charge CaptchaAI |
|---|---|
| reCAPTCHA v2, v2 Invisible, v2 Enterprise, v3 | ✅ Oui |
| Cloudflare Turnstile, Cloudflare Challenge | ✅ Oui |
| CAPTCHA image (OCR), grilles, GeeTest v3 | ✅ Oui |
| CaptchaFox, Friendly Captcha, Lemin | ✅ Bêta |
| hCaptcha | ❌ Pas encore pris en charge |
| FunCaptcha (Arkose Labs) | ❌ Pas encore pris en charge |
| GeeTest v4 | ❌ À venir |
Dépannage
Quatre symptômes couvrent l'essentiel des incidents.
| Problème | Cause probable | Correctif |
|---|---|---|
| Un défi à chaque recherche | Les cookies ne survivent pas | Réutilisez un seul objet requests.Session() |
| Prix vides malgré un token accepté | Token CSRF absent | Renvoyez les champs masqués du formulaire |
| Coupure après quelques dizaines de recherches | Rate limiting | Allongez les délais, renouvelez la session |
| Token systématiquement refusé | pageurl ne correspond pas au widget |
Envoyez l'URL exacte du formulaire |
FAQ
Quel plan CaptchaAI faut-il pour un comparateur de prix en pharmacie ?
Cela dépend des résolutions simultanées, pas du volume total : la facturation se fait par thread, résolutions illimitées sur chacun. BASIC ($15/mois, 5 threads) suffit à une collecte séquentielle sur une dizaine de sources.
Combien de temps prend la résolution d'un reCAPTCHA v2 ?
Moins de 60 secondes d'après les plafonds publiés, avec un taux de réussite élevé sur les types pris en charge. Un Turnstile passe en moins de 10 secondes : calez vos timeouts dessus.
Que faire si un portail remplace reCAPTCHA v2 par hCaptcha ?
Retirez la source du comparateur : hCaptcha n'est pas pris en charge, FunCaptcha non plus. Surveillez le marqueur dans le HTML pour le voir venir.
Collecter les prix des médicaments est-il autorisé en France ?
Cela dépend du portail et de l'usage. Un tarif public n'est pas une donnée personnelle, mais les CGU et le RGPD s'imposent dès qu'un utilisateur identifiable entre en jeu. Cet article décrit une méthode, pas un avis juridique.
Faut-il un proxy résidentiel pour chaque officine ?
Rarement. Mesurez la fréquence des défis source par source, puis ne basculez que les portails qui exigent un widget dès la première requête.
Articles connexes
- Résoudre un reCAPTCHA v2 à callback via l'API
- Gérer reCAPTCHA v2 et Turnstile sur un même site
- Comprendre le mécanisme de callback de reCAPTCHA v2
Prochaines étapes
Commencez par une seule officine : récupérez votre clé API CaptchaAI, faites passer une recherche de bout en bout, puis ajoutez les sources une par une.