Does this web page have a captcha? The Python version of captcha-detector: the same 69 named captcha and bot-check products plus catch-all rules, the same answers, zero dependencies.
- Check what your scraper got back: pass a
requests,httpx,aiohttp,curl_cffi, Scrapy or urllib response, or the JSON a scraper API sent, and find out whether you got the real page or a captcha, a bot check or a block page. - Every kind: checkbox widgets, invisible/score captchas, puzzles and sliders, proof-of-work, image captchas, math and text questions, full-page bot checks ("Just a moment…", "Press & Hold"), and access-denied pages.
- Real browsers too: Playwright (sync and async) and Selenium, cross-origin frames included, with
visibletelling you whether the captcha is on screen.
pip install captcha-detectorimport requests
from captcha_detector import detect_response
url = "https://shop.example/product/42"
res = requests.get(url)
r = detect_response(res)
if r.captcha or r.challenge_page or r.blocked:
print("Not the real page:", r.summary) # e.g. "Captcha found: DataDome captcha (slider) [visible]"
else:
parse(res.text)detect_response() takes:
| Input | Example |
|---|---|
requests / httpx / curl_cffi / niquests response |
detect_response(requests.get(url)) |
| Scrapy response | detect_response(response) inside parse() |
urllib.request / urllib3 response |
detect_response(urlopen(url)) |
aiohttp response |
await detect_response_async(resp) |
HTML as str or bytes |
detect_response(html, url=url) |
A scraper API's JSON (str, dict or list) |
detect_response(api_res.json(), url=target) |
HTML wrapped in JSON ({"html": …}, {"result": {"content": …}} and the like) is found on its own, along with the status code, headers and URL next to it.
target = "https://shop.example/product/42"
res = requests.get("https://scraper-api.example/v1", params={"url": target, "key": KEY})
r = detect_response(res, url=target)
if r.captcha or r.challenge_page or r.blocked:
... # retry with another proxy or provider, don't parse or store this onePass url, the page you asked the API to scrape, not the API's own address. Most scraper APIs answer 200 whatever the site did; that's fine, the page itself gives a captcha away. If your API reports the site's real status or headers somewhere else, pass them as status= and headers=.
from captcha_detector import detect_html
r = detect_html(html, url="https://shop.example/", status=403, headers={"server": "cloudflare"})Raw HTML only shows what the server sent, and can't tell what is on screen. In a browser the detector sees what scripts added and whether the captcha is visible.
from playwright.sync_api import sync_playwright
from captcha_detector import detect_in_page, wait_for_captcha
with sync_playwright() as p:
page = p.chromium.launch().new_page()
response = page.goto("https://www.google.com/recaptcha/api2/demo")
r = detect_in_page(page, response=response) # checks every frame, cross-origin ones too
print(r.summary) # Captcha found: reCAPTCHA (v2-checkbox) [visible]
r = wait_for_captcha(page, timeout=10, visible=True) # wait until one is on screenAsync: await detect_in_page_async(page) and await wait_for_captcha_async(page).
Selenium:
from selenium import webdriver
from captcha_detector import detect_selenium
driver = webdriver.Chrome()
driver.get("https://www.google.com/recaptcha/api2/demo")
r = detect_selenium(driver) # switches into each frame and backAnything else that runs JavaScript can load captcha_detector.BROWSER_SCRIPT (it defines window.CaptchaDetector) or evaluate captcha_detector.expression(), then convert the result with captcha_detector.from_js().
A dict (so json.dumps works) whose keys can also be read as attributes:
{
"captcha": True, # a captcha or bot check is on the page (shown, invisible or not rendered yet)
"visible": None, # on screen right now: True/False in a browser, None for raw HTML
"challenge_page": False, # the whole page is a bot check
"blocked": False, # an access-denied page
"detections": [{
"id": "recaptcha", "name": "reCAPTCHA", "vendor": "Google",
"kind": "widget", # widget | invisible | image | question | challenge-page | block-page | protection
"subtype": "v2-checkbox", "confidence": 0.99, "visible": None,
"evidence": [{"type": "scripts", "value": "https://www.google.com/recaptcha/api.js"},
{"type": "elements", "value": ".g-recaptcha"}],
"site": "https://developers.google.com/recaptcha",
}],
"summary": "Captcha found: reCAPTCHA (v2-checkbox)",
"url": "https://shop.example/", "mode": "html", "version": "1.0.1",
}kind: "protection" means a bot-protection service is loaded (DataDome, Akamai, PerimeterX…) but no captcha is shown; it does not set captcha. When the reCAPTCHA or hCaptcha image challenge is open, details["challenge_open"] is True.
Options for every function: min_confidence (0..1, default 0.5) and signatures (your own list; SIGNATURES + mine adds to the built-in ones).
captcha-detector https://www.google.com/recaptcha/api2/demo
captcha-detector https://example.com --browser --json # needs: pip install playwright
curl -s https://example.com | captcha-detector -
curl -s "https://scraper-api.example/v1?url=..." | captcha-detector -Options: --browser, --headful, --wait MS, --json, --fail (exit code 1 when a captcha is found), --min 0..1. Also python -m captcha_detector.
from captcha_detector import SIGNATURES, detect_html
acme = {
"id": "acme-check", "name": "ACME bot check", "kind": "challenge-page",
"title": [(r"^acme says hold on", 0.9)], # regex, or (regex, weight)
"scripts": [r"cdn\.acme-shield\.example/"],
"elements": ["#acme-check", {"sel": "form", "attr": ["action"], "re": r"/acme/verify", "w": 0.9}],
"refine": lambda d, x: d if x.short else None, # optional: adjust or drop a match
}
r = detect_html(html, signatures=SIGNATURES + [acme])Matchers: scripts, frames, elements (CSS: tag, #id, .class, [attr op "v" i], :not()), inline (inline script text), urls, title, text (short pages only, unless (re, w, "any")), cookies, headers ((name, re, w)), status. Matches combine into a confidence; the default threshold is 0.5.
To add a product for everyone, add it to src/signatures.js in the JavaScript repository: the JavaScript and Python packages share one signature database.
The signature database (signatures.json), the browser script (captcha-detector.js) and the test fixtures (tests/fixtures.json) are generated from the JavaScript repository, and the test suite checks that the Python engine gives exactly the same result as the JavaScript engine on every test page. captcha_detector.SIGNATURES_VERSION is the JavaScript version they came from.
To work on both, clone the two repositories side by side:
git clone https://github.com/ScrapePass/captcha-detector
git clone https://github.com/ScrapePass/captcha-detector-python
cd captcha-detector && npm install && npm run test:python # rebuilds, exports to ../captcha-detector-python, runs its testsA signature with a refine() step in JavaScript needs the same step in src/captcha_detector/refine.py.
Tests on their own: PYTHONPATH=src python3 -m unittest discover -s tests -t .
MIT