Jak zbudować własny scraper w Pythonie

16 kwietnia, 2026

Scraper w Pythonie to sposób na zbieranie danych z internetu bez płacenia za drogie API – w kilka godzin zbudujesz narzędzie, które pobierze dane cenowe od konkurencji, SERP results, content z branżowych portali lub product catalog. Ten tutorial to kompletny przewodnik od zera: setup środowiska, pierwszy scraper, obsługa JavaScript, storage danych i anti-bot techniques.

Zakładamy podstawową znajomość Pythona – jeśli nie kodowałeś wcześniej, ten tutorial przeprowadzi cię przez wszystko, ale zrozumienie zmiennych, funkcji i if-ów jest wymagane. Kod w tutorialu działa na Pythonie 3.10+ i biblioteki aktualne na 2026 rok.

W skrócie

  • Czas zbudowania działającego scrapera: 2–4 godziny dla początkującego, 30 min dla doświadczonego.
  • Stack: Python 3.10+, requests, BeautifulSoup, Playwright (dla JS), pandas (dla storage).
  • Koszt: 0 zł – wszystkie biblioteki open source, lokalny serwer wystarczy.
  • Legalne scrapowanie wymaga: respektowania robots.txt, rozsądnego rate limit, nie pobierania danych osobowych.
  • Anti-bot techniques rosną – w 2026 Cloudflare Turnstile, Akamai Bot Manager, PerimeterX są standardem dla e-commerce.

Setup środowiska Python

Krok 1: Instalacja Pythona

Python 3.10+ z oficjalnej strony python.org (Windows, Mac) lub brew install python3 (Mac). Linux zwykle ma Pythona 3, sprawdź python3 --version.

Krok 2: Virtualenv i pip

python3 -m venv scraper-env
source scraper-env/bin/activate  # Linux/Mac
scraper-envScriptsactivate   # Windows
pip install requests beautifulsoup4 lxml pandas playwright
playwright install chromium

Krok 3: Edytor

VS Code z rozszerzeniem Python lub PyCharm Community (darmowy). Konfiguracja formatowania – Black auto-formatter, pip install black. Powiązane zagadnienia opisujemy w narzędzia SEO.

Krok 4: Struktura projektu

scraper/
├── main.py
├── scrapers/
│   ├── __init__.py
│   └── example_scraper.py
├── storage/
│   └── data.csv
├── requirements.txt
└── README.md

Pierwszy scraper – statyczna strona HTML

Najprostszy scraper do statycznej strony (bez JavaScript). Przykład: scrape listy artykułów z bloga. Praktyczne wskazówki zawiera przewodniku po stacku marketingowym 2026.

import requests
from bs4 import BeautifulSoup
import pandas as pd

URL = "https://example.com/blog"
HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; MyBot/1.0)"
}

def scrape_blog():
    response = requests.get(URL, headers=HEADERS, timeout=10)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "lxml")

    articles = []
    for article in soup.select("article.post"):
        title = article.select_one("h2").get_text(strip=True)
        link = article.select_one("a")["href"]
        date = article.select_one(".date").get_text(strip=True)
        articles.append({
            "title": title,
            "link": link,
            "date": date
        })

    return articles

if __name__ == "__main__":
    data = scrape_blog()
    df = pd.DataFrame(data)
    df.to_csv("storage/blog.csv", index=False)
    print(f"Scraped {len(data)} articles")

Co się dzieje

  • requests.get pobiera HTML z URL.
  • BeautifulSoup parsuje HTML do struktury, którą można przeszukiwać.
  • CSS selectory (article.post, h2) znajdują elementy.
  • pandas zapisuje do CSV.

Jak znaleźć CSS selectory

  1. Otwórz stronę w Chrome.
  2. F12 – DevTools.
  3. Prawy klik na element, który chcesz – Inspect.
  4. Prawy klik na HTML w DevTools – Copy → Copy selector.
  5. Uprość selector (usuwając zbędne ID/klasy z dziwnymi nazwami).

Obsługa paginacji

Większość stron ma więcej niż jedną stronę wyników. Scraper musi iterować.

def scrape_all_pages():
    all_articles = []
    page = 1

    while True:
        url = f"{URL}?page={page}"
        response = requests.get(url, headers=HEADERS)
        soup = BeautifulSoup(response.text, "lxml")

        articles = soup.select("article.post")
        if not articles:
            break

        for article in articles:
            title = article.select_one("h2").get_text(strip=True)
            all_articles.append({"title": title})

        page += 1
        time.sleep(2)  # be polite

        if page > 100:  # safety limit
            break

    return all_articles

Dobre praktyki paginacji

  • time.sleep(2) – 2 sekundy między requestami. Nie zabijaj serwera.
  • Safety limit – nieskończone pętle to problem.
  • Logging progresji – print(f"Page {page}: {len(articles)} items").
  • Save partial results – co 10 stron zapisuj do CSV, żeby nie stracić wszystkiego przy crash.

Scraping stron z JavaScript – Playwright

Nowoczesne strony (React, Vue, Angular) renderują content przez JavaScript. requests zwraca pustą stronę. Rozwiązanie: Playwright, który odpala pełną przeglądarkę.

from playwright.sync_api import sync_playwright

def scrape_spa():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto("https://spa-example.com")
        page.wait_for_selector("article.post")

        articles = page.query_selector_all("article.post")
        data = []
        for article in articles:
            title = article.query_selector("h2").inner_text()
            data.append({"title": title})

        browser.close()
        return data

Kiedy Playwright vs requests

  • requests – statyczne HTML, starsze strony, szybko.
  • Playwright – SPA, wolniej (2–5x), ale obsługuje JavaScript.
  • Test: curl https://url.com – jeśli content widzisz w HTML, requests wystarczą. Jeśli nie, Playwright.

Playwright headless vs headed

  • headless=True – bez UI, szybciej, dla produkcji.
  • headless=False – widzisz przeglądarkę, debug.
  • slow_mo=1000 – spowalnia dla obserwacji.

Storage danych – CSV, SQLite, PostgreSQL

CSV (prosty start)

import pandas as pd
df = pd.DataFrame(data)
df.to_csv("output.csv", index=False, encoding="utf-8")

Plusy: prosty, Excel-friendly. Minusy: nie skaluje się > 100k rows, brak struktury, trudny query.

SQLite (dobry middle ground)

import sqlite3

conn = sqlite3.connect("scraper.db")
df.to_sql("articles", conn, if_exists="append", index=False)
conn.close()

Plusy: file-based, jeden plik, SQL queries. Minusy: lokalny, nie networked.

PostgreSQL (scale)

from sqlalchemy import create_engine
engine = create_engine("postgresql://user:pass@localhost/db")
df.to_sql("articles", engine, if_exists="append", index=False)

Plusy: production-ready, concurrent access, indexing. Minusy: setup wymaga serwera.

Error handling i resiliencja

Produkcyjny scraper musi obsługiwać awarie: timeouts, 503, network errors, zmiany w HTML.

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=4, max=10)
)
def safe_scrape(url):
    response = requests.get(url, headers=HEADERS, timeout=15)
    response.raise_for_status()
    return response.text

try:
    html = safe_scrape(url)
except Exception as e:
    print(f"Failed after 3 retries: {e}")
    log_to_file(url, str(e))

Co catchować

  • requests.Timeout – serwer wolny, retry.
  • requests.HTTPError – 4xx/5xx, retry lub skip.
  • requests.ConnectionError – sieć, retry.
  • AttributeError – BeautifulSoup selector nie znalazł elementu, może HTML się zmienił – log i skip.

Rate limiting i grzeczne scrapowanie

Agresywne scrapowanie = ban IP. Zasady grzeczności.

Respect robots.txt

from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
if rp.can_fetch("MyBot", "https://example.com/page"):
    # OK to scrape
    pass

Rate limit

  • 1–2 sekundy między requestami dla większości stron.
  • 5–10 sekund dla wrażliwych stron (banki, gov).
  • Randomizuj – time.sleep(random.uniform(1, 3)) zamiast stałego 2s.
  • Concurrent requests: max 2–5 naraz, nie 100.

User-Agent rotation

import random

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0) Chrome/120.0",
    "Mozilla/5.0 (Macintosh) Safari/604.1",
    "Mozilla/5.0 (X11; Linux) Firefox/120.0"
]

headers = {"User-Agent": random.choice(USER_AGENTS)}

Anti-bot detection i jak sobie radzić

W 2026 Cloudflare, Akamai, PerimeterX blokują scrapery. Etyczne techniki bypass.

Cloudflare challenge

Cloudflare Turnstile pokazuje „Checking your browser” page. Rozwiązania:

  • Playwright stealthpip install playwright-stealth, ukrywa sygnały automatyzacji.
  • undetected-chromedriver – Selenium-based, ale bardziej skuteczny niż vanilla.
  • Commercial APIs – ScraperAPI, Bright Data solve Cloudflare automatycznie. Zobacz porównanie scraping API.

Rate limit detection

  • 429 Too Many Requests – wait 60 sekund, retry.
  • IP rotation – proxy list (ScraperAPI, Bright Data).
  • Session cookies – często strona memoryzuje „ta sesja już pobrała X” – twórz nowe sesje.

CAPTCHA

Jeśli pojawia się CAPTCHA – scraping tej strony jest sygnalizowany jako problematyczny. Etyczne opcje:

  • Użyj oficjalnego API (jeśli istnieje).
  • Kontakt z właścicielem strony – często zgadzają się na dostęp w zamian za atrybucję.
  • Commercial scraping API, które mają umowy na CAPTCHA solving.
  • Jeśli strona jawnie zabrania scrapingu w ToS – nie scrapuj.

Aspekty prawne scrapowania

Krótka wersja: scrapowanie jest legalne w większości przypadków, ale są granice. Pełne informacje w artykule o legalnym scrapowaniu.

Co jest OK

  • Publicznie dostępne dane (bez logowania).
  • Dane niekomercyjne (dla analiz własnych).
  • Respektowanie robots.txt i rate limit.
  • Dane bez osobowych informacji (RODO).

Co nie jest OK

  • Omijanie zabezpieczeń technicznych – CFAA w USA, art. 267 KK w Polsce.
  • Scrapowanie treści chronionych copyrightem do dalszej publikacji.
  • Zbieranie danych osobowych bez podstawy RODO.
  • DDoS-like scrapowanie (1000 requestów/sekundę).

Trzy gotowe przykłady scraperów

Przykład 1: SERP scraper (dla SEO research)

Pobiera top 10 wyników Google dla listy fraz. Używaj z proxy i rate limit – Google aktywnie blokuje scraperów.

import requests
from bs4 import BeautifulSoup

def scrape_google(query):
    url = f"https://www.google.com/search?q={query}"
    response = requests.get(url, headers={
        "User-Agent": "Mozilla/5.0..."
    })
    soup = BeautifulSoup(response.text, "lxml")
    results = []
    for result in soup.select("div.g"):
        title = result.select_one("h3")
        link = result.select_one("a")
        if title and link:
            results.append({
                "title": title.text,
                "url": link["href"]
            })
    return results

Przykład 2: Price monitoring konkurencji

Pobiera ceny produktów z konkurencyjnych sklepów. Daily cron, storage w SQLite, alerty przy zmianie > 10%.

Przykład 3: Content aggregator

Pobiera tytuły i wstępy z 20 branżowych blogów. Używany do content research – co publikuje konkurencja, jakie tematy rosną.

Deploy scrapera – od local do cloud

Local cron

crontab -e, dodaj: 0 6 * * * /path/to/python /path/to/scraper.py. Scrapuje codziennie o 6:00. Minusy: komputer musi być włączony.

VPS – Hetzner, DigitalOcean

20–50 zł/mies VPS, deploy scrapera, cron. Zalety: always-on, tani. Minusy: setup systemu, utrzymanie.

Serverless – AWS Lambda, Cloudflare Workers

Płacisz za run, nie za utrzymanie. Lambda: max 15 min per run, ~2 USD za 1 mln runs. Dobry dla lekkich scraperów.

Docker + Kubernetes (enterprise)

Dla wielu scraperów, load balancing, scaling. Overkill dla pojedynczego projektu.

Scrapy framework dla większych projektów

Scrapy to pełen framework scrapingu – strukturyzuje kod, zarządza concurrent requests, ma built-in storage i middleware. Warto gdy robisz > 3 scrapery lub scrapujesz > 10k stron dziennie.

Instalacja i setup

pip install scrapy
scrapy startproject myscraper
cd myscraper
scrapy genspider example example.com

Anatomia Scrapy spidera

import scrapy

class ExampleSpider(scrapy.Spider):
    name = "example"
    start_urls = ["https://example.com/blog"]
    custom_settings = {
        "DOWNLOAD_DELAY": 2,
        "CONCURRENT_REQUESTS": 4,
        "USER_AGENT": "MyBot/1.0",
    }

    def parse(self, response):
        for article in response.css("article.post"):
            yield {
                "title": article.css("h2::text").get(),
                "link": article.css("a::attr(href)").get(),
            }
        next_page = response.css("a.next::attr(href)").get()
        if next_page:
            yield response.follow(next_page, self.parse)

Zalety Scrapy

  • Auto-handling concurrent requests, throttling.
  • Built-in storage – JSON, CSV, SQLite, custom pipelines.
  • Middleware system – proxy rotation, user-agent rotation, retry logic.
  • Extensions – monitoring, statistics, memory debugger.
  • Scrapyd – deployment platform.

Wady Scrapy

  • Learning curve – więcej konceptów do opanowania.
  • Overkill dla pojedynczego skryptu.
  • Async model może być mylący dla początkujących.

Proxy – kiedy i jak używać

Proxy to serwer pośredni – twoje requesty wychodzą z proxy IP, nie twojego. Przydatne dla: rotacji IP (unikanie bana), dostępu do stron geo-blokowanych, anonimowości.

Typy proxy

  • Datacenter proxy – tanie (2–5 USD/mies per IP), szybkie, ale łatwo wykrywalne. Dobre dla statycznych stron.
  • Residential proxy – droższe (5–15 USD/GB ruchu), IP od realnych użytkowników. Trudno wykrywalne. Dla e-commerce, Google.
  • Mobile proxy – najdroższe (10–50 USD/GB), IP od urządzeń mobilnych. Najlepsze unmask, ale overkill dla większości zadań.

Dostawcy proxy

  • Bright Data – największy dostawca, residential i mobile, od $500/mies.
  • Oxylabs – konkurent Bright Data, premium quality.
  • Smartproxy – tańszy, od $75/mies.
  • ScrapingBee – proxy + headless browser w jednym, od $49/mies.
  • ProxyMesh – tanie datacenter, od $10/mies.

Implementacja proxy w requests

proxies = {
    "http": "http://user:pass@proxy.example.com:8080",
    "https": "http://user:pass@proxy.example.com:8080"
}
response = requests.get(url, proxies=proxies)

Rotacja proxy

import random

PROXY_LIST = [
    "http://proxy1.com:8080",
    "http://proxy2.com:8080",
    "http://proxy3.com:8080",
]

def get_random_proxy():
    return {"https": random.choice(PROXY_LIST)}

response = requests.get(url, proxies=get_random_proxy())

Monitoring działającego scrapera

Scraper uruchomiony na VPS może failować bez twojej wiedzy. Monitoring obowiązkowy.

Health checks

  • Każdy udany run zapisuje timestamp w pliku lub DB.
  • Cron job (lub external monitor) sprawdza, czy timestamp jest recent.
  • Jeśli > 2x expected interval – alert (email, Slack).

Metryki do śledzenia

  • Liczba pobranych URL per run.
  • Liczba errors (4xx, 5xx, timeouts).
  • Czas wykonania.
  • % udanych parsings (items zwracane vs planned).

Alerty

  • Run failed completely – natychmiast.
  • > 20% errors w runie – alert.
  • < 50% expected items – możliwa zmiana HTML, alert.
  • Run trwa > 2x average – możliwa blockada, alert.

Najczęstsze błędy

Błąd 1: Nie handlować błędów HTML

Strona zmieniła strukturę – selector zwraca None, AttributeError, scraper wywala się. Zawsze sprawdzaj, czy element istnieje: if elem is None: continue.

Błąd 2: Za agresywne rate limit

100 requestów/sek = ban IP w 5 minut. Domyślnie 1–2 sek między requestami, rosnące z feedbacku (429 → wait dłużej).

Błąd 3: Storage bez deduplikacji

Scrapujesz blog codziennie, każdy artykuł trafia do DB ponownie. Unique constraint na URL lub hash content.

Błąd 4: Ignorowanie robots.txt

Pierwsza rzecz, którą sprawdzają prawnicy przy dispute. Zawsze respektuj.

Błąd 5: Brak logging

Scraper failuje w nocy – nie wiesz dlaczego. Używaj logging module, zapisuj do pliku, rotate codziennie.

FAQ — najczęstsze pytania

Czy scrapowanie jest legalne w Polsce?

W większości przypadków tak, dla publicznych danych bez zabezpieczeń. Art. 267 KK karze „bezprawne uzyskanie dostępu” – kluczowe słowo „bezprawne”. Dane publiczne, respect robots.txt, rozsądne rate limit – bezpiecznie. Dane chronione hasłem, ToS zakazujące scrapingu, omijanie CAPTCHA – ryzyko. Szczegóły w artykule o legalnym scrapowaniu.

Requests czy Playwright – co wybrać?

Requests dla statycznego HTML (90% starszych stron). Playwright dla SPA (React, Vue). Test: curl url.com | grep "target-element" – jeśli element jest, requests wystarczą. Requests są 5–10x szybsze, więc preferuj je, gdy możliwe.

Ile kosztuje utrzymanie produkcyjnego scrapera?

VPS (Hetzner, DO) 20–50 zł/mies + proxy (jeśli potrzebne) 50–200 zł/mies + storage DB 0–30 zł/mies = typowo 100–250 zł/mies per scraper. Enterprise z proxy, commercial APIs – 500–2000 zł/mies. Dla pojedynczego projektu lokalny cron na laptopie wystarczy – 0 zł.

Czy warto budować własny scraper zamiast używać API?

Zależy od skali. Pojedynczy projekt, < 1000 requestów dziennie – własny scraper jest tańszy (0 zł vs 50 USD/mies API). Wiele stron, > 10k requestów dziennie, anti-bot protection – commercial API jest tańsze niż czas na building custom infrastructure. Zobacz porównanie scraping API.

Jak często aktualizować scraper po zmianach na stronie?

Statyczne strony – raz na 3–6 miesięcy, jeśli monitor nie sygnalizuje błędów. E-commerce, frequent redesigns – co miesiąc check. Monitor: alert, gdy scraper zwraca < 50% expected results. Dobrze napisany scraper z defensive coding (try/except, optional chaining) przeżyje małe zmiany, ale major redesign wymaga rewrite selectorów.

Czy mogę sprzedawać dane ze scrapingu?

Ostrożnie. Surowe dane ze strony A sprzedawane jako produkt B to prawdopodobnie copyright infringement. Przetworzone, zagregowane dane (analytics, insights, statystyki) – zwykle OK. Dane osobowe – nie bez podstawy RODO. Zawsze konsultuj z prawnikiem przed monetyzacją, zasady różnią się jurysdykcyjnie.

Czy scraping w Pythonie jest lepszy niż Node.js?

Oba działają. Python: więcej bibliotek (BeautifulSoup, Scrapy, pandas), łatwiejszy dla data science. Node.js: Puppeteer/Playwright też działają, lepsze dla JavaScript-heavy sites. Dla 90% projektów Python wystarczy. Wybór zależy od tego, co twój zespół lepiej zna.

Co dalej

Na początek sprawdź scraping API. Gdy opanujesz podstawy, przejdź do legalne scrapowanie — tam czekają zaawansowane techniki.