Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Web scraping - zbieranie danych z sieci

Witaj ponownie, @name! Darwin tutaj z fascynującą lekcją.

Nauczyłeś/aś się jak pobierać dane z REST API - eleganckich, strukturalnych interfejsów. Ale co gdy strona nie ma API? Co gdy dane o gatunkach są ukryte w HTML tabeli na stronie WWW?

Wtedy używamy web scrapingu - automatycznego pobierania i ekstrakcji danych ze stron internetowych! 🕷️🌐

Analogia Safari: REST API to jak oficjalny raport od parku narodowego w formacie JSON. Web scraping to jak czytanie broszury turystycznej i ręczne wyciąganie informacji: "Aha, tu pisze że jest 120 lwów, a tu że siedlisko to sawanna"! 📄🔍

Czym jest web scraping?

Web scraping to automatyczna ekstrakcja danych ze stron internetowych przez:

  1. Pobranie HTML strony (jak przeglądarka)
  2. Parsowanie struktury HTML (DOM)
  3. Znalezienie interesujących elementów (tabele, listy, nagłówki)
  4. Wyciągnięcie danych (tekst, linki, obrazy)

Kiedy używać web scrapingu?

Używaj gdy:

  • Strona nie ma API
  • API jest płatne, a dane publiczne
  • Potrzebujesz danych historycznych (archiwa)
  • Agregacja danych z wielu źródeł

Unikaj gdy:

  • Strona ma oficjalne API (użyj API!)
  • Regulamin zabrania scrapingu
  • Dane są chronione prawnie
  • Możesz naruszyć RODO/prywatność

Legalność i etyka

WAŻNE: Web scraping jest legalny gdy:

  • ✅ Scrapisz publiczne dane (dostępne bez logowania)
  • ✅ Przestrzegasz robots.txt (
    https://example.com/robots.txt
    )
  • ✅ Nie przeciążasz serwera (rate limiting!)
  • ✅ Nie naruszasz praw autorskich

Złe praktyki:

  • Ignorowanie robots.txt
  • Tysiące zapytań na sekundę (DDoS)
  • Scraping danych osobowych
  • Łamanie logowania/paywall

Złota zasada: Zachowuj się jak człowiek - odwiedzaj stronę rozsądnie, nie przeciążaj serwera!

BeautifulSoup4 - parser HTML w Pythonie

BeautifulSoup4 (bs4) to najpopularniejsza biblioteka do parsowania HTML/XML w Pythonie.

Instalacja

1pip install beautifulsoup4
2pip install requests  # Do pobierania HTML
3pip install lxml      # Szybki parser (opcjonalnie)

Podstawowe użycie

1import requests
2from bs4 import BeautifulSoup
3
4# 1. Pobierz HTML
5response = requests.get("https://example.com/species")
6html_content = response.text
7
8# 2. Sparsuj HTML
9soup = BeautifulSoup(html_content, "html.parser")  # lub "lxml"
10
11# 3. Znajdź elementy
12title = soup.find("h1")
13print(title.text)  # Tekst nagłówka
14
15# 4. Znajdź wszystkie elementy
16species_items = soup.find_all("div", class_="species-card")
17for item in species_items:
18    name = item.find("span", class_="name").text
19    print(name)

Anatomia HTML - co parsujemy?

HTML to drzewo elementów (tagów):

1<div class="species-card" id="lion" data-population="120">
2    <h2>Lew</h2>
3    <p class="description">Panthera leo</p>
4    <img src="/images/lion.jpg" alt="Lew">
5    <a href="/species/lion">Więcej informacji</a>
6</div>

Elementy do ekstrakcji:

  • Tekst:
    <h2>Lew</h2>
    → "Lew"
  • Atrybuty:
    class="species-card"
    ,
    id="lion"
  • Linki:
    <a href="/species/lion">
  • Obrazy:
    <img src="/images/lion.jpg">
  • Dane:
    data-population="120"

Znajdowanie elementów - selektory

find()
- pierwszy pasujący element

1from bs4 import BeautifulSoup
2
3html = """
4<div class="container">
5    <h1>Safari Data</h1>
6    <div class="species">Lew</div>
7    <div class="species">Słoń</div>
8</div>
9"""
10
11soup = BeautifulSoup(html, "html.parser")
12
13# Znajdź pierwszy element
14title = soup.find("h1")
15print(title.text)  # "Safari Data"
16
17# Znajdź pierwszy element z klasą
18species = soup.find("div", class_="species")
19print(species.text)  # "Lew" (pierwszy!)

find_all()
- wszystkie pasujące elementy

1# Znajdź wszystkie elementy z klasą "species"
2all_species = soup.find_all("div", class_="species")
3print(len(all_species))  # 2
4
5for species in all_species:
6    print(species.text)
7# Lew
8# Słoń
9
10# Limit wyników
11first_two = soup.find_all("div", limit=2)

Selektor CSS -
select()

1# CSS selectors (jak w CSS!)
2all_species = soup.select(".species")  # Klasa
3title = soup.select_one("#main-title")  # ID
4links = soup.select("div.container a")  # Zagnieżdżone

Selektory CSS:

  • .species
    - klasa
  • #lion
    - ID
  • div.species
    - tag + klasa
  • div > p
    - bezpośrednie dziecko
  • div p
    - dowolne zagnieżdżenie
  • [data-population]
    - atrybut

Ekstrakcja danych

Tekst elementu

1html = '<div class="name">Lew <span>(Panthera leo)</span></div>'
2soup = BeautifulSoup(html, "html.parser")
3
4# .text - cały tekst (włącznie z zagnieżdżonymi tagami)
5print(soup.find("div").text)  # "Lew (Panthera leo)"
6
7# .get_text() - z separatorem
8print(soup.find("div").get_text(strip=True))  # "Lew(Panthera leo)" (bez spacji)
9
10# .string - tylko bezpośredni tekst (None jeśli zagnieżdżone)
11print(soup.find("div").string)  # None (bo ma <span>)

Atrybuty elementu

1html = '<img src="/lion.jpg" alt="Lew" data-population="120">'
2soup = BeautifulSoup(html, "html.parser")
3img = soup.find("img")
4
5# Dostęp jak do słownika
6print(img["src"])              # "/lion.jpg"
7print(img["alt"])              # "Lew"
8print(img["data-population"])  # "120"
9
10# .get() - bezpieczny (zwraca None jeśli brak)
11print(img.get("title"))        # None
12print(img.get("src", "default.jpg"))  # "/lion.jpg"
13
14# Wszystkie atrybuty
15print(img.attrs)  # {'src': '/lion.jpg', 'alt': 'Lew', 'data-population': '120'}

Linki i URLe

1html = '<a href="/species/lion" class="link">Zobacz lwa</a>'
2soup = BeautifulSoup(html, "html.parser")
3link = soup.find("a")
4
5# URL
6print(link["href"])  # "/species/lion"
7
8# Tekst linku
9print(link.text)     # "Zobacz lwa"
10
11# Wszystkie linki na stronie
12for link in soup.find_all("a"):
13    print(f"{link.text}: {link.get('href')}")

Nawigacja po drzewie DOM

1html = """
2<div class="parent">
3    <h2>Tytuł</h2>
4    <p class="child1">Paragraph 1</p>
5    <p class="child2">Paragraph 2</p>
6</div>
7"""
8soup = BeautifulSoup(html, "html.parser")
9
10# Dzieci (children)
11parent = soup.find("div", class_="parent")
12for child in parent.children:
13    if child.name:  # Pomiń tekst
14        print(child.name)  # h2, p, p
15
16# Potomkowie (descendants) - wszystkie zagnieżdżone
17for descendant in parent.descendants:
18    if hasattr(descendant, 'name') and descendant.name:
19        print(descendant.name)
20
21# Rodzic (parent)
22p = soup.find("p")
23print(p.parent.name)  # "div"
24
25# Rodzeństwo (siblings)
26h2 = soup.find("h2")
27for sibling in h2.next_siblings:
28    if sibling.name:
29        print(sibling.text)  # Paragraph 1, Paragraph 2

Safari przykład - scraping danych o gatunkach

1import requests
2from bs4 import BeautifulSoup
3from typing import List, Dict
4import time
5
6class SafariWebScraper:
7    """Scraper danych o gatunkach z witryny Safari"""
8
9    def __init__(self, base_url: str):
10        self.base_url = base_url
11        self.session = requests.Session()
12        self.session.headers.update({
13            "User-Agent": "Safari-Research-Bot/1.0 (Educational purposes)"
14        })
15
16    def get_soup(self, url: str) -> BeautifulSoup:
17        """Pobierz i sparsuj HTML"""
18        response = self.session.get(url, timeout=10)
19        response.raise_for_status()
20        return BeautifulSoup(response.text, "html.parser")
21
22    def scrape_species_list(self, url: str) -> List[Dict[str, str]]:
23        """
24        Scrapuj listę gatunków z tabeli HTML
25
26        Przykładowa struktura:
27        <table class="species-table">
28            <tr>
29                <td class="name">Lew</td>
30                <td class="scientific">Panthera leo</td>
31                <td class="population">120</td>
32                <td class="habitat">Sawanna</td>
33            </tr>
34        </table>
35        """
36        soup = self.get_soup(url)
37        species_list = []
38
39        # Znajdź tabelę
40        table = soup.find("table", class_="species-table")
41        if not table:
42            print("⚠️ Nie znaleziono tabeli")
43            return []
44
45        # Iteruj po wierszach (pomijając nagłówek)
46        rows = table.find_all("tr")[1:]  # [1:] pomija header
47
48        for row in rows:
49            cells = row.find_all("td")
50            if len(cells) >= 4:
51                species = {
52                    "common_name": cells[0].text.strip(),
53                    "scientific_name": cells[1].text.strip(),
54                    "population": cells[2].text.strip(),
55                    "habitat": cells[3].text.strip()
56                }
57                species_list.append(species)
58
59        return species_list
60
61    def scrape_species_cards(self, url: str) -> List[Dict[str, str]]:
62        """
63        Scrapuj gatunki z kart HTML
64
65        <div class="species-card">
66            <h3>Lew</h3>
67            <p class="scientific">Panthera leo</p>
68            <span class="population" data-count="120">120 osobników</span>
69            <img src="/images/lion.jpg" alt="Lew">
70            <a href="/species/lion">Więcej</a>
71        </div>
72        """
73        soup = self.get_soup(url)
74        species_list = []
75
76        # Znajdź wszystkie karty
77        cards = soup.find_all("div", class_="species-card")
78
79        for card in cards:
80            # Bezpieczna ekstrakcja (może brakować elementów)
81            name_tag = card.find("h3")
82            scientific_tag = card.find("p", class_="scientific")
83            pop_tag = card.find("span", class_="population")
84            img_tag = card.find("img")
85            link_tag = card.find("a")
86
87            species = {
88                "common_name": name_tag.text.strip() if name_tag else "Unknown",
89                "scientific_name": scientific_tag.text.strip() if scientific_tag else "Unknown",
90                "population": pop_tag.get("data-count", "0") if pop_tag else "0",
91                "image_url": img_tag["src"] if img_tag else None,
92                "detail_url": link_tag["href"] if link_tag else None
93            }
94            species_list.append(species)
95
96        return species_list
97
98    def scrape_species_detail(self, url: str) -> Dict[str, any]:
99        """
100        Scrapuj szczegóły gatunku
101
102        <div class="species-detail">
103            <h1>Lew</h1>
104            <div class="info">
105                <dl>
106                    <dt>Nazwa naukowa:</dt>
107                    <dd>Panthera leo</dd>
108                    <dt>Status:</dt>
109                    <dd class="endangered">Zagrożony</dd>
110                    <dt>Siedlisko:</dt>
111                    <dd>Sawanna, step</dd>
112                </dl>
113            </div>
114            <div class="description">
115                <p>Lew to największy kot Afryki...</p>
116            </div>
117        </div>
118        """
119        soup = self.get_soup(url)
120
121        # Nazwa
122        title = soup.find("h1")
123        common_name = title.text.strip() if title else "Unknown"
124
125        # Definition list - pary dt/dd
126        info = {}
127        dl = soup.find("dl")
128        if dl:
129            terms = dl.find_all("dt")
130            definitions = dl.find_all("dd")
131
132            for term, definition in zip(terms, definitions):
133                key = term.text.strip().replace(":", "").lower()
134                value = definition.text.strip()
135                info[key] = value
136
137        # Opis
138        description_div = soup.find("div", class_="description")
139        description = description_div.get_text(strip=True) if description_div else ""
140
141        return {
142            "common_name": common_name,
143            "scientific_name": info.get("nazwa naukowa", "Unknown"),
144            "status": info.get("status", "Unknown"),
145            "habitat": info.get("siedlisko", "Unknown"),
146            "description": description
147        }
148
149    def scrape_with_pagination(self, base_url: str, max_pages: int = 5) -> List[Dict]:
150        """
151        Scrapuj wiele stron z paginacją
152
153        URL pattern: /species?page=1, /species?page=2, ...
154        """
155        all_species = []
156
157        for page_num in range(1, max_pages + 1):
158            url = f"{base_url}?page={page_num}"
159            print(f"Scrapuję stronę {page_num}: {url}")
160
161            soup = self.get_soup(url)
162
163            # Sprawdź czy jest następna strona
164            next_button = soup.find("a", class_="next-page")
165            if not next_button:
166                print(f"Ostatnia strona: {page_num}")
167                break
168
169            # Scrapuj dane ze strony
170            species = self.scrape_species_cards(url)
171            all_species.extend(species)
172
173            # Rate limiting - czekaj między zapytaniami!
174            time.sleep(1)  # 1 sekunda między stronami
175
176        return all_species
177
178
179# === DEMONSTRACJA ===
180
181print("=== SAFARI WEB SCRAPER ===\n")
182
183scraper = SafariWebScraper(base_url="https://example-safari.com")
184
185# 1. Scrapuj listę z tabeli
186print("1. Scrapowanie tabeli gatunków...")
187species_table = scraper.scrape_species_list("https://example-safari.com/species-table")
188print(f"   Znaleziono {len(species_table)} gatunków")
189for species in species_table[:3]:
190    print(f"   - {species['common_name']} ({species['scientific_name']})")
191
192# 2. Scrapuj karty
193print("\n2. Scrapowanie kart gatunków...")
194species_cards = scraper.scrape_species_cards("https://example-safari.com/species")
195print(f"   Znaleziono {len(species_cards)} kart")
196
197# 3. Scrapuj szczegóły
198print("\n3. Scrapowanie szczegółów lwa...")
199lion_detail = scraper.scrape_species_detail("https://example-safari.com/species/lion")
200print(f"   Nazwa: {lion_detail['common_name']}")
201print(f"   Status: {lion_detail['status']}")
202print(f"   Siedlisko: {lion_detail['habitat']}")
203
204# 4. Scrapuj z paginacją
205print("\n4. Scrapowanie z paginacją...")
206all_species = scraper.scrape_with_pagination("https://example-safari.com/species", max_pages=3)
207print(f"   Łącznie: {len(all_species)} gatunków z 3 stron")
208
209print("\n✓ Scraping zakończony")

Selenium - scraping dynamicznych stron

Problem: Niektóre strony ładują dane przez JavaScript - BeautifulSoup widzi pusty HTML!

Rozwiązanie: Selenium - automatyzuje prawdziwą przeglądarkę!

Instalacja

1pip install selenium
2# Pobierz ChromeDriver: https://chromedriver.chromium.org/

Podstawy Selenium

1from selenium import webdriver
2from selenium.webdriver.common.by import By
3from selenium.webdriver.support.ui import WebDriverWait
4from selenium.webdriver.support import expected_conditions as EC
5import time
6
7# Uruchom przeglądarkę
8driver = webdriver.Chrome()  # Wymaga ChromeDriver
9
10try:
11    # Otwórz stronę
12    driver.get("https://example.com/species")
13
14    # Czekaj aż element się załaduje (JavaScript!)
15    wait = WebDriverWait(driver, 10)
16    species_cards = wait.until(
17        EC.presence_of_all_elements_located((By.CLASS_NAME, "species-card"))
18    )
19
20    # Ekstrakcja danych
21    for card in species_cards:
22        name = card.find_element(By.TAG_NAME, "h3").text
23        population = card.find_element(By.CLASS_NAME, "population").text
24        print(f"{name}: {population}")
25
26    # Kliknij przycisk "Load More"
27    load_more = driver.find_element(By.ID, "load-more-btn")
28    load_more.click()
29    time.sleep(2)  # Czekaj na załadowanie
30
31    # Przewiń w dół (infinite scroll)
32    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
33    time.sleep(2)
34
35finally:
36    # Zawsze zamknij przeglądarkę!
37    driver.quit()

Headless mode - bez GUI

1from selenium.webdriver.chrome.options import Options
2
3options = Options()
4options.add_argument("--headless")  # Bez okna przeglądarki
5options.add_argument("--no-sandbox")
6options.add_argument("--disable-dev-shm-usage")
7
8driver = webdriver.Chrome(options=options)
9# Reszta jak wyżej

Best Practices Web Scrapingu

1. Sprawdź robots.txt

1import requests
2
3response = requests.get("https://example.com/robots.txt")
4print(response.text)
5
6# Sprawdź czy Twój scraper może:
7# User-agent: *
8# Disallow: /admin/
9# Crawl-delay: 10  # Czekaj 10s między zapytaniami

2. Rate Limiting - nie przeciążaj serwera!

1import time
2import random
3
4for url in urls:
5    scrape_page(url)
6
7    # Czekaj między zapytaniami
8    time.sleep(random.uniform(1, 3))  # 1-3 sekundy losowo

3. User-Agent - identyfikuj się

1headers = {
2    "User-Agent": "MyResearchBot/1.0 (+https://mysite.com/bot-info; contact@mysite.com)"
3}
4response = requests.get(url, headers=headers)

4. Obsługa błędów

1from requests.exceptions import RequestException
2
3def safe_scrape(url):
4    try:
5        response = requests.get(url, timeout=10)
6        response.raise_for_status()
7        soup = BeautifulSoup(response.text, "html.parser")
8        return soup
9
10    except RequestException as e:
11        print(f"❌ Błąd: {e}")
12        return None

5. Cache - nie pobieraj dwa razy tego samego

1import pickle
2from pathlib import Path
3
4def get_cached_html(url, cache_dir="cache"):
5    Path(cache_dir).mkdir(exist_ok=True)
6    cache_file = Path(cache_dir) / f"{hash(url)}.pkl"
7
8    # Sprawdź cache
9    if cache_file.exists():
10        with open(cache_file, "rb") as f:
11            return pickle.load(f)
12
13    # Pobierz i zapisz
14    response = requests.get(url)
15    html = response.text
16
17    with open(cache_file, "wb") as f:
18        pickle.dump(html, f)
19
20    return html

Częste pułapki

1. Zmiana struktury HTML

Strony zmieniają layout - Twój scraper przestaje działać!

Rozwiązanie: Używaj wielu selektorów, obsługa błędów

1# Kilka prób znalezienia elementu
2name = (
3    soup.find("h3", class_="species-name") or
4    soup.find("span", class_="name") or
5    soup.find("div", attrs={"data-type": "name"})
6)

2. Dynamiczny JavaScript

BeautifulSoup widzi tylko HTML wyjściowy, nie wynik JS!

Rozwiązanie: Selenium, lub analiza API (sprawdź Network w DevTools)

3. Anti-scraping mechanizmy

  • CAPTCHA
  • Rate limiting (429 Too Many Requests)
  • IP ban
  • Wymagane ciasteczka/sesje

Rozwiązanie: Przestrzegaj zasad, nie atakuj, rozważ API

Podsumowanie

W tej lekcji nauczyłeś/aś się:

  • ✅ Czym jest web scraping i kiedy go używać
  • ✅ Legalność i etyka scrapingu (robots.txt, rate limiting)
  • ✅ BeautifulSoup4:
    find()
    ,
    find_all()
    ,
    select()
  • ✅ Ekstrakcja: tekst, atrybuty, linki
  • ✅ Nawigacja DOM: children, parent, siblings
  • ✅ Selenium dla dynamicznych stron (JavaScript)
  • ✅ Best practices: User-Agent, rate limiting, cache, błędy
  • ✅ Częste pułapki i jak ich unikać

Checkpoint

Przed przejściem dalej:

  • [ ] Potrafisz sparsować HTML przez BeautifulSoup
  • [ ] Wiesz jak znaleźć element:
    find()
    ,
    select()
  • [ ] Potrafisz wyciągnąć tekst i atrybuty
  • [ ] Rozumiesz różnicę BeautifulSoup vs Selenium
  • [ ] Znasz etykę web scrapingu

Analogia Safari: Web scraping to jak czytanie broszur turystycznych o parkach narodowych i ręczne zbieranie danych do arkusza - czasochłonne, ale skuteczne gdy nie ma oficjalnych raportów (API)! 📄🔍

W następnej lekcji Darwin nauczy Cię baz danych SQL - jak przechowywać i zarządzać dużymi zbiorami danych w sposób strukturalny! 🗄️💾

Vai a CodeWorlds