Cara Memecahkan CAPTCHA di Tes Appium (Client Python)

appium captcha - How to Solve CAPTCHAs in Appium Tests (Python Client)

Langkah CAPTCHA di Appium adalah titik tempat proses mobile biasanya berhenti dan menunggu manusia. Sebenarnya tidak harus begitu. Appium sudah bisa mengambil screenshot elemen yang memuat challenge, dan CapSkip berjalan di mesin Anda sendiri lalu menjawabnya, sehingga tes mengetikkan hasilnya dan melanjutkan. Bagian yang merepotkan bukan pemecahannya, melainkan sesinya: Appium mengakhiri sesi yang terdiam, dan pemecahan adalah persis jenis keheningan yang tidak disukainya. Panduan ini membahas kedua bentuk yang akan Anda temui, yaitu field gambar native dan CAPTCHA di dalam WebView, dengan Python.

Apa yang Anda butuhkan

  • CapSkip berjalan di mesin Windows, dengan server API dinyalakan.
  • Appium 2 dan driver yang berfungsi, UiAutomator2 untuk Android atau XCUITest untuk iOS, ditambah perangkat atau emulator yang sudah bisa Anda kendalikan.
  • Python 3.10 atau lebih baru dengan client Appium dan paket CapSkip terinstal.
  • Alamat untuk pemecah. Mode Lokal menjawab di 127.0.0.1, jadi hanya kode yang berjalan di mesin CapSkip itu sendiri yang bisa menjangkaunya, dan Mode Server mendengarkan di alamat jaringan atau IP publik Anda sehingga build agent atau CI runner juga bisa menjangkaunya. Langkah 4 menjelaskan mana yang berlaku, dan keduanya berada di bawah pengaturan koneksi.
# pip install Appium-Python-Client capskip
pip install Appium-Python-Client capskip

Langkah 1: beri sesi ruang untuk menunggu

Lakukan ini sebelum hal lain, karena inilah kegagalan yang paling banyak membuang waktu. Appium menyimpan timer idle per sesi bernama newCommandTimeout. Nilai default-nya 60 detik, dan ketika tidak ada perintah baru yang datang dalam jendela waktu itu, server menyimpulkan client sudah pergi lalu mengakhiri sesi. Semua panggilan setelahnya kemudian gagal terhadap sesi yang sudah tidak ada.

Pemecahan adalah jeda dalam aliran perintah. Kode Python Anda berbicara dengan CapSkip, bukan dengan Appium, jadi selama pemecahan itu berlangsung driver hanya diam. Bandingkan kedua penghitung waktu itu dan masalahnya menjadi jelas.

Penghitung waktuBawaanApa yang dicakupnya
Appium newCommandTimeout60 detikWaktu idle antara dua perintah driver, per sesi
CapSkip defaultTimeout120 detikPolling CAPTCHA gambar dan ALTCHA
CapSkip recaptchaTimeout300 detikPolling reCAPTCHA, Turnstile dan GeeTest

CAPTCHA gambar biasanya kembali cukup cepat sehingga tidak ada yang menyadarinya. reCAPTCHA pada pemecah yang sedang sibuk tidak begitu, dan client bersedia menunggu lima kali lebih lama daripada Appium. Naikkan timer idle melewati pemecahan terlama yang bersedia Anda tunggu.

# pip install Appium-Python-Client capskip
from appium import webdriver
from appium.options.android import UiAutomator2Options

options = UiAutomator2Options()
options.platform_name = "Android"
options.device_name = "emulator-5554"
options.app = "/path/to/app.apk"

# Default is 60 seconds. A reCAPTCHA solve can outlast that.
options.new_command_timeout = 300

driver = webdriver.Remote("http://127.0.0.1:4723", options=options)

Properti itu menulis capability appium:newCommandTimeout, jadi driver atau client yang tidak menyediakannya dengan nama yang ramah tetap menerima nilai yang sama lewat set_capability. Di iOS kelasnya adalah XCUITestOptions dan capability-nya identik, karena setiap driver mewarisi yang satu ini dari base driver bersama milik Appium, bukan membuat versinya sendiri. Perhatikan juga alamat server-nya: Appium 2 melayani langsung di port-nya, tanpa path setelahnya.

Langkah 2: pecahkan CAPTCHA gambar native

Inilah bentuk yang umum di aplikasi mobile: sebuah ImageView berisi teks terdistorsi, dan field teks di bawahnya. Appium bisa mengambil screenshot satu elemen saja dan mengembalikannya sebagai base64, yang kebetulan persis salah satu dari tiga bentuk input yang diterima metode gambar, jadi tidak ada yang perlu menyentuh disk.

from appium.webdriver.common.appiumby import AppiumBy
from capskip import CapSkip

solver = CapSkip(host="127.0.0.1", port=8080)

# Appium crops the element out of a device screenshot for you.
image = driver.find_element(AppiumBy.ID, "com.example.app:id/captcha_image")
result = solver.normal("data:image/png;base64," + image.screenshot_as_base64)

field = driver.find_element(AppiumBy.ID, "com.example.app:id/captcha_input")
field.send_keys(result["code"])

Key code memuat teks yang berhasil dibaca. Metode gambar juga menerima path berkas atau URL jarak jauh, jadi jika sebuah langkah sudah menyimpan screenshot Anda bisa meneruskan path-nya, tetapi jalur base64 menghindari berkas sementara dalam sebuah proses tes dan lebih mudah dibersihkan setelahnya.

Ada dua hal tentang metode ini yang perlu Anda tahu sebelum membangun di sekitarnya. Metode ini tidak mendukung proxy, dan itu tidak masalah di sini karena gambarnya tidak pernah meninggalkan mesin Anda. Dan metode ini melakukan polling terhadap timeout default 120 detik, bukan terhadap timeout reCAPTCHA yang lebih panjang, karena tidak ada sesi browser yang terlibat.

Bidik elemen gambarnya, bukan layarnya. Screenshot satu layar penuh dengan CAPTCHA di suatu tempat di dalamnya memberi pemecah antarmuka ponsel untuk dibaca, dan jawabannya akan salah dengan cara yang terlihat seperti pemecahan yang buruk, padahal yang buruk adalah pemotongannya. Jika elemen yang bisa Anda temukan adalah container dengan padding dan label di dalamnya, cari view yang ada di dalamnya, atau piksel tambahan itu akan mengurangi akurasi Anda.

Langkah 3: pecahkan reCAPTCHA di dalam WebView

Bentuk yang satu lagi adalah layar login atau pendaftaran yang sebenarnya adalah halaman web di dalam WebView. Tidak ada gambar yang perlu dibaca di sini, jadi beralihlah ke konteks web dan bekerjalah dengan DOM persis seperti yang Anda lakukan di browser.

# contexts looks like ['NATIVE_APP', 'WEBVIEW_com.example.app']
web = [c for c in driver.contexts if c.startswith("WEBVIEW")][0]
driver.switch_to.context(web)

# Narrow to g-recaptcha: hCaptcha also carries data-sitekey.
sitekey = driver.find_element(
    AppiumBy.CSS_SELECTOR,
    ".g-recaptcha[data-sitekey]").get_attribute("data-sitekey")

result = solver.recaptcha(sitekey=sitekey, url=driver.current_url)

driver.execute_script(
    "document.getElementById('g-recaptcha-response').value = arguments[0];",
    result["code"],
)
driver.switch_to.context("NATIVE_APP")

Periksa dulu widget-nya benar-benar apa sebelum memanggil metode reCAPTCHA. hCaptcha juga memasang data-sitekey pada widget-nya, dan hCaptcha bukan tipe yang didukung, jadi selector atribut yang polos akan dengan senang hati memberi Anda key yang salah. Cari class g-recaptcha, atau singkirkan kemungkinan hCaptcha dengan memeriksa adanya class h-captcha atau script js.hcaptcha.com. Login di WebView adalah tempat yang umum untuk menemuinya. FunCaptcha dan Arkose juga tidak didukung.

Baca URL halaman dari driver, jangan menuliskannya secara hard-code. WebView sering memuat URL dengan sesi atau return path di query string, dan pemecahan terikat pada halaman yang memintanya, jadi URL hasil tebakan menghasilkan token yang ditolak situs.

Mengisi field respons sudah cukup pada form yang mengirim data secara normal. Itu tidak cukup pada halaman yang menunggu reCAPTCHA memanggilnya balik, yaitu susunan ketika tombol submit terhubung ke callback milik widget, bukan ke form. Kasus itu juga butuh callback-nya dipanggil, dan itu adalah persoalan tersendiri, bukan persoalan mobile: halaman pemecah callback membahas apa yang harus dicari. Beralihlah kembali ke konteks native sebelum menyentuh tombol native lagi, atau find_element berikutnya akan mencari di DOM dan gagal.

Jika daftar contexts hanya pernah menampilkan NATIVE_APP, WebView-nya tidak bisa di-debug. Di Android itu adalah pengaturan di sisi aplikasi yang dikendalikan para developer, jadi ada baiknya memeriksanya bersama mereka sebelum menganggap Appium yang bersalah.

Langkah 4: di mana pemecah CAPTCHA berjalan, dan mode koneksi apa yang dibutuhkan

Inilah bagian yang sering dipahami terbalik di ranah mobile, jadi layak dikatakan terus terang. Pemecah dipanggil oleh kode tes Python Anda. Ponsel tidak memanggilnya, emulator tidak memanggilnya, dan server Appium pun tidak. Jadi satu-satunya pertanyaan adalah di mana proses tes Anda berjalan, dan jaringan pada perangkat itu sendiri sama sekali tidak ada hubungannya.

Artinya saran umum soal emulator Android untuk menjangkau mesin host tidak relevan di sini, begitu juga alamat server Appium jarak jauh. Yang penting lebih sederhana: jika proses yang menjalankan tes Anda ada di mesin CapSkip, loopback sudah benar. Jika ada di tempat lain, loopback salah, dan pemecahan pertama memunculkan NetworkException.

Di mana proses tes berjalanMode koneksi yang mana
Laptop Anda, dengan CapSkip terbuka di dalamnyaMode Local. 127.0.0.1 memang benar
Laptop Anda, mengendalikan server Appium jarak jauh atau device cloudTetap Mode Lokal. Hanya panggilan driver yang keluar
Build agent di jaringan yang samaMode Server, di alamat privat mesin CapSkip
CI runner yang dihosting atau sebuah containerPakai mode Server dengan IP publik statis dan sebuah aturan firewall

Alihkan CapSkip ke Mode Server dan ia akan mendengarkan di alamat jaringan atau IP publik Anda alih-alih loopback, sehingga semuanya bisa menjangkaunya lewat HTTP API yang sama. IP publik statis dianjurkan ketika rutenya melintasi internet, disertai aturan firewall yang hanya mengizinkan alamat yang Anda harapkan. Mode Server hanya mengubah tempat pemecah mendengarkan dan tidak lebih: perangkat kerasnya tetap milik Anda, dan tetap tanpa kuota. Baca host dan port dari environment agar satu suite berjalan di kedua tempat. Client tidak membaca CAPSKIP_HOST atau CAPSKIP_PORT dengan sendirinya, jadi teruskan keduanya ke konstruktor, seperti yang dilakukan contoh lengkap di bawah.

Contoh lengkap yang berfungsi

import os
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from capskip import CapSkip, ApiException, NetworkException, TimeoutException

solver = CapSkip(
    host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
    port=int(os.environ.get("CAPSKIP_PORT", 8080)),
)

options = UiAutomator2Options()
options.platform_name = "Android"
options.device_name = "emulator-5554"
options.app = "/path/to/app.apk"
options.new_command_timeout = 300      # must outlast the longest solve

driver = webdriver.Remote("http://127.0.0.1:4723", options=options)

try:
    image = driver.find_element(AppiumBy.ID, "com.example.app:id/captcha_image")
    result = solver.normal("data:image/png;base64," + image.screenshot_as_base64)

    driver.find_element(
        AppiumBy.ID, "com.example.app:id/captcha_input").send_keys(result["code"])
    driver.find_element(AppiumBy.ACCESSIBILITY_ID, "Submit").click()
except ApiException:
    print("the solver refused the image")
except NetworkException:
    print("solver unreachable: check the host and the connection mode")
except TimeoutException:
    print("no answer inside defaultTimeout")
finally:
    driver.quit()

Keempat exception diturunkan dari CapSkipError, jadi menangkap yang satu itu saja sudah menangani setiap kegagalan yang bisa dimunculkan SDK dalam satu blok. Tangkap yang spesifik ketika responsnya berbeda, seperti di atas, dan CapSkipError ketika tidak berbeda. Simpan driver.quit di dalam blok finally: tes yang mati di tengah pemecahan kalau tidak akan meninggalkan sesi yang menahan perangkat sampai timer idle yang baru saja Anda naikkan akhirnya habis.

Tipe selebihnya bekerja dengan cara yang sama dari client yang sama. Turnstile menerima sitekey dan URL halaman, GeeTest menerima nilai gt, sebuah challenge dan URL halaman, dan ALTCHA menerima URL halaman serta sebuah endpoint challenge. Setiap metode yang disediakan paket ini terdaftar di halaman pemecah CAPTCHA Python, dan tipe gambar punya halaman tersendiri.

Kesalahan umum dan artinya

Apa yang Anda lihatPenyebabPerbaiki
Sesi hilang setelah pemecahan yang lambat, dan setiap perintah berikutnya gagalnewCommandTimeout habis selagi kode Anda menunggu pemecahNaikkan melewati pemecahan terlama, bukan hanya melewati rata-ratanya
NetworkException pada pemecahan pertamaCapSkip tidak berjalan, atau proses tes ada di build agent dan diarahkan ke loopbackJalankan CapSkip, lalu pilih antara Mode Lokal dan Mode Server
TimeoutException yang menyebut 120 detik pada sebuah gambarTipe gambar memakai timeout polling default, bukan timeout reCAPTCHA yang lebih panjangPastikan pemecah berjalan dan tidak kelebihan beban sebelum menaikkan apa pun
Jawabannya selalu salah, padahal gambarnya jelasScreenshot satu layar penuh, atau elemen yang menyertakan padding dan labelAmbil screenshot view paling dalam yang hanya memuat CAPTCHA
ValidationException yang menyebut base64 atau berkas yang hilangScreenshot elemen kembali kosong, jadi string-nya terlalu pendek untuk dibaca sebagai gambarPastikan elemennya ada di layar dan terlihat sebelum screenshot diambil, dan bahwa pencariannya memang cocok dengan elemen itu
Metode reCAPTCHA mengembalikan token yang selalu ditolak situsWidget-nya adalah hCaptcha, yang juga membawa data-sitekey dan bukan tipe yang didukungPersempit selector ke class g-recaptcha dan pastikan widget mana yang dimuat halaman
Daftar contexts hanya berisi NATIVE_APPWebView-nya tidak bisa di-debug, jadi Appium tidak bisa menempel padanyaMinta tim aplikasi mengaktifkan debugging WebView di build yang Anda uji
find_element gagal tepat setelah langkah WebViewDriver masih berada di konteks web dan sedang mencari di DOMBeralihlah kembali ke NATIVE_APP sebelum menyentuh elemen native
Field reCAPTCHA sudah terisi tetapi tombolnya tidak melakukan apa-apaHalaman sedang menunggu callback milik widget, bukan membaca field-nyaPanggil callback-nya juga, atau kirim form secara langsung
Token yang dikembalikan pemecah ditolak oleh situsURL halaman yang diberikan ke pemecah ditebak, bukan dibaca dari WebViewBerikan driver.current_url dari dalam konteks web

FAQ

Apakah ponsel atau emulator perlu menjangkau pemecah?

Tidak, dan inilah satu hal paling berguna untuk dipahami tentang penyiapan ini. Panggilan HTTP ke CapSkip dilakukan oleh proses Python Anda, jadi perangkat hanya pernah melihat permintaan screenshot dan sebuah send_keys. Tidak ada yang perlu diinstal di ponsel, tidak ada trafik dari aplikasi yang dialihkan, dan alamat host milik emulator sama sekali tidak terlibat. Perangkat sungguhan yang tersambung kabel, emulator dan perangkat cloud semuanya berperilaku identik dari sudut pandang pemecah.

Bisakah tes Appium yang berjalan di CI memakai CapSkip?

Bisa, lewat Mode Server. Runner yang dihosting tidak bisa melihat alamat loopback Anda, jadi alihkan CapSkip agar mendengarkan di alamat jaringan atau IP publik Anda di pengaturan koneksi lalu arahkan variabel environment host ke sana. Gunakan IP publik statis ketika rutenya melintasi internet dan batasi dengan aturan firewall. Pemecah tetap berada di perangkat keras milik Anda dalam setiap kasus, jadi tidak ada yang berubah pada lisensi maupun jumlah pemecahan ketika tes berpindah dari meja Anda.

Apakah semua ini hanya untuk Android?

Tidak. Tukar UiAutomator2Options dengan XCUITestOptions, yang diimpor dari appium.options.ios, dan bentuk prosesnya identik, karena screenshot elemen, pergantian konteks dan timer idle semuanya berada di atas driver. Hanya locator-nya yang berubah, karena iOS tidak punya resource id: gunakan accessibility id jika aplikasi menyetelnya, dan predicate atau class chain jika tidak. Pemecah tidak pernah tahu dari platform mana sebuah gambar diserahkan kepadanya.

Sebaiknya saya memecahkan CAPTCHA-nya atau mematikannya untuk pengujian?

Matikan, jika itu aplikasi Anda dan Anda bisa melakukannya. Build tes yang melewati pemeriksaan, atau test key dari penyedia yang selalu lolos, lebih cepat dan lebih deterministik daripada pemecahan apa pun serta menghapus satu ketergantungan dari suite Anda. Pemecahan baru layak dipakai ketika Anda tidak menguasai layarnya: login pihak ketiga di dalam alur Anda, form pendaftaran milik mitra, environment staging yang tidak akan diubah siapa pun demi Anda, atau proses device cloud terhadap produksi. Itulah kasus ketika pilihannya adalah pemecah atau manusia.

Versi singkatnya

Naikkan newCommandTimeout sebelum Anda menulis apa pun yang lain, karena default 60 detik lebih pendek daripada batas waktu yang diberikan kepada pemecah dan sesi yang mati di tengah tes terlihat seperti bug yang sama sekali berbeda. Ambil screenshot elemennya, bukan layarnya, teruskan sebagai data URI base64, lalu ketikkan hasilnya kembali dengan send_keys. Untuk WebView, ganti konteks, baca sitekey dan URL saat ini dari DOM, isi field respons, lalu kembali lagi. Pindahlah ke Mode Server begitu proses tes berhenti berbagi mesin dengan pemecah.

Satu hal terakhir yang membentuk cara Anda menulis percobaan ulang. Karena pemecah captcha ini berjalan di perangkat keras yang sudah Anda miliki, percobaan kedua pada gambar yang terpotong buruk tidak memakan biaya apa pun, sehingga sebuah tes sanggup mengambil screenshot yang lebih bersih lalu mencoba lagi daripada menggagalkan seluruh prosesnya.