{"id":26757,"date":"2026-10-30T09:05:38","date_gmt":"2026-10-30T09:05:38","guid":{"rendered":"https:\/\/capskip.com\/?p=26757"},"modified":"2026-10-30T09:05:38","modified_gmt":"2026-10-30T09:05:38","slug":"pydoll-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/pydoll-captcha\/","title":{"rendered":"\u5982\u4f55\u5728\u5f02\u6b65 Python \u4e2d\u7528 CapSkip \u8bc6\u522b Pydoll \u9a8c\u8bc1\u7801"},"content":{"rendered":"<p>To solve a Pydoll CAPTCHA, read the sitekey from the page, solve it with CapSkip&#8217;s AsyncCapSkip client on the same asyncio event loop, write the token into the g-recaptcha-response box and run the page&#8217;s callback or submit the form. Pydoll 3.0 has no reCAPTCHA code of its own, so the token has to come from a solver. CapSkip runs on your own Windows PC, so a crawl that opens many pages has no per-solve cost. This guide builds the whole Pydoll CAPTCHA flow in about 80 lines and covers the traps that cost time on Windows.<\/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 reCAPTCHA solver switched on. The <a href=\"https:\/\/capskip.com\/setup-guide\/\">setup guide<\/a> walks through the first start.<\/li>\n<li>Python 3.10 or later. Pydoll 3.0.0 and the capskip 1.3.0 package both need it. Everything below ran on Windows 11 with Python 3.14.0, and the full script also ran on Python 3.11.7. Python 3.10, 3.12 and 3.13 were not run.<\/li>\n<li>Google Chrome installed in its default Program Files folder. The guide used Chrome 155, and the full script also ran against Chrome for Testing 146.<\/li>\n<li>A PowerShell prompt. The commands use PowerShell syntax.<\/li>\n<li>A page that you own, with a reCAPTCHA v2 checkbox. The example form is a plain sign-up form. Change the selectors to match yours.<\/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 Pydoll on another PC, a VPS or a hosted platform can call 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>The measurements below come from a mock CapSkip and a mock sign-up page on the same PC, used with the real Pydoll, the real SDK, a real Chrome and Google&#8217;s real api.js script. The browser and SDK behaviour holds for your own site too. Solve times and the accept and reject rules of the page are the mock&#8217;s own. One solve against a running CapSkip, using Google&#8217;s public test sitekey and default settings, took 9.0 seconds and returned a token of 1,764 characters. Solve times vary from run to run. That token was not used anywhere, and no live site was tried.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Where Pydoll stops and CapSkip starts<\/h2>\n<p>Pydoll drives Chrome over the DevTools protocol, with no WebDriver in between. It gives you the browser, the page&#8217;s elements and a way to run JavaScript. Its only CAPTCHA code in 3.0.0 is a helper for the Cloudflare Turnstile checkbox. That helper finds the checkbox and clicks it. It never reads a token and it never raises an error when nothing happens. There is no reCAPTCHA handling at all. A reCAPTCHA v2 checkbox is solved by a service that returns a token, and the page then needs that token in the right place. That is the job CapSkip does here, and the rest of this guide is the glue.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: create the project<\/h2>\n<p>Use a virtual environment in an empty folder. The commands run the environment&#8217;s own Python by its path, so you do not need to activate it, and a restricted PowerShell script policy does not get in the way. Run every script below with the environment&#8217;s Python, for example .\\.venv\\Scripts\\python.exe solve.py. An older Pydoll install, such as 2.27.0, would fail at the first import of the script below with ImportError: cannot import name &#8216;Chrome&#8217; from &#8216;pydoll&#8217;. Pydoll 3.0 exports Chrome from the top level.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># Windows PowerShell, in an empty folder\npython -m venv .venv\n.\\.venv\\Scripts\\python.exe -m pip install pydoll-python capskip<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: start Chrome and open the page<\/h2>\n<p>Save this as solve.py. It is the smallest Pydoll script that opens your page and prints its title. The browser starts inside an async with block, so it closes again when the block ends.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install pydoll-python capskip\nimport asyncio\n\nfrom pydoll import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nPAGE = 'https:\/\/example.com\/page-with-recaptcha'\n\n\nasync def main():\n    options = ChromiumOptions()\n    options.headless = True  # set to False to watch the window\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to(PAGE)\n        print(await tab.title())\n\n\nasyncio.run(main())<\/pre>\n<\/div>\n<p>Pydoll looks for Chrome in two places only: Program Files and Program Files (x86), under Google, Chrome, Application. A Chrome installed for one user, under AppData, is not found, and the script stops with InvalidBrowserPath. Set options.binary_location to the full path of chrome.exe to fix it. A path that does not exist fails with FailedToStartBrowser instead. The same line points Pydoll at Chrome for Testing, and that worked too. Pydoll does not match browser versions to anything, so 146 and 155 both ran.<\/p>\n<p>Start-up is slower than you expect on Windows. On the test PC, browser.start() took 2.8 seconds with the default settings and headless Chrome 155. The first go_to took 2.1 seconds more on a page with no outside script, and about 3.1 seconds on the sign-up page, which loads Google&#8217;s script. Pydoll connects to localhost, and localhost tries the IPv6 address first. Chrome&#8217;s debugging port does not listen on IPv6, and Windows takes about 2 seconds to refuse a loopback connection, so every new connection to Chrome pays about 2 seconds before the IPv4 one works. That means the browser and every extra tab you open. Pydoll has no setting for it. Making localhost resolve to 127.0.0.1 inside the Python process removes the delay in a test, but it patches the standard library, so this guide leaves it alone. Plan for it when you time a run.<\/p>\n<p>The with block closes Chrome on a normal exit, after an exception inside it and after a cancelled task, and it removes the temporary profile folder. Ending the Python process with taskkill \/F skips that. In the test it left 13 chrome.exe processes running and the profile folder on disk. Ctrl+C at a console was not tested. If you killed a run, look in Task Manager for chrome.exe processes before you start the next one.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: check that CapSkip answers<\/h2>\n<p>Before you add a browser, confirm the solver call works on its own. Save this as first_solve.py. AsyncCapSkip is the async client from the same package. The SDK reads no environment variables, so the host and port are written into the client. The placeholders are literal. YOUR_SITEKEY and the page address must come from a page that really carries a reCAPTCHA v2 widget, or CapSkip answers with an error. Copy the sitekey from the page source, where it is the data-sitekey value. Step 4 reads it from the page for you. Run the file with .\\.venv\\Scripts\\python.exe first_solve.py.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\nimport asyncio\n\nfrom capskip import AsyncCapSkip\n\n\nasync def main():\n    solver = AsyncCapSkip(host='127.0.0.1', port=8080)\n    result = await solver.recaptcha(\n        sitekey='YOUR_SITEKEY',\n        url='https:\/\/example.com\/page-with-recaptcha',\n    )\n    print(result['code'][:30])\n\n\nasyncio.run(main())<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 4: read the sitekey from the page<\/h2>\n<p>The sitekey is on the page, and Pydoll can read it. A normal widget is a div with the class g-recaptcha and a data-sitekey attribute. A widget that the page draws with grecaptcha.render() has no such div. It leaves an iframe whose address carries the sitekey in its k parameter. This function handles both with one selector, and the tag name tells it which one it got. Add it to solve.py above async def main().<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">from urllib.parse import parse_qs, urlparse\n\n\nasync def find_sitekey(tab):\n    # A normal widget carries data-sitekey. A widget drawn by\n    # grecaptcha.render() only leaves its iframe, with k=&lt;sitekey&gt;.\n    found = await tab.query(\n        '.g-recaptcha[data-sitekey], '\n        'iframe[src*=&quot;recaptcha\/api2\/anchor&quot;]',\n        timeout=10,\n    )\n    if found.tag_name == 'iframe':\n        src = found.get_attribute('src')\n        return parse_qs(urlparse(src).query)['k'][0]\n    return found.get_attribute('data-sitekey')<\/pre>\n<\/div>\n<p>Two Pydoll details matter here. get_attribute is a plain function, not a coroutine, so do not await it, and it returns None for a missing attribute. And query raises ElementNotFound when nothing matches. With a timeout it waits and raises WaitElementTimeout instead, which is the right choice for a widget that appears a moment after the page loads. A widget injected 3 seconds after load was found about 3 seconds later, with the 10 second timeout in find_sitekey.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 5: solve on the same event loop<\/h2>\n<p>Pydoll runs on asyncio, and your solve call runs on the same loop. That makes the choice of client matter. Add this above async def main() too, and do the same for the function in Step 6.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">from capskip import AsyncCapSkip\n\nsolver = AsyncCapSkip(host='127.0.0.1', port=8080)\n\n\nasync def solve(sitekey, url):\n    result = await solver.recaptcha(sitekey, url)\n    return result['code']<\/pre>\n<\/div>\n<p>The sync CapSkip client would also work, but it blocks the loop for the whole solve, and Pydoll stops answering Chrome while it waits. A mock run counted how often the page answered when asked for the time every 50 milliseconds, while a 3 second mock solve ran. The longest gap is the longest time between two answers:<\/p>\n<table>\n<thead>\n<tr>\n<th>Client<\/th>\n<th>Solve time<\/th>\n<th>Counter ticks<\/th>\n<th>Longest gap<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>The sync CapSkip client, called directly in the async function<\/td>\n<td>3.76 s<\/td>\n<td>5<\/td>\n<td>3.78 s<\/td>\n<\/tr>\n<tr>\n<td>CapSkip in asyncio.to_thread<\/td>\n<td>3.76 s<\/td>\n<td>65<\/td>\n<td>0.06 s<\/td>\n<\/tr>\n<tr>\n<td>AsyncCapSkip<\/td>\n<td>5.0 s<\/td>\n<td>68<\/td>\n<td>0.3 s<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The page itself kept running in the first case. Only the Python side froze: Python cannot react to anything the page does until the solve ends, so events from a second tab are handled late. AsyncCapSkip took about a second longer here, but that depends on where its next poll falls after the solve, and with 2, 4 and 8 second mock solves it finished earlier than the thread version. What it always costs is about 0.15 seconds of blocked loop for each request, because it builds a new HTTP client every time. That still beats a frozen loop.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 6: write the token into the page<\/h2>\n<p>A solved reCAPTCHA is a token. The page expects it in a textarea named g-recaptcha-response, and if the widget has a callback, the page expects the callback to run with the token. This function does both. It uses Pydoll&#8217;s execute_script and returns True when it found and ran a callback. Add it above async def main() as well.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">import json\n\nFILL_JS = &quot;&quot;&quot;\n(function (token) {\n  var seen = new Set(), fn = null;\n  document.querySelectorAll('textarea[name=&quot;g-recaptcha-response&quot;]')\n    .forEach(function (box) { box.value = token; });\n  (function walk(o, depth) {\n    if (!o || typeof o !== 'object' || seen.has(o) || depth &gt; 6) return;\n    seen.add(o);\n    for (var k in o) {\n      var v; try { v = o[k]; } catch (e) { continue; }\n      if (k === 'callback' &amp;&amp; !fn) {\n        fn = typeof v === 'string' ? window[v] : v;\n      }\n      if (v &amp;&amp; typeof v === 'object' &amp;&amp; !(v instanceof Node)) {\n        walk(v, depth + 1);\n      }\n    }\n  })(window.___grecaptcha_cfg, 0);\n  if (typeof fn === 'function') fn(token);\n  return typeof fn === 'function';\n})(%s)\n&quot;&quot;&quot;\n\n\nasync def fill_token(tab, token):\n    reply = await tab.execute_script(\n        FILL_JS % json.dumps(token), return_by_value=True)\n    outcome = reply['result']\n    if 'exceptionDetails' in outcome:\n        error = outcome['exceptionDetails']['exception']\n        raise RuntimeError(error['description'])\n    return outcome['result']['value']  # True if a callback ran<\/pre>\n<\/div>\n<p>The JavaScript walks Google&#8217;s own configuration object to find the callback, because the keys inside it are short names that Google can change. A data-callback attribute is stored there as the name of a function, so the walk looks the name up on window. A callback passed to grecaptcha.render() is stored as the function itself. Both cases ran against Google&#8217;s real api.js.<\/p>\n<p>Pydoll returns the raw DevTools reply from execute_script, so the value sits at reply[&#8216;result&#8217;][&#8216;result&#8217;][&#8216;value&#8217;], and you must await the call. An object comes back as a reference unless you pass return_by_value=True. A JavaScript error does not raise in Python. It adds exceptionDetails to the reply, which is why the function checks for it and raises a RuntimeError of its own.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 7: put it together<\/h2>\n<p>Delete async def main() and the asyncio.run(main()) line at the bottom of solve.py, keep the functions above them, and paste this in their place. It solves after the page has loaded, because the sitekey comes from the page, and it solves right before the token is used. If a callback ran, the page takes over. If not, the script clicks the form&#8217;s submit button and waits for the navigation.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">async def main():\n    options = ChromiumOptions()\n    options.headless = True  # set to False to watch the window\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to(PAGE)\n        sitekey = await find_sitekey(tab)\n        token = await solve(sitekey, await tab.current_url())\n        if not await fill_token(tab, token):\n            button = await tab.query('form button[type=&quot;submit&quot;]')\n            async with tab.expect_navigation():\n                await button.click()\n        print(await tab.title())\n\n\nasyncio.run(main())<\/pre>\n<\/div>\n<p>On a page where a callback does the sending, there is nothing to click. This version replaces main() the same way. What happens next depends on the page, so wait for something the page itself changes. This version waits for a result element on the test page to change from waiting to something else. Change the selector to whatever your page updates.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">async def main():\n    options = ChromiumOptions()\n    options.headless = True  # set to False to watch the window\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to(PAGE)\n        sitekey = await find_sitekey(tab)\n        token = await solve(sitekey, await tab.current_url())\n        if await fill_token(tab, token):\n            # The page's own callback takes over. What it does next is\n            # page specific: this page writes its answer into #result.\n            await tab.wait_for_script(\n                &quot;var r = document.querySelector('#result');&quot;\n                &quot;return r &amp;&amp; r.textContent !== 'waiting';&quot;\n            )\n        print(await (await tab.query('#result')).text())\n\n\nasyncio.run(main())<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The whole script<\/h2>\n<p>Here is solve.py in one piece, with every import at the top. It replaces the blocks you added one by one, so delete those first. Run on the plain form page, it printed Welcome and the page accepted the token once. On a page whose callback does the sending, this version can print the old title before the page changes, so use the callback variant above there.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install pydoll-python capskip\nimport asyncio\nimport json\nfrom urllib.parse import parse_qs, urlparse\n\nfrom capskip import AsyncCapSkip\nfrom pydoll import Chrome\nfrom pydoll.browser.options import ChromiumOptions\n\nPAGE = 'https:\/\/example.com\/page-with-recaptcha'\n\nsolver = AsyncCapSkip(host='127.0.0.1', port=8080)\n\n# Put the token in every response box, then run the page's callback.\nFILL_JS = &quot;&quot;&quot;\n(function (token) {\n  var seen = new Set(), fn = null;\n  document.querySelectorAll('textarea[name=&quot;g-recaptcha-response&quot;]')\n    .forEach(function (box) { box.value = token; });\n  (function walk(o, depth) {\n    if (!o || typeof o !== 'object' || seen.has(o) || depth &gt; 6) return;\n    seen.add(o);\n    for (var k in o) {\n      var v; try { v = o[k]; } catch (e) { continue; }\n      if (k === 'callback' &amp;&amp; !fn) {\n        fn = typeof v === 'string' ? window[v] : v;\n      }\n      if (v &amp;&amp; typeof v === 'object' &amp;&amp; !(v instanceof Node)) {\n        walk(v, depth + 1);\n      }\n    }\n  })(window.___grecaptcha_cfg, 0);\n  if (typeof fn === 'function') fn(token);\n  return typeof fn === 'function';\n})(%s)\n&quot;&quot;&quot;\n\n\nasync def find_sitekey(tab):\n    # A normal widget carries data-sitekey. A widget drawn by\n    # grecaptcha.render() only leaves its iframe, with k=&lt;sitekey&gt;.\n    found = await tab.query(\n        '.g-recaptcha[data-sitekey], '\n        'iframe[src*=&quot;recaptcha\/api2\/anchor&quot;]',\n        timeout=10,\n    )\n    if found.tag_name == 'iframe':\n        src = found.get_attribute('src')\n        return parse_qs(urlparse(src).query)['k'][0]\n    return found.get_attribute('data-sitekey')\n\n\nasync def solve(sitekey, url):\n    result = await solver.recaptcha(sitekey, url)\n    return result['code']\n\n\nasync def fill_token(tab, token):\n    reply = await tab.execute_script(\n        FILL_JS % json.dumps(token), return_by_value=True)\n    outcome = reply['result']\n    if 'exceptionDetails' in outcome:\n        error = outcome['exceptionDetails']['exception']\n        raise RuntimeError(error['description'])\n    return outcome['result']['value']  # True if a callback ran\n\n\nasync def main():\n    options = ChromiumOptions()\n    options.headless = True  # set to False to watch the window\n    async with Chrome(options=options) as browser:\n        tab = await browser.start()\n        await tab.go_to(PAGE)\n        sitekey = await find_sitekey(tab)\n        token = await solve(sitekey, await tab.current_url())\n        if not await fill_token(tab, token):\n            button = await tab.query('form button[type=&quot;submit&quot;]')\n            async with tab.expect_navigation():\n                await button.click()\n        print(await tab.title())\n\n\nasyncio.run(main())<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Solve two tabs at once<\/h2>\n<p>Two tabs can solve at the same time with asyncio.gather. Two 3 second solves started together finished together in under 4 seconds, with different tokens. The trap is cookies. Tabs in the default browser context share one cookie jar, so the second page load can replace the session or CSRF cookie that the first tab is about to submit with. The mock site sets a new session cookie on every page load, as some real sites do, so the second load replaced the first tab&#8217;s cookie and one of two simultaneous submits was rejected. Giving each tab its own browser context fixed it, and both submits were accepted. This version replaces main() the same way, and it clicks submit, so it suits a plain form.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\">async def solve_page(tab):\n    sitekey = await find_sitekey(tab)\n    token = await solve(sitekey, await tab.current_url())\n    await fill_token(tab, token)\n    button = await tab.query('form button[type=&quot;submit&quot;]')\n    async with tab.expect_navigation():\n        await button.click()\n    return await tab.title()\n\n\nasync def main():\n    options = ChromiumOptions()\n    options.headless = True\n    async with Chrome(options=options) as browser:\n        await browser.start()\n        # One browser context per tab: tabs in the default context\n        # share cookies, and a second page load can replace the first\n        # tab's session or CSRF cookie.\n        contexts = [await browser.create_browser_context() for _ in range(2)]\n        tabs = [\n            await browser.new_tab(PAGE, browser_context_id=context)\n            for context in contexts\n        ]\n        print(await asyncio.gather(*[solve_page(tab) for tab in tabs]))\n        for context in contexts:\n            await browser.delete_browser_context(context)\n\n\nasyncio.run(main())<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Run Pydoll against CapSkip on another machine<\/h2>\n<p>Pydoll and the solver do not have to share a PC. Switch CapSkip to Server mode under connection settings, and it listens on your network address or public IP. A static public IP is recommended for stable remote access. Then only the client line changes. The solver is still your own Windows machine, and solves are still unmetered.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># CapSkip on another Windows PC, in Server mode\nsolver = AsyncCapSkip(\n    host='192.168.1.50',  # that PC's network address\n    port=8080,\n    apiKey='YOUR_API_KEY',  # only if API key validation is on\n)<\/pre>\n<\/div>\n<p>Mind two things. The SDK talks to CapSkip over plain HTTP, so the API key travels in clear text. Keep that link on a private network or a VPN, and turn on API key validation. And CapSkip opens its port to every address when Server mode starts, so a Windows Firewall block rule for the addresses you do not expect is the way to narrow it. With AsyncCapSkip, an unreachable address fails after about 5 seconds with a NetworkException that has an empty message. The remote setup was not run for this guide, only the constructor was read.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Timeouts and what a failure looks like<\/h2>\n<p>Solve, then submit, quickly. A reCAPTCHA token works once and expires, so solve after the page has loaded and just before you submit. The strict mock site refused a second submit of the same token with a 403, which is the mock&#8217;s own rule. The page on <a href=\"https:\/\/capskip.com\/recaptcha-token-expiration\/\">reCAPTCHA token expiration<\/a> has the timing. Here is what each failure looked like with AsyncCapSkip and the default settings, against the mock:<\/p>\n<table>\n<thead>\n<tr>\n<th>Case<\/th>\n<th>What you see<\/th>\n<th>After<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>CapSkip is not running<\/td>\n<td>NetworkException: All connection attempts failed<\/td>\n<td>2.2 s<\/td>\n<\/tr>\n<tr>\n<td>CapSkip accepts the call and goes silent<\/td>\n<td>NetworkException with an empty message<\/td>\n<td>5.3 s<\/td>\n<\/tr>\n<tr>\n<td>The solve cannot be done<\/td>\n<td>ApiException: ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>0.7 s<\/td>\n<\/tr>\n<tr>\n<td>A 30 s solve with recaptchaTimeout set to 6<\/td>\n<td>TimeoutException: timeout 6.0 exceeded<\/td>\n<td>8.7 s<\/td>\n<\/tr>\n<tr>\n<td>The same solve inside asyncio.wait_for with 4 s<\/td>\n<td>Python&#8217;s own TimeoutError (asyncio.TimeoutError on Python 3.10)<\/td>\n<td>4.0 s<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Set recaptchaTimeout on the client, not on the call. A timeout argument on the call raises TypeError with the message got multiple values for keyword argument &#8216;timeout&#8217;. The SDK checks its limit only between polls, which is why the 6 second limit fired at 8.7 seconds. Wrap the call in asyncio.wait_for if a hard ceiling matters. That cancels your side only. CapSkip keeps the job until its own limits, which was not tested with the real app. Pydoll has its own errors. A navigation that takes longer than go_to allows raises PageLoadTimeout, and a refused connection raises NavigationError with net::ERR_CONNECTION_REFUSED in the message. Pydoll also has a TimeoutException class, and CapSkip has one with the same name. They are different classes, so import the modules and write capskip.TimeoutException when you catch the solver&#8217;s.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Common errors with a Pydoll CAPTCHA<\/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>ImportError: cannot import name &#8216;Chrome&#8217; from &#8216;pydoll&#8217;<\/td>\n<td>Pydoll 2.x is installed<\/td>\n<td>Upgrade to pydoll-python 3<\/td>\n<\/tr>\n<tr>\n<td>InvalidBrowserPath: No valid browser path found<\/td>\n<td>Chrome is installed for one user, not in Program Files<\/td>\n<td>Set options.binary_location<\/td>\n<\/tr>\n<tr>\n<td>FailedToStartBrowser: The browser exited with code 21<\/td>\n<td>A fixed user-data-dir is already in use by another browser<\/td>\n<td>Use a different profile folder, or none<\/td>\n<\/tr>\n<tr>\n<td>ArgumentAlreadyExistsInOptions<\/td>\n<td>The same ChromiumOptions object was reused for a second browser, or you added a flag that Pydoll adds itself<\/td>\n<td>Build a new options object for each browser<\/td>\n<\/tr>\n<tr>\n<td>WaitElementTimeout on the sitekey query<\/td>\n<td>The page has no widget yet, or the selector does not match it<\/td>\n<td>Check the page in a visible window, then raise the timeout<\/td>\n<\/tr>\n<tr>\n<td>NetworkException: All connection attempts failed<\/td>\n<td>CapSkip is not running, or the port is wrong<\/td>\n<td>Start CapSkip and check connection settings<\/td>\n<\/tr>\n<tr>\n<td>ApiException: ERROR_CAPTCHA_UNSOLVABLE<\/td>\n<td>CapSkip could not finish the solve<\/td>\n<td>Check the sitekey and the page address, then retry once. See <a href=\"https:\/\/capskip.com\/error-captcha-unsolvable\/\">ERROR_CAPTCHA_UNSOLVABLE<\/a><\/td>\n<\/tr>\n<tr>\n<td>RuntimeError: ReferenceError, a name is not defined<\/td>\n<td>The JavaScript in fill_token threw inside the page<\/td>\n<td>Run the same script in the browser console<\/td>\n<\/tr>\n<tr>\n<td>The page says the CAPTCHA failed, no error in Python<\/td>\n<td>The token was used twice, or the callback did not run<\/td>\n<td>Solve again right before each submit<\/td>\n<\/tr>\n<tr>\n<td>FailedToStartBrowser: Could not launch<\/td>\n<td>options.binary_location points at a file that is not there<\/td>\n<td>Check the path to chrome.exe<\/td>\n<\/tr>\n<tr>\n<td>Every run takes about 4 seconds longer than the work<\/td>\n<td>Each new connection to Chrome pays the localhost delay of about 2 seconds<\/td>\n<td>Reuse one browser and one tab: a second go_to on the same tab took under 0.2 seconds<\/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;\">Does Pydoll solve reCAPTCHA by itself?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. Pydoll 3.0.0 has no reCAPTCHA code. Its one CAPTCHA helper looks for a Cloudflare Turnstile checkbox and clicks it, which is not a solve and returns no token.<\/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 headless Chrome?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The full script passed headless and headed against the mock page, on Chrome 155 and Chrome for Testing 146. How a live site treats a headless browser was not tested, and that is a separate question from the token.<\/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 Pydoll on another PC and CapSkip at home?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Yes, if that PC can reach CapSkip. Put CapSkip in Server mode, and set the host and port in the client. A static public IP is recommended for stable remote access over the internet. The key and the page address travel in clear text over plain HTTP, so prefer a VPN or a private network, and add a Windows Firewall block rule for other addresses. No remote setup was tested for this guide.<\/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;\">What about invisible reCAPTCHA, v3 and Enterprise?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">The SDK&#8217;s recaptcha call takes options for them: invisible set to 1, enterprise set to 1, and version set to v3. The Pydoll side needs different page reading, such as the action name for v3, and none of those variants were run for this guide. For the v3 solver call itself, see <a href=\"https:\/\/capskip.com\/recaptcha-v3-python\/\">solving reCAPTCHA v3 in Python<\/a>.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>A Pydoll CAPTCHA flow has five parts: read the sitekey from data-sitekey or from the iframe address, solve with AsyncCapSkip so the event loop stays alive, write the token into g-recaptcha-response, run the page&#8217;s callback or submit, and give every parallel tab its own browser context. Solve right before you submit, and set recaptchaTimeout on the client with asyncio.wait_for around the call as a hard ceiling.<\/p>\n<ul>\n<li>Every CAPTCHA type the Python package handles: <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/li>\n<li>The checkbox reCAPTCHA in a plain Python script: <a href=\"https:\/\/capskip.com\/recaptcha-v2-python\/\">solving reCAPTCHA v2 in Python<\/a>.<\/li>\n<li>How the checkbox solver works: <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/li>\n<li>A similar browser library with its own API: <a href=\"https:\/\/capskip.com\/nodriver-captcha\/\">solving CAPTCHAs in nodriver<\/a>.<\/li>\n<\/ul>\n<p>A browser crawl gets slow long before it gets expensive, and with a <a href=\"https:\/\/capskip.com\/\">local captcha solver<\/a> on your own Windows machine the solves add no bill at all.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Pydoll \u552f\u4e00\u7684\u9a8c\u8bc1\u7801\u4ee3\u7801\u53ea\u662f\u70b9\u51fb Turnstile\uff0c\u6240\u4ee5 reCAPTCHA token \u5fc5\u987b\u7531\u8bc6\u522b\u5de5\u5177\u63d0\u4f9b\u3002\u4e0b\u9762\u4ecb\u7ecd\u5982\u4f55\u8bfb\u53d6 sitekey\uff0c\u5728\u540c\u4e00\u4e2a\u4e8b\u4ef6\u5faa\u73af\u4e0a\u7528 AsyncCapSkip \u8bc6\u522b\uff0c\u628a token \u5199\u5165\u9875\u9762\u5e76\u63d0\u4ea4\u3002<\/p>","protected":false},"author":1,"featured_media":26756,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Pydoll CAPTCHA: Solve reCAPTCHA v2 With CapSkip | CapSkip","rank_math_description":"Solve a Pydoll CAPTCHA with CapSkip: read the sitekey, solve on the same event loop, write the token into the page and submit. Tested on Windows.","rank_math_focus_keyword":"pydoll captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-26757","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\/26757","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=26757"}],"version-history":[{"count":2,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26757\/revisions"}],"predecessor-version":[{"id":26762,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/26757\/revisions\/26762"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/26756"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=26757"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=26757"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=26757"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}