Cara Menangani CAPTCHA Scrapy dengan Downloader Middleware

scrapy captcha - How to Handle a Scrapy CAPTCHA with Downloader Middleware

Captcha di Scrapy sebaiknya ditangani di downloader middleware, bukan di spider Anda. Middleware melihat setiap respons, sehingga ia dapat mengenali tantangan sekali, memecahkannya, dan menyerahkan halaman aslinya kembali ke spider seolah-olah tidak terjadi apa-apa. Metode parse Anda tetap bersih. Panduan ini membangun middleware tersebut, menjaga pemecahan tetap di luar event loop, dan membahas pengaturan yang menentukan apakah ia berhasil pada skala besar atau justru menghentikan crawl Anda.

Apa yang Anda butuhkan

  • Scrapy 2.x dan Python 3.10 atau lebih baru
  • CapSkip berjalan secara lokal, dengan API server aktif. Lihat panduan penyiapan
  • SDK Python: pip install capskip

Semua di bawah ini mengasumsikan pemecahnya ada di 127.0.0.1:8080. Tidak ada yang keluar dari komputer Anda, yang lebih penting dari biasanya pada sebuah crawler: Anda sudah mengirim banyak permintaan, dan perjalanan bolak-balik per pemecahan ke pihak ketiga menambah latensi pada setiap permintaan itu.

Mengapa penanganan CAPTCHA di Scrapy sebaiknya berada dalam middleware

Menangani tantangan di dalam callback berarti setiap callback memerlukan percabangan yang sama. Lewatkan satu saja dan spider itu diam-diam mengurai halaman blokir seolah-olah itu data. Downloader middleware berada di antara downloader dan spider, sehingga ia menerima respons lebih dulu dan dapat menggantinya.

Penempatan itu memberi Anda tiga hal. Deteksi berada di satu fungsi alih-alih disalin-tempel ke berbagai spider. Callback spider hanya menerima halaman yang sungguhan, sehingga selektornya boleh mengasumsikan markup yang mereka harapkan. Dan ketika sebuah situs mengubah tantangannya, Anda menyunting satu berkas alih-alih mengaudit satu proyek.

Urutan itu penting, dan di situlah orang biasanya keliru. Scrapy memanggil process_request dalam urutan menaik dan process_response di menurun urutannya. Mendaftar di 585 berarti middleware kami melihat respons sebelum RetryMiddleware pada 550.

# settings.py
DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.CaptchaMiddleware": 585,
}

# The async client needs Scrapy's asyncio reactor.
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

Langkah 1: deteksi challenge

Dua sinyal mencakup sebagian besar situs. Kode status, dan sebuah penanda di body. Periksa keduanya, karena banyak situs menyajikan tantangan dengan status 200.

# Markers for the two widgets you will hit most often.
CAPTCHA_MARKERS = ("g-recaptcha", "cf-turnstile")

def looks_like_captcha(response):
    if response.status in (403, 429):
        return True
    # Only touch the body for HTML; binary responses have no text.
    ctype = response.headers.get("Content-Type", b"").decode()
    if "html" not in ctype:
        return False
    return any(m in response.text for m in CAPTCHA_MARKERS)

Jaga fungsi ini tetap membosankan dan murah. Ia berjalan pada setiap respons dalam crawl.

Langkah 2: ambil sitekey dari halaman

Sitekey adalah atribut publik pada elemen widget. Baca dari respons yang sudah Anda miliki, jangan pernah dari konstanta hardcoded, karena situs merotasinya.

def extract_sitekey(response):
    # reCAPTCHA v2 and invisible both use data-sitekey.
    key = response.css(".g-recaptcha::attr(data-sitekey)").get()
    if key:
        return "recaptcha", key
    key = response.css(".cf-turnstile::attr(data-sitekey)").get()
    if key:
        return "turnstile", key
    return None, None

Jika widget disuntikkan oleh JavaScript, sitekey tidak akan ada di HTML yang diunduh Scrapy. Itu adalah masalah rendering, bukan masalah pemecahan, dan biasanya berarti Anda perlu mengambil kuncinya dari tag skrip dengan regex.

Langkah 3: pecahkan tanpa memblokir proses crawling

Inilah langkah yang diam-diam merusak throughput. Scrapy berjalan pada event loop berulir tunggal. Satu pemecahan memakan waktu beberapa detik, jadi memanggil klien yang memblokir langsung di dalam middleware Anda akan membekukan semua permintaan lain yang sedang berjalan selama waktu itu. Dengan CONCURRENT_REQUESTS di 16, Anda baru saja membuat ke-16-nya berjalan berurutan.

Dua jalan keluar yang benar. Python SDK menyertakan AsyncCapSkip, yang merupakan client async sungguhan dan bukan sekadar alias, sehingga di bawah reactor asyncio Anda dapat langsung meng-await-nya:

# pip install capskip
from capskip import AsyncCapSkip

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

# Submit and poll both happen inside this await.
result = await solver.recaptcha(
    sitekey="YOUR_SITEKEY",
    url="https://example.com/page-with-recaptcha",
)
token = result["code"]

Jika Anda masih memakai reactor Twisted klasik, pindahkan client yang blocking ke sebuah thread dan await Deferred-nya:

from capskip import CapSkip
from twisted.internet.threads import deferToThread

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

# deferToThread keeps the reactor free while the solve runs.
result = await deferToThread(
    solver.recaptcha,
    sitekey="YOUR_SITEKEY",
    url="https://example.com/page-with-recaptcha",
)

Keduanya berhasil karena Scrapy mengizinkan metode downloader middleware apa pun menjadi fungsi coroutine. Definisikan dengan async def dan Scrapy menangani sisanya.

Langkah 4: kirim ulang dengan token

Token saja tidak melakukan apa-apa. Token harus dikembalikan ke situs dengan cara yang sama seperti front end situs itu sendiri mengirimnya, yang untuk formulir klasik berarti sebuah field bernama g-recaptcha-response.

Kembalikan Request dari process_response dan Scrapy menjadwalkan ulang permintaan itu. Dua detail mencegah hal ini berjalan keliru: dont_filter=True, karena URL-nya sudah pernah dilihat dan filter duplikat akan membuangnya, dan sebuah penghitung di meta sehingga situs yang terus memberikan challenge tidak dapat berulang selamanya.

from scrapy import FormRequest

def resubmit(response, token, tries):
    return FormRequest.from_response(
        response,
        formdata={"g-recaptcha-response": token},
        dont_filter=True,          # the dupe filter has seen this URL
        meta={"captcha_tries": tries + 1},
    )

Middleware lengkap

# myproject/middlewares.py
import logging

from capskip import (
    AsyncCapSkip, ApiException, NetworkException, TimeoutException)
from scrapy import FormRequest
from scrapy.exceptions import IgnoreRequest

logger = logging.getLogger(__name__)
MAX_CAPTCHA_TRIES = 2


class CaptchaMiddleware:
    def __init__(self):
        self.solver = AsyncCapSkip(host="127.0.0.1", port=8080)

    async def process_response(self, request, response, spider):
        if not looks_like_captcha(response):
            return response

        tries = request.meta.get("captcha_tries", 0)
        if tries >= MAX_CAPTCHA_TRIES:
            raise IgnoreRequest("captcha not cleared: %s" % request.url)

        kind, sitekey = extract_sitekey(response)
        if not sitekey:
            return response          # not a shape we handle

        try:
            if kind == "turnstile":
                result = await self.solver.turnstile(
                    sitekey=sitekey, url=response.url)
            else:
                result = await self.solver.recaptcha(
                    sitekey=sitekey, url=response.url)
        except (ApiException, NetworkException, TimeoutException) as e:
            logger.warning("solve failed for %s: %s", request.url, e)
            return response

        logger.info("solved %s captcha for %s", kind, request.url)

        return FormRequest.from_response(
            response,
            formdata={"g-recaptcha-response": result["code"]},
            dont_filter=True,
            meta={**request.meta, "captcha_tries": tries + 1},
        )

SDK ini melemparkan empat jenis exception: ValidationException, NetworkException, ApiException dan TimeoutException. Tiga di atas adalah yang terjadi saat runtime; sedangkan ValidationException berarti parameter Anda salah dan seharusnya gagal secara mencolok selama pengembangan alih-alih ditelan diam-diam. Mengembalikan respons asli saat gagal membuat sisa pipeline Anda yang memutuskan langkah berikutnya, dan itu lebih baik daripada membuat spider crash.

Pengaturan yang benar-benar penting

PengaturanMengapa
CONCURRENT_REQUESTS_PER_DOMAINTurunkan. Terkena CAPTCHA biasanya merupakan sinyal laju permintaan, dan memecahkan lebih cepat tidak memperbaiki alasan Anda ditantang
DOWNLOAD_DELAY plus AUTOTHROTTLE_ENABLEDLebih murah daripada memecahkan. Setiap challenge yang Anda hindari tidak memakan biaya
COOKIES_ENABLEDHarus tetap aktif. Cookie clearance dari tantangan yang sudah dipecahkan itulah yang mencegah permintaan berikutnya ditantang lagi
RETRY_TIMESTerpisah dari penghitung CAPTCHA Anda. Pisahkan kedua batas itu atau keduanya akan berlipat ganda

Baris ketiga itulah yang perlu benar-benar dipahami. Jika cookie dimatikan, setiap permintaan terlihat seperti kunjungan pertama dan Anda akan memecahkan tantangan yang sama selamanya. Sebagian besar perulangan captcha Scrapy yang dilaporkan orang ternyata persis seperti ini.

CAPTCHA gambar di dalam spider

Beberapa situs memakai gambar teks terdistorsi biasa pada formulir login. Tidak ada sitekey yang terlibat, jadi lebih sederhana: unduh gambarnya dan kirim byte-nya sebagai data URI.

import base64
import scrapy
from capskip import CapSkip

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

class LoginSpider(scrapy.Spider):
    def parse_captcha_image(self, response):
        # response.body is the raw image, fetched with session cookies.
        b64 = base64.b64encode(response.body).decode()
        result = solver.normal("data:image/png;base64," + b64)
        return result["code"]     # the text on the image

normal() juga menerima jalur berkas atau URL jarak jauh. Bentuk data URI adalah yang Anda perlukan di Scrapy, karena gambarnya biasanya hanya dirender dengan benar untuk sesi yang memintanya. Perlu dicatat bahwa proxy tidak didukung untuk CAPTCHA gambar, hanya untuk reCAPTCHA, Turnstile, dan GeeTest.

Kesalahan CAPTCHA Scrapy yang umum

GejalaPenyebabPerbaiki
Throughput crawl anjlokPemecahan yang memblokir event loopGunakan AsyncCapSkip atau deferToThread
Permintaan yang dikirim ulang tidak pernah berjalanFilter duplikat membuangnyaTambah dont_filter=True
Halaman yang sama terus memunculkan challengeCookie dinonaktifkan, atau meta tidak diteruskanAktifkan cookie, gabungkan request.meta ke dalam permintaan baru
ERROR_GOOGLEKEYsitekey sudah usang atau di-hardcodeBaca dari respons langsung setiap kali
NetworkException pada setiap pemecahanCapSkip tidak berjalan atau portnya berbedaJalankan aplikasinya, periksa port di Settings

Setiap string error yang dapat dikembalikan API tercantum di dokumentasi API. Scrapy sendiri memiliki referensi downloader middleware membahas aturan urutan secara lengkap.

Pertanyaan yang sering diajukan

Apakah ini bekerja dengan reactor bawaan Scrapy?

Ya, tetapi hanya deferToThread varian. AsyncCapSkip adalah klien asyncio, jadi ia membutuhkan TWISTED_REACTOR yang disetel ke reactor asyncio. Kedua pendekatan menjaga crawl tetap berjalan.

Haruskah saya memecahkan setiap captcha Scrapy yang saya temui?

Tidak. Serbuan tantangan yang tiba-tiba berarti pola crawl Anda tertandai. Perlambat dulu. Memecahkannya hanya mengobati gejala dan biasanya justru mendatangkan blokir yang lebih keras.

Bisakah saya mengarahkan pemecahan melalui proxy yang sama dengan permintaannya?

Ya, untuk reCAPTCHA, Turnstile dan GeeTest. Teruskan proxy={"type": "HTTPS", "uri": "user:[email protected]:3128"} ke panggilan pemecahan agar token dihasilkan dari exit IP yang sama dengan yang dilihat halaman.

Di mana middleware ini ditempatkan jika saya juga memakai middleware proxy?

Di bawahnya dalam hal process_response ketentuan itu, yang berarti angka yang lebih tinggi. Middleware proxy bekerja pada permintaan; milik kami bekerja pada respons. Pada 585 ia berjalan sebelum middleware retry melihat responsnya.

Ringkasan

Captcha di Scrapy adalah masalah lima bagian: deteksi tantangan di middleware, baca sitekey dari respons langsung, pecahkan tanpa memblokir reactor, kirim ulang dengan dont_filter=True, dan batasi percobaan ulang di meta. Buat itu benar dan spider tetap bersih, karena satu berkas menangani semuanya.

Untuk gambaran yang lebih luas, lihat bagaimana CapSkip cocok dalam sebuah crawler di Pemecah CAPTCHA untuk web scraping halaman, panduan integrasi Python, atau detail widget itu sendiri di pemecah reCAPTCHA v2 halaman. CapSkip adalah pemecah captcha yang berjalan di komputer Anda sendiri, sehingga menambahkannya ke sebuah crawl tidak menambah hop jaringan.