{"id":25507,"date":"2026-09-19T06:15:02","date_gmt":"2026-09-19T06:15:02","guid":{"rendered":"https:\/\/capskip.com\/?p=25507"},"modified":"2026-09-19T06:15:02","modified_gmt":"2026-09-19T06:15:02","slug":"patchright-captcha","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/patchright-captcha\/","title":{"rendered":"\u5982\u4f55\u5728 Patchright \u4e2d\u8bc6\u522b\u9a8c\u8bc1\u7801\u4e14\u4e0d\u7834\u574f\u9690\u8eab\u6548\u679c"},"content":{"rendered":"<p>A Patchright captcha flow is the same three moves as any other: read the sitekey, send it to a solver, put the token back in the page. The third move is where this library surprises people. Patchright runs your JavaScript in an isolated context by default, and an isolated context shares the page&#8217;s DOM but not the page&#8217;s JavaScript global. So the token write lands, the textarea really does hold the value, and the site&#8217;s own callback never runs because it does not exist in the scope your code is executing in. One extra argument fixes it.<\/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, or Node.js if you prefer the JavaScript package. Patchright ships for both runtimes and is a drop-in replacement for Playwright in each.<\/li>\n<li>A Chromium browser downloaded through Patchright&#8217;s own installer. Firefox and WebKit are not patched and are not supported.<\/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\"># pip install patchright\r\npip install -U capskip patchright\r\n\r\n# Pulls the browser. Real Chrome is the recommended channel,\r\n# and the maintainers say so explicitly.\r\npatchright install chrome<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What Patchright actually changes<\/h2>\n<p>Patchright is Playwright with the obvious automation tells removed. The headline patch is that it never calls Runtime.enable, which is the single loudest signal a stock Playwright session gives off, and it gets there by running your scripts in isolated ExecutionContexts instead. It also disables the Console API outright, adds the flag that hides navigator.webdriver, and drops several Playwright defaults that mark a session as automated: the automation flag itself, the popup blocker override, the component update block, and the switches that disabled default apps and extensions.<\/p>\n<p>Two of those have direct consequences for CAPTCHA work, and both are easy to trip over.<\/p>\n<p>The console being off means nothing you log from inside the page reaches you. Console functionality does not work at all in Patchright, so the usual habit of dropping a log line into an evaluate call and reading it from the driver is dead here. Return a value out of the call instead. That is better practice anyway and it is the only option you have.<\/p>\n<p>Extensions being re-enabled is the friendlier one. Playwright normally launches with extensions disabled, and Patchright removes that switch, so a browser extension loaded into a persistent profile actually runs. If you would rather not write any of this code, <a href=\"https:\/\/capskip.com\/captcha-solver-extension\/\">the CapSkip browser extension<\/a> handles the widget in the page and you drive the form as if a human had passed it.<\/p>\n<p>One more capability worth knowing: Patchright reaches into closed shadow roots with ordinary locators and XPaths. Widgets that hide their markup behind a closed root are addressable without any special handling.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: launch it the way the maintainers recommend<\/h2>\n<p>Patchright&#8217;s stealth depends on the launch configuration as much as on the patches. The documented setup is a persistent context on the real Chrome channel, with a visible window and no viewport override, and no custom user agent or headers at all. Those last two matter: a hand-set user agent contradicts the rest of the fingerprint and undoes the work.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install patchright\r\nfrom patchright.sync_api import sync_playwright\r\n\r\nwith sync_playwright() as p:\r\n    context = p.chromium.launch_persistent_context(\r\n        user_data_dir=&quot;C:\\\\profiles\\\\scraper&quot;,\r\n        channel=&quot;chrome&quot;,\r\n        headless=False,\r\n        no_viewport=True,\r\n        # Do not set user_agent or extra headers here.\r\n    )\r\n    page = context.new_page()\r\n    page.goto(&quot;https:\/\/example.com\/page-with-recaptcha&quot;)<\/pre>\n<\/div>\n<p>Note the visible window. Headless is where most detection budget gets spent, and the recommended configuration does not use it. On Windows that means the account running the script needs an interactive desktop session, which is worth planning for before you put this on a server.<\/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 sits on the host document, not inside the widget iframe, and a plain locator reads it. Locators travel over the browser protocol rather than through any execution context, so nothing about the isolated world affects this step.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Locators auto-wait, so this doubles as a wait condition\r\n# for a widget that renders late.\r\nholder = page.locator(&quot;div.g-recaptcha&quot;)\r\nholder.wait_for(state=&quot;attached&quot;, timeout=15000)\r\n\r\nsitekey = holder.get_attribute(&quot;data-sitekey&quot;)\r\nprint(sitekey)   # 6Lxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<\/pre>\n<\/div>\n<p>Some sites never put the key on the host page and pass it only in the widget iframe URL. Read it out of the query string in that case, and check it before spending a solve, because an empty value travels all the way to the solver and comes back as <a href=\"https:\/\/capskip.com\/error-googlekey-pageurl\/\">ERROR_GOOGLEKEY<\/a>, a long way from the read that actually failed.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Fallback: the k= parameter on the anchor iframe.\r\nfrom urllib.parse import urlparse, parse_qs\r\n\r\nsrc = page.locator(&quot;iframe[src*='recaptcha\/api2\/anchor']&quot;).get_attribute(&quot;src&quot;)\r\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 3: solve it on your own machine<\/h2>\n<p>The Python SDK talks to CapSkip on port 8080 and hands back the token as a plain string. One method covers reCAPTCHA v2, Invisible, Enterprise and v3, with the variants passed as options rather than as separate calls.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\r\nfrom capskip import CapSkip\r\n\r\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\r\n\r\n# Invisible v2 takes invisible=1, Enterprise takes enterprise=1,\r\n# and v3 takes version=&quot;v3&quot; with an action.\r\nresult = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)\r\n\r\ntoken = result[&quot;code&quot;]   # the g-recaptcha-response value<\/pre>\n<\/div>\n<p>Turnstile and GeeTest have their own methods and take the same shape. Turnstile also returns the user agent the solve was made with, and a challenge page rejects the token unless that user agent is sent back with it. Send it on the form request only. Do not feed it to the browser launcher, because a hand-set user agent is exactly what the stealth configuration tells you to avoid. Full parameter lists 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 4: put the token where the page can use it<\/h2>\n<p>Here is the part that is specific to this library. The response textarea is hidden with display:none, so nothing can type into it and the value has to be assigned with JavaScript. That assignment works from the isolated context, because the DOM is shared. Build the string with json.dumps rather than an f-string, since a JSON string literal is also a valid JavaScript string literal, quoting and escaping included.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># A DOM write is fine from the isolated context.\r\nimport json\r\n\r\npage.evaluate(\r\n    &quot;document.getElementById('g-recaptcha-response').value = &quot;\r\n    + json.dumps(token)\r\n)\r\n\r\npage.click(&quot;button[type=submit]&quot;)<\/pre>\n<\/div>\n<p>That covers the sites whose form reads the textarea on submit. Plenty of sites do not. They register a success callback with the widget and never look at the textarea at all, so the token has to be handed to a function the page defined. Patchright&#8217;s isolated context has its own JavaScript global, which means the page&#8217;s functions and the reCAPTCHA client configuration are simply absent from it. The call fails with a reference error and no amount of retrying changes that.<\/p>\n<p>Patchright&#8217;s answer is an extra argument. The evaluate, evaluate_handle and evaluate_all methods all take isolated_context, it defaults to True, and setting it to False runs the script in the page&#8217;s own main world.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># The main world is where the page's own globals live.\r\npage.evaluate(\r\n    &quot;token =&gt; window.onRecaptchaSuccess(token)&quot;,\r\n    token,\r\n    isolated_context=False,\r\n)<\/pre>\n<\/div>\n<p>Read the callback name out of the page&#8217;s markup rather than guessing it. Use the main world for the injection and nothing else: code running there is visible to the site, which is the whole reason the isolated context is the default. Either submission style is still ordinary reCAPTCHA v2 underneath, and both are written up on <a href=\"https:\/\/capskip.com\/recaptcha-v2-solver\/\">the reCAPTCHA v2 solver page<\/a>.<\/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, the sitekey is checked before a solve is spent on it, and the token is verified by returning a length rather than by logging inside the page.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip patchright\r\nimport json\r\nfrom patchright.sync_api import sync_playwright\r\nfrom capskip import CapSkip\r\n\r\nPAGE_URL = &quot;https:\/\/example.com\/page-with-recaptcha&quot;\r\n\r\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080)\r\n\r\nwith sync_playwright() as p:\r\n    context = p.chromium.launch_persistent_context(\r\n        user_data_dir=&quot;C:\\\\profiles\\\\scraper&quot;,\r\n        channel=&quot;chrome&quot;,\r\n        headless=False,\r\n        no_viewport=True,\r\n    )\r\n    page = context.new_page()\r\n    page.goto(PAGE_URL)\r\n\r\n    holder = page.locator(&quot;div.g-recaptcha&quot;)\r\n    holder.wait_for(state=&quot;attached&quot;, timeout=15000)\r\n    sitekey = holder.get_attribute(&quot;data-sitekey&quot;)\r\n    if not sitekey:\r\n        raise RuntimeError(&quot;Widget found but data-sitekey was empty.&quot;)\r\n\r\n    token = solver.recaptcha(sitekey=sitekey, url=PAGE_URL)[&quot;code&quot;]\r\n    page.evaluate(\r\n        &quot;document.getElementById('g-recaptcha-response').value = &quot;\r\n        + json.dumps(token)\r\n    )\r\n\r\n    length = page.evaluate(\r\n        &quot;document.getElementById('g-recaptcha-response').value.length&quot;\r\n    )\r\n    print(length)   # 0 means the injection did not land\r\n\r\n    page.click(&quot;button[type=submit]&quot;)\r\n    page.wait_for_load_state(&quot;networkidle&quot;)\r\n    context.close()<\/pre>\n<\/div>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on another machine<\/h2>\n<p>Patchright usually ends up on a bigger box than the one you wrote the script on, and the recommended visible-window setup pushes people towards a dedicated VM fairly quickly. The solver does not have to move with it.<\/p>\n<p>CapSkip has two connection modes. Local binds to 127.0.0.1 and answers that device only, which is right while you are writing the script on the machine the app runs on. Server binds to your network or public IP, so a scraping VM, a second workstation or a whole pool of them call the same Windows machine over the API. A static public IP keeps that address stable. Nothing in the code changes except the host, and nothing about the cost changes either, because it is still hardware you own.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Same SDK, same call. Only the host moves.\r\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 listens on a network address, and give each machine its own key so one can be revoked without touching the others. Both modes are walked through in the <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">CapSkip setup guide<\/a>.<\/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>The textarea holds the token but the form fails<\/td>\n<td>The site uses a callback and never reads the textarea<\/td>\n<td>Call the callback with isolated_context=False<\/td>\n<\/tr>\n<tr>\n<td>A reference error naming a page function<\/td>\n<td>Page globals do not exist in the isolated context<\/td>\n<td>Same fix: run that one call in the main world<\/td>\n<\/tr>\n<tr>\n<td>Nothing arrives from a console log in the page<\/td>\n<td>Patchright disables the Console API completely<\/td>\n<td>Return a value out of evaluate instead of logging<\/td>\n<\/tr>\n<tr>\n<td>Firefox or WebKit behaves like plain Playwright<\/td>\n<td>Only Chromium browsers are patched<\/td>\n<td>Use the Chromium or Chrome channel<\/td>\n<\/tr>\n<tr>\n<td>Blocked despite the patches<\/td>\n<td>A custom user agent or header contradicts the fingerprint<\/td>\n<td>Remove them and use a persistent context on Chrome<\/td>\n<\/tr>\n<tr>\n<td>ERROR_GOOGLEKEY<\/td>\n<td>An empty sitekey reached the solver<\/td>\n<td>Assert the value before calling recaptcha<\/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 of 300 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;\">Can I port my Playwright script over unchanged?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Change the import and most of it runs. Locators, contexts, routes, waits and navigation all behave as they did. Three things need a second look: any evaluate call that touches something the page defined, which now needs the extra argument; anything that relied on console output, which is gone; and any Firefox or WebKit target, which is unpatched. The wider Playwright picture is on <a href=\"https:\/\/capskip.com\/playwright-captcha-solver\/\">the Playwright CAPTCHA solver page<\/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;\">Does using the main world get me detected?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">In principle yes, since the page can see code running there. In practice the exposure is one function call lasting microseconds, and it looks the same as what the widget&#8217;s own script does when a human passes the challenge. Keep everything else in the isolated context, do the injection in a single call rather than several, and the surface stays small.<\/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;\">Should I click the Turnstile widget instead of injecting a token?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Sometimes. A Turnstile checkbox in managed mode can pass on its own when the browser looks convincing, which is exactly what Patchright is for, so it is worth trying the click first and falling back to a solve. A reCAPTCHA checkbox click only opens an image challenge, so there is nothing to gain there. The widget side is covered on <a href=\"https:\/\/capskip.com\/cloudflare-turnstile-solver\/\">the Cloudflare Turnstile solver page<\/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 crawler runs on Linux. Where does CapSkip go?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">On a Windows machine you control, with Server mode switched on. The Linux box then calls it over the API like any other internal service, so Patchright and the solver do not need to share an operating system or even a network segment. Point the host argument at that address, enable key validation, and give the crawler its own key.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The short version<\/h2>\n<p>Install Patchright, launch a persistent context on the Chrome channel with a visible window and no custom user agent, read the sitekey with an ordinary locator, and solve it against CapSkip on 127.0.0.1:8080. Write the token straight into the textarea from the default isolated context, and reach for isolated_context=False only when the site wants a callback called. For the rest of the Python landscape, including Selenium and Playwright, see <a href=\"https:\/\/capskip.com\/python-captcha-solver\/\">the Python CAPTCHA solver page<\/a>.<\/p>\n<p>One thing is worth knowing before you scale a crawl up. CapSkip is an <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> that runs on hardware you already own, so retrying a thousand pages costs exactly what retrying ten costs.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Patchright \u5728\u9694\u79bb\u7684\u4e0a\u4e0b\u6587\u4e2d\u8fd0\u884c evaluate\uff0c\u56e0\u6b64\u5199\u5165\u7684 token \u867d\u7136\u80fd\u8fdb\u5165 DOM\uff0c\u9875\u9762\u81ea\u8eab\u7684 callback \u5374\u4e0d\u4f1a\u88ab\u89e6\u53d1\u3002\u591a\u52a0\u4e00\u4e2a\u53c2\u6570\uff0c\u5c31\u80fd\u8ba9\u811a\u672c\u56de\u5230\u4e3b\u4e16\u754c\u3002<\/p>","protected":false},"author":1,"featured_media":25506,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Patchright CAPTCHA: Make the Token Actually Fire | CapSkip","rank_math_description":"A patchright captcha token reaches the textarea but the page callback never runs, because evaluate uses an isolated context. Pass isolated_context=False.","rank_math_focus_keyword":"patchright captcha","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25507","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\/25507","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=25507"}],"version-history":[{"count":3,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25507\/revisions"}],"predecessor-version":[{"id":25514,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25507\/revisions\/25514"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25506"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25507"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25507"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25507"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}