C# CAPTCHA Algılayıcı: Türü Bulun, CapSkip Metodunu Çağırın

C#’ta bir CAPTCHA algılayıcısı, bir sayfanın HTML’inde widget’ın script’ini ve kapsayıcısını okur, sitekey’i bunlardan çıkarır ve aileyi onu çözen CapSkip metoduna eşler. Bir reCAPTCHA v2 kutusu, bir v3 script’i ve bir Turnstile widget’ı farklı seçeneklerle üç ayrı çağrı gerektirir; yanlış bir tahmin bir çözümün tamamını boşa harcar ve size kullanamayacağınız bir token verir. Bu rehber, CapSkip dışında paket kullanmayan yaklaşık 200 satırlık bir algılayıcı oluşturur. Türü, metodu ve argümanları döndürür ya da ayırt edemediğini açıkça söyler. CapSkip kendi Windows bilgisayarınızda çalışır ve çözüm başına ücret yoktur; bu yüzden yüz sayfayı denetleyen bir program ekstra hiçbir şeye mal olmaz. Aynı algılayıcı Python ve Node.js için de vardır; bağlantıları sonunda verilmiştir.
Neye ihtiyacınız var
- Gerekli çözücüler açık hâlde bir Windows bilgisayarında çalışan CapSkip. İlk başlatmayı kurulum kılavuzu adım adım anlatır.
- Gerekli olan: .NET 8 SDK veya üzeri, çünkü kod C# 11 gerektiren raw string literals (ham dize değişmezleri) kullanır. SDK 10.0.100 ve CapSkip paketi 1.3.0 ile, net8.0 ve net10.0 hedeflenerek test edildi. Proje dosyasına LangVersion 11 eklendikten sonra net6.0 hedeflemek de çalıştı. .NET Framework denenmedi.
- Bir terminal. Komutlar PowerShell’de çalışır.
- Sahibi olduğunuz ya da otomatikleştirmenize izin verilen, üzerinde CAPTCHA bulunan bir sayfa.
- Çözücü için bir adres. Local modunda CapSkip 127.0.0.1 üzerinden yalnızca o cihaza yanıt verir. Server modunda ağ adresinizi veya genel IP’nizi dinler, böylece başka bir bilgisayardaki, bir VPS’teki veya barındırılan bir platformdaki program API üzerinden ona erişebilir. İkisi de şurada ayarlanır: bağlantı ayarları.
Nasıl test edildi: algılayıcı, Python olanla aynı 66 kayıtlı sayfada çalıştırıldı ve her sayfa için aynı sonucu verdi. Bunların 59’u elle hazırlandı (yaklaşık 40’ı sağlayıcıların belgelerini izler, gerisi benim uydurduğum uç durumlardır) ve 7’si 29 Ekim 2026’da alınmış herkese açık sayfa anlık görüntüleridir. Bir metodu adlandıran 40 sonuç, gerçek CapSkip NuGet paketi üzerinden, uygulamanın girdi denetimlerini kopyalayan bir mock CapSkip’e gönderildi. Gerçek uygulama çalışmıyordu, bu yüzden aşağıdaki hiçbir sayı bir çözüm süresi değildir.
CAPTCHA türünü neden çözmeden önce algılarsınız
CapSkip’in her aile için farklı bir metodu vardır: SDK’da RecaptchaAsync, TurnstileAsync, GeetestAsync, AltchaAsync, CapyAsync, CaptchaFoxAsync, FriendlyCaptchaAsync ve görseller için NormalAsync; ayrıca HTTP API’de, SDK’nın henüz sarmalamadığı hcaptcha. reCAPTCHA içinde seçenekler de önemlidir. Invisible, Enterprise ve v3 aynı çağrıdaki bayraklardır. Yanlış olanları gönderirseniz en iyi ihtimalle iş başarısız olur, en kötü ihtimalle site nedenini göstermeden bir token reddeder. Yanlış bir reCAPTCHA sürümünde gerçek uygulamanın bu ikisinden hangisini yaptığını kontrol edemedim. Her iki durumda da bir çözümün zamanı boşa gider.
Diğer hatalar hızlı ve gürültülü biçimde ortaya çıkar. reCAPTCHA’yı açıkça (explicit) render eden bir sayfadan sitekey okumak explicit kelimesini verir ve CapSkip bunu ERROR_GOOGLEKEY ile reddeder. ERROR_GOOGLEKEY ve ERROR_PAGEURL kılavuzu bu hatayı öbür taraftan ele alır. CAPTCHA türünü güvenilir biçimde algılamak için algılayıcının sinyalleri sabit bir sırayla denetlemesi gerekir ve bu sıra iki tür hatadan da kaçınır. Algılayıcı, birden fazla siteyle karşılaşan bir programdan önce yazılacak ilk şeydir. Her türün ne olduğuna dair arka plan için CAPTCHA nedir yazısı aileleri kodsuz açıklar.
1. Adım: projeyi oluşturun
Bir konsol projesi oluşturun ve CapSkip paketini ekleyin. Algılayıcının kendisi yalnızca .NET’in beraberinde gelenleri kullanır: düzenli ifadeler (regex) ve HttpClient.
dotnet new console -o detect-demo cd detect-demo dotnet add package CapSkip
C#’ta bir CAPTCHA algılayıcısı metin üzerinde çalışır, bu yüzden yalnızca iki girdiye ihtiyaç duyar: sayfanın HTML’i ve adresi. Düz bir istek, sunucunun JavaScript çalışmadan önce gönderdiğini gösterir. Google’ın reCAPTCHA demo sayfaları, Cloudflare’in Turnstile demosu ve hCaptcha demosu widget’larını bu HTML’de taşır. Widget’ı JavaScript ile ekleyen sayfalar taşımaz ve “HTML hiçbir şey söylemediğinde” bölümü bu durumu ele alır. Bir HttpClient tek bir çerez deposu tutar; bu, görsel CAPTCHA adımı için önemlidir: aşağıdaki program sayfa için de resim için de aynı istemciyi kullanır.
2. Adım: yardımcıları yazın
Buradan 9. Adım’a kadar her şeyi, blok blok art arda, Detector.cs adlı tek bir dosyaya kaydedin. 9. Adım sınıfı kapatana kadar dosya derlenmez ve dotnet new console’un açtığı implicit usings (örtük using’ler) ayarına dayanır. İlk blok using’leri, iki küçük record’u ve ilk yardımcıları içerir. Her okuyucu hiçbir şey eşleşmediğinde null döndürür, bu yüzden eksik bir öznitelik hiçbir zaman hata fırlatmaz.
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
using CapSkip;
public record Info(string Type, string? Method, Dictionary<string, object?> Args,
string? NeedsBrowser = null, bool Unsupported = false, string[]? Also = null);
record Page(string H, string Srcs, string Js, string Url);
public static class Detector
{
const RegexOptions F = RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.CultureInvariant;
const string SiteKeyJs = """sitekey['"]?\s*:\s*['"]([^'"]+)""";
static string? Grab(string pattern, string? text) // first capture group that matched, or null
{
var m = Regex.Match(text ?? "", pattern, F);
for (var i = 1; i < m.Groups.Count; i++) if (m.Groups[i].Success) return m.Groups[i].Value;
return null;
}
static string? Or(params string?[] v) => v.FirstOrDefault(s => !string.IsNullOrEmpty(s));
static string Abs(string url, string rel) => Uri.TryCreate(new Uri(url), rel, out var u) ? u.AbsoluteUri : rel;
static Dictionary<string, object?> Args(params (string, object?)[] kv) => // null values are left out
kv.Where(x => x.Item2 != null).ToDictionary(x => x.Item1, x => x.Item2);Grab metodu eşleşen ilk yakalama grubunu döndürür. Tag metodu bir açılış etiketini CSS sınıfına göre bulur ve adı yalnızca daha uzun bir sınıfın içinde geçen sınıfı reddeder; bu yüzden cf-turnstile-response adlı bir sınıf bir Turnstile widget’ı değildir. Attr metodu her iki tırnak biçimiyle yazılmış bir özniteliği okur ve HTML varlıklarını çözer; bu, bir özniteliğin içinde tutulan challenge için önemlidir. Tırnaksız öznitelikler okunmaz.
İkinci blok bir sayfayı denetimlerin paylaşabileceği bir şeye dönüştürür. Yorumları ve style bloklarını ayıklar, böylece bir widget sınıfından söz eden bir CSS kuralı eşleşemez; script adreslerini ve satır içi JavaScript’i ayrı ayrı toplar. Son yardımcı, HTML’in yetmediği sayfalar için yanıtı oluşturur. NeedsBrowser alanı gerekçeyi taşır, örneğin sitekey’in JavaScript ile ayarlandığını; bu yazı buna tarayıcı gerekir sonucu der.
static string? Attr(string? tag, string name) => // attribute value, either quote style, HTML-decoded
Grab("""\s""" + name + """\s*=\s*(?:"([^"]*)"|'([^']*)')""", tag) is { } v ? WebUtility.HtmlDecode(v) : null;
static string? Tag(string cls, string h) => // opening tag that has CSS class cls
Grab("""(<\w+[^>]*\sclass\s*=\s*["'][^"']*(?<![\w-])""" + cls + """(?![\w-])[^"']*["'][^>]*>)""", h);
static string? Opts(string call, string js) => Grab(call + """\([^,]+,\s*\{(.{0,400})""", js);
static Page Parse(string html, string url)
{
var h = Regex.Replace(html, """<!--.*?-->|<style\b.*?</style>""", "", F);
var srcs = Regex.Matches(h, """<script\b[^>]*?\ssrc\s*=\s*["']([^"']+)""", RegexOptions.IgnoreCase);
var js = Regex.Matches(h, """<script\b[^>]*>(.*?)</script>""", F);
return new(h, WebUtility.HtmlDecode(string.Join(" ", srcs.Select(m => m.Groups[1].Value))),
string.Join("\n", js.Select(m => m.Groups[1].Value)), url);
}
static Info Browser(string type, string method, Page p, string why) => // HTML is not enough: do not guess
new(type, method, new() { ["url"] = p.Url }, why);
static readonly (string, string)[] Vendors = { ("arkose", """arkoselabs\.com/v2/|funcaptcha\.com/fc/"""),
("geetest_v4", """gt4\.js|initGeetest4""") };
static Info? Unsup(Page p) => Vendors.Where(v => Regex.IsMatch(p.Srcs + " " + p.Js, v.Item2)) // CapSkip cannot solve these
.Select(v => new Info(v.Item1, null, new(), Unsupported: true)).FirstOrDefault();
static Info? Cf(Page p) => Regex.IsMatch(p.Js, """_cf_chl_opt\s*=\s*\{""") // the "Just a moment..." page, not a widget
? Browser("turnstile_challenge", "turnstile", p, "data and pagedata (Cloudflare calls them cData and chlPageData) only exist at run time") : null;
static Info? Hcaptcha(Page p) // before reCAPTCHA: a site that moved to hCaptcha may keep the g-recaptcha class
{
if (!Regex.IsMatch(p.Srcs + " " + p.Js, """hcaptcha\.com/1/api\.js""")) return null;
if (Regex.IsMatch(p.Js, """\.setData\s*\(|rqdata""")) // Enterprise rqdata is issued per challenge
return Browser("hcaptcha_enterprise", "hcaptcha", p, "rqdata is issued for one challenge at run time");
var t = Tag("h-captcha", p.H) ?? Tag("g-recaptcha", p.H);
var key = Or(Attr(t, "data-sitekey"), Grab(SiteKeyJs, Opts("""hcaptcha\.render""", p.Js)));
if (key == null) return Browser("hcaptcha", "hcaptcha", p, "no sitekey in the static HTML");
var invisible = Attr(t, "data-size") == "invisible" ? 1 : (int?)null;
return new("hcaptcha", "hcaptcha", Args(("sitekey", key), ("url", p.Url), ("invisible", invisible)));
}3. Adım: hCaptcha ve CapSkip’in çözemedikleri
Bu üç denetim yukarıdaki ikinci bloğun sonunda yer alır, bu yüzden bu adımda yapıştıracak yeni bir şey yok. Doğru yanıtın hayır olduğu sayfalarla başlayın. CapSkip FunCaptcha veya Arkose’u çözmez; GeeTest v4 de v3 için olan GeeTest metodunun dışındadır. Algılayıcı bunları atlasaydı, Arkose kutusu olan bir sayfa sonraki denetime düşer ve olmadığı bir şey olarak dönebilirdi. Bu yüzden bu denetim ilk çalışır ve metodu olmayan bir sonuç döndürür.
Aynı blok, Cloudflare’in tam sayfa denetimini, yani “Just a moment” ara sayfasını da ele alır. Bu sayfa bir Turnstile widget’ı değildir. turnstile metodu ondan data ve pagedata olmak üzere iki değer ister ve düz bir istek bunları içermez. Algılayıcı sayfanın satır içi challenge seçeneklerini arar ve bir tarayıcı gerektiği yanıtını verir. cf_clearance kılavuzu bir challenge sayfası geçildikten sonra olanları ele alır.
Sırada, reCAPTCHA’dan önce hCaptcha var. CapSkip, widget ister onay kutusu, ister invisible, ister Enterprise olsun hCaptcha’yı çözer; ancak reCAPTCHA’dan hCaptcha’ya geçen bir site widget’ında çoğu zaman g-recaptcha sınıfını korur ve ikisini yalnızca hCaptcha script’i ayırt eder. Denetim sitekey’i widget’tan ya da bir hcaptcha.render çağrısından okur ve widget’ta data-size invisible olarak ayarlıysa invisible ekler. Widget’a hcaptcha.setData üzerinden bir rqdata değeri veren bir Enterprise sayfası ise bunun yerine tarayıcı gerektiren bir sonuç alır, çünkü bu değer tek bir challenge için verilir ve kısa sürede geçerliliğini yitirir.
4. Adım: reCAPTCHA, doğru sırayla
reCAPTCHA en çok dikkat ister, çünkü birkaç CapSkip varyantı iki script adını paylaşır. Onları ayrı tutan şey soruların sırasıdır. Önce script adı: enterprise.js Enterprise bayrağı demektir, api.js değildir. Sonra script adresindeki render parametresi. render= sonrasındaki bir anahtar v3 demektir, ama render= sonrasındaki explicit kelimesi widget’ını koddan render eden bir v2 sayfası demektir ve asla anahtar olarak kullanılmamalıdır. Sonra işaretleme. g-recaptcha sınıfına ve data-action özniteliğine sahip bir düğme Google’ın v3 düğme bağlamasıdır. data-action olmayan bir düğme ya da data-size değeri invisible olan herhangi bir öğe, invisible v2 widget’ıdır. Sınıfa sahip diğer her şey onay kutusudur.
static Info? Recaptcha(Page p) // Enterprise flag, then v3 (render=KEY), then v2
{
var m = Regex.Match(p.Srcs, """/recaptcha/(api|enterprise)\.js(\?\S*)?""");
if (!m.Success) return null;
var render = Grab("""[?&]render=([^&\s]+)""", m.Groups[2].Value); // a v3 key, or "explicit" (a v2 page)
var t = Tag("g-recaptcha", p.H);
var o = Opts("""\.render""", p.Js);
var button = (t ?? "").StartsWith("<button", StringComparison.OrdinalIgnoreCase);
var action = Attr(t, "data-action");
var v3 = (render != null && render != "explicit") || (button && !string.IsNullOrEmpty(action));
var key = Or(render == "explicit" ? null : render, Attr(t, "data-sitekey"), Grab(SiteKeyJs, o));
if (key == null) return Browser("recaptcha", "recaptcha", p, "no sitekey in the static HTML (set by JavaScript)");
var small = Attr(t, "data-size") == "invisible" || Grab("""size['"]?\s*:\s*['"](invisible)""", o) != null;
var invisible = !v3 && (button || small); // a <button> is invisible v2 unless it has data-action (v3)
action = Or(action, Grab("""execute\(\s*[^,)]+,\s*\{\s*['"]?action['"]?\s*:\s*['"]([^'"]+)""", p.Js));
var ent = m.Groups[1].Value == "enterprise";
var a = Args(("sitekey", key), ("url", p.Url), ("enterprise", ent ? 1 : null), ("version", v3 ? "v3" : null),
("action", v3 ? action : null), ("invisible", invisible ? 1 : null));
return new("recaptcha_" + (v3 ? "v3" : invisible ? "v2_invisible" : "v2") + (ent ? "_enterprise" : ""), "recaptcha", a);
}Her sonuç tek bir RecaptchaAsync çağrısına eşlenir. Bir onay kutusu; bir sitekey, sayfa URL’si ve isteğe bağlı bir seçenek sözlüğüyle RecaptchaAsync çağrısıdır. Invisible "invisible" ekler, Enterprise "enterprise" ekler, v3 ise "version" ile action değerini ekler. Action adı data-action özniteliğinden ya da sayfanın execute çağrısından gelir; ikisi de HTML’de yoksa girdi dışarıda bırakılır ve CapSkip varsayılan verify değerini kullanır. reCAPTCHA çözücü sayfası ele aldığı her varyantı listeler.
5. Adım: Turnstile ve GeeTest
Bir Turnstile widget’ı sayfada iki şeye ihtiyaç duyar: Cloudflare’in api.js dosyası ve bir sitekey. Anahtar, cf-turnstile sınıfına sahip bir div üzerindeki data-sitekey özniteliğinde ya da bir turnstile.render çağrısında bulunur. Denetim script’i şart koşar, bu yüzden alakasız bir sayfadaki başıboş bir sınıf adı onu tetikleyemez. Ayrıca data-action ve data-cdata varsa onları da okur ve action ile data olarak iletir. Turnstile çözücü sayfası seçenekleri listeler.
GeeTest v3 size iki değer verir ve biri tuzaktır. gt değeri site için sabittir ve sayfada bulunabilir. challenge değeri tek kullanımlıktır ve siteler onu çalışma zamanında bir register endpoint’inden alır. Sayfa ikisini de taşıyorsa algılayıcı ikisini döndürür. Yalnızca initGeetest çağrısını taşıyorsa HTML’in yetmediği yanıtını verir, çünkü kayıtlı bir sayfadan kopyalanan bir challenge zaten geçersiz olur.
static Info? Turnstile(Page p) // widget only: div.cf-turnstile or turnstile.render
{
if (!p.Srcs.Contains("challenges.cloudflare.com/turnstile/v0/api.js")) return null;
var t = Tag("cf-turnstile", p.H);
var o = Opts("""turnstile\.render""", p.Js);
var key = Or(Attr(t, "data-sitekey"), Grab(SiteKeyJs, o));
if (key == null) return Browser("turnstile", "turnstile", p, "no sitekey in the static HTML");
var action = Or(Attr(t, "data-action"), Grab("""\baction['"]?\s*:\s*['"]([^'"]+)""", o));
var cdata = Or(Attr(t, "data-cdata"), Grab("""\bcData['"]?\s*:\s*['"]([^'"]+)""", o));
return new("turnstile", "turnstile", Args(("sitekey", key), ("url", p.Url), ("action", action), ("data", cdata)));
}
static Info? Geetest(Page p) // v3 only: gt is static, challenge comes from the register endpoint
{
if (!Regex.IsMatch(p.Js, """initGeetest\s*\(""") && !Regex.IsMatch(p.Srcs, """/gt(\.[\d.]+)?\.js""")) return null;
var gt = Grab("""\bgt['"]?\s*:\s*['"]([0-9a-f]{32})""", p.Js);
var ch = Grab("""\bchallenge['"]?\s*:\s*['"]([0-9a-f]{32,34})""", p.Js);
return gt == null || ch == null ? Browser("geetest", "geetest", p, "gt and challenge come from the register endpoint")
: new("geetest", "geetest", Args(("gt", gt), ("challenge", ch), ("url", p.Url)));
}6. Adım: ALTCHA ve Capy
ALTCHA öznitelik adlarını sürümler arasında değiştirdi. 1. ve 2. sürümler challengeurl, satır içi bir challenge için ise challengejson kullanır. 3. sürüm ve sonrası, bir URL’yi ya da challenge verisini alabilen challenge adlı tek bir öznitelik kullanır. Denetim üçünü de okur ve göreli bir URL’yi sayfa adresine ekler. C# içinde seçenek anahtarları challenge_url ve challenge_json olur.
Capy bu denetim için bir widget div’i gerektirmez. Anahtarı PUZZLE_ ile başlar ve widget script adresinin k parametresinde bulunur; https://jp.api.capy.me dışındaki bir host api_server olarak iletilir.
static Info? Altcha(Page p) // challengeurl / challengejson (v1, v2) or challenge (v3+)
{
var t = Grab("""(<altcha-widget\b[^>]*>)""", p.H);
if (t == null) return null;
var c = Attr(t, "challenge");
var json = Or(Attr(t, "challengejson"), (c ?? "").TrimStart().StartsWith('{') ? c : null);
var url = Or(Attr(t, "challengeurl"), json != null ? null : c);
return json != null ? new("altcha", "altcha", Args(("url", p.Url), ("challenge_json", json)))
: url != null ? new("altcha", "altcha", Args(("url", p.Url), ("challenge_url", Abs(p.Url, url))))
: Browser("altcha", "altcha", p, "no challenge attribute");
}
static Info? Capy(Page p) // the widget script is .../puzzle/get_js/?k=PUZZLE_...
{
var m = Regex.Match(p.Srcs, """((?:https?:)?//[^\s"']+?)/puzzle/get_js/\S*?(PUZZLE_[\w-]+)""");
if (!m.Success) return null;
var full = m.Groups[1].Value.StartsWith("http") ? m.Groups[1].Value : "https:" + m.Groups[1].Value;
var host = full == "https://jp.api.capy.me" ? null : full;
return new("capy", "capy", Args(("sitekey", m.Groups[2].Value), ("url", p.Url), ("api_server", host)));
}7. Adım: CaptchaFox ve Friendly Captcha
CaptchaFox widget’ı sayfalara iki yerden ulaşır. Düz olan cdn.captchafox.com adresinden yüklenir. Diğeri, bir barındırma platformu tarafından ortak bir CDN yolunun altında paketlenir; bu yüzden algılayıcı yalnızca yola değil, bu yoldaki CaptchaFox paket adına bakar, çünkü yolu başka widget’lar da kullanır.
Friendly Captcha en sinsi olanıdır. 1. ve 2. sürüm aynı div’i ve aynı sitekey ad alanını kullanır ama protokolleri farklıdır; CapSkip yanlış olan için düzgün biçimli bir token döndürür ve site onu reddeder. Script’ler ikisini ayırır: 2. sürüm @friendlycaptcha/sdk paketini, 1. sürüm friendly-challenge yükler. Sayfa hiçbir işaret vermediğinde algılayıcı tahmin yürütmeyi reddeder ve HTML’in yetmediğini söyler.
const string Mam = "mampkg/@mamdev/core.frontend.libs.captchafox";
static Info? CaptchaFox(Page p) // plain build from cdn.captchafox.com, or the bundled build from s.uicdn.com
{
var t = Tag("captchafox", p.H);
var key = Grab("""captchafox[^"']*/api\.js\?key=([^"'&]+)""", p.Srcs);
if (t == null && key == null) return null;
var mam = p.Srcs.Contains(Mam) ? "https://s.uicdn.com/" + Mam + "/" : null;
var sitekey = Or(Attr(t, "data-sitekey"), key);
if (sitekey == null) return Browser("captchafox", "captchafox", p, "no sitekey in the static HTML");
return new("captchafox", "captchafox", Args(("sitekey", sitekey), ("url", p.Url), ("api_server", mam)));
}
static Info? Friendly(Page p) // v2 loads @friendlycaptcha/sdk (site.min.js), v1 loads friendly-challenge
{
var t = Tag("frc-captcha", p.H);
if (t == null) return null;
var e2 = Attr(t, "data-api-endpoint");
var e1 = Attr(t, "data-puzzle-endpoint");
if (Attr(t, "data-sitekey") == null) return Browser("friendly_captcha", "friendly_captcha", p, "no sitekey in the static HTML");
var v2 = e2 != null || Regex.IsMatch(p.Srcs, """friendlycaptcha/sdk|site(\.compat)?\.min\.js""");
var v1 = e1 != null || Regex.IsMatch(p.Srcs, """friendly-challenge|widget(\.module)?\.min\.js""");
if (v2 == v1) return Browser("friendly_captcha", "friendly_captcha", p, "cannot tell v1 from v2 in the HTML");
return new(v2 ? "friendly_captcha_v2" : "friendly_captcha_v1", "friendly_captcha", Args(("sitekey", Attr(t, "data-sitekey")),
("url", p.Url), ("version", v2 ? "v2" : "v1"), ("api_server", Or(e2, e1))));
}Bunların her birinin, metodun aldığı seçenekleri içeren kendi çözücü sayfası vardır:
- Capy Puzzle çözücü sayfası: bir Capy yanıtının döndürdüğü üç değer.
- CaptchaFox çözücü sayfası: CaptchaFox metodunun aldığı seçenekler.
- Friendly Captcha çözücü sayfası: Friendly Captcha metodu.
8. Adım: görsel CAPTCHA, bir tahmin ve denetimler
Görsel bir CAPTCHA’nın bulunacak bir sağlayıcı script’i yoktur, bu yüzden bu denetim bir tahmindir ve yalnızca başka hiçbir şey eşleşmediğinde çalışır. Denetim, işaretlemesinde captcha geçen bir görsel ve sayfanın başka bir yerinde işaretlemesinde captcha ya da security code geçen bir metin kutusu arar. İşaretlemenin bu sözcükleri kullanmadığı her durumda kaçırdı ve çizilmiş bir canvas ona görünmezdir. Bir eşleşmeyi kesin değil, olası sayın.
Aynı blok denetimleri sırayla listeler ve Detect’i sunar. Sıra mantığın kendisidir: önce desteklenmeyen sağlayıcılar, sonra Cloudflare sayfası, sonra hCaptcha, sonra diğer her aile ve görsel tahmini yalnızca liste hiçbir şey bulmadığında. Detect metodu ilk sonucu döndürür ve sayfa birden fazla aile taşıdığında, örneğin bir v3 script’i artı bir Turnstile kutusu, bir Also listesi ekler. Liste aileleri adlandırır, başka anahtarlı ikinci bir reCAPTCHA widget’ını değil.
static Info? Image(Page p) // a guess: an <img> that says captcha, next to a text box that says captcha
{
const string box = """<input\b(?![^>]*type\s*=\s*["']?(?:hidden|checkbox|submit))[^>]*(?:captcha|security.?code)""";
if (!Regex.IsMatch(p.H, box, RegexOptions.IgnoreCase)) return null;
foreach (Match m in Regex.Matches(p.H, """<img\b[^>]*>""", RegexOptions.IgnoreCase))
if (Regex.IsMatch(m.Value, "captcha", RegexOptions.IgnoreCase) && Attr(m.Value, "src") is { Length: > 0 } src)
return new("image", "normal", Args(("image", Abs(p.Url, src))));
return null;
}
public static Info? Detect(string html, string pageUrl) // every check runs; the first hit wins, the rest are "also"
{
var p = Parse(html, pageUrl);
var found = new[] { Unsup(p), Cf(p), Hcaptcha(p), Recaptcha(p), Turnstile(p), Geetest(p), Altcha(p), Capy(p), CaptchaFox(p), Friendly(p) }
.Where(r => r != null).ToList();
return found.Count == 0 ? Image(p) : found.Count == 1 ? found[0]
: found[0]! with { Also = found.Skip(1).Select(r => r!.Type).ToArray() };
}9. Adım: çözün
Son blok sonucu doğru CapSkip metoduna yönlendirir. unsupported olan ya da bir tarayıcı gerektiren her şeyi bir InvalidOperationException ile reddeder. CapSkip metotları ana değerlerini konuma göre, gerisini bir seçenek sözlüğü olarak alır; bu yüzden blok konumsal değerleri argümanlardan çeker ve geri kalan her şeyi ham API adlarıyla iletir. Bir görsel kendi HttpClient’ınızla indirilir, böylece çerez deposu isteğe eşlik eder; çünkü SDK’nın bir URL’yi kendi indirmesi hiçbir çerez göndermez. O indirmedeki kötü bir durum bir HttpRequestException fırlatır.
SDK’da henüz hCaptcha çağrısı yok, bu yüzden bir hCaptcha sonucu bunun yerine SolveHcaptcha’ya gider. Bu metot sitekey’i ve sayfa adresini method=hcaptcha ile in.php’ye gönderir, ardından token hazır olana kadar res.php’yi 5 saniyede bir yoklar. Bu akışı API belgelerinin hCaptcha bölümü anlatır. Adresi ve API key’i, verdiğiniz CapSkipClient’tan okur; bu yüzden ayrıca yapılandırılacak bir şey yoktur. Reddedilen bir iş, hata koduyla birlikte SDK’nın kendi ApiException’ını fırlatır; böylece aşağıdaki programdaki CapSkipError catch bloğu bunu yakalar. Sonuç token’ı Code içinde, token’ın alındığı User-Agent’ı ise UserAgent içinde taşır.
public static async Task<SolveResult> Solve(Info i, CapSkipClient s, HttpClient http)
{
if (i.Unsupported || i.NeedsBrowser != null) throw new InvalidOperationException(i.NeedsBrowser ?? i.Type + " is not supported by CapSkip");
if (i.Method == "hcaptcha") return await SolveHcaptcha(i.Args, s, http);
string K(string k) => (string)i.Args[k]!; // the positional arguments
var o = i.Args.Where(kv => kv.Key is not ("sitekey" or "url" or "gt" or "challenge" or "image"))
.ToDictionary(kv => kv.Key, kv => kv.Value); // everything else is an options entry
return i.Method switch
{
"recaptcha" => await s.RecaptchaAsync(K("sitekey"), K("url"), o),
"turnstile" => await s.TurnstileAsync(K("sitekey"), K("url"), o),
"geetest" => await s.GeetestAsync(K("gt"), K("challenge"), K("url"), o),
"altcha" => await s.AltchaAsync(K("url"), o),
"capy" => await s.CapyAsync(K("sitekey"), K("url"), o),
"captchafox" => await s.CaptchaFoxAsync(K("sitekey"), K("url"), o),
"friendly_captcha" => await s.FriendlyCaptchaAsync(K("sitekey"), K("url"), o),
"normal" => await s.NormalAsync(await ImageData(K("image"), http)), // fetched with YOUR cookies
_ => throw new InvalidOperationException("unknown method " + i.Method),
};
}
static async Task<string> ImageData(string src, HttpClient http) => src.StartsWith("data:") ? src
: "data:image/png;base64," + Convert.ToBase64String(await http.GetByteArrayAsync(src));
static async Task<SolveResult> SolveHcaptcha(Dictionary<string, object?> a, CapSkipClient s, HttpClient http)
{ // no SDK call yet: post to in.php and poll res.php at the address the client already uses
var form = new Dictionary<string, string> { ["method"] = "hcaptcha", ["sitekey"] = (string)a["sitekey"]!,
["pageurl"] = (string)a["url"]!, ["invisible"] = a.ContainsKey("invisible") ? "1" : "0", ["json"] = "1" };
if (a.TryGetValue("data", out var d) && d != null) form["data"] = d.ToString()!;
var key = string.IsNullOrEmpty(s.ApiKey) ? "" : "&key=" + Uri.EscapeDataString(s.ApiKey);
if (key != "") form["key"] = s.ApiKey;
using var sent = await http.PostAsync(s.ApiClient.BaseUrl + "/in.php", new FormUrlEncodedContent(form));
var job = JsonDocument.Parse(await sent.Content.ReadAsStringAsync()).RootElement;
var id = job.GetProperty("request").GetString()!;
if (job.GetProperty("status").GetInt32() != 1) throw new ApiException(id);
for (var n = 0; n < 36; n++) // a solve takes 10 to 30 seconds, so give up after 3 minutes
{
await Task.Delay(5000);
var r = JsonDocument.Parse(await http.GetStringAsync($"{s.ApiClient.BaseUrl}/res.php?action=get&id={id}&json=1{key}")).RootElement;
var value = r.GetProperty("request").GetString()!;
if (value == "CAPCHA_NOT_READY") continue;
if (r.GetProperty("status").GetInt32() != 1) throw new ApiException(value);
return new SolveResult { CaptchaId = id, Code = value, UserAgent = r.TryGetProperty("useragent", out var ua) ? ua.GetString() : null };
}
throw new CapSkip.TimeoutException("hCaptcha was not solved within 3 minutes");
}
}Bir görsel çözüldükten sonra yanıtı aynı http nesnesiyle bir form gönderimi olarak geri gönderin, böylece site resmi size gösterirken ayarladığı çerezi görür. Bir Turnstile ya da hCaptcha çözümünün sonucu ayrıca bir UserAgent taşır; bu, bir challenge sayfasında token ile birlikte gitmelidir. Aşağıdaki program yalnızca Code alanını yazdırır, bu yüzden bir GeeTest ya da Capy sonucu ham JSON olarak görünür.
C# CAPTCHA algılayıcısını çalıştırın
Program.cs içeriğini bu blokla değiştirin. Sayfanızı çeker, HTTP durumunu yazdırır, algılayıcının bulduğunu yazdırır ve CapSkip’i yalnızca çağrılacak bir metot varsa ve hiçbir şey eksik değilse çağırır. SDK’nın istisnalarını tek tek adlandırmak yerine onların tabanı olan CapSkipError’ı yakalar. Bu aynı zamanda bir tuzaktan da kaçınır: implicit usings açıkken yalın bir TimeoutException, System ile CapSkip arasında belirsizdir ve derlenmez. dotnet run ile, iki tire ve https:// dahil tam sayfa adresiyle, aşağıdaki komuttaki gibi çalıştırın. 8080 portu varsayılandır; sizinki farklıysa CapSkip’in bağlantı ayarlarında gösterilen portu kullanın.
dotnet run -- "https://example.com/login"
Programın tamamı bu:
using CapSkip;
var pageUrl = args.Length > 0 ? args[0] : "https://example.com/login";
var http = new HttpClient(); // one client keeps one cookie jar for the page and the image
http.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/130.0 Safari/537.36");
Info? info;
try
{
var page = await http.GetAsync(pageUrl);
Console.WriteLine((int)page.StatusCode); // a Cloudflare challenge page arrives as a 403
var finalUrl = page.RequestMessage!.RequestUri!.ToString(); // the URL after redirects
info = Detector.Detect(await page.Content.ReadAsStringAsync(), finalUrl);
}
catch (Exception e) when (e is HttpRequestException or InvalidOperationException or TaskCanceledException or NotSupportedException)
{
Console.WriteLine("Could not fetch the page: " + e.Message);
return;
}
if (info is null) { Console.WriteLine("No CAPTCHA found."); return; }
Console.WriteLine($"{info.Type} via {info.Method ?? "nothing"}"
+ (info.Also is null ? "" : " (also " + string.Join(", ", info.Also) + ")"));
try
{
var solver = new CapSkipClient(host: "127.0.0.1", port: 8080);
var result = await Detector.Solve(info, solver, http);
Console.WriteLine(result.Code.Length > 80 ? result.Code[..80] + "..." : result.Code);
}
catch (InvalidOperationException e) { Console.WriteLine("Not solvable from this HTML: " + e.Message); }
catch (HttpRequestException e) { Console.WriteLine("Image download failed: " + e.Message); }
catch (CapSkipError e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }reCAPTCHA v2 onay kutusu olan bir sayfada ilk satır türü ve metodu, ikincisi token’ı gösterir. Mock çalıştırmasında şöyle görünüyordu:
| Sayfa | Program şunu yazdırır |
|---|---|
| reCAPTCHA v2 checkbox | 200, ardından recaptcha_v2 via recaptcha, ardından token |
| hCaptcha widget’ı | 200, ardından hcaptcha via hcaptcha, ardından token |
| Cloudflare “Just a moment” sayfası | 403, ardından turnstile_challenge via turnstile, ardından data ve pagedata değerlerinin yalnızca çalışma zamanında var olduğunu söyleyen bir mesaj |
| CAPTCHA olmayan sayfa | 200, ardından No CAPTCHA found. |
| 404 döndüren adres | 404, ardından No CAPTCHA found. |
HTML hiçbir şey söylemediğinde: tarayıcı konsolu
Bazı sayfalar widget’larını yüklendikten sonra JavaScript ile ekler. O zaman düz bir istek hiç CAPTCHA bulamaz ve algılayıcı, CAPTCHA’sı olan bir sayfa için null döndürür. İki durum bilinir ve beklenir: kodla eklenen bir Google ya da Cloudflare script’i ve kodla ayarlanan bir sitekey. Bunlar için sayfayı Chrome’da açın, F12’ye basın ve bunu Console’a yapıştırın. Chrome yapıştırmayı engellerse önce allow pasting yazıp Enter’a basın. Sitekey’leri sayfanın zaten yüklediği kaynaklardan okur, yani tarayıcının oluşturduğunu görür.
(() => {
const iframes = [...document.querySelectorAll('iframe')].map((f) => f.src);
const urls = performance.getEntriesByType('resource').map((r) => r.name).concat(iframes);
const found = new Map(), add = (o) => found.set(JSON.stringify(o), o);
for (const u of urls) {
let m;
if ((m = u.match(/recaptcha\/(?:api2|(enterprise))\/anchor\?.*?\bk=([\w-]+)/)))
add({ type: 'recaptcha', sitekey: m[2], enterprise: !!m[1], invisible: /size=invisible/.test(u) });
else if ((m = u.match(/recaptcha\/(?:api|enterprise)\.js\?.*?\brender=([\w-]+)/)) && m[1] !== 'explicit')
add({ type: 'recaptcha v3', sitekey: m[1], enterprise: /enterprise\.js/.test(u) });
else if ((m = u.match(/challenges\.cloudflare\.com\/.*turnstile\/.*\/(0x4[\w-]+|[123]x0+[A-Za-z]+)\//)))
add({ type: 'turnstile', sitekey: m[1] });
else if ((m = u.match(/hcaptcha\.com\/captcha\/.*[#&?]sitekey=([\w-]+)/))) add({ type: 'hcaptcha', sitekey: m[1] });
else if (/arkoselabs\.com\/fc\//.test(u)) add({ type: 'arkose: CapSkip cannot solve' });
}
if (window._cf_chl_opt) add({ type: 'cloudflare challenge page' });
document.querySelectorAll('altcha-widget').forEach((e) => add({ type: 'altcha', challenge: e.getAttribute('challenge') || e.getAttribute('challengeurl') }));
document.querySelectorAll('.h-captcha[data-sitekey]').forEach((e) => add({ type: 'hcaptcha', sitekey: e.dataset.sitekey }));
document.querySelectorAll('.frc-captcha, .captchafox').forEach((e) => add({ type: e.classList.contains('captchafox') ? 'captchafox' : 'frc-captcha', sitekey: e.dataset.sitekey }));
const v3 = [...found.values()].filter((o) => o.type === 'recaptcha v3').map((o) => o.sitekey); // a v3 badge also adds an invisible anchor frame
const res = [...found.values()].filter((o) => !(o.type === 'recaptcha' && v3.includes(o.sitekey)));
return res.length ? res : 'no known CAPTCHA found (GeeTest, Capy and CaptchaFox scripts are not checked)';
})();Bir reCAPTCHA v2 sayfasında, bir invisible sayfada, bir v3 sayfasında, bir Turnstile sayfasında ve hCaptcha demosunda sitekey’i ve bayrakları buldu, CAPTCHA olmayan bir sayfada ise bir mesaj yazdırdı. Üç sınır var: tarayıcı varsayılan olarak yalnızca yaklaşık 250 kaynak girdisi tutar, bu yüzden uzun süre açık kalan tek sayfalı bir uygulama erken olanları kaybedebilir ve kod parçası Friendly Captcha 1. sürümü ile 2. sürümü ayırt edemez, GeeTest, Capy veya CaptchaFox script’lerini de aramaz. Cloudflare’in tam sayfa denetiminde sitekey’i bir kaynak adresinden gösterebilir ama render çağrısına bir hook gerektiren data ve pagedata değerlerini gösteremez. Bu hook burada test edilmedi.
Algılamanın başarısız olduğu yerler
| Durum | Detect ne döndürür | Ne yapmalı |
|---|---|---|
| JavaScript ile eklenen widget script’i | null | Sayfayı bir tarayıcıda yükleyin ve konsol kod parçasını çalıştırın |
| JavaScript ile ayarlanan sitekey | tarayıcı gerektiren bir sonuç | Anahtarı canlı sayfadan okuyun |
| Cloudflare tam sayfa denetimi | tarayıcı gerektiren bir sonuç | Değerleri bir tarayıcıda okuyun, cf_clearance kılavuzuna bakın |
| HTML’de frc-captcha div’i olmayan Friendly Captcha script’i | null | Konsol kod parçasını çalıştırın ya da sayfa kaynağını okuyun |
| JavaScript ile eklenen CaptchaFox ya da Capy script’i | null | Konsol kod parçasını çalıştırın ya da sayfa kaynağını okuyun |
| Sitenin kendisinden gelen bir 403 veya 503 sayfası | Bir Cloudflare challenge sayfası değilse null | Program önce durumu yazdırır: bir engel sayfasında bulunacak CAPTCHA yoktur |
| Arkose, GeeTest v4 | unsupported | CapSkip bunları çözemez |
| rqdata ayarlayan hCaptcha Enterprise | bir tarayıcı gerektirir | Her görev için canlı sayfadan taze bir rqdata okuyun ve bunu SolveHcaptcha’ya data olarak verin |
| Script ipucu olmayan Friendly Captcha | tarayıcı gerektiren bir sonuç | Sayfanın hangi script’i yüklediğini kontrol edin |
| captcha sözcüğünü içermeyen görsel CAPTCHA | null | O site için kendi kuralınızı ekleyin |
Yaygın hatalar
| Hata | Neden | Düzeltme |
|---|---|---|
| ERROR_GOOGLEKEY | Anahtar explicit kelimesiydi ya da bir reCAPTCHA anahtarı olmayan bir anahtardı | Anahtarı render parametresinden değil, widget’tan okuyun |
| ERROR_CAPTCHA_UNSOLVABLE | Başarısız her iş böyle görünür ve kod nedeni belirtmez. Yanlış bir reCAPTCHA sürümü olası nedenlerden biridir | Önce algılanan türü kontrol edin, sonra şuna bakın: API belgeleri |
| ValidationException | Metodun almadığı bir seçenek, örneğin bir v2 çağrısında action | Algılayıcının döndürdüğü argümanları kullanın |
| NetworkException | CapSkip çalışmıyor ya da host veya port yanlış; Windows’ta yaklaşık 2 saniye sonra gelir | CapSkip’i başlatın ve bağlantı ayarlarını kontrol edin |
| Solve kaynaklı InvalidOperationException | Sonuç unsupported ya da bir tarayıcı gerektiriyor | Solve çağırmadan önce bu iki durumu ele alın |
Çözücüyü bir sunucuda çalıştırmak
Algılayıcının CapSkip’ten yalnızca iki şeye ihtiyacı vardır: bir host ve bir port. Local modunda bu 127.0.0.1 ve kendi bilgisayarınızdır. Server modunda CapSkip ağ adresinizi ya da genel bir IP’yi dinler, böylece başka bir bilgisayarda, bir VPS’te ya da barındırılan bir platformda çalışan bir algılayıcı aynı API üzerinden onu çağırabilir. CapSkipClient constructor’ında host ve portu değiştirin, koddaki başka hiçbir şey değişmez. Kararlı uzaktan erişim için statik bir genel IP önerilir. Server modu yine kendi donanımınızdır ve yine kullanım başına ücretlendirilmez. Server modunun portu her adrese açtığını unutmayın; bu yüzden ulaşmaması gereken adresler için bir Windows Firewall engelleme kuralı ekleyin. Modları nasıl değiştireceğiniz için bağlantı ayarları bölümüne bakın.
FAQ
Kod olmadan bir sitenin hangi CAPTCHA’yı kullandığını söyleyebilir miyim?
Evet. DevTools’u açın, Network sekmesine gidin, sayfayı yeniden yükleyin ve script adreslerine bakın. google.com/recaptcha ya da recaptcha.net script’i reCAPTCHA, challenges.cloudflare.com Turnstile, js.hcaptcha.com ise hCaptcha’dır. Yukarıdaki Console kod parçası aynı aramayı sizin için yapar.
Detect neden tahmin yerine tarayıcı gerektiren bir sonuç döndürür?
Çünkü yanlış bir tahmin tam bir çözümü boşa harcar ve site nedenini göstermeden reddeden bir token döndürür. Statik HTML sitekey’i, bir GeeTest challenge’ını ya da bir Friendly Captcha sürümünü içermediğinde bunu söylemek, başarısız bir işten daha çabuktur.
CapSkip hCaptcha çözer mi?
Evet. CapSkip, hCaptcha onay kutusu, invisible ve Enterprise widget’larını HTTP API’si üzerinden method=hcaptcha ile çözer. Ayrıntıları API belgeleri anlatır. .NET SDK’sında henüz hCaptcha çağrısı yok, bu yüzden yukarıdaki SolveHcaptcha işi in.php’ye kendisi gönderir. FunCaptcha, Arkose ve GeeTest v4 hâlâ desteklenmez; algılayıcı bunları unsupported olarak bildirir, böylece bir program sayfada başarısız olmak yerine onu atlayabilir.
Barındırılan bir platform algılayıcıyı benim CapSkip’imle kullanabilir mi?
Evet, Server modu ile. CapSkip’i ağ ya da genel IP’nizi dinleyecek şekilde ayarlayın, CapSkipClient constructor’ını o host ve porta yöneltin; algılayıcı ve Solve çağrısı tek bir bilgisayardaki gibi çalışır. Çözücü sizin donanımınızda kalır.
Kısa özet
C#’ta bir CAPTCHA algılayıcısı ilk çalışarak yerini hak eder: önce CAPTCHA türünü algılayın, sonra çözün. Script’i ve işaretlemeyi sabit bir sırayla okuyun, argümanları doğru metoda verin ve null, tarayıcı gerektiren bir sonucu ve unsupported değerini çökme değil, yanıt olarak ele alın. CapSkip kendi makinenizde olduğunda bir sayfayı denetlerken işleyen bir sayaç yoktur; bu yüzden captcha çözücü yeni bir siteyle birkaç dakikada bir karşılaşan bir tarama için pratiktir. .NET SDK’nın kendisi için şuraya bakın: C# CAPTCHA çözücü sayfası.
Aynı algılayıcı diğer dillerde:
- Python’da CAPTCHA türü algılama: orijinali, aynı 66 sayfayla.
- Node.js CAPTCHA algılayıcısı: ek paket olmadan aynı denetimler.
