{"id":25464,"date":"2026-08-31T23:00:58","date_gmt":"2026-08-31T23:00:58","guid":{"rendered":"https:\/\/capskip.com\/?p=25464"},"modified":"2026-08-31T23:00:58","modified_gmt":"2026-08-31T23:00:58","slug":"drissionpage-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/drissionpage-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 DrissionPage \u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801\u5e76\u6ce8\u5165 token"},"content":{"rendered":"<p>A DrissionPage captcha step is three moves: read the sitekey off the page, send it to a solver, then write the token back into the form with JavaScript. DrissionPage drives a real Chrome over the DevTools Protocol instead of a webdriver, so the browser side is simpler than Selenium. The element model is not, and it has one default that turns a missing widget into an error from the solver instead of an error from your locator. That default is the reason most of these scripts fail confusingly, so it gets its own section.<\/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 DrissionPage 4.1 and the CapSkip SDK installed.<\/li>\n<li>A Chromium browser DrissionPage can launch or attach to.<\/li>\n<li>The page URL of the protected form. The sitekey you read at runtime.<\/li>\n<li>CapSkip running in Local mode if the script and the solver share a machine, or in Server mode if the script runs on a different box. 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 DrissionPage capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Why DrissionPage changes the shape of this<\/h2>\n<p>DrissionPage talks to Chrome over CDP directly. There is no chromedriver process in the middle, so there is no driver binary to patch and no separate service to keep in step with your browser version. It also attaches to a browser that is already running, which is genuinely useful here: you can log in by hand once, leave the profile open, and let the script pick up the session.<\/p>\n<p>What does not change is the token. A solved reCAPTCHA is a string that has to end up in the hidden <code>g-recaptcha-response<\/code> textarea before you submit. That textarea is <code>display:none<\/code>, so typing into it is not an option in any automation tool. You write it with JavaScript, and DrissionPage&#8217;s run_js method is how you do it.<\/p>\n<p>One thing worth being straight about: no webdriver does not mean no detection. Running Chrome over CDP removes one signal, and leaves the rest of your fingerprint exactly where it was. Solving the challenge is a separate job from looking like a browser, and this post only 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 sits on the host document, not inside the widget iframe. Google&#8217;s own markup puts it on a container element as <code>data-sitekey<\/code>, and DrissionPage&#8217;s attribute locator finds it without a CSS selector.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install DrissionPage\nfrom DrissionPage import ChromiumPage\n\npage = ChromiumPage()\npage.get(&quot;https:\/\/example.com\/page-with-recaptcha&quot;)\n\n# @attr finds by attribute. The default search timeout is 10s,\n# which is plenty for a widget that renders on load.\nholder = page.ele(&quot;@data-sitekey&quot;)\nsitekey = holder.attr(&quot;data-sitekey&quot;)\n\nprint(sitekey)   # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<\/pre>\n<\/div>\n<p>DrissionPage&#8217;s locator prefixes are worth learning here, because they replace most of the XPath you would otherwise write: @attr=value matches exactly, @attr:value matches a substring, @attr^value matches the start, and @attr$value matches the end. Prefixing a locator with a tag name, as in tag:iframe, pins the element type as well.<\/p>\n<p>Some sites never put the sitekey on the host page and only pass it in the iframe URL. Read it from there instead, using contains to match the frame you want.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Fallback: the k= query parameter on the widget iframe.\nfrom urllib.parse import urlparse, parse_qs\n\nframe = page.ele(&quot;tag:iframe@src:recaptcha\/api2\/anchor&quot;)\nsrc = frame.attr(&quot;src&quot;)\nsitekey = parse_qs(urlparse(src).query)[&quot;k&quot;][0]<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: solve it against the local API<\/h2>\n<p>The solver runs on your own machine on port 8080 and speaks the 2captcha API, so the SDK call is one line and there is no per solve billing behind it. Pass the page URL from <code>page.url<\/code> rather than retyping it, because a redirect between <code>get()<\/code> and the solve would otherwise leave you submitting the wrong URL.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\nfrom capskip import CapSkip\n\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\n# Same call shape for v3 (version=&quot;v3&quot;) and Enterprise (enterprise=1).\nresult = solver.recaptcha(sitekey=sitekey, url=page.url)\n\ntoken = result[&quot;code&quot;]   # the g-recaptcha-response value<\/pre>\n<\/div>\n<p>Turnstile and GeeTest have methods of their own, solver.turnstile() and solver.geetest(), and both take the same shape as the call above. The full parameter list for each is 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>DrissionPage passes extra arguments to <code>run_js<\/code> positionally, and the script reads them as <code>arguments[0]<\/code> and so on. Pass the token as an argument rather than building the JavaScript with an f-string: a token is long, opaque and occasionally full of characters you do not want to think about quoting.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Extra args arrive as arguments[0], arguments[1], ...\npage.run_js(\n    &quot;document.getElementById('g-recaptcha-response').value = arguments[0];&quot;,\n    token,\n)\n\n# Then submit the form the way the page expects.\npage.ele(&quot;tag:button@type=submit&quot;).click()<\/pre>\n<\/div>\n<p>If the site defines a callback rather than reading the textarea on submit, call it after setting the value. That is a site-specific function name, so read it out of the page&#8217;s own markup instead of guessing, and note that this is still ordinary reCAPTCHA v2: 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;\">The silent failure that costs the most time<\/h2>\n<p>DrissionPage ships with Settings.raise_when_ele_not_found set to False. A locator that matches nothing does not raise. It returns a NoneElement, which is falsy and compares equal to Python&#8217;s None, and your script keeps going.<\/p>\n<p>That is a deliberate design choice and it is pleasant when you are probing optional elements. It is unpleasant here, because the next thing you do is read an attribute, and the error you get names the locator rather than the widget. Worse is the case where the element genuinely exists but carries no data-sitekey attribute at all. Then attr() hands back None, that None travels all the way to the solver as an empty key, and the failure finally surfaces as <a href=\"https:\/\/capskip.com\/error-googlekey-pageurl\/\">ERROR_GOOGLEKEY<\/a> from an API call you were not suspicious of.<\/p>\n<p>Two lines fix it. Turn raising on, and check the sitekey before you spend a solve on it.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Make a missing element fail where it happened.\nfrom DrissionPage.common import Settings\n\nSettings.set_raise_when_ele_not_found(True)\n\n# And still check the value, because a found element can hold nothing.\nif not sitekey:\n    raise RuntimeError(&quot;No sitekey on the page. Check the widget rendered.&quot;)<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<p>Everything above, in one script. The finally block matters more than usual because DrissionPage can attach to a browser you did not start, and quitting one you inherited is rarely what you want.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install DrissionPage capskip\nfrom DrissionPage import ChromiumPage\nfrom DrissionPage.common import Settings\nfrom capskip import CapSkip\n\nSettings.set_raise_when_ele_not_found(True)\n\nPAGE_URL = &quot;https:\/\/example.com\/page-with-recaptcha&quot;\npage = ChromiumPage()\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\n\ntry:\n    page.get(PAGE_URL)\n    sitekey = page.ele(&quot;@data-sitekey&quot;).attr(&quot;data-sitekey&quot;)\n    if not sitekey:\n        raise RuntimeError(&quot;Widget found but data-sitekey was empty.&quot;)\n\n    result = solver.recaptcha(sitekey=sitekey, url=page.url)\n    page.run_js(\n        &quot;document.getElementById('g-recaptcha-response').value = arguments[0];&quot;,\n        result[&quot;code&quot;],\n    )\n    page.ele(&quot;tag:button@type=submit&quot;).click()\n    page.wait.doc_loaded()\n\n    print(page.title)   # the page you land on after submitting\nfinally:\n    page.quit()<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on another machine<\/h2>\n<p>Scrapers get moved to a server sooner or later, and DrissionPage moves with them: it needs a Chromium binary and a display buffer, not much else. The solver does not have to follow it onto the same box.<\/p>\n<p>CapSkip has two connection modes. <strong>Local<\/strong> binds to 127.0.0.1 and is reachable only from that device, which is the right setting while you are writing the script. <strong>Server<\/strong> binds to your network or public IP, so a scraping VM, a container host or a second workstation can call the same solver over the API. A static public IP keeps that address stable. Nothing else in the code changes except the host address you pass in, and the pricing does not change 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 = CapSkip(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 is listening on a network address, and give each machine its own key so one can be revoked without touching the others. The <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">setup guide<\/a> walks through both modes.<\/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>ElementNotFoundError on attr()<\/td>\n<td>The locator matched nothing and returned NoneElement<\/td>\n<td>Widen the locator, or wait for the widget to render<\/td>\n<\/tr>\n<tr>\n<td>Script continues past a missing element<\/td>\n<td>raise_when_ele_not_found defaults to False<\/td>\n<td>Settings.set_raise_when_ele_not_found(True)<\/td>\n<\/tr>\n<tr>\n<td>ERROR_GOOGLEKEY<\/td>\n<td>attr() returned None and an empty key was submitted<\/td>\n<td>Check the value, or read the k parameter off the iframe<\/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>Form rejects a token that solved fine<\/td>\n<td>The token aged out before submit<\/td>\n<td>Solve immediately before submitting, not on page load<\/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;\">Should I use ChromiumPage or the newer Chromium class?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Either. Version 4.1 exports Chromium, ChromiumPage, SessionPage and WebPage from the top level package, and ChromiumPage is still the shortest route to one tab. The CAPTCHA work is identical whichever you pick, because the sitekey read, the solve and the token injection all happen through the tab object.<\/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 switch into the reCAPTCHA iframe?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No, and that 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;\">Why does my script pass a None sitekey without complaining?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Because DrissionPage&#8217;s default is to return a falsy placeholder rather than raise, and because an element that exists can still lack the attribute you asked for. Both paths end with a None in a variable you assumed was a string. Turn raising on at the top of the file and add an explicit check on the value, which together cover both cases.<\/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.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Read the sitekey with an attribute locator, solve it on <code>127.0.0.1:8080<\/code>, inject the token with <code>run_js<\/code> and positional arguments, then submit. Turn <code>raise_when_ele_not_found<\/code> on before you do any of it, because the default hides exactly the failure you will hit first. For the wider Python picture, including Selenium and Playwright, see <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>. And because the solver is a <a href=\"https:\/\/capskip.com\/\">local captcha solver<\/a> rather than a metered service, a crawl that retries a thousand pages costs the same as one that retries ten.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>DrissionPage \u901a\u8fc7 CDP \u9a71\u52a8\u771f\u5b9e\u7684 Chrome\uff0c\u6240\u4ee5 token \u4ecd\u7136\u8981\u9760 JavaScript \u5199\u8fdb\u53bb\u3002\u8bfb\u51fa sitekey\uff0c\u5728 127.0.0.1 \u4e0a\u8bc6\u522b\uff0c\u7528 run_js \u6ce8\u5165\uff0c\u5e76\u5f53\u5fc3\u90a3\u4e2a\u4f1a\u9759\u9ed8\u5931\u8d25\u7684\u5143\u7d20\u3002<\/p>","protected":false},"author":1,"featured_media":25463,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"DrissionPage CAPTCHA: Read, Solve, Inject | CapSkip","rank_math_description":"A drissionpage captcha step is three moves: read the sitekey, solve it on 127.0.0.1, inject the token with run_js. Plus the trap that hides failures.","rank_math_focus_keyword":"drissionpage captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25464","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\/25464","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=25464"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25464\/revisions"}],"predecessor-version":[{"id":25467,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25464\/revisions\/25467"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25463"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25464"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25464"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25464"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}