Tutorials

CaptchaAI ile İş İlanı Toplayıcı Oluşturun

İş ilanı toplayıcı, birden fazla iş panosunu tarayıp ilanları tek bir aranabilir veritabanında birleştiren bir veri hattıdır. İşin zor kısmı kazımanın kendisi değil: çoğu iş panosu arama sonuçlarını reCAPTCHA v2 gibi doğrulamaların arkasına koyar ve otomatik trafiği tam da burada durdurur. Bu rehberde Python ile böyle bir toplayıcıyı sıfırdan kuruyoruz — CaptchaAI API'si CAPTCHA doğrulamasını çözerken kod, ilanları normalleştirip SQLite'a yazıyor.

Serbest çalışan bir otomasyon geliştiricisiyseniz senaryo tanıdık gelecektir: bir müşteri için uzaktan Python ve veri mühendisliği ilanlarını tek panelde toplamak istiyorsunuz, ama her panonun kendi HTML yapısı ve kendi CAPTCHA akışı var. Aşağıdaki mimari, her yeni panoyu birkaç satır yapılandırmayla ekleyebileceğiniz biçimde tasarlandı.

KVKK notu: İş ilanları çoğu zaman kişisel veri (ad, iletişim bilgisi) içerir. Yalnızca erişim yetkiniz olan ve kullanım koşullarına uygun kaynakları toplayın; topladığınız kişisel veriyi KVKK kapsamında işleyin.


Sistem mimarisi

Toplayıcı, birbirinden bağımsız çalışan üç katmandan oluşur:

  • Kazıyıcı katmanı — her panonun arama sonuçlarını sayfa sayfa indirir.
  • Çözüm katmanı — bir sayfada CAPTCHA çıktığında CaptchaAI API'sini çağırır ve token'ı geri yazar.
  • Normalleştirici — farklı panolardan gelen ilanları tek bir şemaya indirger ve SQLite'a yazar.

Bu ayrım sayesinde yeni bir pano eklemek çözüm ve depolama mantığını hiç değiştirmez. Sonuç, doğrudan sorgulayabileceğiniz bir SQLite veritabanıdır.

[Job Board A] ──┐
[Job Board B] ──┼──> Scraper + CAPTCHA Solver ──> Normalizer ──> SQLite DB
[Job Board C] ──┘

Veri modeli ve SQLite deposu

Her ilanı tek bir JobListing veri sınıfında topluyoruz; başlık, şirket, konum ve kaynak zorunlu, maaş aralığı ile yayın tarihi opsiyonel. JobDatabase sınıfı tabloyu oluşturur ve url alanına UNIQUE kısıtı koyar — böylece aynı ilan farklı sayfalarda tekrar görünse bile veritabanına yalnızca bir kez girer. INSERT OR IGNORE bu tekilleştirmeyi sessizce halleder; search metodu ise anahtar kelime ve konuma göre basit bir sorgu sunar.

# models.py
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import sqlite3
import json


@dataclass
class JobListing:
    title: str
    company: str
    location: str
    url: str
    source: str
    salary_min: Optional[float] = None
    salary_max: Optional[float] = None
    posted_date: Optional[str] = None
    description: str = ""
    tags: list = field(default_factory=list)
    scraped_at: str = field(default_factory=lambda: datetime.now().isoformat())


class JobDatabase:
    def __init__(self, db_path="jobs.db"):
        self.conn = sqlite3.connect(db_path)
        self._create_table()

    def _create_table(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS jobs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                company TEXT NOT NULL,
                location TEXT,
                url TEXT UNIQUE,
                source TEXT,
                salary_min REAL,
                salary_max REAL,
                posted_date TEXT,
                description TEXT,
                tags TEXT,
                scraped_at TEXT
            )
        """)
        self.conn.commit()

    def insert(self, job: JobListing):
        try:
            self.conn.execute(
                """INSERT OR IGNORE INTO jobs
                   (title, company, location, url, source,
                    salary_min, salary_max, posted_date,
                    description, tags, scraped_at)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (job.title, job.company, job.location, job.url,
                 job.source, job.salary_min, job.salary_max,
                 job.posted_date, job.description,
                 json.dumps(job.tags), job.scraped_at),
            )
            self.conn.commit()
        except sqlite3.IntegrityError:
            pass  # Duplicate URL

    def search(self, keyword, location=None):
        query = "SELECT * FROM jobs WHERE title LIKE ?"
        params = [f"%{keyword}%"]
        if location:
            query += " AND location LIKE ?"
            params.append(f"%{location}%")
        query += " ORDER BY scraped_at DESC"
        cursor = self.conn.execute(query, params)
        return cursor.fetchall()

CAPTCHA çözen kazıyıcı tabanı

BaseScraper, tüm panolar için ortak istek mantığını taşır. fetch metodu önce sayfayı normal bir oturumla ister; yanıtta data-sitekey veya g-recaptcha işaretini görürse CAPTCHA olduğunu anlar ve _solve_captcha devreye girer.

Çözüm akışı iki adımlıdır:

  • Görev gönderimi — sitekey ve sayfa URL'si in.php uç noktasına userrecaptcha yöntemiyle gönderilir ve bir görev kimliği alınır.
  • Sonuç sorgulamares.php periyodik olarak sorgulanır; yanıt status: 1 olduğunda çözülmüş token döner.

CAPCHA_NOT_READY dışındaki her yanıt gerçek bir hatadır ve döngüyü sonlandırır. Toplam bekleme bir zaman aşımıyla sınırlıdır, böylece bir çözüm takılırsa kazıyıcı sonsuza kadar beklemez.

Önemli ayrıntı: token'ı aynı self.session üzerinden geri gönderiyoruz. Oturumu istekler arasında yeniden kullanmak çerezlerin korunmasını ve her sayfada yeni bir CAPTCHA tetiklenmemesini sağlar.

# scraper_base.py
import requests
import re
import time
import os


class BaseScraper:
    API_KEY = os.environ["CAPTCHAAI_API_KEY"]

    def __init__(self, source_name):
        self.source = source_name
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 Chrome/125.0.0.0 Safari/537.36",
        })

    def fetch(self, url):
        resp = self.session.get(url, timeout=20)

        if self._has_captcha(resp.text):
            token = self._solve_captcha(url, resp.text)
            resp = self.session.post(url, data={
                "g-recaptcha-response": token,
            }, timeout=30)

        return resp.text

    def _has_captcha(self, html):
        return "data-sitekey" in html or "g-recaptcha" in html

    def _solve_captcha(self, url, html):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if not match:
            raise ValueError("No sitekey found")

        sitekey = match.group(1)

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.API_KEY,
            "method": "userrecaptcha",
            "googlekey": sitekey,
            "pageurl": url,
            "json": 1,
        }, timeout=30)
        task_id = resp.json()["request"]
        time.sleep(15)

        for _ in range(24):
            resp = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.API_KEY, "action": "get",
                "id": task_id, "json": 1,
            }, timeout=15)
            data = resp.json()
            if data.get("status") == 1:
                return data["request"]
            if data["request"] != "CAPCHA_NOT_READY":
                raise RuntimeError(data["request"])
            time.sleep(5)

        raise TimeoutError("CAPTCHA solve timeout")

İş panosu kazıyıcısı

GenericJobScraper, taban sınıfı somut bir panoya bağlar. Her panoyu bir URL şablonu ve bir CSS seçici sözlüğüyle tanımlarsınız; aynı sınıf farklı sitelere yeniden kullanılır. scrape_search sayfa sayfa ilerler ve bir sayfa hiç kart döndürmezse erken durur — bu, boş sayfalar için gereksiz istek atmayı önler.

_parse_listings her karttan başlık, şirket, konum ve bağlantıyı çıkarır; başlık veya şirket eksikse kartı atlar. _extract_salary ise serbest metindeki maaş aralığını bir regex ile yakalar. Maaş biçimleri panodan panoya değiştiği için bu regex'i her kaynak için özelleştirmeniz gerekebilir.

# scrapers.py
from bs4 import BeautifulSoup
from scraper_base import BaseScraper
from models import JobListing
import re


class GenericJobScraper(BaseScraper):
    """Scrape a job board search results page."""

    def __init__(self, source_name, base_url, selectors):
        super().__init__(source_name)
        self.base_url = base_url
        self.selectors = selectors

    def scrape_search(self, keyword, location="", max_pages=3):
        jobs = []

        for page in range(1, max_pages + 1):
            url = self.base_url.format(
                keyword=keyword.replace(" ", "+"),
                location=location.replace(" ", "+"),
                page=page,
            )
            html = self.fetch(url)
            page_jobs = self._parse_listings(html)

            if not page_jobs:
                break
            jobs.extend(page_jobs)

        return jobs

    def _parse_listings(self, html):
        soup = BeautifulSoup(html, "html.parser")
        cards = soup.select(self.selectors["card"])
        jobs = []

        for card in cards:
            title_el = card.select_one(self.selectors["title"])
            company_el = card.select_one(self.selectors["company"])
            location_el = card.select_one(self.selectors.get("location", ".location"))
            link_el = card.select_one(self.selectors.get("link", "a"))

            if not title_el or not company_el:
                continue

            salary = self._extract_salary(card.get_text())

            jobs.append(JobListing(
                title=title_el.get_text(strip=True),
                company=company_el.get_text(strip=True),
                location=location_el.get_text(strip=True) if location_el else "",
                url=link_el["href"] if link_el else "",
                source=self.source,
                salary_min=salary[0],
                salary_max=salary[1],
            ))

        return jobs

    def _extract_salary(self, text):
        match = re.search(
            r'\$?([\d,]+)\s*[-–to]+\s*\$?([\d,]+)', text
        )
        if match:
            return (
                float(match.group(1).replace(",", "")),
                float(match.group(2).replace(",", "")),
            )
        return (None, None)

Çalıştırıcı

main.py, panoları BOARDS listesinden okur, her anahtar kelime için arama yapar ve sonuçları veritabanına yazar. Panolar arasında time.sleep() ile gecikme bırakmak istekleri makul bir hızda tutar. Yeni bir pano eklemek için listeye URL şablonu ve seçicilerden oluşan yeni bir giriş eklemeniz yeterlidir; kodun geri kalanı değişmez.

# main.py
import time
from models import JobDatabase
from scrapers import GenericJobScraper

BOARDS = [
    {
        "name": "Board A",
        "base_url": "https://board-a.example.com/search?q={keyword}&l={location}&p={page}",
        "selectors": {
            "card": ".job-card",
            "title": ".job-title",
            "company": ".company-name",
            "location": ".job-location",
            "link": "a.job-link",
        },
    },
]


def main():
    db = JobDatabase()
    keywords = ["python developer", "data engineer"]

    for board in BOARDS:
        scraper = GenericJobScraper(board["name"], board["base_url"], board["selectors"])

        for keyword in keywords:
            print(f"Scraping {board['name']} for '{keyword}'...")
            jobs = scraper.scrape_search(keyword, location="Remote")

            for job in jobs:
                db.insert(job)
                print(f"  {job.title} at {job.company}")

            time.sleep(5)

    # Search example
    results = db.search("python", "Remote")
    print(f"\nFound {len(results)} matching jobs")


if __name__ == "__main__":
    main()

Ölçekleme ve maliyet

Tek bir panoyu ara ara tararken CAPTCHA nadiren çıkar. Onlarca panoyu düzenli aralıklarla taramaya başladığınızda ise eşzamanlı çözüm ihtiyacı artar. CaptchaAI thread tabanlı fiyatlandırır: her thread aynı anda bir CAPTCHA'yı işler ve thread başına çözüm sayısı sınırsızdır. İhtiyaca göre iki tipik başlangıç noktası:

  • BASIC ($15/ay, 5 thread) — birkaç panolu küçük bir toplayıcı için yeterli.
  • ADVANCE ($90/ay, 50 thread) — çok sayıda panoyu paralel taramanız gerektiğinde eşzamanlılığı belirgin biçimde artırır.

Fiyatlar USD üzerinden sabittir — TL dalgalanmasından etkilenmeyen öngörülebilir bir aylık maliyet, düzenli çalışan bir veri hattı için gerçek bir avantajdır.

Bu toplayıcı reCAPTCHA v2 çözer. Panolarınız farklı bir doğrulama türü kullanıyorsa aynı desen geçerlidir; yalnızca gönderdiğiniz method parametresini ve geri yazdığınız token alanını değiştirin. CaptchaAI reCAPTCHA v2/v3, Cloudflare Turnstile ve Challenge, GeeTest v3, görüntü/OCR, grid ve BLS doğrulamalarını çözer; hCaptcha ile FunCaptcha ise desteklenmez.


Sorun giderme

Sorun Sebep Düzeltme
Yinelenen ilanlar Aynı iş birden çok sayfada görünüyor UNIQUE kısıtı ile URL tabanlı tekilleştirme
Maaş çıkarımı başarısız Panoda standart olmayan biçim _extract_salary regex'ini pano başına özelleştirin
Her sayfada CAPTCHA Oturum korunmuyor self.session'ı istekler arasında yeniden kullanın
Çözümden sonra ilanlar boş CAPTCHA formu JS gerektiriyor Selenium + CaptchaAI akışına geçin

Sık sorulan sorular

Bu toplayıcı hangi CAPTCHA türlerini çözebilir?

Örnek reCAPTCHA v2 için yazıldı. CaptchaAI ayrıca reCAPTCHA v3, Cloudflare Turnstile ve Challenge, GeeTest v3, görüntü/OCR, grid ve BLS doğrulamalarını çözer. hCaptcha ve FunCaptcha desteklenmez; panolarınız bunları kullanıyorsa bu deseni uygulayamazsınız.

Toplayıcıyı çalıştırmak için hangi plan yeterli?

Birkaç panolu küçük bir kurulum için BASIC ($15/ay, 5 thread) genelde yeterlidir. Daha fazla panoyu paralel taramanız ve gecikmeyi düşürmeniz gerekiyorsa ADVANCE ($90/ay, 50 thread) eşzamanlı thread sayısını artırır. Fiyatlandırma çözüm başına değil, thread başınadır.

Veri toplamayı yasal olarak nasıl yürütürüm?

Yalnızca erişim ve kullanım hakkınız olan kaynakları tarayın, sitelerin kullanım koşullarına uyun ve istekleri makul bir hızda tutun. Türkiye'de topladığınız kişisel veriler KVKK kapsamına girer; bu veriyi yalnızca yetkili ve tanımlı bir amaçla işleyin.

Çözümden sonra ilan sayfası neden boş geliyor?

Sayfa büyük olasılıkla içeriği JavaScript ile yüklüyor; ham HTML'de kart bulunmaz. Bu durumda requests yerine bir headless tarayıcı (Selenium veya Playwright) kullanın ve token'ı CaptchaAI ile çözdükten sonra sayfayı tarayıcıda oluşturun.

Yeni bir iş panosu eklemek zor mu?

Hayır. BOARDS listesine panonun URL şablonunu ve CSS seçicilerini içeren yeni bir giriş eklemeniz yeterli; GenericJobScraper aynı sınıfla çalışır, ek kod gerekmez.


İlgili rehberler


İş ilanı verilerinizi tek yerde toplayın — CaptchaAI ile başlayın.

Bu makale için yorumlar devre dışı bırakılmıştır.