{"id":25480,"date":"2026-09-03T07:41:03","date_gmt":"2026-09-03T07:41:03","guid":{"rendered":"https:\/\/capskip.com\/?p=25480"},"modified":"2026-09-03T07:41:03","modified_gmt":"2026-09-03T07:41:03","slug":"nodriver-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/nodriver-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 nodriver \u4e2d\u7528\u5f02\u6b65 SDK \u8bc6\u522b\u9a8c\u8bc1\u7801"},"content":{"rendered":"<p>A nodriver captcha step is the usual three moves: read the sitekey off the page, send it to a solver, write the token back with JavaScript. What changes is that nodriver is asynchronous all the way down. It drives Chrome over an asyncio websocket, so a blocking solve does not just make your script wait, it stalls the socket that carries every DevTools message. Pair it with the async client and the whole thing stays responsive.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Python 3.10 or newer, with nodriver 0.50 and the CapSkip SDK installed.<\/li>\n<li>Chrome, Chromium, Edge or Brave installed where the script runs. nodriver launches it directly.<\/li>\n<li>The page URL of the protected form. The sitekey is read at runtime.<\/li>\n<li>CapSkip running in Local mode when the script and the solver share a machine, or in Server mode when they do not. Both are described under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Both packages, one line.\npip install nodriver capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why nodriver changes the shape of this<\/h2>\n<p>nodriver is the official successor to undetected-chromedriver, written by the same author, and its headline is that there is no webdriver and no Selenium anywhere in the stack. It speaks the DevTools Protocol to a browser it launched itself. No chromedriver binary to patch, no driver version to keep in step with Chrome.<\/p>\n<p>The part that matters for CAPTCHA work is the second half of that sentence: it is fully asynchronous. The connection is a websocket handled by asyncio, and a background task reads protocol messages off it. Every element lookup, every navigation and every event handler depends on that task getting scheduled. Call a synchronous solver in the middle and nothing else in the process runs for the length of the solve, which for reCAPTCHA v2 is routinely fifteen to forty-five seconds.<\/p>\n<p>So the rule for this framework is short. Use the async client, and await it.<\/p>\n<p>One thing worth being straight about: no webdriver does not mean no detection. Removing the driver removes one signal and leaves the rest of your fingerprint where it was. Solving a challenge and looking like a browser are separate jobs, and this post covers the first.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: read the sitekey off the page<\/h2>\n<p>The sitekey lives on the host document, not inside the widget iframe. Google&#8217;s markup puts it on a container as a data-sitekey attribute, and nodriver&#8217;s select method finds that container by CSS selector. Watch the bracket access on the last line, because it is the thing that catches people out.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install nodriver\nimport nodriver as uc\n\nasync def main():\n    browser = await uc.start()\n    page = await browser.get(&quot;https:\/\/example.com\/page-with-recaptcha&quot;)\n\n    # select() retries for 10 seconds by default, so it doubles\n    # as a wait condition for a widget that renders late.\n    holder = await page.select(&quot;div.g-recaptcha&quot;)\n    sitekey = holder.attrs[&quot;data-sitekey&quot;]\n\n    print(sitekey)   # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\nuc.loop().run_until_complete(main())<\/pre>\n<\/div>\n<p>Attribute names are stored on the element exactly as the HTML spelled them, hyphens included, so bracket access is the only reliable way to read one. The dotted shortcut looks like it should work and quietly does not: asking an element for data_sitekey hands back None rather than raising, because that lookup falls through to a default. The None then travels to the solver as an empty key and surfaces much later as <a href=\"https:\/\/capskip.com\/error-googlekey-pageurl\/\">ERROR_GOOGLEKEY<\/a>, a long way from the line that caused it. There is exactly one real rename to know about, which is that the class attribute is stored under class_ to keep it off the Python keyword.<\/p>\n<p>Some sites never expose the sitekey on the host page and only pass it in the widget iframe URL. Read it from the query string instead.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Fallback: the k= parameter on the anchor iframe.\nfrom urllib.parse import urlparse, parse_qs\n\nframe = await page.select(&quot;iframe[src*='recaptcha\/api2\/anchor']&quot;)\nsitekey = parse_qs(urlparse(frame.attrs[&quot;src&quot;]).query)[&quot;k&quot;][0]<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: solve it without stalling the socket<\/h2>\n<p>The Python SDK ships two clients. CapSkip is synchronous and AsyncCapSkip is a genuine asyncio implementation rather than an alias, which is exactly what this framework needs. Both talk to the solver on your own machine on port 8080, and neither bills per solve.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\nfrom capskip import AsyncCapSkip\n\nsolver = AsyncCapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n# Same call shape for v3 (version=&quot;v3&quot;) and Enterprise\n# (enterprise=1). Invisible v2 takes invisible=1.\nresult = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)\n\ntoken = result[&quot;code&quot;]   # the g-recaptcha-response value<\/pre>\n<\/div>\n<p>Because the call is awaitable, several tabs can solve at once without any threading. Open the pages, gather the solves, then inject each token into the tab it belongs to. The wider pattern, including how the SDK backs its polling off instead of sleeping on a flat interval, is covered in <a href=\"https:\/\/capskip.com\/solve-captchas-parallel-python\/\">solving CAPTCHAs in parallel<\/a>.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Three tabs, three solves, one wait.\nimport asyncio\n\nresults = await asyncio.gather(*[\n    solver.recaptcha(sitekey=k, url=u) for k, u in targets\n])<\/pre>\n<\/div>\n<p>Turnstile and GeeTest have their own methods, and both take the same shape as the call above. Full parameter lists for each are in <a href=\"https:\/\/capskip.com\/api-docs\/\">the CapSkip API documentation<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: inject the token and submit<\/h2>\n<p>The response textarea is hidden with display:none, so typing into it is not an option in any automation tool. You write it with JavaScript. nodriver&#8217;s evaluate method takes an expression string with no way to pass arguments alongside it, so the token has to be embedded in that string, and the safe way to do that is json.dumps rather than an f-string. A JSON string literal is a valid JavaScript string literal, quoting and escaping included.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># json.dumps gives a correctly quoted JS string literal.\nimport json\n\nawait page.evaluate(\n    &quot;document.getElementById('g-recaptcha-response').value = &quot;\n    + json.dumps(token)\n)\n\n# Then submit the form the way the page expects.\nbutton = await page.select(&quot;button[type=submit]&quot;)\nawait button.click()<\/pre>\n<\/div>\n<p>Checking that the value landed has a trap of its own, and it is worth ten seconds of your attention. With return_by_value set, evaluate only hands back the plain Python value when that value is truthy. An empty string or a zero falls through and you get a protocol object instead. So do not read the length and test it, because a length of zero is the exact case you are trying to detect. Return something that can never be falsy.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># String() keeps a zero-length answer truthy, so the check\n# reports the real number instead of a protocol object.\nlength = await page.evaluate(\n    &quot;String(document.getElementById('g-recaptcha-response').value.length)&quot;,\n    return_by_value=True,\n)\nprint(length)   # &quot;0&quot; means the injection did not land<\/pre>\n<\/div>\n<p>If the site defines a callback instead of reading the textarea on submit, call it after setting the value. The function name is site-specific, so read it out of the page&#8217;s own markup rather than guessing. This is still ordinary reCAPTCHA v2 either way: the callback changes how you hand the token over, not how it gets solved. The <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">reCAPTCHA v2 solver page<\/a> covers both submission styles.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<p>Everything above in one script. The solver is created once and reused, and the browser is stopped in a finally block so a failed solve does not leave a Chrome process behind.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install nodriver capskip\nimport json\nimport nodriver as uc\nfrom capskip import AsyncCapSkip\n\nPAGE_URL = &quot;https:\/\/example.com\/page-with-recaptcha&quot;\n\nasync def main():\n    solver = AsyncCapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n    browser = await uc.start()\n    try:\n        page = await browser.get(PAGE_URL)\n\n        holder = await page.select(&quot;div.g-recaptcha&quot;)\n        sitekey = holder.attrs[&quot;data-sitekey&quot;]\n        if not sitekey:\n            raise RuntimeError(&quot;Widget found but data-sitekey was empty.&quot;)\n\n        result = await solver.recaptcha(sitekey=sitekey, url=PAGE_URL)\n        await page.evaluate(\n            &quot;document.getElementById('g-recaptcha-response').value = &quot;\n            + json.dumps(result[&quot;code&quot;])\n        )\n\n        button = await page.select(&quot;button[type=submit]&quot;)\n        await button.click()\n        await page.sleep(2)\n\n        print(page.target.url)   # the page you land on after submitting\n    finally:\n        browser.stop()\n\nuc.loop().run_until_complete(main())<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on another machine<\/h2>\n<p>nodriver ends up on a server sooner or later, and it has two needs there: a Chromium binary, and a desktop session for Chrome to draw on. Headless mode is off by default, so a server without one needs headless switched on explicitly. The solver does not have to make that trip with it.<\/p>\n<p>CapSkip has two connection modes. Local binds to 127.0.0.1 and answers only that device, which is the right setting while you are writing the script. Server binds to your network or public IP, so a scraping VM, a container host or a second workstation calls the same solver over the API. A static public IP keeps that address stable. Nothing in the code changes except the host you pass in, and nothing about the cost changes either, because it is still your hardware.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Same SDK, same call. Only the host moves.\nsolver = AsyncCapSkip(host=&quot;10.0.0.12&quot;, port=8080, apiKey=&quot;YOUR_API_KEY&quot;)<\/pre>\n<\/div>\n<p>Turn on key validation once the solver listens on a network address, and give each machine its own key so one can be revoked without touching the rest. The <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">setup guide<\/a> walks through both modes.<\/p>\n<p>One naming collision to keep straight while you do that. nodriver&#8217;s own start function also accepts a host and a port, and those describe a Chrome debugging endpoint you want to attach to, not the solver. Supply both and nodriver will not launch a browser at all. The solver address belongs to the client constructor and nowhere else.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors and what they mean<\/h2>\n<table>\n<thead>\n<tr>\n<th>What you see<\/th>\n<th>Cause<\/th>\n<th>Fix<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>AttributeError on an attrs lookup<\/td>\n<td>select() found nothing and handed back None<\/td>\n<td>Widen the selector, or raise the select timeout<\/td>\n<\/tr>\n<tr>\n<td>Sitekey is None with no error at all<\/td>\n<td>Dotted access cannot reach a hyphenated attribute<\/td>\n<td>Read it from attrs with bracket access<\/td>\n<\/tr>\n<tr>\n<td>ERROR_GOOGLEKEY<\/td>\n<td>An empty sitekey reached the solver<\/td>\n<td>Check the value before spending a solve on it<\/td>\n<\/tr>\n<tr>\n<td>The script hangs for the whole solve<\/td>\n<td>A synchronous client blocked the event loop<\/td>\n<td>Use AsyncCapSkip and await the call<\/td>\n<\/tr>\n<tr>\n<td>NetworkException<\/td>\n<td>CapSkip is not running, or the host is wrong<\/td>\n<td>Start the app, or point host at the server address<\/td>\n<\/tr>\n<tr>\n<td>TimeoutException<\/td>\n<td>The solve outlasted recaptchaTimeout<\/td>\n<td>Raise it above the default 300 seconds<\/td>\n<\/tr>\n<tr>\n<td>evaluate returns an object, not a string<\/td>\n<td>The value was falsy, so the plain return was skipped<\/td>\n<td>Wrap the length in String(), not the value itself<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\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 keep the synchronous client if I only solve once?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">You can, and on a short script you may never notice. What you are trading away is every protocol message that arrives during the solve: navigation events, load events, anything an event handler was waiting for. On a long run that shows up as lookups timing out for no visible reason. The async client costs one import and one await, so there is not much reason to take the trade.<\/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;\">Do I need to enter the reCAPTCHA iframe?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No, and this is the part people over-engineer. The checkbox lives in an iframe, but the sitekey attribute and the hidden response textarea both belong to the host document. You only touch a frame when the site withholds the sitekey from the page and you have to read it out of the frame&#8217;s own URL.<\/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;\">I am migrating from undetected-chromedriver. What carries over?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The three moves carry over unchanged, because they were never driver-specific: read the sitekey, solve it, write the token into the textarea. What does not carry over is the API around them, since every call is now awaitable and there is no driver object. nodriver also ships a helper that converts a running undetected-chromedriver instance into a browser object, which lets you move a script in stages. The older approach is written up in the <a href=\"https:\/\/capskip.com\/undetected-chromedriver-captcha\/\">undetected-chromedriver CAPTCHA guide<\/a>.<\/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;\">My scraper runs on a VPS. Where does the solver go?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Wherever you like, as long as the two can reach each other. Server mode makes the solver listen on a network address instead of the loopback address, so the VPS calls it over the API exactly as it would any internal service. Point the host argument at that address, enable key validation, and give the VPS its own key. The solver needs no display, which is convenient given that the browser does.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Read the sitekey with bracket access on attrs, solve it with AsyncCapSkip on 127.0.0.1:8080, inject the token through evaluate with json.dumps, then submit. Await everything, because a synchronous solve holds the socket that drives the browser. For the wider Python picture, including Selenium and Playwright, see <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/p>\n<p>One consequence is worth spelling out before you scale a crawl up. Because CapSkip is an <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> running on your own hardware, a run that retries a thousand pages costs exactly what one that retries ten costs.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>nodriver \u662f\u5168\u5f02\u6b65\u7684\uff0c\u6240\u4ee5\u4e00\u6b21\u963b\u585e\u5f0f\u7684\u8bc6\u522b\u4f1a\u5361\u4f4f\u9a71\u52a8\u6d4f\u89c8\u5668\u7684\u90a3\u4e2a\u5957\u63a5\u5b57\u3002\u6539\u7528\u5f02\u6b65\u5ba2\u6237\u7aef\uff0c\u7528\u65b9\u62ec\u53f7\u53d6\u503c\u8bfb\u51fa sitekey\uff0c\u518d\u7528 evaluate \u6ce8\u5165 token\u3002<\/p>","protected":false},"author":1,"featured_media":25479,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"nodriver CAPTCHA: Solve It Without Stalling CDP | CapSkip","rank_math_description":"A nodriver captcha solve stalls the socket driving Chrome unless you await it. Use the async client, read the sitekey, and inject the token with evaluate.","rank_math_focus_keyword":"nodriver captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25480","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\/25480","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=25480"}],"version-history":[{"count":3,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25480\/revisions"}],"predecessor-version":[{"id":25487,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25480\/revisions\/25487"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25479"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25480"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25480"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25480"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}