{"id":25210,"date":"2026-08-14T18:35:03","date_gmt":"2026-08-14T18:35:03","guid":{"rendered":"https:\/\/capskip.com\/?p=25210"},"modified":"2026-08-14T22:03:13","modified_gmt":"2026-08-14T22:03:13","slug":"undetected-chromedriver-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/undetected-chromedriver-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 undetected-chromedriver \u4e2d\u5904\u7406\u9a8c\u8bc1\u7801\uff08Python\uff09"},"content":{"rendered":"<p>undetected-chromedriver patches Chrome so the automation flags stop leaking. That gets you onto the page. It does not get you past a reCAPTCHA checkbox, because a widget is not a fingerprint check, it is a token requirement. An undetected-chromedriver captcha still has to be solved and the answer injected into the DOM.<\/p>\n<p>Four steps: start the patched driver, read the sitekey off the page, solve locally, put the token where the page expects it. Here is all of it in Python.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What undetected-chromedriver does and does not do<\/h2>\n<table>\n<thead>\n<tr>\n<th>Signal<\/th>\n<th>Handled by the driver<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>navigator.webdriver<\/code> and the CDP giveaways<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td>Patched binary so the driver is not detected as one<\/td>\n<td>Yes<\/td>\n<\/tr>\n<tr>\n<td>A reCAPTCHA v2 checkbox on a login form<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>An invisible v2 or v3 challenge fired by the page<\/td>\n<td>No<\/td>\n<\/tr>\n<tr>\n<td>A Turnstile widget<\/td>\n<td>No<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The distinction matters because the usual advice for a blocked scraper is &#8220;add stealth&#8221;. If a widget is on screen, no amount of stealth removes it. You need a token.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Chrome installed, and CapSkip running on <code>127.0.0.1:8080<\/code>. The <a href=\"https:\/\/capskip.com\/setup-guide\/\">setup guide<\/a> covers the app.<\/li>\n<li>Python 3.10 or newer.<\/li>\n<li>Three packages.<\/li>\n<\/ul>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># the patched driver, selenium itself, and the local solver\npip install undetected-chromedriver selenium capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: start the patched driver<\/h2>\n<p>Import it as <code>uc<\/code> and build the browser the same way you would with plain Selenium. Options come from <code>uc.ChromeOptions()<\/code>, not Selenium&#8217;s, because the library patches them on the way through:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install undetected-chromedriver\nimport undetected_chromedriver as uc\n\noptions = uc.ChromeOptions()\noptions.add_argument(&quot;--window-size=1280,800&quot;)\n\n# use_subprocess keeps the patched binary alive long enough\n# to shut down cleanly on Windows.\ndriver = uc.Chrome(options=options, use_subprocess=True)\n\ndriver.get(&quot;https:\/\/example.com\/login&quot;)<\/pre>\n<\/div>\n<p>Do not stack another stealth plugin on top of this. Two patchers fighting over the same properties is a detection signal in itself, and it is a common reason a setup that used to work suddenly stops.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: read the sitekey off the page<\/h2>\n<p>The sitekey is a public attribute on the widget element. Read it from the live DOM rather than hardcoding it, because plenty of sites rotate keys per environment:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">from selenium.webdriver.common.by import By\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.webdriver.support import expected_conditions as EC\n\n# The widget is injected by script, so wait for it.\nwidget = WebDriverWait(driver, 20).until(\n    EC.presence_of_element_located((By.CSS_SELECTOR, &quot;[data-sitekey]&quot;))\n)\n\nsitekey = widget.get_attribute(&quot;data-sitekey&quot;)\npage_url = driver.current_url\n\nprint(sitekey)<\/pre>\n<\/div>\n<p>Use <code>driver.current_url<\/code> rather than the URL you asked for. If the site redirected you to a challenge path, the solver has to be told about the page you actually landed on.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: solve it locally<\/h2>\n<p>CapSkip runs on your own machine, so the solve is a call to localhost. No account, no per-solve billing, nothing leaves the box except the sitekey and the page 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(\n    host=&quot;127.0.0.1&quot;,\n    port=8080,\n    recaptchaTimeout=300,   # seconds, also covers Turnstile\n)\n\nresult = solver.recaptcha(sitekey=sitekey, url=page_url)\n\ntoken = result[&quot;code&quot;]   # the g-recaptcha-response value<\/pre>\n<\/div>\n<p>Variants are options rather than different methods. Add <code>invisible=1<\/code> for an invisible v2 widget, <code>version=&quot;v3&quot;<\/code> with an <code>action<\/code> for v3, and <code>enterprise=1<\/code> for the Enterprise product.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: inject the token and fire the callback<\/h2>\n<p>The token has to land in the hidden <code>g-recaptcha-response<\/code> textarea. That alone is enough for a plain form submit, because the field is posted with the rest of the form:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">driver.execute_script(\n    &quot;document.getElementById('g-recaptcha-response')&quot;\n    &quot;.innerHTML = arguments[0];&quot;,\n    token,\n)\n\ndriver.find_element(By.CSS_SELECTOR, &quot;button[type='submit']&quot;).click()<\/pre>\n<\/div>\n<p>Single-page apps are different. They never read the textarea, they wait for the widget&#8217;s own callback to hand them the token. The function name is declared on the element, so you can call it directly:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">callback = widget.get_attribute(&quot;data-callback&quot;)\n\nif callback:\n    # Call the page's own handler with the token, exactly as\n    # the widget would have done.\n    driver.execute_script(f&quot;{callback}(arguments[0]);&quot;, token)<\/pre>\n<\/div>\n<p>If nothing happens after injection and there is no <code>data-callback<\/code>, the page is wiring the callback up in JavaScript instead of markup. The <a href=\"https:\/\/capskip.com\/recaptcha-v2-callback-solver\/\">reCAPTCHA v2 callback solver<\/a> page covers how to find it in that case.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Turnstile in the same driver<\/h2>\n<p>Cloudflare Turnstile works the same way with two names changed. The response field is <code>cf-turnstile-response<\/code>, and it is an input rather than a textarea:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">result = solver.turnstile(sitekey=sitekey, url=page_url)\n\ndriver.execute_script(\n    &quot;document.querySelector('[name=cf-turnstile-response]')&quot;\n    &quot;.value = arguments[0];&quot;,\n    result[&quot;code&quot;],\n)<\/pre>\n<\/div>\n<p>That covers a widget embedded in a form. A full-page interstitial challenge needs two extra values and the user agent that the solve was performed with, which the <a href=\"https:\/\/capskip.com\/cloudflare-turnstile-solver\/\">Turnstile solver<\/a> page explains.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Full working example<\/h2>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install undetected-chromedriver selenium capskip\nimport undetected_chromedriver as uc\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom capskip import (\n    CapSkip, NetworkException, ApiException, TimeoutException,\n)\n\nURL = &quot;https:\/\/example.com\/login&quot;\n\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\ndriver = uc.Chrome(options=uc.ChromeOptions(), use_subprocess=True)\n\ntry:\n    driver.get(URL)\n\n    widget = WebDriverWait(driver, 20).until(\n        EC.presence_of_element_located((By.CSS_SELECTOR, &quot;[data-sitekey]&quot;))\n    )\n    sitekey = widget.get_attribute(&quot;data-sitekey&quot;)\n\n    result = solver.recaptcha(sitekey=sitekey, url=driver.current_url)\n\n    driver.execute_script(\n        &quot;document.getElementById('g-recaptcha-response')&quot;\n        &quot;.innerHTML = arguments[0];&quot;,\n        result[&quot;code&quot;],\n    )\n\n    callback = widget.get_attribute(&quot;data-callback&quot;)\n    if callback:\n        driver.execute_script(f&quot;{callback}(arguments[0]);&quot;, result[&quot;code&quot;])\n    else:\n        driver.find_element(By.CSS_SELECTOR, &quot;button[type='submit']&quot;).click()\n\nexcept NetworkException:\n    print(&quot;CapSkip is not running on 127.0.0.1:8080&quot;)\nexcept (ApiException, TimeoutException) as err:\n    print(f&quot;solve failed: {err}&quot;)\nfinally:\n    driver.quit()   # always, or the patched binary lingers<\/pre>\n<\/div>\n<p>Solve and submit in one run. A reCAPTCHA token is accepted for roughly two minutes, so a script that solves, then does thirty seconds of other work, then submits, is fine. One that queues tokens for later is not.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors<\/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><code>This version of ChromeDriver only supports Chrome version N<\/code><\/td>\n<td>The patched driver guessed the wrong Chrome major version<\/td>\n<td>Pass it explicitly: <code>uc.Chrome(version_main=N)<\/code> where N is your installed Chrome major version<\/td>\n<\/tr>\n<tr>\n<td><code>OSError: [WinError 6] The handle is invalid<\/code> on exit<\/td>\n<td>The driver was garbage collected before it shut down<\/td>\n<td>Call <code>driver.quit()<\/code> in a finally block and keep <code>use_subprocess=True<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>NoSuchElementException<\/code> when locating <code>[data-sitekey]<\/code><\/td>\n<td>The widget is in an iframe, or has not rendered yet<\/td>\n<td>Wait for it as above. For an iframe, read the sitekey from the iframe <code>src<\/code> query string instead<\/td>\n<\/tr>\n<tr>\n<td><code>ERROR_GOOGLEKEY<\/code><\/td>\n<td>An empty or truncated sitekey reached the solver<\/td>\n<td>Print it before solving. See <a href=\"https:\/\/capskip.com\/error-googlekey-pageurl\/\">fixing ERROR_GOOGLEKEY<\/a><\/td>\n<\/tr>\n<tr>\n<td><code>NetworkException<\/code><\/td>\n<td>CapSkip is not running, or the port differs<\/td>\n<td>Start the app and check the port in its settings<\/td>\n<\/tr>\n<tr>\n<td>Token injected, form still refused<\/td>\n<td>The page uses a callback and never reads the textarea<\/td>\n<td>Call <code>data-callback<\/code> with the token<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Every error code and parameter is listed in the <a href=\"https:\/\/capskip.com\/api-docs\/\">API documentation<\/a>.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Frequently asked questions<\/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;\">Does undetected-chromedriver solve CAPTCHAs by itself?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. It hides the automation signals that get a browser flagged. A rendered widget still expects a valid token, so you solve it and inject the result. The two jobs are complementary, not alternatives.<\/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 I run it headless?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">You can, with <code>uc.Chrome(headless=True)<\/code>, which applies the library&#8217;s own headless patches rather than Selenium&#8217;s flag. Expect more challenges than in headed mode. Token injection itself works identically either way.<\/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 a proxy for the solve?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Only if the browser is already going through one. Pass the same one as <code>proxy={&quot;type&quot;: &quot;HTTPS&quot;, &quot;uri&quot;: &quot;user:pass@host:port&quot;}<\/code> so the solve and the submit share a network path. Proxies apply to reCAPTCHA, Turnstile and GeeTest, never to image CAPTCHAs.<\/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 this work with plain Selenium too?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes. Steps 2 to 4 are ordinary WebDriver calls, so they run unchanged on standard Selenium, on Selenium Grid, and on remote drivers. Only step 1 is specific to the patched library.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Summary<\/h2>\n<p>Let undetected-chromedriver handle the fingerprint, and handle the widget yourself. Wait for <code>[data-sitekey]<\/code>, solve against localhost, write the token into <code>g-recaptcha-response<\/code>, and call <code>data-callback<\/code> when the page declares one. Quit the driver in a <code>finally<\/code> block so the patched binary does not linger.<\/p>\n<p>The wider Selenium surface is on the <a href=\"https:\/\/capskip.com\/selenium-captcha-solver\/\">Selenium CAPTCHA solver<\/a> page, the full method list is on the <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">Python CAPTCHA solver<\/a> page, and <a href=\"https:\/\/capskip.com\/captcha-solver-for-web-scraping\/\">CAPTCHA solving for web scraping<\/a> covers running this at volume. CapSkip is a <a href=\"https:\/\/capskip.com\/\">local captcha solver<\/a>, so every solve above happens on your own machine.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>undetected-chromedriver \u80fd\u5e2e\u4f60\u901a\u8fc7\u6307\u7eb9\u68c0\u6d4b\uff0c\u5374\u8fc7\u4e0d\u4e86\u9a8c\u8bc1\u7801\u5c0f\u7ec4\u4ef6\u672c\u8eab\u3002\u4e0b\u9762\u4ecb\u7ecd\u5982\u4f55\u8bfb\u53d6 sitekey\u3001\u5728\u672c\u5730\u8bc6\u522b\uff0c\u5e76\u5728\u540c\u4e00\u4f1a\u8bdd\u4e2d\u6ce8\u5165 token\u3002<\/p>","protected":false},"author":1,"featured_media":25209,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"undetected-chromedriver CAPTCHA Handling | CapSkip","rank_math_description":"An undetected-chromedriver captcha still needs a token. Read the sitekey, solve it locally, inject the response and fire the callback. Working Python code.","rank_math_focus_keyword":"undetected-chromedriver captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25210","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\/25210","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=25210"}],"version-history":[{"count":3,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25210\/revisions"}],"predecessor-version":[{"id":25227,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25210\/revisions\/25227"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25209"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25210"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25210"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25210"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}