WebView2’de C# ile reCAPTCHA Nasıl Çözülür (WinForms, WPF)

WebView2’de reCAPTCHA çözmek için NavigationCompleted olayını bekleyin, sitekey’i ExecuteScriptAsync ile okuyun ve kodunuz UI thread’ine geri dönsün diye onu düz bir await ile CapSkip’e gönderin. Ardından token’ı her g-recaptcha-response alanına yazın ve sayfanın callback’ini çağırın ya da formu gönderin. Hataların çoğuna iki ayrıntı yol açar. ExecuteScriptAsync bir dize değil JSON döndürür, bu yüzden sitekey tırnak içinde gelir. Ayrıca çözümdeki tek bir ConfigureAwait(false), bir sonraki WebView2 çağrısının InvalidOperationException fırlatmasına yol açar, çünkü CoreWebView2 yalnızca kendisini oluşturan thread üzerinde yanıt verir. Bu rehber ikisini de ele alan küçük bir WinForms uygulaması oluşturur; görünmez widget’ları, açıkça render edilen widget’ları ve çözüm sürerken pencerenin kapatılmasını da kapsar.
Neye ihtiyacınız var
- reCAPTCHA çözücüsü açık hâlde bir Windows makinesinde çalışan CapSkip.
- SDK olarak .NET 8 veya üzeri. Aşağıdaki kod .NET 8 ve .NET 10 üzerinde çalıştırıldı ve SDK 10.0.100 ile derlendi.
- WebView2 Runtime. Windows 11 ile birlikte gelir ve Edge tarayıcısından bağımsız olarak kendini otomatik olarak günceller.
- NuGet’ten Microsoft.Web.WebView2 ve CapSkip paketleri.
- Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ise ağ adresinizi veya genel IP’nizi dinler, böylece başka bir makinedeki bir uygulama onu API üzerinden çağırabilir. İkisi de şurada ayarlanır: bağlantı ayarları.
dotnet new winforms -n WvApp cd WvApp dotnet add package Microsoft.Web.WebView2 dotnet add package CapSkip
İki paket de net8.0-windows ve net10.0-windows için sorunsuz restore edilir. Bir WinForms derlemesi, WindowsBase’in çakışan sürümleriyle ilgili zararsız bir MSB3277 uyarısı yazdırır, çünkü WebView2 paketi her zaman kendi WPF derlemesine (assembly) de başvurur. Bu uyarı uygulamayı etkilemez ve bir WPF projesinde hiç görünmez. UseWPF değerini true yapmak da uyarıyı gizler, ancak o zaman System.IO örtük using listesinin (implicit usings) dışında kalır ve Path.Combine derlenmez hâle gelir.
1. Adım: WebView2’ye yazabileceği bir veri klasörü verin
WebView2 çerezleri, önbelleği ve local storage verilerini bir kullanıcı veri klasöründe tutar. Siz bir klasör belirtmezseniz, klasörü exe dosyanızın yanında WvApp.exe.WebView2 adıyla oluşturur. Uygulamayı bin\Debug klasöründen çalıştırdığınız sürece bu işe yarar. Uygulama Program Files gibi standart bir kullanıcının yazamadığı bir yere kurulduğunda ise başlatma hemen bir UnauthorizedAccessException ile, Access is denied mesajıyla başarısız olur. Bunun yerine kullanıcının yerel uygulama verileri (local app data) altında bir klasör belirtin.
async Task StartAsync()
{
var dataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WvApp");
var env = await CoreWebView2Environment.CreateAsync(null, dataDir);
await webView.EnsureCoreWebView2Async(env);
webView.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
webView.CoreWebView2.Navigate(LoginUrl);
}Kontrole başka bir şey dokunmadan önce ortamı oluşturun. Önce Source özelliğini ayarlamak, başlatmayı varsayılan ortamla başlatır; daha sonra kendi ortamınızla yapılan bir EnsureCoreWebView2Async çağrısı da WebView2’nin farklı bir CoreWebView2Environment ile zaten başlatıldığını söyleyen bir ArgumentException fırlatır. Başlatmadan önce kontrol üzerinde ExecuteScriptAsync çağırmak da başarısız olur; bu kez sizi EnsureCoreWebView2Async’e yönlendiren bir InvalidOperationException alırsınız. Bu rehberin kodunda olduğu gibi webView.CoreWebView2 üzerinden yapıldığında aynı hata bir NullReferenceException olur, çünkü CoreWebView2 hâlâ null değerindedir. Microsoft’un WebView2 kullanıcı veri klasörü hakkındaki sayfası, klasörün nerede bulunabileceğini anlatıyor.
2. Adım: sitekey’i okuyun ve ExecuteScriptAsync’in döndürdüğü değeri ayrıştırın
Sitekey sayfanın içindedir, bu yüzden onu JavaScript ile okuyun. Onay kutulu ya da görünmez bir widget onu bir data-sitekey özniteliğinde taşır. Widget’ı grecaptcha.render ile kendisi oluşturan bir sayfada böyle bir öznitelik yoktur, ancak Google’ın anchor iframe’i aynı anahtarı k sorgu parametresinde taşır; bu yüzden script bu durumda ona başvurur.
// data-sitekey first, then the anchor iframe that an explicit render leaves
// behind (api2 for reCAPTCHA, enterprise for reCAPTCHA Enterprise)
const string FindSitekeyJs = """
(() => {
const el = document.querySelector('.g-recaptcha[data-sitekey]');
if (el) return el.getAttribute('data-sitekey');
const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
return f ? new URL(f.src).searchParams.get('k') : null;
})()
""";
async Task<string?> EvalStringAsync(string js) =>
JsonSerializer.Deserialize<string>(await webView.CoreWebView2.ExecuteScriptAsync(js));ExecuteScriptAsync, script’in sonucunu JSON olarak serileştirilmiş hâlde geri verir. Bir sitekey tırnaklarıyla birlikte gelir, eksik bir öğe ise dört harfli null olarak gelir. Hata fırlatan bir script de, örneğin orada olmayan bir öğe üzerinde bir TypeError, C# tarafında hiçbir istisna olmadan null döndürür. System.Text.Json ile ayrıştırmak (deserialize) size gerçek bir dize ya da gerçek bir null verir. Tırnakları elle kırpmak, bir değer kaçış karakterli (escaped) bir karakter içerene kadar aynı sonucu verir gibi görünür, sonra bozulur.
Bunu DOMContentLoaded sonrasında değil, NavigationCompleted sonrasında çalıştırın. Google’ın api.js dosyası asenkron yüklenir, bu yüzden DOMContentLoaded anında widget’ın div öğesi vardır ama yanıt textarea’sı, grecaptcha nesnesi ve anchor iframe’i henüz olmayabilir. Bizim çalıştırmalarımızda ilk ziyarette bunlar 0.3 ile 1.4 saniye sonra ortaya çıktı, her seferinde NavigationCompleted öncesinde. Script önbelleğe alındıktan sonra ise DOMContentLoaded anında zaten oradaydılar. Dolayısıyla çok erken okuyan bir kod sizin makinenizde çalışıp yeni bir kullanıcıda başarısız olabilir.
3. Adım: UI thread’inden ayrılmadan CAPTCHA’yı çözün
WebView2’de reCAPTCHA çözme girişimlerinin çoğunu bozan kural budur. CoreWebView2, onu oluşturan UI thread’ine aittir ve ona başka herhangi bir thread’den dokunmak istisna fırlatır. Kütüphane kodu için yazılmış rehberler her await’e ConfigureAwait(false) eklemenizi söyler. Bir WebView2 olay işleyicisinde bu tavsiye yanlıştır.
// In OnNavigationCompleted, where core is webView.CoreWebView2.
// Throws: ConfigureAwait(false) resumes on a thread-pool thread
var result = await solver.RecaptchaAsync(sitekey, core.Source).ConfigureAwait(false);
await webView.CoreWebView2.ExecuteScriptAsync("document.title");
// System.InvalidOperationException: CoreWebView2 can only be accessed from the UI thread.ConfigureAwait(false), çözümden sonraki kodun thread havuzundaki bir thread üzerinde devam etmesine izin verir. Bu durumda CoreWebView2’ye yapılan bir sonraki çağrı o InvalidOperationException ile başarısız olur. Script çağrısını Task.Run içine sarsanız da aynısı olur. WPF’te mesaj farklıdır: The calling thread cannot access this object because a different thread owns it. Bu istisna bir async void işleyicisinden dışarı kaçarsa uygulamayı çökertir. Kuralın tamamı için Microsoft’un WebView2 threading modeli hakkındaki notlarını okuyun.
// Works: a plain await inside a WinForms event handler comes back on the UI
// thread
var result = await solver.RecaptchaAsync(sitekey, core.Source);
await webView.CoreWebView2.ExecuteScriptAsync("document.title");Bir WinForms veya WPF olay işleyicisi içindeki düz bir await, UI’ın senkronizasyon bağlamını (synchronization context) yakalar, böylece ondan sonraki kod yine UI thread’inde çalışır. CapSkip çalışırken UI yanıt vermeye devam eder: testimizde 7.88 saniyelik bir çözüm sırasında 100 milisaniyelik bir zamanlayıcı 71 kez tetiklendi. GetAwaiter().GetResult() ile engellemek kilitlenmeye (deadlock) yol açmaz, çünkü CapSkip SDK’sı içeride ConfigureAwait(false) kullanır; ancak çözüm boyunca pencereyi dondurur.
4. Adım: token’ı enjekte edin ve formu gönderin
CapSkip yanıt döndürdüğünde token’ı Google’ın script’inin koyacağı yere koyun, ardından sayfanın bir sonraki adımı neyse onu tetikleyin.
// fill every response textarea, then call data-callback or submit the form
const string InjectJs = """
(token => {
const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
boxes.forEach(b => { b.value = token; });
const cb = document.querySelector('.g-recaptcha[data-callback]')?.dataset.callback;
if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called ' + cb; }
const form = boxes[0]?.form;
if (!form) return 'no form found';
if (!form.checkValidity()) return 'form has empty required fields';
form.requestSubmit();
return 'submitted the form';
})
""";Yalnızca ilkini değil, her g-recaptcha-response textarea’sını doldurun. İki widget’ı olan ya da Google’ınkinin yanında kendi textarea’sı bulunan bir formda bu ada sahip iki alan vardır ve PHP gibi bir sunucu yalnızca sonuncusunu tutar. İki alanlı test sayfamızda yalnızca ilkini doldurmak reddedildi, ikisini birden doldurmak ise kabul edildi.
data-callback özniteliğine sahip bir widget, sayfanın callback’inin token ile çalışmasını bekler, bu yüzden script onu çağırır. Callback test sayfamızda formu callback olmadan göndermek reddedildi. Widget’ı grecaptcha.render ile oluşturan bir sayfa ise callback’ini JavaScript içinde verir ve script bunu göremez. Fonksiyonun adını sayfanın kendi script’inde bulun ve onu da aynı şekilde token ile çağırın.
Aksi hâlde script formu form.requestSubmit() ile gönderir; bu yöntem, formun düğmesine tıklanmış gibi sayfanın kendi submit işleyicilerini ve HTML5 doğrulamasını çalıştırır. Geriye tek bir fark kalır: düğmenin kendi name ve value değerleri yalnızca düğmeyi form.requestSubmit(button) şeklinde verirseniz gönderilir. Eski form.submit() metodu işleyicileri ve doğrulamayı atlar, bu yüzden boş bırakılmış zorunlu bir alan yine de sunucuya gider. checkValidity satırı, yarı boş bir form göndermek yerine bu durumu bildirir.
Token, JsonSerializer.Serialize ile oluşturulmuş bir JSON dize sabiti (string literal) olarak verilir; böylece içindeki tırnaklar ya da ters eğik çizgiler script’i asla bozamaz. Token’ı hemen enjekte edin. Google, bir reCAPTCHA token’ının verildikten iki dakika sonra süresinin dolduğunu belgeliyor.
Tam WinForms örneği
Projeyi WvApp adıyla oluşturun; böylece bu dosya, şablonun oluşturduğu Form1.Designer.cs ile aynı namespace’i paylaşır. Form1.cs dosyasını aşağıdaki kodla değiştirin, LoginUrl değerini kendi sayfanıza ayarlayın ve dotnet run ile çalıştırın. Program.cs, şablonun yazdığı hâliyle kalır.
// Form1.cs, in a project made with: dotnet new winforms -n WvApp
using System.Text.Json;
using CapSkip;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
namespace WvApp;
public partial class Form1 : Form
{
const string LoginUrl = "https://example.com/login";
// data-sitekey first, then the anchor iframe that an explicit render leaves
// behind (api2 for reCAPTCHA, enterprise for reCAPTCHA Enterprise)
const string FindSitekeyJs = """
(() => {
const el = document.querySelector('.g-recaptcha[data-sitekey]');
if (el) return el.getAttribute('data-sitekey');
const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
return f ? new URL(f.src).searchParams.get('k') : null;
})()
""";
const string IsInvisibleJs = """
(() => {
const f = document.querySelector('iframe[src*="/recaptcha/"][src*="/anchor?"]');
if (f) return new URL(f.src).searchParams.get('size') === 'invisible';
const el = document.querySelector('.g-recaptcha[data-sitekey]');
if (!el) return false;
if (el.getAttribute('data-size') === 'invisible') return true;
return el.tagName === 'BUTTON' && !el.hasAttribute('data-action');
})()
""";
// fill every response textarea, then call data-callback or submit the form
const string InjectJs = """
(token => {
const boxes = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
boxes.forEach(b => { b.value = token; });
const cb = document.querySelector('.g-recaptcha[data-callback]')?.dataset.callback;
if (cb && typeof window[cb] === 'function') { window[cb](token); return 'called ' + cb; }
const form = boxes[0]?.form;
if (!form) return 'no form found';
if (!form.checkValidity()) return 'form has empty required fields';
form.requestSubmit();
return 'submitted the form';
})
""";
readonly WebView2 webView = new() { Dock = DockStyle.Fill };
readonly CapSkipClient solver = new(host: "127.0.0.1", port: 8080);
readonly CancellationTokenSource cts = new();
public Form1()
{
InitializeComponent();
Controls.Add(webView);
Load += async (_, _) => await StartAsync();
FormClosing += (_, _) => cts.Cancel();
}
async Task StartAsync()
{
var dataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WvApp");
var env = await CoreWebView2Environment.CreateAsync(null, dataDir);
await webView.EnsureCoreWebView2Async(env);
webView.CoreWebView2.NavigationCompleted += OnNavigationCompleted;
webView.CoreWebView2.Navigate(LoginUrl);
}
async Task<string?> EvalStringAsync(string js) =>
JsonSerializer.Deserialize<string>(await webView.CoreWebView2.ExecuteScriptAsync(js));
async void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
{
var core = webView.CoreWebView2;
var sitekey = await EvalStringAsync(FindSitekeyJs);
if (sitekey is null) return; // no widget on this page
core.NavigationCompleted -= OnNavigationCompleted; // solve once, not on the result page
var invisible = await core.ExecuteScriptAsync(IsInvisibleJs) == "true";
var options = invisible ? new Dictionary<string, object?> { ["invisible"] = 1 } : null;
Text = "Solving reCAPTCHA...";
try
{
var result = await solver.RecaptchaAsync(sitekey, core.Source, options, cts.Token);
var token = JsonSerializer.Serialize(result.Code);
Text = "reCAPTCHA solved, " + await EvalStringAsync($"({InjectJs})({token})");
}
catch (Exception) when (cts.IsCancellationRequested) { } // window closed mid-solve
catch (CapSkipError ex) { Text = "CapSkip error: " + ex.Message; }
}
}İşleyici bir sitekey bulur bulmaz kendini kaldırır. NavigationCompleted, formun gönderildiği sayfa için yeniden tetiklenir ve o satır olmasaydı uygulama sonuç sayfasını da çözmeye çalışırdı. Widget olmayan bir sayfada işleyici erkenden döner ve bağlı kalır, böylece bir sonraki gezinmeden sonra yeniden kontrol eder. Aynı sayfada sonradan beliren bir widget için aşağıda gösterilen yoklama döngüsü gerekir. Görünmez bir widget, Google’ın anchor iframe’inin size parametresinden tespit edilir. O iframe henüz yokken bir data-size özniteliği ya da bir düğmeye bağlı bir widget onu belli eder. Ardından CapSkip’e invisible seçeneğiyle gönderilir. Durum pencere başlığında gösterilir. Bir araç için bu yeterlidir; isterseniz durumu bunun yerine bir Label gösterebilir.
Pencere kapanmaya başladığında CancellationTokenSource iptal edilir. Uygulamayı bir çözümün ortasında kapatın: SDK yoklamayı bir TaskCanceledException ile hemen durdurur, ilk catch bloğu da bunu sessizce yutar. Token olmadan, ikincil bir formdaki çözüm form kapandıktan sonra da devam eder. Çözüm döndüğünde CoreWebView2 null olur ve bir sonraki script çağrısı, WinForms’un işlenmemiş istisna iletişim kutusunda gösterdiği bir NullReferenceException fırlatır.
Aynı kod WPF’te
WPF altında WebView2’de reCAPTCHA çözmek için yukarıdaki adımları koruyun. WPF kontrolünün altında aynı CoreWebView2 vardır, bu yüzden WebView2 ve CapSkip çağrıları değişmez. Kontrolü MainWindow.xaml içinde Microsoft.Web.WebView2.Wpf namespace’iyle tanımlayın ve adını webView koyun; ardından webView alanını ve Controls.Add satırını kaldırın. Geri kalanını MainWindow.xaml.cs dosyasına taşıyın, Loaded olayından başlatın ve token’ı Closing olay işleyicisinde iptal edin. WPF penceresinin Text özelliği yoktur, bu yüzden durumu bunun yerine Title özelliğine yazın. WPF’in örtük using listesi System.IO’yu dışarıda bırakır, bu yüzden Path.Combine kendi using satırına ihtiyaç duyar. Şablonun System.Windows.Shapes import satırını o dosyadan kaldırın, çünkü o namespace’in de kendi Path sınıfı vardır ve ad belirsiz hâle gelir.
Geç yüklenen ve iframe içinde duran widget’lar
ExecuteScriptAsync yalnızca üst belgede çalışır. Giriş formunun kendisi bir iframe içindeyse sitekey script’i orada null döndürür. Sayfanın frame’lerini FrameCreated olayıyla toplayın (bu olayın gezinmeden önce bağlanması gerekir) ve her frame’i yok edildiğinde listeden çıkarın. Artık var olmayan bir frame’i çağırırsanız InvalidOperationException fırlatır.
// a field: the page's iframes, filled before Navigate() and pruned as they go
readonly List<CoreWebView2Frame> frames = new();
// in StartAsync, before Navigate():
webView.CoreWebView2.FrameCreated += (_, e) =>
{
var frame = e.Frame; // read it once: each read returns a new wrapper
frames.Add(frame);
frame.Destroyed += (_, _) => frames.Remove(frame);
};Her CoreWebView2Frame’in kendi ExecuteScriptAsync metodu vardır, bu yüzden aynı script’ler frame içinde de çalışır. Sayfa URL’si olarak CapSkip’e frame’in kendi adresini gönderin, çünkü widget’ın bulunduğu sayfa odur. Üst belge sitekey döndürmediğinde bu metodu OnNavigationCompleted içinden, ana çözümle aynı try ve catch blokları içinde çağırın. FrameCreated, Google’ın kendi anchor ve challenge iframe’leri için de tetiklenir; döngü bunları atlar, çünkü script bunların içinde null döndürür.
// widget inside a cross-origin iframe: run the same scripts in that frame
async Task SolveInFramesAsync()
{
foreach (var frame in frames.ToList())
{
var key = JsonSerializer.Deserialize<string>(await frame.ExecuteScriptAsync(FindSitekeyJs));
if (key is null) continue;
var href = JsonSerializer.Deserialize<string>(await frame.ExecuteScriptAsync("location.href"))!;
var result = await solver.RecaptchaAsync(key, href, null, cts.Token);
var token = JsonSerializer.Serialize(result.Code);
await frame.ExecuteScriptAsync($"({InjectJs})({token})");
return;
}
}Bazı sayfalar widget’ı load olayından sonra ekler, örneğin bir çerez banner’ından ya da ilk tıklamadan sonra. Bu durumda NavigationCompleted hiçbir şey bulamaz. Kısa bir yoklama döngüsü bu durumu halleder: widget’ını yüklemeden iki saniye sonra ekleyen bir sayfada bu döngü anahtarı 2.11 saniye sonra buldu.
// for pages that add the widget after the load event
async Task<string?> WaitForSitekeyAsync(TimeSpan timeout)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
var key = await EvalStringAsync(FindSitekeyJs);
if (key is not null) return key;
await Task.Delay(250);
}
return null;
}CapSkip’i başka bir makinede çalıştırmak
Uygulama ile çözücünün aynı bilgisayarı paylaşması gerekmez. Bağlantı ayarlarından CapSkip’i Server moduna alın; ağ adresinizi veya genel IP’nizi dinlemeye başlar. Ardından istemciyi oraya yönlendirin.
// CapSkip in Server mode on another Windows machine on your network var solver = new CapSkipClient(apiKey: "YOUR_API_KEY", host: "192.168.1.50", port: 8080);
Rota internetten geçiyorsa statik bir genel IP kullanın ve API anahtarı doğrulamasını açıp aynı anahtarı istemcide de kullanın. CapSkip, Server modu başladığında portunu her adrese açar; portu yalnızca beklediğiniz makinelere açık tutmak için diğerlerine yönelik bir Windows Firewall engelleme kuralı ekleyin. Aynı makinede localhost yerine 127.0.0.1 yazın. Bizim çalıştırmalarımızda localhost önce IPv6 adresini denedi ve her yeni bağlantıda bu denemenin başarısız olmasını yaklaşık iki saniye bekledi. Çalışmayan bir çözücü, durumu iki yerine dört saniyede bildirdi.
Uygulamada CapSkip hatalarını ele almak
SDK, tümü CapSkipError’dan türeyen dört istisna fırlatır. Her hata için farklı bir mesaj göstermek istiyorsanız OnNavigationCompleted içindeki try bloğunu bununla değiştirin. İptal filtresini ilk sırada tutun: görev gönderilirken kapatılan bir pencere bir NetworkException fırlatır ve daha sonra gelen bir filtre bunu hata olarak gösterirdi.
try
{
var result = await solver.RecaptchaAsync(sitekey, core.Source, options, cts.Token);
var token = JsonSerializer.Serialize(result.Code);
Text = "reCAPTCHA solved, " + await EvalStringAsync($"({InjectJs})({token})");
}
catch (Exception) when (cts.IsCancellationRequested) { } // window closed mid-solve
catch (NetworkException ex) { Text = "Is CapSkip running? " + ex.Message; }
catch (ApiException ex) { Text = "CapSkip could not solve it: " + ex.Message; }
catch (CapSkip.TimeoutException) { Text = "No answer before the timeout"; }
catch (CapSkipError ex) { Text = "CapSkip error: " + ex.Message; }Bir ada dikkat etmek gerekir. Bir WinForms projesinin örtük using listesiyle, TimeoutException’ı nitelemeden yakalayan bir catch bloğu CS0104 hatasıyla derlenmez, çünkü hem System.TimeoutException hem de CapSkip.TimeoutException kapsam içindedir. Yukarıdaki gibi tam CapSkip adını yazın. RecaptchaAsync, SDK’nın recaptchaTimeout süresi dolunca vazgeçer; bu süre varsayılan olarak 300 saniyedir ve CapSkipClient yapıcısında (constructor) ayarlanır. Yavaş bir çözüm genellikle daha önce CapSkip’in kendi içinde başarısız olur ve size bir ApiException olarak ulaşır.
WebView2’de reCAPTCHA çözerken sık görülen hatalar
| Gördüğünüz | Neden | Düzeltme |
|---|---|---|
| InvalidOperationException: CoreWebView2 can only be accessed from the UI thread | Çözümden sonraki kod, ConfigureAwait(false) ya da Task.Run yüzünden thread havuzundaki bir thread üzerinde devam etti | Olay işleyicisinde düz bir await kullanın |
| Sitekey tırnak içinde geliyor ve her çözüm başarısız oluyor | ExecuteScriptAsync’ten gelen ham JSON CapSkip’e gönderildi | Sonucu System.Text.Json ile ayrıştırın |
| Görünür bir widget olan sayfada sitekey null geliyor | Sayfa widget’ı kendisi oluşturuyor, geç ekliyor ya da widget bir iframe içinde | Anchor iframe’ini okuyun, anahtar için yoklama yapın ya da script’i frame içinde çalıştırın |
| Uygulama başlar başlamaz Access is denied | WebView2, veri klasörünü kullanıcının yazma izni olmayan bir exe dosyasının yanında oluşturmaya çalıştı | Ortamı yerel uygulama verileri (local app data) altındaki bir klasörle oluşturun |
| ArgumentException: farklı bir ortamla zaten başlatılmış | Source, sizin ortamınız oluşturulmadan önce ayarlandı | Önce kendi ortamınızla başlatın, sonra sayfaya gidin |
| Sayfa gönderimden sonra giriş formunu yeniden yüklüyor | Yalnızca bir yanıt alanı dolduruldu, callback hiç çalışmadı ya da token’ın süresi doldu | Her alanı doldurun, callback’i çağırın ve token’ı çözümden hemen sonra enjekte edin |
| Uygulama, girişten sonra açılan sayfayı çözmeye çalışıyor | NavigationCompleted her gezinmede tetiklenir | Bir sitekey bulunduğunda işleyiciyi kaldırın |
| NetworkException: hedef makine bağlantıyı etkin olarak reddetti | CapSkip çalışmıyor ya da host veya port yanlış | CapSkip’i başlatın ve adresi bağlantı ayarlarıyla karşılaştırın |
| Bir pencereyi kapattıktan sonra NullReferenceException iletişim kutusu | Bir çözüm, formu kapatıldıktan sonra tamamlandı | Bir CancellationToken verin ve form kapanınca onu iptal edin |
FAQ
Bu, reCAPTCHA v3 ve Enterprise için de çalışır mı?
Evet, farklı bir metotla değil, seçeneklerle. Bir Enterprise sayfası api.js yerine enterprise.js yükler. RecaptchaAsync seçeneklerine 1 değeriyle enterprise ekleyin; bu rehberin geri kalanı aynı kalır. Bir v3 sayfası ise farklıdır: sitekey’i api.js dosyasının render parametresinde bulunur ve sayfa token’ı bir textarea’dan değil, kendi script’inden gönderir. version değerini v3 olarak ve sayfanın action değerini verin; SDK’nın v3 için reddettiği invisible seçeneğini ise eklemeyin. C# ile reCAPTCHA v2 çözme rehberi seçenekleri daha ayrıntılı olarak ele alıyor.
Bir script hatasını gerçek bir null’dan nasıl ayırt ederim?
Güncel WebView2 paketinde bulunan ExecuteScriptWithResultAsync’i kullanın. Bu metot script’in başarılı olup olmadığını bildirir ve başarısız olduğunda istisna mesajını verir; ExecuteScriptAsync ise iki durumda da null döndürür. Yukarıdaki basit okumalar için ExecuteScriptAsync’i kullanmaya devam edin; orada eksik bir widget ile bozuk bir seçici aynı anlama gelir: çözülecek bir şey yok.
Bu, mobil bir WebView içinde CAPTCHA çözmekle aynı şey mi?
Hayır. WebView2, bir Windows masaüstü uygulamasının içindeki Edge motorudur ve onu doğrudan C# ile kontrol edersiniz. Bir Android veya iOS uygulamasının içindeki WebView ise dışarıdan, genellikle kendi oturum zaman aşımları olan Appium üzerinden yönetilir. Bu durum şurada ele alınıyor: Appium ile CAPTCHA çözme rehberi.
Masaüstü uygulaması CapSkip’ten farklı bir makinede çalışabilir mi?
Evet. Bağlantı ayarlarından CapSkip’i Server moduna alın; böylece loopback yerine bir ağ adresini dinler. Ardından bu adresi CapSkipClient’ın host değeri olarak verin. Uygulama bundan sonra porta ulaşabilen her makineden onu aynı HTTP API üzerinden çağırır. Porta ulaşmaması gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Çözücü yine sizin sahip olduğunuz donanımda çalışır ve hiçbir çözüm üçüncü taraf bir hizmetten geçmez.
Kısa özet
WebView2’de reCAPTCHA çözmek için sitekey’i NavigationCompleted sonrasında okuyun ve ExecuteScriptAsync’in döndürdüğü JSON’u ayrıştırın. RecaptchaAsync’i ConfigureAwait(false) olmadan await edin, böylece bir sonraki CoreWebView2 çağrısı UI thread’inde gerçekleşir. Her yanıt alanını doldurun, ardından sayfanın callback’ini çağırın ya da form.requestSubmit() kullanın. WebView2’ye yazabileceği bir veri klasörü verin, pencere kapandığında çözümü iptal edin ve uygulama başka bir makineye taşındığında CapSkip’i Server moduna alın.
- CapSkip .NET paketinin desteklediği tüm CAPTCHA türleri: C# CAPTCHA çözücü sayfası.
- Onay kutulu reCAPTCHA’nın nasıl çalıştığı ve çözücünün neleri kapsadığı: reCAPTCHA v2 çözücü sayfası.
Aynı Windows bilgisayarında duran bir masaüstü uygulaması ve onun çözücüsü, olabilecek en basit kurulumdur. Bir yerel captcha çözücü tam olarak bunun için vardır: token kendi makinenizden gelir ve hiçbir çözüm makinenizden dışarı çıkmaz.
