Detektor CAPTCHA di C#: Temukan Jenisnya, Panggil CapSkip

Detektor CAPTCHA di C# membaca HTML halaman untuk menemukan skrip dan container widget, mengambil sitekey dari keduanya, lalu memetakan keluarganya ke metode CapSkip yang memecahkannya. Kotak reCAPTCHA v2, skrip v3, dan widget Turnstile memerlukan tiga panggilan berbeda dengan opsi berbeda, dan tebakan yang salah menyia-nyiakan satu pemecahan utuh dan memberi Anda token yang tidak bisa dipakai. Panduan ini membangun detektor sepanjang sekitar 200 baris tanpa paket selain CapSkip. Detektor mengembalikan jenis, metode, dan argumennya, atau menyatakan dengan jelas bahwa ia tidak bisa menentukan. CapSkip berjalan di PC Windows Anda sendiri tanpa biaya per pemecahan, jadi program yang memeriksa seratus halaman tidak menambah biaya apa pun. Detektor yang sama tersedia di Python dan Node.js, ditautkan di bagian akhir.
Apa yang Anda butuhkan
- CapSkip yang berjalan di PC Windows, dengan pemecah yang Anda perlukan diaktifkan. Lihat panduan penyiapan untuk menjalankannya pertama kali.
- SDK .NET 8 atau lebih baru, karena kode memakai raw string literal, yang memerlukan C# 11. Diuji dengan SDK 10.0.100 dan paket CapSkip 1.3.0, dengan target net8.0 dan net10.0. Menargetkan net6.0 juga berhasil setelah menambahkan LangVersion 11 ke file proyek. .NET Framework tidak dicoba.
- Sebuah terminal. Perintahnya bekerja di PowerShell.
- Halaman milik Anda atau yang boleh Anda otomatisasi, yang memiliki CAPTCHA di dalamnya.
- Sebuah alamat untuk pemecah CAPTCHA. Dalam mode Local, CapSkip menjawab di 127.0.0.1 hanya untuk perangkat itu. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga program di PC lain, VPS, atau platform hosting dapat menjangkaunya melalui API. Keduanya diatur di pengaturan koneksi.
Cara pengujiannya: detektor dijalankan pada 66 halaman tersimpan yang sama dengan versi Python dan mengembalikan hasil yang sama untuk setiap halaman. Dari jumlah itu, 59 dibuat dengan tangan (sekitar 40 mengikuti dokumentasi vendor, sisanya kasus tepi yang saya buat sendiri) dan 7 adalah snapshot halaman publik yang diambil pada 29 Oktober 2026. Ke-40 hasil yang menyebut sebuah metode dikirim melalui paket NuGet CapSkip yang asli ke mock CapSkip yang meniru pemeriksaan input aplikasi. Aplikasi yang asli tidak berjalan, jadi tidak ada angka di bawah ini yang merupakan waktu pemecahan.
Mengapa Anda mendeteksi jenis CAPTCHA sebelum memecahkannya
CapSkip punya metode berbeda untuk setiap keluarga: RecaptchaAsync, TurnstileAsync, GeetestAsync, AltchaAsync, CapyAsync, CaptchaFoxAsync, FriendlyCaptchaAsync, dan NormalAsync untuk gambar di SDK, ditambah hcaptcha di HTTP API, yang belum dibungkus oleh SDK. Di dalam reCAPTCHA, opsinya juga penting. Invisible, Enterprise, dan v3 adalah flag pada panggilan yang sama. Kirim yang salah dan kemungkinan terbaiknya adalah job yang gagal, yang terburuk adalah token yang ditolak situs tanpa petunjuk apa pun mengapa. Saya tidak bisa memeriksa mana yang dilakukan aplikasi asli untuk versi reCAPTCHA yang salah. Bagaimanapun, waktu satu pemecahan terbuang.
Kesalahan lain gagal dengan cepat dan jelas. Membaca sitekey dari halaman yang merender reCAPTCHA secara eksplisit menghasilkan kata explicit, dan CapSkip menolaknya dengan ERROR_GOOGLEKEY. Panduan ERROR_GOOGLEKEY dan ERROR_PAGEURL membahas error itu dari sisi sebaliknya. Agar dapat mendeteksi jenis CAPTCHA dengan andal, detektor harus memeriksa sinyal dalam urutan tetap, dan urutan itu menghindari kedua jenis kesalahan. Detektor juga hal pertama yang perlu dibuat sebelum program yang menemui lebih dari satu situs. Jika Anda ingin tahu latar belakang tiap jenis, lihat apa itu CAPTCHA untuk penjelasan tiap keluarga tanpa kode.
Langkah 1: buat proyek
Buat proyek console dan tambahkan paket CapSkip. Detektor itu sendiri hanya memakai yang disertakan .NET: regular expression dan HttpClient.
dotnet new console -o detect-demo cd detect-demo dotnet add package CapSkip
Detektor CAPTCHA di C# bekerja pada teks, jadi hanya memerlukan dua input: HTML halaman dan alamatnya. Permintaan biasa menampilkan apa yang dikirim server sebelum JavaScript apa pun dijalankan. Halaman demo reCAPTCHA dari Google, demo Turnstile dari Cloudflare, dan demo hCaptcha semuanya memuat widget-nya dalam HTML itu. Halaman yang menambahkan widget dengan JavaScript tidak demikian, dan bagian “Saat HTML tidak menunjukkan apa-apa” membahas kasus itu. Satu HttpClient menyimpan satu cookie jar, yang penting untuk langkah CAPTCHA gambar: program di bawah memakai client yang sama untuk halaman dan untuk gambarnya.
Langkah 2: tulis fungsi bantu
Simpan semuanya dari sini sampai Langkah 9 dalam satu file bernama Detector.cs, blok demi blok. File ini tidak akan dikompilasi sampai Langkah 9 menutup kelasnya, dan bergantung pada implicit usings yang diaktifkan oleh dotnet new console. Blok pertama berisi usings, dua record kecil, dan fungsi bantu pertama. Setiap pembaca mengembalikan null jika tidak ada yang cocok, sehingga atribut yang hilang tidak pernah melempar exception.
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);Metode Grab mengembalikan capture group pertama yang cocok. Metode Tag menemukan tag pembuka berdasarkan class CSS dan menolak class yang hanya memuat namanya di dalam nama yang lebih panjang, sehingga class bernama cf-turnstile-response bukan widget Turnstile. Metode Attr membaca atribut yang ditulis dengan kedua gaya tanda kutip dan mendekode entitas HTML, yang penting untuk challenge yang disimpan di dalam atribut. Atribut tanpa tanda kutip tidak dibaca.
Blok kedua mengubah sebuah halaman menjadi sesuatu yang dapat dipakai bersama oleh pemeriksaan. Blok ini menghapus komentar dan blok style, sehingga aturan CSS yang menyebut class widget tidak bisa cocok, dan mengumpulkan alamat skrip serta JavaScript inline secara terpisah. Fungsi bantu terakhir membangun jawaban untuk halaman yang HTML-nya tidak cukup. Field NeedsBrowser-nya berisi alasannya, misalnya sitekey ditetapkan oleh JavaScript, dan tulisan ini menyebutnya hasil yang membutuhkan browser.
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)));
}Langkah 3: hCaptcha, dan yang tidak bisa dipecahkan CapSkip
Ketiga pemeriksaan ini berada di akhir blok kedua di atas, jadi tidak ada yang baru untuk ditempel pada langkah ini. Mulailah dengan halaman yang jawaban benarnya adalah tidak. CapSkip tidak memecahkan FunCaptcha atau Arkose, dan GeeTest v4 berada di luar metode GeeTest, yang ditujukan untuk v3. Jika detektor melewati semua itu, halaman dengan kotak Arkose akan jatuh ke pemeriksaan berikutnya dan bisa dikembalikan sebagai sesuatu yang bukan dirinya. Karena itu pemeriksaan ini dijalankan pertama dan mengembalikan hasil tanpa metode.
Blok yang sama menangani pemeriksaan halaman penuh Cloudflare, interstisial “Just a moment”. Halaman itu bukan widget Turnstile. Metode turnstile membutuhkan dua nilai darinya, data dan pagedata, dan permintaan biasa tidak memilikinya. Detektor mencari opsi challenge inline pada halaman dan menjawab bahwa browser diperlukan. Panduan cf_clearance membahas apa yang terjadi setelah halaman challenge berhasil dilewati.
hCaptcha diperiksa berikutnya, sebelum reCAPTCHA. CapSkip memecahkannya, baik widget-nya berupa checkbox, invisible, maupun Enterprise, tetapi situs yang pindah dari reCAPTCHA ke hCaptcha sering mempertahankan class g-recaptcha pada widget-nya, dan hanya skrip hCaptcha yang membedakan keduanya. Pemeriksaan ini membaca sitekey dari widget atau dari panggilan hcaptcha.render, dan menambahkan invisible bila widget memiliki data-size yang diatur ke invisible. Halaman Enterprise yang memberikan nilai rqdata ke widget lewat hcaptcha.setData justru mendapat hasil butuh browser, karena nilai itu diterbitkan untuk satu challenge dan cepat kedaluwarsa.
Langkah 4: reCAPTCHA, dalam urutan yang benar
reCAPTCHA memerlukan kehati-hatian paling besar karena beberapa varian CapSkip berbagi dua nama skrip. Urutan pertanyaan itulah yang memisahkannya. Pertama nama skripnya: enterprise.js berarti flag Enterprise, api.js tidak. Lalu parameter render pada alamat skrip. Kunci setelah render= berarti v3, tetapi kata explicit setelah render= berarti halaman v2 yang merender widget-nya dari kode, dan kata itu tidak boleh dipakai sebagai kunci. Lalu markup. Tombol dengan class g-recaptcha dan atribut data-action adalah binding tombol v3 dari Google. Tombol tanpa data-action, atau elemen apa pun dengan data-size diatur ke invisible, adalah widget v2 invisible. Selain itu, apa pun dengan class tersebut adalah checkbox.
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);
}Setiap hasil dipetakan ke satu panggilan RecaptchaAsync. Checkbox adalah RecaptchaAsync dengan sitekey, URL halaman, dan options dictionary opsional. Invisible menambahkan "invisible", Enterprise menambahkan "enterprise", dan v3 menambahkan "version" serta action. Nama action berasal dari atribut data-action atau dari panggilan execute pada halaman; jika keduanya tidak ada di HTML, entri itu dihilangkan dan CapSkip memakai nilai defaultnya, yaitu verify. Lihat halaman pemecah reCAPTCHA untuk setiap varian yang ditanganinya.
Langkah 5: Turnstile dan GeeTest
Widget Turnstile memerlukan dua hal pada halaman: api.js dari Cloudflare dan sebuah sitekey. Kuncinya berada di atribut data-sitekey pada sebuah div dengan class cf-turnstile, atau di panggilan turnstile.render. Pemeriksaan ini mensyaratkan skripnya, sehingga nama class nyasar di halaman yang tidak terkait tidak bisa memicunya. Pemeriksaan ini juga membaca data-action dan data-cdata bila ada, lalu meneruskannya sebagai action dan data. Halaman pemecah Turnstile mencantumkan opsinya.
GeeTest v3 memberi Anda dua nilai, dan salah satunya adalah jebakan. Nilai gt tetap untuk situs dan bisa berada di halaman. Nilai challenge hanya berlaku sekali, dan situs mengambilnya dari endpoint register saat dijalankan. Jika halaman memuat keduanya, detektor mengembalikannya. Jika halaman hanya memuat panggilan initGeetest, detektor menjawab bahwa HTML tidak cukup, karena challenge yang disalin dari halaman tersimpan sudah mati.
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)));
}Langkah 6: ALTCHA dan Capy
ALTCHA mengubah nama atributnya antargenerasi. Versi 1 dan 2 memakai challengeurl, atau challengejson untuk challenge inline. Versi 3 dan seterusnya memakai satu atribut bernama challenge, yang menerima URL atau data challenge. Pemeriksaan ini membaca ketiganya dan menggabungkan URL relatif dengan alamat halaman. Di C# kunci opsinya adalah challenge_url dan challenge_json.
Capy tidak memerlukan div widget untuk pemeriksaan ini. Kuncinya diawali PUZZLE_ dan berada di parameter k pada alamat skrip widget, dan host selain https://jp.api.capy.me diteruskan sebagai api_server.
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)));
}Langkah 7: CaptchaFox dan Friendly Captcha
Widget CaptchaFox sampai ke halaman dari dua tempat. Yang biasa dimuat dari cdn.captchafox.com. Yang lain dibundel oleh platform hosting di bawah path CDN bersama, sehingga detektor mencari nama paket CaptchaFox di path itu dan bukan path-nya saja, yang juga dipakai widget lain.
Friendly Captcha adalah yang paling rumit. Versi 1 dan versi 2 memakai div yang sama dan namespace sitekey yang sama, tetapi protokolnya berbeda, dan CapSkip mengembalikan token yang bentuknya benar untuk yang salah, lalu situs menolaknya. Skripnya membedakan keduanya: versi 2 memuat paket @friendlycaptcha/sdk, versi 1 memuat friendly-challenge. Bila halaman tidak memberi petunjuk, detektor menolak menebak dan menjawab bahwa HTML tidak cukup.
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))));
}Masing-masing punya halaman pemecahnya sendiri dengan opsi yang diterima metodenya:
- Halaman pemecah Capy Puzzle: tiga nilai yang dikembalikan jawaban Capy.
- Halaman pemecah CaptchaFox: opsi yang diterima metode CaptchaFox.
- Halaman pemecah Friendly Captcha: metode Friendly Captcha.
Langkah 8: CAPTCHA gambar, sebuah tebakan, dan pemeriksaan
CAPTCHA gambar tidak memiliki skrip vendor untuk ditemukan, jadi pemeriksaan ini hanya tebakan dan dijalankan hanya bila tidak ada yang lain cocok. Pemeriksaan ini mencari gambar yang tag-nya menyebut captcha, dan kotak teks di tempat lain pada halaman yang tag-nya menyebut captcha atau security code. Pemeriksaan ini meleset di setiap kasus ketika markup tidak memakai kata-kata itu, dan canvas yang digambar tidak terlihat olehnya. Anggap sebuah hit sebagai kemungkinan, bukan kepastian.
Blok yang sama mendaftar pemeriksaan secara berurutan dan mengekspos Detect. Urutan itulah logikanya: vendor unsupported dulu, lalu halaman Cloudflare, lalu hCaptcha, lalu setiap keluarga lainnya, dan tebakan gambar hanya bila daftar tidak menemukan apa-apa. Metode Detect mengembalikan hasil pertama dan menambahkan daftar Also bila halaman memuat lebih dari satu keluarga, misalnya skrip v3 ditambah kotak Turnstile. Daftar itu menyebut keluarga, bukan widget reCAPTCHA kedua dengan kunci lain.
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() };
}Langkah 9: solve
Blok terakhir meneruskan hasil ke metode CapSkip yang tepat. Blok ini menolak apa pun yang unsupported atau ditandai butuh browser dengan InvalidOperationException. Metode CapSkip menerima nilai utamanya berdasarkan posisi dan sisanya sebagai options dictionary, jadi blok menarik nilai posisional dari argumen dan meneruskan sisanya dengan nama API mentah. Gambar diunduh dengan HttpClient Anda sendiri, sehingga cookie jar-nya ikut dalam permintaan, karena unduhan URL oleh SDK sendiri tidak mengirim cookie. Status buruk pada unduhan itu melempar HttpRequestException.
SDK belum punya panggilan hCaptcha, jadi hasil hCaptcha diteruskan ke SolveHcaptcha. Metode ini mengirim sitekey dan alamat halaman ke in.php dengan method=hcaptcha, lalu memeriksa res.php setiap 5 detik sampai token siap, sebagaimana bagian hCaptcha dalam dokumentasi API menjelaskannya. Metode ini membaca alamat dan API key dari CapSkipClient yang Anda berikan, jadi tidak ada konfigurasi tambahan. Job yang ditolak melempar ApiException milik SDK itu sendiri beserta kode error-nya, sehingga catch untuk CapSkipError pada program di bawah menanganinya. Hasilnya membawa token di Code dan User-Agent yang dipakai untuk memperolehnya di UserAgent.
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");
}
}Setelah gambar dipecahkan, kirim jawabannya kembali dengan objek http yang sama, sebagai post formulir, agar situs melihat cookie yang diaturnya saat menampilkan gambar kepada Anda. Hasil pemecahan Turnstile atau hCaptcha juga membawa UserAgent, yang harus ikut bersama token pada halaman challenge. Program di bawah hanya mencetak field Code, jadi hasil GeeTest atau Capy tampil sebagai JSON mentah.
Jalankan detektor CAPTCHA di C#
Ganti isi Program.cs dengan blok ini. Program mengambil halaman Anda, mencetak status HTTP, mencetak apa yang ditemukan detektor, dan hanya memanggil CapSkip bila ada metode untuk dipanggil dan tidak ada yang kurang. Program menangkap CapSkipError, basis dari exception SDK, alih-alih menyebutnya satu per satu. Itu juga menghindari jebakan: dengan implicit usings aktif, TimeoutException polos ambigu antara System dan CapSkip dan tidak dikompilasi. Jalankan dengan dotnet run, dua tanda hubung, dan alamat halaman lengkap, termasuk https://, seperti pada perintah di bawah. Port 8080 adalah default; pakai port yang ditampilkan di pengaturan koneksi CapSkip bila port Anda berbeda.
dotnet run -- "https://example.com/login"
Ini seluruh programnya:
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}"); }Pada halaman dengan checkbox reCAPTCHA v2, baris pertama menampilkan jenis dan metode, dan baris kedua menampilkan token. Pada eksekusi mock, hasilnya seperti ini:
| Halaman | Program mencetak |
|---|---|
| reCAPTCHA v2 checkbox | 200, lalu recaptcha_v2 via recaptcha, lalu token |
| Widget hCaptcha | 200, lalu hcaptcha melalui hcaptcha, lalu token |
| Halaman Cloudflare “Just a moment” | 403, lalu turnstile_challenge via turnstile, lalu pesan bahwa data dan pagedata hanya ada saat dijalankan |
| Halaman tanpa CAPTCHA | 200, lalu No CAPTCHA found. |
| Alamat yang mengembalikan 404 | 404, lalu No CAPTCHA found. |
Saat HTML tidak menunjukkan apa-apa: konsol browser
Beberapa halaman menambahkan widget-nya dengan JavaScript setelah dimuat. Permintaan biasa lalu tidak menemukan CAPTCHA sama sekali, dan detektor mengembalikan null untuk halaman yang memilikinya. Dua kasus diketahui dan memang diharapkan: skrip Google atau Cloudflare yang ditambahkan lewat kode, dan sitekey yang ditetapkan lewat kode. Untuk kasus itu, buka halaman di Chrome, tekan F12, dan tempel ini ke Console. Jika Chrome memblokir penempelan, ketik allow pasting lalu tekan Enter lebih dulu. Snippet ini membaca sitekey dari resource yang sudah dimuat halaman, sehingga ia melihat apa yang dibangun browser.
(() => {
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)';
})();Snippet itu menemukan sitekey dan flag pada halaman reCAPTCHA v2, halaman invisible, halaman v3, halaman Turnstile, dan demo hCaptcha, dan mencetak pesan pada halaman tanpa CAPTCHA. Tiga batasan: browser hanya menyimpan sekitar 250 entri resource secara default, jadi single page app yang berjalan lama bisa kehilangan entri awal, dan snippet tidak bisa membedakan Friendly Captcha versi 1 dari versi 2, serta tidak mencari skrip GeeTest, Capy, atau CaptchaFox. Pada pemeriksaan halaman penuh Cloudflare, snippet bisa menampilkan sitekey dari alamat resource, tetapi tidak nilai data dan pagedata, yang memerlukan hook pada panggilan render. Hook itu tidak diuji di sini.
Tempat deteksi gagal
| Situasi | Yang dikembalikan Detect | Apa yang harus dilakukan |
|---|---|---|
| Skrip widget ditambahkan oleh JavaScript | null | Muat halaman di browser dan jalankan snippet console |
| Sitekey ditetapkan oleh JavaScript | hasil yang membutuhkan browser | Baca kunci dari halaman yang sedang berjalan |
| Pemeriksaan halaman penuh Cloudflare | hasil yang membutuhkan browser | Baca nilainya di browser, lihat panduan cf_clearance |
| Skrip Friendly Captcha tanpa div frc-captcha di HTML | null | Jalankan snippet console atau baca source halaman |
| Skrip CaptchaFox atau Capy yang ditambahkan oleh JavaScript | null | Jalankan snippet console atau baca source halaman |
| Halaman 403 atau 503 dari situs itu sendiri | null, kecuali itu challenge Cloudflare | Program mencetak status lebih dulu: halaman blokir tidak memuat CAPTCHA untuk ditemukan |
| Arkose, GeeTest v4 | unsupported | CapSkip tidak bisa memecahkan ini |
| hCaptcha Enterprise yang mengatur rqdata | butuh browser | Baca rqdata baru dari halaman live untuk setiap tugas dan berikan ke SolveHcaptcha sebagai data |
| Friendly Captcha tanpa petunjuk skrip | hasil yang membutuhkan browser | Periksa skrip mana yang dimuat halaman |
| CAPTCHA gambar tanpa kata captcha | null | Tambahkan aturan Anda sendiri untuk situs itu |
Kesalahan umum
| Kesalahan | Penyebab | Perbaiki |
|---|---|---|
| ERROR_GOOGLEKEY | Kuncinya adalah kata explicit, atau kunci yang bukan kunci reCAPTCHA | Baca kunci dari widget, bukan dari parameter render |
| ERROR_CAPTCHA_UNSOLVABLE | Setiap job yang gagal terbaca seperti ini dan kodenya tidak menyebut penyebabnya. Versi reCAPTCHA yang salah adalah salah satu kemungkinan penyebab | Periksa dulu jenis yang terdeteksi, lalu lihat Dokumentasi API |
| ValidationException | Opsi yang tidak diterima metode, seperti action pada panggilan v2 | Gunakan argumen yang dikembalikan detektor |
| NetworkException | CapSkip tidak berjalan, atau host atau port salah; di Windows error muncul setelah sekitar 2 detik | Jalankan CapSkip dan periksa pengaturan koneksi |
| InvalidOperationException dari Solve | Hasilnya unsupported atau butuh browser | Tangani kedua kasus itu sebelum memanggil Solve |
Menjalankan solver di sebuah server
Detektor hanya memerlukan dua hal dari CapSkip: host dan port. Dalam mode Local itu adalah 127.0.0.1 dan PC Anda sendiri. Dalam mode Server, CapSkip mendengarkan di alamat jaringan atau IP publik Anda, sehingga detektor yang berjalan di PC lain, VPS, atau platform hosting dapat memanggilnya melalui API yang sama. Ubah host dan port di konstruktor CapSkipClient, dan tidak ada hal lain dalam kode yang berubah. IP publik statis disarankan untuk akses jarak jauh yang stabil. Mode Server tetap berjalan di hardware Anda sendiri dan tetap tanpa meteran. Perhatikan bahwa mode Server membuka port untuk semua alamat, jadi tambahkan aturan blokir Windows Firewall untuk alamat yang tidak boleh menjangkaunya. Lihat pengaturan koneksi untuk cara beralih mode.
FAQ
Bisakah saya mengetahui CAPTCHA apa yang dipakai sebuah situs tanpa kode?
Bisa. Buka DevTools, masuk ke tab Network, muat ulang halaman, dan lihat alamat skripnya. Skrip google.com/recaptcha atau recaptcha.net adalah reCAPTCHA, challenges.cloudflare.com adalah Turnstile, dan js.hcaptcha.com adalah hCaptcha. Snippet Console di atas melakukan pencarian yang sama untuk Anda.
Mengapa Detect mengembalikan hasil yang membutuhkan browser alih-alih menebak?
Karena tebakan yang salah menyia-nyiakan satu pemecahan penuh dan mengembalikan token yang ditolak situs tanpa petunjuk mengapa. Bila HTML statis tidak memuat sitekey, challenge GeeTest, atau versi Friendly Captcha, menyatakannya lebih singkat daripada job yang gagal.
Apakah CapSkip memecahkan hCaptcha?
Ya. CapSkip memecahkan widget hCaptcha checkbox, invisible, dan Enterprise melalui HTTP API-nya dengan method=hcaptcha, sebagaimana Dokumentasi API menjelaskannya. SDK .NET belum punya panggilan hCaptcha, jadi SolveHcaptcha di atas mengirim job ke in.php sendiri. FunCaptcha, Arkose, dan GeeTest v4 masih belum didukung, dan detektor melaporkannya sebagai unsupported agar program dapat melewati halaman itu alih-alih gagal di sana.
Bisakah platform hosting memakai detektor dengan CapSkip saya?
Bisa, lewat mode Server. Atur CapSkip untuk mendengarkan di jaringan atau IP publik Anda, arahkan konstruktor CapSkipClient ke host dan port itu, dan detektor serta panggilan Solve bekerja sama seperti pada satu PC. Pemecah tetap berjalan di hardware Anda.
Versi singkatnya
Detektor CAPTCHA di C# membuktikan gunanya dengan berjalan lebih dulu. Deteksi jenis CAPTCHA, lalu pecahkan. Baca skrip dan markup dalam urutan tetap, serahkan argumen ke metode yang tepat, dan perlakukan null, hasil yang membutuhkan browser, dan unsupported sebagai jawaban, bukan sebagai crash. Dengan CapSkip di komputer Anda sendiri, tidak ada meteran yang berjalan saat Anda memeriksa halaman, dan itulah yang membuat pemecah captcha praktis untuk crawl yang menemui situs baru setiap beberapa menit. Untuk SDK .NET itu sendiri, lihat Halaman pemecah CAPTCHA C#.
Detektor yang sama dalam bahasa lain:
- Deteksi jenis CAPTCHA dengan Python: yang asli, dengan 66 halaman yang sama.
- Detektor CAPTCHA di Node.js: pemeriksaan yang sama tanpa paket tambahan.
