Scrapy CAPTCHA'sı downloader middleware ile nasıl ele alınır

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

Scrapy'de CAPTCHA'nın yeri spider değil, downloader middleware'dir. Middleware her yanıtı gördüğü için challenge'ı bir kez yakalayabilir, çözebilir ve hiçbir şey olmamış gibi gerçek sayfayı spider'a geri verebilir. parse metotlarınız temiz kalır. Bu rehber o middleware'i kurar, çözümü olay döngüsünden uzak tutar ve işin ölçekte çalışıp çalışmayacağını belirleyen ayarları ele alır.

Neye ihtiyacınız var

  • Scrapy 2.x ve Python 3.10 veya üzeri
  • CapSkip yerelde çalışıyor ve API server açık olmalı. Bkz. kurulum kılavuzu
  • Python SDK: pip install capskip

Aşağıdakilerin tamamı çözücünün şu adreste olduğunu varsayar: 127.0.0.1:8080. Hiçbir veri makinenizden çıkmaz; bu bir tarayıcıda her zamankinden daha önemlidir: zaten çok sayıda istek gönderiyorsunuz ve her çözüm için üçüncü tarafa gidiş dönüş, bunların her birine gecikme ekler.

Scrapy'de CAPTCHA işlemenin yeri neden middleware

Challenge'ı callback içinde ele almak, her callback'in aynı dalı içermesi demektir. Birini atlarsanız o spider bir engelleme sayfasını sessizce veri gibi ayrıştırır. Downloader middleware, downloader ile spider arasında durur; yanıtı ilk o alır ve onu değiştirebilir.

Bu konumlandırma size üç şey kazandırır. Tespit, spider'lar arasında kopyalanmak yerine tek bir fonksiyonda yaşar. Spider'ın callback'leri yalnızca gerçek sayfaları alır, bu yüzden seçicileri bekledikleri işaretlemeyi varsayabilir. Ve bir site challenge'ını değiştirdiğinde, bir projeyi baştan sona gözden geçirmek yerine tek bir dosyayı düzenlersiniz.

Sıra önemlidir ve insanların yanıldığı yer tam burasıdır. Scrapy, process_request çağrılarını artan sırada, process_response çağrılarını ise azalan sırada yapar. 585'e kaydetmek, middleware'imizin yanıtları 550'deki RetryMiddleware bileşeninden önce görmesi demektir.

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

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

Adım 1: challenge'ı tespit edin

İki sinyal çoğu siteyi kapsar: durum kodu ve gövdedeki bir işaret. İkisine de bakın, çünkü pek çok site challenge'ı 200 ile sunar.

# 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)

Bu fonksiyonu sıkıcı ve ucuz tutun. Taramadaki her yanıtta çalışır.

Adım 2: sitekey'i sayfadan çekin

sitekey, widget öğesi üzerindeki herkese açık bir özniteliktir. Onu zaten elinizde olan yanıttan okuyun, asla sabit bir değişmezden değil, çünkü siteler bunu döndürür.

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

Widget JavaScript ile enjekte ediliyorsa sitekey, Scrapy'nin indirdiği HTML'de bulunmaz. Bu bir çözüm sorunu değil bir render sorunudur ve genellikle anahtarı script etiketinden düzenli ifadeyle almak anlamına gelir.

Adım 3: taramayı engellemeden çözün

Verimi sessizce mahveden adım budur. Scrapy tek iş parçacıklı bir olay döngüsünde çalışır. Bir çözüm saniyeler sürer, dolayısıyla engelleyici istemciyi doğrudan middleware'inizin içinde çağırmak, o süre boyunca uçuştaki diğer tüm istekleri dondurur. CONCURRENT_REQUESTS 16 iken 16'sını da sıraya dizmiş olursunuz.

İki doğru çıkış yolu var. Python SDK, AsyncCapSkipsınıfını içerir; bu bir takma ad değil gerçek bir asenkron istemcidir, dolayısıyla asyncio reactor altında doğrudan await edebilirsiniz:

# 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"]

Hâlâ klasik Twisted reactor üzerindeyseniz, engelleyici istemciyi bir iş parçacığına itip Deferred'ı await edin:

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",
)

İkisi de çalışır, çünkü Scrapy herhangi bir downloader middleware metodunun coroutine olmasına izin verir. Onu async def ile tanımlayın, gerisini Scrapy halleder.

Adım 4: token ile yeniden gönderin

Token tek başına bir işe yaramaz. Sitenin kendi ön yüzünün göndereceği şekilde siteye geri gitmesi gerekir; klasik bir formda bu, şu adı taşıyan bir alandır: g-recaptcha-response.

Yeni bir Request nesnesini process_response içinden döndürün, Scrapy onu yeniden planlar. İki ayrıntı bunun ters gitmesini önler: dont_filter=True, çünkü bu URL zaten görülmüştür ve yinelenen filtresi onu düşürür; bir de meta içinde bir sayaç, böylece sizi durmadan zorlayan bir site sonsuz döngüye sokamaz.

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'in tamamı

# 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 dört istisna türü fırlatır: ValidationException, NetworkException, ApiException ve TimeoutException. Yukarıdaki üçü çalışma zamanında oluşanlardır; ValidationException ise parametrelerinizin yanlış olduğu anlamına gelir ve yutulmak yerine geliştirme sırasında yüksek sesle başarısız olmalıdır. Hata durumunda özgün yanıtı döndürmek, ne yapılacağına boru hattınızın geri kalanının karar vermesini sağlar; bu, spider'ı çökertmekten iyidir.

Gerçekten önemli olan ayarlar

AyarNeden
CONCURRENT_REQUESTS_PER_DOMAINDüşürün. CAPTCHA'ya çarpmak genellikle bir hız sinyalidir ve daha hızlı çözmek zorlanma nedeninizi ortadan kaldırmaz
DOWNLOAD_DELAY artı AUTOTHROTTLE_ENABLEDÇözmekten ucuzdur. Kaçındığınız her challenge size hiçbir şeye mal olmaz
COOKIES_ENABLEDAçık kalmalı. Çözülmüş bir challenge'dan gelen izin çerezi, sonraki isteğin yeniden zorlanmasını engelleyen şeydir
RETRY_TIMESCAPTCHA sayacınızdan bağımsızdır. İki sınırı ayrı tutun, yoksa çarpışırlar

Üçüncü satır içselleştirilmesi gerekendir. Çerezler kapalıysa her istek ilk ziyaret gibi görünür ve aynı challenge'ı sonsuza kadar çözersiniz. İnsanların bildirdiği Scrapy CAPTCHA döngülerinin çoğu tam olarak budur.

Spider içinde görsel CAPTCHA'lar

Bazı siteler giriş formunda düz, bozulmuş metinli bir görsel kullanır. Burada sitekey yoktur, dolayısıyla daha basittir: görseli indirin ve baytları bir data URI olarak geçirin.

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() ayrıca bir dosya yolu veya uzak URL de alır. Scrapy'de isteyeceğiniz biçim data URI'dir, çünkü görsel genellikle yalnızca onu isteyen oturumda doğru render edilir. Görsel CAPTCHA'lar için proxy desteklenmediğini, yalnızca reCAPTCHA, Turnstile ve GeeTest için desteklendiğini unutmayın.

Yaygın Scrapy CAPTCHA hataları

BelirtiNedenDüzeltme
Tarama verimi çöküyorOlay döngüsünde engelleyici bir çözümKullan AsyncCapSkip veya deferToThread
Yeniden gönderilen istek hiç çalışmıyorYinelenen filtresi onu düşürdüEkle dont_filter=True
Aynı sayfa sonsuza kadar zorluyorÇerezler devre dışı ya da meta ileri taşınmıyorÇerezleri açın, request.meta değerini yeni isteğe birleştirin
ERROR_GOOGLEKEYsitekey bayat ya da sabit yazılmışHer seferinde canlı yanıttan okuyun
NetworkException her çözümdeCapSkip çalışmıyor ya da port farklıUygulamayı başlatın, Settings'te portu kontrol edin

API'nin dönebileceği her hata dizesi şurada listelenmiştir: API dokümantasyonu. Scrapy'nin kendi downloader middleware referansı sıralama kurallarını eksiksiz anlatır.

Sık sorulan sorular

Bu, Scrapy'nin varsayılan reactor'ıyla çalışır mı?

Evet, ama yalnızca deferToThread varyantı. AsyncCapSkip bir asyncio istemcisidir, bu yüzden TWISTED_REACTOR ayarının asyncio reactor'a çekilmesi gerekir. Her iki yaklaşım da taramayı akar hâlde tutar.

Karşılaştığım her Scrapy CAPTCHA'sını çözmeli miyim?

Hayır. Aniden bir challenge duvarıyla karşılaşmak, tarama deseninizin işaretlendiği anlamına gelir. Önce yavaşlayın. Çözerek ilerlemek belirtiyi tedavi eder ve genellikle daha sert bir engel kazandırır.

Çözümü istekle aynı proxy üzerinden geçirebilir miyim?

Evet, reCAPTCHA, Turnstile ve GeeTest için. Çözüm çağrısına proxy={"type": "HTTPS", "uri": "user:[email protected]:3128"} geçirin, böylece token sayfanın gördüğü çıkış IP'sinden üretilir.

Ayrıca bir proxy middleware kullanıyorsam bu middleware nereye girer?

Onun altına, process_response açısından, yani daha büyük bir sayıyla. Proxy middleware'ler isteklere, bizimki yanıtlara etki eder. 585'te, retry middleware yanıtı görmeden önce çalışır.

Özet

Scrapy CAPTCHA'sı beş parçalı bir sorundur: challenge'ı bir middleware'de tespit edin, sitekey'i canlı yanıttan okuyun, reactor'ı engellemeden çözün, dont_filter=Trueile yeniden gönderin ve yeniden denemeleri metaiçinde sınırlayın. Bunları doğru yapın, spider'lar temiz kalır, çünkü işin tamamını tek bir dosya üstlenir.

Daha geniş resim için CapSkip'in bir tarayıcıya nasıl oturduğunu web scraping için CAPTCHA çözücü sayfasında, Python entegrasyon rehberiiçinde ya da widget'ın kendi ayrıntılarını reCAPTCHA v2 çözücü sayfasında bulabilirsiniz. CapSkip, kendi makinenizde çalışan bir captcha çözücü olduğundan, onu bir taramaya eklemek size fazladan bir ağ atlaması getirmez.