{"id":27027,"date":"2026-11-01T19:47:05","date_gmt":"2026-11-01T19:47:05","guid":{"rendered":"https:\/\/capskip.com\/?p=27027"},"modified":"2026-11-03T03:35:47","modified_gmt":"2026-11-03T03:35:47","slug":"detect-captcha-type","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/detect-captcha-type\/","title":{"rendered":"\u5982\u4f55\u68c0\u6d4b\u9875\u9762\u4e0a\u7684\u9a8c\u8bc1\u7801\u7c7b\u578b\u5e76\u8bc6\u522b\u5b83\uff08Python\uff09"},"content":{"rendered":"<p>To detect CAPTCHA type on a page, read the page&#8217;s HTML for the widget&#8217;s script and container, pull the sitekey out of them, and map the family to the CapSkip method that solves it. A reCAPTCHA v2 box, a v3 script and a Turnstile widget need three different calls with different options, and a wrong guess wastes a whole solve and gives you a token you cannot use. This guide builds a detector of about 175 lines in Python that returns the type, the method and the arguments, or says plainly that it cannot tell. CapSkip runs on your own Windows PC with no per-solve fee, so a script that checks a hundred pages costs nothing extra.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>CapSkip running on a Windows PC, with the solvers you need switched on. The <a href=\"https:\/\/capskip.com\/setup-guide\/\">setup guide<\/a> covers the first start.<\/li>\n<li>Python 3.10 or later. The code below ran on Python 3.14.0 and 3.11.7 with requests 2.33.0 and 2.34.2 and capskip 1.3.0.<\/li>\n<li>A PowerShell prompt. The commands use PowerShell syntax.<\/li>\n<li>A page that you own or may automate, with a CAPTCHA on it.<\/li>\n<li>An address for the solver. In Local mode CapSkip answers on 127.0.0.1 for that device only. In Server mode it listens on your network address or public IP, so a script on another PC, a VPS or a hosted platform can reach it over the API. Both are set under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<p>How this was tested: the detector ran against 66 saved pages and every page returned the expected result. Of those, 59 were built by hand (about 40 follow the vendors&#8217; documentation, the rest are edge cases I made up) and 7 are snapshots of public pages taken on 29 October 2026. The solves ran through the real capskip package against a mock CapSkip that copies the app&#8217;s input checks. The real app was not running, so no number below is a solve time.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why you detect CAPTCHA type before you solve<\/h2>\n<p>CapSkip has a different method for each family: recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly_captcha and normal for images in the SDK, plus hcaptcha on the HTTP API, which the SDK does not wrap yet. Within reCAPTCHA the options matter too. Invisible, Enterprise and v3 are flags on the same call. Send the wrong ones and the best case is a failed job, the worst a token the site rejects with no hint why. I could not check which one the real app does for a wrong reCAPTCHA version. Either way the solve is spent.<\/p>\n<p>Other mistakes fail fast and loudly. Reading the sitekey from a page that renders reCAPTCHA explicitly gives the word explicit, and CapSkip refuses it with ERROR_GOOGLEKEY. The <a href=\"https:\/\/capskip.com\/error-googlekey-pageurl\/\">ERROR_GOOGLEKEY and ERROR_PAGEURL guide<\/a> covers that error from the other side. To detect CAPTCHA type reliably, a detector has to check the signals in a fixed order, and that order avoids both kinds of mistake. A detector is also the first thing to build before a scraper that meets more than one site. If you want the background on what each type is, <a href=\"https:\/\/capskip.com\/what-is-captcha\/\">what a CAPTCHA is<\/a> explains the families without code.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: install and fetch the page<\/h2>\n<p>Create a virtual environment in an empty folder and install the two packages. The commands call the environment&#8217;s own Python by path, so you do not need to activate it.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\">py -m venv .venv\n.\\.venv\\Scripts\\python.exe -m pip install capskip requests<\/pre>\n<\/div>\n<p>To detect CAPTCHA type from text, the detector needs only two inputs: the page&#8217;s HTML and its address. Fetch the page with a requests Session and keep that Session, because the image CAPTCHA step later downloads the picture with the same cookies. A plain requests fetch shows what the server sends before any JavaScript runs. Google&#8217;s reCAPTCHA demo pages, Cloudflare&#8217;s Turnstile demo and the hCaptcha demo all carry their widget in that HTML. Pages that add the widget with JavaScript do not, and the section &#8220;When the HTML says nothing&#8221; covers that case.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: write the helpers<\/h2>\n<p>Save everything from here to Step 9 in one file called detect.py, one block after another. The first block imports what is needed and writes four small readers. Each returns None when nothing matches, so a missing attribute never raises.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">import base64, html as htmllib, re, time\nfrom types import SimpleNamespace as NS\nfrom urllib.parse import urljoin\nimport requests\n\nSITEKEY_JS = r&quot;sitekey['\\&quot;]?\\s*:\\s*['\\&quot;]([^'\\&quot;]+)&quot;   # sitekey: '...' inside a render() call\n\n\ndef grab(pattern, text):  # first capture group that matched, or None\n    m = re.search(pattern, text or &quot;&quot;, re.I | re.S)\n    return next(g for g in m.groups() if g is not None) if m else None\n\ndef tag(cls, h):  # opening tag with CSS class cls (and not g-recaptcha-response)\n    pat = r'(&lt;\\w+[^&gt;]*\\sclass\\s*=\\s*[&quot;\\'][^&quot;\\']*(?&lt;![\\w-])%s(?![\\w-])[^&quot;\\']*[&quot;\\'][^&gt;]*&gt;)'\n    return grab(pat % cls, h)\n\ndef attr(t, name):  # attribute value from a tag string, either quote style, HTML-unescaped\n    v = grab(r'\\s%s\\s*=\\s*(?:&quot;([^&quot;]*)&quot;|\\'([^\\']*)\\')' % name, t)\n    return None if v is None else htmllib.unescape(v)\n\ndef render_opts(call, js):  # first 400 characters of the options in call(el, {...})\n    return grab(call + r&quot;\\([^,]+,\\s*\\{(.{0,400})&quot;, js)<\/pre>\n<\/div>\n<p>The grab function returns the first capture group that matched. The tag function finds an opening tag by CSS class and refuses a class that only contains the name inside a longer one, so a class called cf-turnstile-response is not a Turnstile widget. The attr function reads an attribute written with either quote style and unescapes HTML entities, which matters for a challenge held in an attribute. Unquoted attributes are not read.<\/p>\n<p>The second block turns a page into something the checks can share. It strips comments and style blocks, so a CSS rule that mentions a widget class cannot match, and it collects the script addresses and the inline JavaScript separately. The last two helpers build the two possible answers: a hit, or a needs_browser answer.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def parse(html, url):\n    h = re.sub(r&quot;&lt;!--.*?--&gt;|&lt;style\\b.*?&lt;\/style&gt;&quot;, &quot;&quot;, html, flags=re.S | re.I)\n    srcs = &quot; &quot;.join(re.findall(r'&lt;script\\b[^&gt;]*?\\ssrc\\s*=\\s*[&quot;\\']([^&quot;\\']+)', h, re.I))\n    js = &quot;\\n&quot;.join(re.findall(r&quot;&lt;script\\b[^&gt;]*&gt;(.*?)&lt;\/script&gt;&quot;, h, re.S | re.I))\n    return NS(h=h, srcs=htmllib.unescape(srcs), js=js, url=url)\n\ndef hit(kind, method, **args):\n    return {&quot;type&quot;: kind, &quot;method&quot;: method, &quot;args&quot;: args}\n\ndef browser(kind, method, p, why):  # the static HTML is not enough, so do not guess\n    return {&quot;type&quot;: kind, &quot;method&quot;: method, &quot;args&quot;: {&quot;url&quot;: p.url}, &quot;needs_browser&quot;: why}<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: hCaptcha, and what CapSkip cannot solve<\/h2>\n<p>Start with the pages where the right answer is no. CapSkip does not solve FunCaptcha or Arkose, and GeeTest v4 is outside the GeeTest method, which is for v3. If the detector skipped those, a page with an Arkose box would fall through to the next check and might come back as something it is not. So this check runs first and returns a result with no method.<\/p>\n<p>The same block handles the Cloudflare full-page check, the &#8220;Just a moment&#8221; interstitial. That page is not a Turnstile widget. The turnstile method needs two values from it, data and pagedata, and a plain fetch does not hold them. The detector looks for the page&#8217;s inline challenge options and answers needs_browser. The <a href=\"https:\/\/capskip.com\/cf-clearance-cookie\/\">cf_clearance guide<\/a> covers what comes after a challenge page is passed.<\/p>\n<p>hCaptcha comes next, before reCAPTCHA. CapSkip solves it, whether the widget is a checkbox, invisible or Enterprise, but a site that moved from reCAPTCHA to hCaptcha often keeps the g-recaptcha class on its widget, and only the hCaptcha script tells the two apart. The check reads the sitekey from the widget or from an hcaptcha.render call, and adds invisible when the widget has data-size set to invisible. An Enterprise page that hands the widget an rqdata value through hcaptcha.setData gets needs_browser instead, because that value is issued for one challenge and expires quickly.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">UNSUPPORTED = {&quot;arkose&quot;: r&quot;arkoselabs\\.com\/v2\/|funcaptcha\\.com\/fc\/&quot;, &quot;geetest_v4&quot;: r&quot;gt4\\.js|initGeetest4&quot;}\n\ndef unsupported(p):  # CapSkip cannot solve these: say so instead of misleading the reader\n    for kind, pat in UNSUPPORTED.items():\n        if re.search(pat, p.srcs + &quot; &quot; + p.js):\n            return {&quot;type&quot;: kind, &quot;method&quot;: None, &quot;args&quot;: {}, &quot;unsupported&quot;: True}\n\ndef cf_challenge(p):  # the full-page &quot;Just a moment...&quot; interstitial, not a widget\n    if re.search(r&quot;_cf_chl_opt\\s*=\\s*\\{&quot;, p.js):\n        why = &quot;data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time&quot;\n        return browser(&quot;turnstile_challenge&quot;, &quot;turnstile&quot;, p, why)\n\ndef hcaptcha(p):  # before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class\n    if not re.search(r&quot;hcaptcha\\.com\/1\/api\\.js&quot;, p.srcs + &quot; &quot; + p.js):\n        return None\n    if re.search(r&quot;\\.setData\\s*\\(|rqdata&quot;, p.js):  # Enterprise rqdata is issued per challenge\n        return browser(&quot;hcaptcha_enterprise&quot;, &quot;hcaptcha&quot;, p, &quot;rqdata is issued for one challenge at run time&quot;)\n    w = tag(&quot;h-captcha&quot;, p.h) or tag(&quot;g-recaptcha&quot;, p.h)\n    key = attr(w, &quot;data-sitekey&quot;) or grab(SITEKEY_JS, render_opts(r&quot;hcaptcha\\.render&quot;, p.js))\n    if not key:\n        return browser(&quot;hcaptcha&quot;, &quot;hcaptcha&quot;, p, &quot;no sitekey in the static HTML&quot;)\n    args = {&quot;sitekey&quot;: key, &quot;url&quot;: p.url}\n    if attr(w, &quot;data-size&quot;) == &quot;invisible&quot;:\n        args[&quot;invisible&quot;] = 1\n    return hit(&quot;hcaptcha&quot;, &quot;hcaptcha&quot;, **args)<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: reCAPTCHA, in the right order<\/h2>\n<p>reCAPTCHA needs the most care because several CapSkip variants share two script names. The order of questions is what keeps them apart. First the script name: enterprise.js means the Enterprise flag, api.js does not. Then the render parameter in the script address. A key after render= means v3, but the word explicit after render= means a v2 page that renders its widget from code, and it must never be used as a key. Then the markup. A button with the g-recaptcha class and a data-action attribute is Google&#8217;s v3 button binding. A button without data-action, or any element with data-size set to invisible, is the invisible v2 widget. Anything else with the class is the checkbox.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def recaptcha(p):  # Enterprise flag first, then v3 (render=KEY), then v2\n    m = re.search(r&quot;\/recaptcha\/(api|enterprise)\\.js(\\?\\S*)?&quot;, p.srcs)\n    if not m:\n        return None\n    ent = {&quot;enterprise&quot;: 1} if m.group(1) == &quot;enterprise&quot; else {}\n    render = grab(r&quot;[?&amp;]render=([^&amp;\\s]+)&quot;, m.group(2))  # a v3 sitekey, or &quot;explicit&quot; (a v2 page)\n    t, o = tag(&quot;g-recaptcha&quot;, p.h), render_opts(r&quot;\\.render&quot;, p.js)\n    button, action = (t or &quot;&quot;).lower().startswith(&quot;&lt;button&quot;), attr(t, &quot;data-action&quot;)\n    v3 = bool(render and render != &quot;explicit&quot;) or bool(button and action)\n    key = render if render != &quot;explicit&quot; else None\n    key = key or attr(t, &quot;data-sitekey&quot;) or grab(SITEKEY_JS, o)\n    if not key:\n        return browser(&quot;recaptcha&quot;, &quot;recaptcha&quot;, p, &quot;no sitekey in the static HTML (set by JavaScript)&quot;)\n    small = attr(t, &quot;data-size&quot;) == &quot;invisible&quot; or grab(r&quot;size['\\&quot;]?\\s*:\\s*['\\&quot;](invisible)&quot;, o)\n    invisible = not v3 and bool(button or small)\n    action = action or grab(r&quot;execute\\(\\s*[^,)]+,\\s*\\{\\s*['\\&quot;]?action['\\&quot;]?\\s*:\\s*['\\&quot;]([^'\\&quot;]+)&quot;, p.js)\n    args = {&quot;sitekey&quot;: key, &quot;url&quot;: p.url, **ent}\n    if v3:\n        args.update(version=&quot;v3&quot;, **({&quot;action&quot;: action} if action else {}))\n    if invisible:\n        args[&quot;invisible&quot;] = 1\n    kind = &quot;recaptcha_&quot; + (&quot;v3&quot; if v3 else &quot;v2_invisible&quot; if invisible else &quot;v2&quot;)\n    return hit(kind + (&quot;_enterprise&quot; if ent else &quot;&quot;), &quot;recaptcha&quot;, **args)<\/pre>\n<\/div>\n<p>Each outcome maps to a call you already know. A checkbox is solver.recaptcha with a sitekey and the page URL. Invisible adds invisible=1, Enterprise adds enterprise=1, and v3 adds version=&quot;v3&quot; and the action. The action name comes from the data-action attribute or from the page&#8217;s execute call; if neither is in the HTML, the argument is left out and CapSkip uses its default of verify. See the <a href=\"https:\/\/capskip.com\/recaptcha-solver\/\">reCAPTCHA solver page<\/a> for every variant it handles.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 5: Turnstile<\/h2>\n<p>A Turnstile widget needs two things on the page: Cloudflare&#8217;s api.js and a sitekey. The key sits in a data-sitekey attribute on a div with the cf-turnstile class, or in a turnstile.render call. The check requires the script, so a stray class name on an unrelated page cannot trigger it. It also reads data-action and data-cdata when they are present and passes them on. The <a href=\"https:\/\/capskip.com\/cloudflare-turnstile-solver\/\">Turnstile solver page<\/a> lists the options.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def turnstile(p):  # widget only: implicit (div.cf-turnstile) or explicit (turnstile.render)\n    if &quot;challenges.cloudflare.com\/turnstile\/v0\/api.js&quot; not in p.srcs:\n        return None\n    t, o = tag(&quot;cf-turnstile&quot;, p.h), render_opts(r&quot;turnstile\\.render&quot;, p.js)\n    key = attr(t, &quot;data-sitekey&quot;) or grab(SITEKEY_JS, o)\n    if not key:\n        return browser(&quot;turnstile&quot;, &quot;turnstile&quot;, p, &quot;no sitekey in the static HTML&quot;)\n    action = attr(t, &quot;data-action&quot;) or grab(r&quot;\\baction['\\&quot;]?\\s*:\\s*['\\&quot;]([^'\\&quot;]+)&quot;, o)\n    cdata = attr(t, &quot;data-cdata&quot;) or grab(r&quot;\\bcData['\\&quot;]?\\s*:\\s*['\\&quot;]([^'\\&quot;]+)&quot;, o)\n    extra = {k: v for k, v in ((&quot;action&quot;, action), (&quot;data&quot;, cdata)) if v}\n    return hit(&quot;turnstile&quot;, &quot;turnstile&quot;, sitekey=key, url=p.url, **extra)<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 6: GeeTest and ALTCHA<\/h2>\n<p>GeeTest v3 gives you two values, and one of them is a trap. The gt value is fixed for the site and can sit in the page. The challenge value is single-use, and sites fetch it from a register endpoint at run time. If the page carries both, the detector returns them. If it carries only the initGeetest call, it answers needs_browser, because a challenge found in old HTML would already be dead.<\/p>\n<p>ALTCHA changed its attribute names between generations. Versions 1 and 2 use challengeurl, or challengejson for an inline challenge. Version 3 and later use one attribute called challenge, which takes either a URL or the challenge data. The check reads all three and joins a relative URL to the page address.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def geetest(p):  # v3 only: gt is static, challenge comes from the register endpoint\n    if not (re.search(r&quot;initGeetest\\s*\\(&quot;, p.js) or re.search(r&quot;\/gt(\\.[\\d.]+)?\\.js&quot;, p.srcs)):\n        return None\n    gt = grab(r&quot;\\bgt['\\&quot;]?\\s*:\\s*['\\&quot;]([0-9a-f]{32})&quot;, p.js)\n    challenge = grab(r&quot;\\bchallenge['\\&quot;]?\\s*:\\s*['\\&quot;]([0-9a-f]{32,34})&quot;, p.js)\n    if not (gt and challenge):\n        return browser(&quot;geetest&quot;, &quot;geetest&quot;, p, &quot;gt and challenge come from the register endpoint&quot;)\n    return hit(&quot;geetest&quot;, &quot;geetest&quot;, gt=gt, challenge=challenge, url=p.url)\n\ndef altcha(p):  # challengeurl \/ challengejson (v1, v2) or challenge (v3+)\n    t = grab(r&quot;(&lt;altcha-widget\\b[^&gt;]*&gt;)&quot;, p.h)\n    if not t:\n        return None\n    c = attr(t, &quot;challenge&quot;)\n    as_json = attr(t, &quot;challengejson&quot;) or (c if (c or &quot;&quot;).lstrip().startswith(&quot;{&quot;) else None)\n    as_url = attr(t, &quot;challengeurl&quot;) or (None if as_json else c)\n    if as_json:\n        return hit(&quot;altcha&quot;, &quot;altcha&quot;, url=p.url, challenge_json=as_json)\n    if as_url:\n        return hit(&quot;altcha&quot;, &quot;altcha&quot;, url=p.url, challenge_url=urljoin(p.url, as_url))\n    return browser(&quot;altcha&quot;, &quot;altcha&quot;, p, &quot;no challenge attribute&quot;)<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 7: Capy, CaptchaFox and Friendly Captcha<\/h2>\n<p>These three are easy to find and easy to get slightly wrong. Capy needs no widget div for this check. Its key starts with PUZZLE_ and sits in the k parameter of the widget script&#8217;s address, and a host other than the default is passed as api_server. CaptchaFox comes in two builds. The plain one loads from cdn.captchafox.com. The other is bundled by a hosting platform under a shared CDN path, so the detector looks for the CaptchaFox package name in that path and not for the path alone, which other widgets also use.<\/p>\n<p>Friendly Captcha is the trickiest. Version 1 and version 2 use the same div and the same sitekey namespace, but different protocols, and a token for the wrong one is accepted by CapSkip and rejected by the site. The scripts tell them apart: version 2 loads the @friendlycaptcha\/sdk package, version 1 loads friendly-challenge. When the page gives no sign, the detector refuses to guess and says needs_browser.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def capy(p):  # the widget script is ...\/puzzle\/get_js\/?k=PUZZLE_...\n    m = re.search(r&quot;((?:https?:)?\/\/[^\\s\\&quot;']+?)\/puzzle\/get_js\/\\S*?(PUZZLE_[\\w-]+)&quot;, p.srcs)\n    if m:\n        host = m.group(1) if m.group(1).startswith(&quot;http&quot;) else &quot;https:&quot; + m.group(1)\n        more = {} if host == &quot;https:\/\/jp.api.capy.me&quot; else {&quot;api_server&quot;: host}\n        return hit(&quot;capy&quot;, &quot;capy&quot;, sitekey=m.group(2), url=p.url, **more)\n\nMAM = &quot;mampkg\/@mamdev\/core.frontend.libs.captchafox&quot;\n\ndef captchafox(p):  # plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com\n    t, key = tag(&quot;captchafox&quot;, p.h), grab(r&quot;captchafox[^\\&quot;']*\/api\\.js\\?key=([^\\&quot;'&amp;]+)&quot;, p.srcs)\n    if t or key:\n        more = {&quot;api_server&quot;: &quot;https:\/\/s.uicdn.com\/&quot; + MAM + &quot;\/&quot;} if MAM in p.srcs else {}\n        sitekey = attr(t, &quot;data-sitekey&quot;) or key\n        if not sitekey:\n            return browser(&quot;captchafox&quot;, &quot;captchafox&quot;, p, &quot;no sitekey in the static HTML&quot;)\n        return hit(&quot;captchafox&quot;, &quot;captchafox&quot;, sitekey=sitekey, url=p.url, **more)\n\ndef friendly(p):  # v2 loads @friendlycaptcha\/sdk (site.min.js), v1 loads friendly-challenge\n    t = tag(&quot;frc-captcha&quot;, p.h)\n    if not t:\n        return None\n    v2_end, v1_end = attr(t, &quot;data-api-endpoint&quot;), attr(t, &quot;data-puzzle-endpoint&quot;)\n    server = v2_end or v1_end\n    v2 = v2_end is not None or re.search(r&quot;friendlycaptcha\/sdk|site(\\.compat)?\\.min\\.js&quot;, p.srcs)\n    v1 = v1_end is not None or re.search(r&quot;friendly-challenge|widget(\\.module)?\\.min\\.js&quot;, p.srcs)\n    if bool(v2) == bool(v1):\n        return browser(&quot;friendly_captcha&quot;, &quot;friendly_captcha&quot;, p, &quot;cannot tell v1 from v2 in the HTML&quot;)\n    if not attr(t, &quot;data-sitekey&quot;):\n        return browser(&quot;friendly_captcha&quot;, &quot;friendly_captcha&quot;, p, &quot;no sitekey in the static HTML&quot;)\n    more = {&quot;api_server&quot;: server} if server else {}\n    return hit(&quot;friendly_captcha_&quot; + (&quot;v2&quot; if v2 else &quot;v1&quot;), &quot;friendly_captcha&quot;,\n               sitekey=attr(t, &quot;data-sitekey&quot;), url=p.url, version=&quot;v2&quot; if v2 else &quot;v1&quot;, **more)<\/pre>\n<\/div>\n<p>Each of these has its own solver page with the options the method takes:<\/p>\n<ul>\n<li><a href=\"https:\/\/capskip.com\/capy-puzzle-solver\/\">Capy Puzzle solver page<\/a>: the three values a Capy answer returns.<\/li>\n<li><a href=\"https:\/\/capskip.com\/captchafox-solver\/\">CaptchaFox solver page<\/a>: the options the CaptchaFox method takes.<\/li>\n<li><a href=\"https:\/\/capskip.com\/friendly-captcha-solver\/\">Friendly Captcha solver page<\/a>: the Friendly Captcha method.<\/li>\n<\/ul>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 8: the image CAPTCHA, a guess<\/h2>\n<p>An image CAPTCHA has no vendor script to find, so this check is a guess and runs only when nothing else matched. The check wants an image whose tag says captcha, and a text box elsewhere on the page whose tag says captcha or security code. It missed in every case where the markup did not use those words, and a drawn canvas is invisible to it. Treat a hit as likely, not certain.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">def image(p):  # a guess: an &lt;img&gt; that says captcha, next to a text box that says captcha\n    box = r&quot;&lt;input\\b(?![^&gt;]*type\\s*=\\s*[\\&quot;']?(?:hidden|checkbox|submit))[^&gt;]*(?:captcha|security.?code)&quot;\n    for img in re.findall(r&quot;&lt;img\\b[^&gt;]*&gt;&quot;, p.h, re.I):\n        if re.search(box, p.h, re.I) and re.search(r&quot;captcha&quot;, img, re.I) and attr(img, &quot;src&quot;):\n            return hit(&quot;image&quot;, &quot;normal&quot;, image=urljoin(p.url, attr(img, &quot;src&quot;)))<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 9: the checks, detect and solve<\/h2>\n<p>The last block lists the checks in order and exposes detect and solve, with detect_all as a helper. The order is the logic: unsupported vendors first, then the Cloudflare page, then hCaptcha, then each other family, and the image guess only when the list found nothing. The detect function returns the first result and adds an also list when the page carries more than one family, for example a v3 script plus a Turnstile box. The list names families, not a second reCAPTCHA widget with another key. The solve function dispatches the result to the right CapSkip method. It refuses anything unsupported or marked needs_browser with a RuntimeError, and it downloads an image through your own Session so the cookies match.<\/p>\n<p>The SDK has no hCaptcha call yet, so an hCaptcha result goes to solve_hcaptcha instead. It posts the sitekey and the page address to in.php with method=hcaptcha, then polls res.php every 5 seconds until the token is ready, as the <a href=\"https:\/\/capskip.com\/api-docs\/#hcaptcha\">hCaptcha section of the API docs<\/a> describes. Pass the solver&#8217;s address as api, and your API key as key if API key validation is switched on. The result carries the token in code and the User-Agent it was earned with in userAgent.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">CHECKS = [unsupported, cf_challenge, hcaptcha, recaptcha, turnstile, geetest, altcha, capy, captchafox, friendly]\n\ndef detect_all(html, page_url):\n    p = parse(html, page_url)\n    return [r for r in (check(p) for check in CHECKS) if r] or [r for r in [image(p)] if r]\n\ndef detect(html, page_url):\n    found = detect_all(html, page_url)\n    if len(found) &gt; 1:\n        found[0][&quot;also&quot;] = [r[&quot;type&quot;] for r in found[1:]]\n    return found[0] if found else None\n\ndef solve_hcaptcha(args, api, key=None):  # no SDK call yet: post to in.php, poll res.php\n    form = {&quot;method&quot;: &quot;hcaptcha&quot;, &quot;sitekey&quot;: args[&quot;sitekey&quot;], &quot;pageurl&quot;: args[&quot;url&quot;],\n            &quot;invisible&quot;: args.get(&quot;invisible&quot;, 0), &quot;data&quot;: args.get(&quot;data&quot;), &quot;key&quot;: key, &quot;json&quot;: 1}\n    job = requests.post(api + &quot;\/in.php&quot;, data=form, timeout=30).json()\n    if job[&quot;status&quot;] != 1:\n        raise RuntimeError(job[&quot;request&quot;])\n    for _ in range(36):  # a solve takes 10 to 30 seconds, so give up after 3 minutes\n        time.sleep(5)\n        q = {&quot;key&quot;: key, &quot;action&quot;: &quot;get&quot;, &quot;id&quot;: job[&quot;request&quot;], &quot;json&quot;: 1}\n        r = requests.get(api + &quot;\/res.php&quot;, params=q, timeout=30).json()\n        if r[&quot;request&quot;] != &quot;CAPCHA_NOT_READY&quot;:\n            if r[&quot;status&quot;] != 1:\n                raise RuntimeError(r[&quot;request&quot;])\n            return {&quot;code&quot;: r[&quot;request&quot;], &quot;userAgent&quot;: r.get(&quot;useragent&quot;)}\n    raise TimeoutError(&quot;hCaptcha was not solved within 3 minutes&quot;)\n\ndef solve(info, solver, session=None, api=&quot;http:\/\/127.0.0.1:8080&quot;, key=None):\n    if info.get(&quot;unsupported&quot;) or info.get(&quot;needs_browser&quot;):\n        raise RuntimeError(info.get(&quot;needs_browser&quot;) or info[&quot;type&quot;] + &quot; is not supported by CapSkip&quot;)\n    args = dict(info[&quot;args&quot;])\n    if info[&quot;method&quot;] == &quot;hcaptcha&quot;:\n        return solve_hcaptcha(args, api, key)\n    if info[&quot;method&quot;] == &quot;normal&quot;:  # fetch the image with YOUR session so the cookie matches it\n        src = args.pop(&quot;image&quot;)\n        if not src.startswith(&quot;data:&quot;):\n            r = (session or requests).get(src, timeout=30)\n            r.raise_for_status()\n            src = &quot;data:image\/png;base64,&quot; + base64.b64encode(r.content).decode()\n        args[&quot;file&quot;] = src\n    return getattr(solver, info[&quot;method&quot;])(**args)<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Run it<\/h2>\n<p>Save this as use.py next to detect.py and run it with the environment&#8217;s own Python, as in .\\.venv\\Scripts\\python.exe use.py. The port 8080 is the default; use the port shown in CapSkip&#8217;s connection settings if yours differs. The script fetches your page, prints what the detector found, and only calls CapSkip when there is a method to call and nothing is missing.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">import requests\nfrom capskip import CapSkip\nfrom detect import detect, solve\n\nURL = &quot;https:\/\/example.com\/login&quot;   # a page of yours with a CAPTCHA\nHOST, PORT = &quot;127.0.0.1&quot;, 8080      # from CapSkip's connection settings\nsession = requests.Session()\nhtml = session.get(URL, timeout=30).text\n\ninfo = detect(html, URL)\nprint(info)\n\nif info and info[&quot;method&quot;] and not info.get(&quot;needs_browser&quot;):\n    solver = CapSkip(host=HOST, port=PORT)\n    result = solve(info, solver, session, api=f&quot;http:\/\/{HOST}:{PORT}&quot;)\n    print(result)<\/pre>\n<\/div>\n<p>On a page with a reCAPTCHA v2 checkbox the first print shows the type, the method and the arguments, and the second shows the result with the token in its code field. In the mock run it looked like this:<\/p>\n<table>\n<thead>\n<tr>\n<th>Page<\/th>\n<th>detect returns<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>reCAPTCHA v2 checkbox<\/td>\n<td>type recaptcha_v2, method recaptcha, sitekey and url<\/td>\n<\/tr>\n<tr>\n<td>hCaptcha widget<\/td>\n<td>type hcaptcha, method hcaptcha, sitekey and url<\/td>\n<\/tr>\n<tr>\n<td>Cloudflare &#8220;Just a moment&#8221; page<\/td>\n<td>type turnstile_challenge, needs_browser set<\/td>\n<\/tr>\n<tr>\n<td>v3 script plus a Turnstile widget<\/td>\n<td>type recaptcha_v3, also lists turnstile<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">When the HTML says nothing: the browser console<\/h2>\n<p>Some pages add their widget with JavaScript after loading. A plain fetch then finds no CAPTCHA at all, and the detector returns None for a page that has one. Two cases are known and expected: a Google or Cloudflare script added by code, and a sitekey set by code. For those, open the page in Chrome, press F12 and paste this into the Console. If Chrome blocks the paste, type allow pasting and press Enter first. It reads the sitekeys from the resources the page has already loaded, so it sees what the browser built.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">(() =&gt; {\n  const iframes = [...document.querySelectorAll('iframe')].map((f) =&gt; f.src);\n  const urls = performance.getEntriesByType('resource').map((r) =&gt; r.name).concat(iframes);\n  const found = new Map(), add = (o) =&gt; found.set(JSON.stringify(o), o);\n  for (const u of urls) {\n    let m;\n    if ((m = u.match(\/recaptcha\\\/(?:api2|(enterprise))\\\/anchor\\?.*?\\bk=([\\w-]+)\/)))\n      add({ type: 'recaptcha', sitekey: m[2], enterprise: !!m[1], invisible: \/size=invisible\/.test(u) });\n    else if ((m = u.match(\/recaptcha\\\/(?:api|enterprise)\\.js\\?.*?\\brender=([\\w-]+)\/)) &amp;&amp; m[1] !== 'explicit')\n      add({ type: 'recaptcha v3', sitekey: m[1], enterprise: \/enterprise\\.js\/.test(u) });\n    else if ((m = u.match(\/challenges\\.cloudflare\\.com\\\/.*turnstile\\\/.*\\\/(0x4[\\w-]+|[123]x0+[A-Za-z]+)\\\/\/)))\n      add({ type: 'turnstile', sitekey: m[1] });\n    else if ((m = u.match(\/hcaptcha\\.com\\\/captcha\\\/.*[#&amp;?]sitekey=([\\w-]+)\/))) add({ type: 'hcaptcha', sitekey: m[1] });\n    else if (\/arkoselabs\\.com\\\/fc\\\/\/.test(u)) add({ type: 'arkose: CapSkip cannot solve' });\n  }\n  if (window._cf_chl_opt) add({ type: 'cloudflare challenge page' });\n  document.querySelectorAll('altcha-widget').forEach((e) =&gt; add({ type: 'altcha', challenge: e.getAttribute('challenge') || e.getAttribute('challengeurl') }));\n  document.querySelectorAll('.h-captcha[data-sitekey]').forEach((e) =&gt; add({ type: 'hcaptcha', sitekey: e.dataset.sitekey }));\n  document.querySelectorAll('.frc-captcha, .captchafox').forEach((e) =&gt; add({ type: e.classList.contains('captchafox') ? 'captchafox' : 'frc-captcha', sitekey: e.dataset.sitekey }));\n  const v3 = [...found.values()].filter((o) =&gt; o.type === 'recaptcha v3').map((o) =&gt; o.sitekey);  \/\/ a v3 badge also adds an invisible anchor frame\n  const res = [...found.values()].filter((o) =&gt; !(o.type === 'recaptcha' &amp;&amp; v3.includes(o.sitekey)));\n  return res.length ? res : 'no known CAPTCHA found (GeeTest, Capy and CaptchaFox scripts are not checked)';\n})();<\/pre>\n<\/div>\n<p>It found the sitekey and flags on a reCAPTCHA v2 page, an invisible one, a v3 page, a Turnstile page and the hCaptcha demo, and it printed a message on a page with no CAPTCHA. Three limits: the browser keeps only about 250 resource entries by default, so a long-running single page app can lose early ones, and the snippet cannot tell Friendly Captcha version 1 from version 2, and it does not look for GeeTest, Capy or CaptchaFox scripts. On Cloudflare&#8217;s full-page check it can show the sitekey from a resource address, but not the data and pagedata values, which need a hook on the render call. That hook was not tested here.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Where detection fails<\/h2>\n<table>\n<thead>\n<tr>\n<th>Situation<\/th>\n<th>What detect returns<\/th>\n<th>What to do<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Widget script added by JavaScript<\/td>\n<td>None<\/td>\n<td>Load the page in a browser and run the console snippet<\/td>\n<\/tr>\n<tr>\n<td>Sitekey set by JavaScript<\/td>\n<td>needs_browser<\/td>\n<td>Read the key from the live page<\/td>\n<\/tr>\n<tr>\n<td>Cloudflare full-page check<\/td>\n<td>needs_browser<\/td>\n<td>Read the values in a browser, see the cf_clearance guide<\/td>\n<\/tr>\n<tr>\n<td>CaptchaFox, Friendly Captcha or Capy script with no widget in the HTML<\/td>\n<td>None<\/td>\n<td>Run the console snippet or read the page source<\/td>\n<\/tr>\n<tr>\n<td>A 403 or 503 page from the site itself<\/td>\n<td>None<\/td>\n<td>Print the status code first: a block page holds no CAPTCHA to find<\/td>\n<\/tr>\n<tr>\n<td>Arkose, GeeTest v4<\/td>\n<td>unsupported<\/td>\n<td>CapSkip cannot solve these<\/td>\n<\/tr>\n<tr>\n<td>hCaptcha Enterprise that sets rqdata<\/td>\n<td>needs_browser<\/td>\n<td>Read a fresh rqdata from the live page for each task and pass it to solve_hcaptcha as data<\/td>\n<\/tr>\n<tr>\n<td>Friendly Captcha with no script hint<\/td>\n<td>needs_browser<\/td>\n<td>Check which script the page loads<\/td>\n<\/tr>\n<tr>\n<td>Image CAPTCHA without the word captcha<\/td>\n<td>None<\/td>\n<td>Add your own rule for that site<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors<\/h2>\n<table>\n<thead>\n<tr>\n<th>Error<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>ERROR_GOOGLEKEY<\/td>\n<td>The key was the word explicit, or a key that is not a reCAPTCHA key<\/td>\n<td>Read the key from the widget, not from the render parameter<\/td>\n<\/tr>\n<tr>\n<td>ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>Any failed job reads this way, including a wrong reCAPTCHA version, and it does not name the cause<\/td>\n<td>Check the detected type first, then see the <a href=\"https:\/\/capskip.com\/api-docs\/\">API docs<\/a><\/td>\n<\/tr>\n<tr>\n<td>ValidationException<\/td>\n<td>An option the method does not take, such as an action on a v2 call<\/td>\n<td>Use the arguments the detector returned<\/td>\n<\/tr>\n<tr>\n<td>NetworkException<\/td>\n<td>CapSkip is not running, or the host or port is wrong<\/td>\n<td>Start CapSkip and check the connection settings<\/td>\n<\/tr>\n<tr>\n<td>RuntimeError from solve<\/td>\n<td>The result is unsupported or needs_browser, or CapSkip refused an hCaptcha job and the message is its error code<\/td>\n<td>Handle the first two cases before calling solve, and check the sitekey and the page address for the third<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on a server<\/h2>\n<p>The detector only needs two things from CapSkip: a host and a port. In Local mode that is 127.0.0.1 and your own PC. In Server mode CapSkip listens on your network address or a public IP, so a detector running on another PC, a VPS or a hosted platform can call it over the same API. Change HOST and PORT at the top of use.py, and nothing else in the code changes. A static public IP is recommended for stable remote access. Server mode is still your own hardware and still unmetered. Note that Server mode opens the port to any address, so add a Windows Firewall block rule for the addresses that should not reach it. See the <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a> for how to switch modes.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can I tell which CAPTCHA a site uses without code?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes. Open DevTools, go to the Network tab, reload the page and look at the script addresses. A google.com\/recaptcha or recaptcha.net script is reCAPTCHA, challenges.cloudflare.com is Turnstile, and js.hcaptcha.com is hCaptcha. The Console snippet above does the same lookup for you.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Why does detect return needs_browser instead of a guess?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Because a wrong guess wastes a full solve and returns a token the site rejects with no hint why. When the static HTML does not hold the sitekey, a GeeTest challenge or a Friendly Captcha version, saying so is quicker than a failed job.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does CapSkip solve hCaptcha?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes. CapSkip solves hCaptcha checkbox, invisible and Enterprise widgets through its HTTP API with method=hcaptcha, as the <a href=\"https:\/\/capskip.com\/api-docs\/#hcaptcha\">API docs<\/a> describe. The Python SDK has no hCaptcha call yet, so solve_hcaptcha above posts the job to in.php itself. FunCaptcha, Arkose and GeeTest v4 are still not supported, and the detector reports those as unsupported so a script can skip the page instead of failing on it.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Can a hosted platform use the detector with my CapSkip?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, through Server mode. Set CapSkip to listen on your network or public IP, set HOST and PORT in use.py to that address, and the detector and the solve call work the same as on one PC. The solver stays on your hardware.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Detect CAPTCHA type first, then solve: read the script and the markup in a fixed order, hand the arguments to the right method, and treat None, needs_browser and unsupported as answers, not errors. With CapSkip on your own machine there is no meter running while you check a page, which is what makes an <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> practical for a crawl that meets a new site every few minutes. For the Python SDK itself, see the <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">Python CAPTCHA solver page<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u628a v3 \u5bc6\u94a5\u53d1\u7ed9 v2 \u65b9\u6cd5\uff0c\u8981\u7b49\u5230\u8bc6\u522b\u7ed3\u675f\u624d\u4f1a\u53d1\u73b0\u51fa\u9519\u3002\u4e0b\u9762\u662f\u4e00\u4e2a Python \u68c0\u6d4b\u5668\uff1a\u5b83\u8bfb\u53d6\u9875\u9762\uff0c\u5224\u65ad\u9a8c\u8bc1\u7801\u7c7b\u578b\uff0c\u63d0\u53d6 sitekey\uff0c\u5e76\u8c03\u7528\u5339\u914d\u7684 CapSkip \u65b9\u6cd5\uff0c\u65e0\u6cd5\u5224\u65ad\u65f6\u4f1a\u660e\u786e\u544a\u8bc9\u4f60\u3002<\/p>","protected":false},"author":1,"featured_media":27026,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Detect CAPTCHA Type in Python, Then Solve It | CapSkip","rank_math_description":"Detect CAPTCHA type on a page in Python: read the script and widget, pull the sitekey, then call the right CapSkip method. Tested on 66 pages.","rank_math_focus_keyword":"detect captcha type","footnotes":""},"categories":[70],"tags":[],"class_list":["post-27027","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27027","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=27027"}],"version-history":[{"count":4,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27027\/revisions"}],"predecessor-version":[{"id":27225,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/27027\/revisions\/27225"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/27026"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=27027"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=27027"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=27027"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}