Cara Memecahkan ALTCHA di Selenium Tanpa Intersepsi Jaringan

Untuk memecahkan ALTCHA di Selenium, Anda tidak pernah menunggu widget selesai. ALTCHA adalah proof of work, bukan pengenalan: situs menerbitkan sebuah masalah hashing, dan klien harus menemukan angka yang memenuhinya sebelum ia diizinkan lewat. Tidak ada yang perlu dilihat dan tidak ada yang perlu diklik, jadi browser ada di sana untuk sisa alurnya dan bukan untuk CAPTCHA-nya. Selenium tidak punya padanan satu pemanggilan untuk response wait milik Playwright, dan ini salah satu kasus langka di mana hal itu tidak merugikan Anda sama sekali: widget menyebutkan sendiri endpoint challenge-nya di sebuah atribut, dan hanya itu yang dibutuhkan pemecah CAPTCHA.
Apa yang Anda butuhkan
- CapSkip 1.2.6 atau versi lebih baru yang berjalan di mesin Windows. Dukungan ALTCHA hadir pada rilis tersebut, jadi build yang lebih lama tidak punya metode untuk dipanggil.
- Python 3.10 atau lebih baru, dengan paket Selenium dan CapSkip terpasang, plus browser yang cocok.
- URL halaman tempat widget berada. Tidak ada sitekey, karena ALTCHA tidak memilikinya.
- Sebuah alamat untuk pemecah CAPTCHA. Mode Local menjawab di 127.0.0.1 untuk perangkat itu saja, dan mode Server mendengarkan di alamat jaringan atau IP publik Anda sehingga sebuah node Grid, CI runner atau mesin lain dapat menjangkaunya melalui API yang sama. Langkah 5 membahas mana yang berlaku, dan keduanya ada di pengaturan koneksi.
# pip install selenium capskip pip install selenium capskip
Langkah 1: baca endpoint challenge dari widget
Elemen widget menyebutkan endpoint yang akan dimintainya challenge. Atribut mana yang memuatnya bergantung pada generasi widget, jadi baca kode sumber halaman daripada menebak.
| Generasi widget | Atribut yang menamai challenge |
|---|---|
| v1 dan v2 | challengeurl untuk endpoint, dengan atribut challengejson terpisah ketika challenge bersifat inline |
| v3 dan setelahnya | challenge, dan satu atribut itu menerima URL atau data challenge |
Dua kebiasaan Selenium penting di sini dan keduanya tidak kentara. Selenium tidak menunggu elemen secara otomatis, jadi kalau Anda meminta widget sebelum halaman menaruhnya di DOM, yang Anda dapat adalah NoSuchElementException dan bukan percobaan ulang. Lalu pemanggilan atribut biasa bersifat hibrida: ia mencoba properti JavaScript bernama sama lebih dulu dan baru jatuh ke markup. Sebuah web component mendefinisikan properti untuk atributnya sendiri, jadi apa yang kembali bergantung pada build widget dan bukan pada HTML yang bisa Anda baca. Pemanggilan atribut DOM tidak punya fallback semacam itu dan mengembalikan apa yang tertulis di markup, dan itulah yang Anda inginkan. Referensi API Python menjelaskan perbedaannya di halaman WebElement.
# pip install selenium capskip
from urllib.parse import urljoin
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
PAGE_URL = "https://example.com/signup"
driver = webdriver.Chrome()
driver.get(PAGE_URL)
# Nothing auto-waits in Selenium, so wait for the element.
widget = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "altcha-widget"))
)
# v1 and v2 use challengeurl or challengejson; v3 uses challenge.
value = (widget.get_dom_attribute("challengeurl")
or widget.get_dom_attribute("challengejson")
or widget.get_dom_attribute("challenge"))
if value is None:
raise SystemExit("no challenge attribute on the widget")Dua dari tiga atribut itu memberi Anda dokumennya, bukan penunjuk ke dokumen itu, jadi periksa apa yang Anda dapat sebelum memperlakukannya sebagai URL. Sebuah URL butuh satu langkah tambahan tersendiri: markup biasanya memuat path same-origin seperti garis miring di depan dan nama folder, dan klien HTTP tidak bisa mengambilnya sendiri. Jadikan dulu URL lengkap dengan mengacu pada URL halaman.
import json
challenge, endpoint = None, None
if value.lstrip().startswith("{"):
# challengejson, or a v3 challenge holding the data inline.
challenge = json.loads(value)
else:
# A path in the markup becomes an absolute URL here.
endpoint = urljoin(PAGE_URL, value)Tiga gaya interaksi pada atribut type milik widget, yaitu native, checkbox dan switch, murni bersifat visual dan tidak pernah sampai ke pemecah CAPTCHA, dan atribut display yang terpisah juga bersifat visual dengan cara yang sama. ALTCHA mendokumentasikan semuanya di panduan widget mereka sendiri.
Langkah 2: ambil challenge dengan sesi browser
Anda bisa melewati langkah ini kapan pun endpoint-nya publik, dan banyak yang memang begitu. Berikan URL-nya ke CapSkip dan ia mengambil sendiri challenge-nya. Langkah ini ada karena sebagian situs mengikat challenge pada sesi yang memintanya, sedangkan pemecah CAPTCHA mengambil dari mesin Anda tanpa cookie, jadi apa yang ia terima tidak menjadi milik siapa pun. Akibatnya jawabannya benar tetapi situs tetap menolaknya.
Perbaikannya adalah mengambil challenge di dalam sesi browser dan mengoper dokumennya, bukan penunjuknya. Salin cookie yang sudah dipegang Selenium ke sebuah klien HTTP, bawa user agent yang sama, lalu minta sendiri challenge itu.
import requests
if endpoint:
session = requests.Session()
for cookie in driver.get_cookies():
session.cookies.set(cookie["name"], cookie["value"],
domain=cookie["domain"])
session.headers["User-Agent"] = driver.execute_script(
"return navigator.userAgent")
# Referer matters on sites that check where the ask came from.
challenge = session.get(endpoint,
headers={"Referer": PAGE_URL}).json()Error decode JSON pada baris itu adalah kegagalan yang berguna. Artinya endpoint menjawab dengan HTML, biasanya halaman login atau dinding persetujuan, yang memberi tahu Anda bahwa request itu keluar sebagai orang asing dan bahwa langkah ini memang tepat. Mulai pemecahan tepat setelah pengambilan. Jendelanya singkat dan hitungan waktunya dimulai saat situs menerbitkan challenge, bukan saat Anda sempat memakainya.
Langkah 3: serahkan challenge ke satu metode ALTCHA
Satu metode, dijelaskan selengkapnya di halaman pemecah ALTCHA, dan metode itu menerima challenge dalam bentuk mana pun. Berikan dokumennya dan tidak ada request yang keluar sama sekali. Berikan endpoint-nya dan pemecah CAPTCHA mengambilnya, termasuk mengambil yang baru jika pekerjaan itu cukup lama menunggu di antrean sampai yang pertama kedaluwarsa.
from capskip import CapSkip solver = CapSkip(host="127.0.0.1", port=8080) # The document from step 2, so nothing is fetched twice. result = solver.altcha(url=PAGE_URL, challenge_json=challenge) # Or hand over the endpoint and let CapSkip fetch it. # result = solver.altcha(url=PAGE_URL, challenge_url=endpoint) print(result["token"]) # base64 payload for the form field print(result["number"]) # the counter that satisfied it
Dua key dalam hasil itu hanya ada untuk ALTCHA. Key token memuat payload base64 yang diminta form, dan key number memuat penghitung yang memecahkan challenge. Key code membawa string yang sama dengan token, jadi keduanya sama-sama bisa dipakai, tetapi key token dinamai sesuai field tujuannya. Key GeeTest dan user agent Turnstile tidak ada di sini.
Algoritma apa saja yang dicakup pemecah CAPTCHA
Skema legacy dicakup dengan SHA-1, SHA-256, SHA-384 dan SHA-512, dan proof-of-work v2 dengan PBKDF2 dan SHA iteratif. PBKDF2 adalah default yang direkomendasikan ALTCHA sendiri, jadi itulah sebagian besar situs yang aktif. Argon2id dan scrypt adalah pengecualian dan keduanya ditolak alih-alih dicoba: challenge yang meminta salah satunya kembali dalam sekitar sepertiga detik sebagai ERROR_CAPTCHA_UNSOLVABLE dan tidak pernah dicoba ulang, karena fungsi memory-hard bukan sesuatu yang bisa diperbaiki dengan percobaan ulang. Pada tipe ini hasil tersebut menunjuk ke algoritmanya dan bukan ke gambar yang tidak terbaca, dan kode error itu punya panduan tersendiri.
Langkah 4: tulis token ke field tersembunyi
Widget mengirim payload-nya di sebuah hidden input yang namanya berasal dari atribut name miliknya sendiri, yang defaultnya altcha. Baca atribut itu daripada menebak, sama seperti saat Anda membaca atribut challenge. Tidak ada yang memverifikasi widget dalam proses ini, jadi tidak ada yang mengisi field itu dan Andalah yang mengisinya sendiri.
Selenium bisa mengoper sebuah WebElement langsung ke dalam script, sehingga tombol itu sendiri menjadi cara paling bersih untuk menemukan form yang benar. Serahkan tombol submit dan baca properti form miliknya: halaman pendaftaran sering memuat beberapa form, dan menambahkan field ke form pertama di halaman padahal tombolnya milik form lain berarti server tidak pernah melihat nilainya.
SET_FIELD = """
const form = arguments[0].form || document.querySelector('form');
let field = form.querySelector('[name="' + arguments[1] + '"]');
if (!field) {
field = document.createElement('input');
field.type = 'hidden';
field.name = arguments[1];
form.appendChild(field);
}
field.value = arguments[2];
"""
button = driver.find_element(By.CSS_SELECTOR, "button[type=submit]")
field_name = widget.get_dom_attribute("name") or "altcha"
driver.execute_script(SET_FIELD, button, field_name, result["token"])
button.click()Teruskan string itu apa adanya. Token adalah base64 dari sebuah dokumen JSON yang field-nya tercakup oleh tanda tangan HMAC server, jadi apa pun yang terlihat seperti merapikan akan merusaknya: memangkas spasi, melakukan decode lalu encode ulang, atau menyusun ulang JSON dengan urutan key yang berbeda. Beberapa integrasi membaca payload dari field body JSON, bukan dari field form, dan widget juga dapat dikonfigurasi untuk mengirimkannya lewat cookie, jadi periksa apa yang dikirim oleh submit milik halaman itu sendiri lalu tiru.
Jika tombol submit tidak pernah bisa diklik, halaman itu menggantungkannya pada script-nya sendiri yang mendengar bahwa widget berhasil, bukan pada field yang sudah berisi nilai. Ada dua jawaban jujur di sini. Cari apa yang didengarkan halaman itu lalu penuhi, atau lewati tombolnya dan kirim langsung field form tersebut dengan cookie yang sudah dimiliki browser, yang biasanya lebih pendek dan selalu lebih stabil.
Langkah 5: di mana pemecah CAPTCHA berjalan setelah skrip Anda berpindah
Contoh di sini memakai 127.0.0.1 karena itu benar selama skrip Anda dan CapSkip berbagi satu mesin. Pemecah CAPTCHA dipanggil oleh kode Python Anda, bukan oleh browser dan bukan oleh halaman, jadi alamatnya bergantung pada di mana proses pengujian berjalan dan bukan pada di mana Chrome berjalan. Selenium membuat hal itu mudah terbalik, karena browser sudah begitu sering berada di tempat lain.
| Di mana proses Python berjalan | Mode koneksi yang mana |
|---|---|
| Di mesin CapSkip, mengendalikan browser lokal | Mode Local. 127.0.0.1 memang benar |
| Di mesin Anda, mengendalikan WebDriver jarak jauh atau node Grid | Mode Lokal. Browser tidak pernah berbicara dengan pemecah |
| Di mesin lain pada jaringan yang sama | Mode Server, di alamat privat mesin pemecah CAPTCHA |
| Di dalam container, di CI runner atau di VPS | Pakai mode Server dengan IP publik statis dan sebuah aturan firewall |
Ubah CapSkip ke mode Server dan ia mendengarkan di alamat jaringan atau IP publik Anda, bukan di loopback, lalu runner terhubung melalui API HTTP yang sama seperti yang akan dipakainya secara lokal. IP publik statis dianjurkan ketika rutenya melintasi internet, dengan aturan firewall yang hanya mengizinkan alamat yang Anda harapkan. Mode Server hanya mengubah di mana pemecah CAPTCHA mendengarkan dan tidak ada yang lain: perangkat kerasnya tetap milik Anda dan tetap tanpa kuota. Menjalankan pengujiannya sendiri di seluruh Grid punya rangkaian jebakannya sendiri, yang dibahas terpisah di panduan Selenium Grid.
Baca host dan port dari environment agar satu script bekerja 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.
Satu catatan khusus ALTCHA soal proxy. Proxy didukung di sini, tetapi hanya dipakai untuk pengambilan challenge. Tidak ada sesi browser yang dirutekan melaluinya, jadi proxy tidak berpengaruh pada proof of work itu sendiri, dan sama sekali tidak melakukan apa pun ketika Anda meneruskan dokumen challenge secara inline.
Contoh lengkap yang berfungsi
import json, os, requests
from urllib.parse import urljoin
from capskip import CapSkip, ApiException, NetworkException, TimeoutException
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
PAGE_URL = "https://example.com/signup"
SET_FIELD = """
const form = arguments[0].form || document.querySelector('form');
let field = form.querySelector('[name="' + arguments[1] + '"]');
if (!field) {
field = document.createElement('input');
field.type = 'hidden';
field.name = arguments[1];
form.appendChild(field);
}
field.value = arguments[2];
"""
solver = CapSkip(
host=os.environ.get("CAPSKIP_HOST", "127.0.0.1"),
port=int(os.environ.get("CAPSKIP_PORT", 8080)),
)
driver = webdriver.Chrome()
try:
driver.get(PAGE_URL)
widget = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.CSS_SELECTOR, "altcha-widget"))
)
value = (widget.get_dom_attribute("challengeurl")
or widget.get_dom_attribute("challengejson")
or widget.get_dom_attribute("challenge"))
if value is None:
raise SystemExit("no challenge attribute on the widget")
if value.lstrip().startswith("{"):
challenge = json.loads(value)
else:
endpoint = urljoin(PAGE_URL, value)
session = requests.Session()
for cookie in driver.get_cookies():
session.cookies.set(cookie["name"], cookie["value"],
domain=cookie["domain"])
session.headers["User-Agent"] = driver.execute_script(
"return navigator.userAgent")
challenge = session.get(endpoint,
headers={"Referer": PAGE_URL}).json()
try:
result = solver.altcha(url=PAGE_URL, challenge_json=challenge)
except ApiException:
raise SystemExit("refused: Argon2id, scrypt, or an expired challenge")
except NetworkException:
raise SystemExit("solver unreachable: check host and connection mode")
except TimeoutException:
raise SystemExit("no answer inside defaultTimeout")
button = driver.find_element(By.CSS_SELECTOR, "button[type=submit]")
field_name = widget.get_dom_attribute("name") or "altcha"
driver.find_element(By.NAME, "email").send_keys("[email protected]")
driver.execute_script(SET_FIELD, button, field_name, result["token"])
button.click()
finally:
driver.quit()Keempat exception itu diturunkan dari CapSkipError, jadi menangkap yang satu itu saja sudah menangani setiap kegagalan yang bisa dilempar SDK dalam satu blok. Tangkap yang spesifik ketika responsnya berbeda, seperti di atas, dan tangkap CapSkipError ketika tidak. Perhatikan TimeoutException mana yang Anda tangkap: milik CapSkip dan milik Selenium sendiri memakai nama yang sama, jadi impor salah satunya dengan alias kalau keduanya ada dalam cakupan.
Tipe lainnya bekerja dengan cara yang sama dari client yang sama. reCAPTCHA dan Turnstile menerima sitekey dan URL halaman, GeeTest menerima nilai gt, sebuah challenge dan URL halaman, dan pemecahan gambar menerima path berkas, sebuah URL atau base64. Setiap metode yang disediakan paket ini terdaftar di halaman pemecah CAPTCHA Python, dan cerita browser yang lebih luas ada di halaman pemecah CAPTCHA Selenium.
Kesalahan umum dan artinya
| Apa yang Anda lihat | Penyebab | Perbaiki |
|---|---|---|
| NoSuchElementException pada widget | Elemen itu belum dimasukkan ke DOM, yang terjadi ketika markup dibangun oleh script, dan Selenium tidak menunggu dengan sendirinya | Bungkus pencarian itu dalam explicit wait sampai elemennya ada |
| Ketiga pembacaan atribut kembali sebagai None | Widget dikonfigurasi sepenuhnya dari JavaScript, jadi tidak satu pun dari ketiga nama itu ada di markup | Baca konfigurasinya dari script halaman itu sendiri, atau kembali ke endpoint yang bisa Anda lihat di panel network |
| Nilai atribut kosong padahal kode sumber halaman jelas memuatnya | Pemanggilan atribut biasa mengembalikan properti yang didefinisikan web component, dan properti itu kosong | Pakai pemanggilan atribut DOM, yang membaca markup dan tidak punya fallback ke properti |
| MissingSchema dari klien HTTP, menyebut nilai yang diawali garis miring | Markup memuat path same-origin, dan pemanggilan atribut DOM mengembalikannya persis seperti tertulis | Jadikan URL lengkap dengan mengacu pada URL halaman sebelum mengambilnya, dan sebelum mengopernya ke pemecah CAPTCHA |
| Nilai atribut diawali kurung kurawal | Sebuah atribut challengejson, atau widget v3 yang membawa dokumennya secara inline dan bukan sebuah URL | Parse sebagai JSON dan oper sebagai challenge inline |
| Error decode JSON saat Anda mengambil endpoint | Endpoint menjawab dengan HTML, jadi request itu tiba tanpa sesi browser | Salin cookie dan user agent lebih dulu sebelum mengambil |
| ApiException pada challenge yang baru saja Anda tangkap | Challenge inline sudah kedaluwarsa, jadi pemecah menolaknya alih-alih melakukan hashing | Ambil dan pecahkan dalam satu tarikan napas, atau oper endpoint-nya supaya pemecah CAPTCHA bisa mengambil ulang, selama endpoint itu tidak terikat sesi |
| ERROR_CAPTCHA_UNSOLVABLE di dalam ApiException, dalam sekitar sepertiga detik | Challenge memakai Argon2id atau scrypt | Tidak ada yang perlu dicoba ulang. Keduanya ditolak secara desain |
| Kegagalan verifikasi polos, padahal log Anda menunjukkan token-nya berhasil dipecahkan | Challenge kedaluwarsa di antara pemecahan dan pengiriman | Tangkap, pecahkan dan kirim tanpa hal lambat di antaranya |
| NetworkException pada pemecahan pertama | CapSkip tidak berjalan, atau script berada di dalam container dan diarahkan ke loopback | Jalankan CapSkip, lalu pilih antara Mode Lokal dan Mode Server |
| TimeoutException yang menyebut 120 detik | Pemecah CAPTCHA tidak menjawab dalam timeout polling default, yang dipakai ALTCHA dan bukan timeout reCAPTCHA yang lebih panjang | Pastikan solver berjalan dan tidak jenuh. Menaikkan batas atas hanya menunda jawaban yang sama |
| Form terkirim tetapi server melaporkan nilai altcha yang hilang | Field tersembunyi ditambahkan ke form lain di halaman itu | Baca properti form dari tombol submit, seperti yang dilakukan cuplikan kode itu |
| Tombol submit tetap nonaktif | Halaman menggantungkannya pada script-nya sendiri yang melihat widget berhasil | Kirim field form secara langsung, atau penuhi apa pun yang didengarkan halaman |
| ValidationException pada pemanggilan | Tidak ada opsi challenge yang diberikan, atau sebuah opsi yang tidak diterima ALTCHA ikut diteruskan | Berikan endpoint atau dokumennya, dan buang yang lain |
FAQ
Apakah saya benar-benar butuh browser untuk memecahkan ALTCHA?
Tidak. ALTCHA adalah masalah hashing, jadi ia dijawab dengan CPU dan tidak ada browser yang terlibat dalam jawabannya. Jika CAPTCHA adalah satu-satunya alasan Anda membuka Selenium, tutup saja: ambil challenge dengan klien HTTP lalu kirim tokennya kembali, dan itu semua dibahas oleh panduan Python biasa langkah demi langkah. Selenium layak dipakai ketika sisa alurnya memang membutuhkan halaman sungguhan, misalnya login yang menyetel cookie, form berlangkah banyak, atau markup yang baru ada setelah script berjalan.
Mengapa atribut widget kembali kosong?
Karena pemanggilan atribut biasa bersifat hibrida. Ia mengembalikan properti JavaScript ketika elemennya mendefinisikan satu, dan baru setelah itu jatuh ke markup, sedangkan web component mendefinisikan properti untuk atributnya sendiri, jadi properti yang kosong mengalahkan nilai yang bisa Anda lihat di kode sumber halaman. Pemanggilan atribut DOM tidak punya fallback semacam itu dan mengembalikan apa yang tertulis di markup. Alasan lainnya adalah Anda membaca nama yang salah untuk generasi yang sedang Anda hadapi, jadi coba ketiganya sebelum memutuskan atributnya memang tidak ada.
Bisakah CI runner atau container menjangkau pemecah CAPTCHA?
Bisa, lewat mode Server. Ubah CapSkip dari loopback ke alamat jaringan atau IP publik Anda di pengaturan koneksi, lalu arahkan variabel environment host ke sana. Runner dan pemecah CAPTCHA kemudian berbicara lewat API HTTP yang sama seperti kalau keduanya berada di satu mesin. Gunakan IP publik statis ketika rutenya melintasi internet dan batasi dengan aturan firewall. Node Grid jarak jauh adalah pertanyaan yang berbeda dan biasanya tidak butuh apa pun, karena node itu menjalankan browser sementara skrip Anda, yang memanggil pemecah CAPTCHA, tetap berada di tempat Anda menjalankannya.
Apakah timeout milik Selenium sendiri akan memotong pemecahan?
Tidak, karena pemecahan itu bukan perintah WebDriver. Implicit wait, page load timeout dan script timeout semuanya hanya mencakup panggilan yang dikirim Selenium ke browser, sedangkan panggilan ke pemecah CAPTCHA adalah Python biasa yang duduk di antara dua panggilan itu. Langit-langit yang berlaku adalah timeout polling default milik klien sebesar 120 detik, yang dipakai ALTCHA dan bukan timeout reCAPTCHA yang lebih panjang, karena ini pekerjaan CPU dan bukan sesi browser. Yang justru perlu diawasi adalah batas yang membungkus seluruh pengujian, misalnya timeout plugin per pengujian atau batas job CI.
Versi singkatnya
Tunggu widget-nya, baca atribut challenge miliknya dengan pemanggilan atribut DOM, ubah path menjadi URL lengkap dengan mengacu pada URL halaman, lalu ambil endpoint itu di dalam sesi browser kalau situsnya mengikat challenge pada sebuah sesi. Berikan dokumen atau URL-nya ke satu metode ALTCHA bersama URL halaman, lalu tulis token ke field tersembunyi, yang namanya ditentukan oleh atribut name milik widget itu sendiri, di dalam form tempat tombol submit berada. Jangan menyentuh token di perjalanan. Jaga pengambilan, pemecahan dan pengiriman tetap berdekatan, karena jendelanya bisa tertutup dalam waktu kurang dari dua menit dan challenge yang kedaluwarsa terlihat persis seperti jawaban yang salah.
- Apa itu tipe ini dan bagaimana cara kerjanya: halaman pemecah ALTCHA.
- Pekerjaan yang sama tanpa browser sama sekali: memecahkan ALTCHA di Python biasa.
Satu hal terakhir yang mengubah cara Anda merancang retry. Karena jalur ini menuju pemecah captcha tanpa batas menghitung proof of work di perangkat keras yang sudah Anda miliki, challenge yang dibuang hanya memakan beberapa milidetik CPU Anda sendiri dan tidak lebih, sehingga memuat ulang halaman untuk mendapat yang baru selalu lebih murah daripada merawat token basi sepanjang pengujian yang panjang.
