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"! 📄🔍
Web scraping to automatyczna ekstrakcja danych ze stron internetowych przez:
✅ Używaj gdy:
❌ Unikaj gdy:
WAŻNE: Web scraping jest legalny gdy:
https://example.com/robots.txt)Złe praktyki: ❌
Złota zasada: Zachowuj się jak człowiek - odwiedzaj stronę rozsądnie, nie przeciążaj serwera!
BeautifulSoup4 (bs4) to najpopularniejsza biblioteka do parsowania HTML/XML w Pythonie.
1pip install beautifulsoup4
2pip install requests # Do pobierania HTML
3pip install lxml # Szybki parser (opcjonalnie)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)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:
<h2>Lew</h2> → "Lew"class="species-card", id="lion"<a href="/species/lion"><img src="/images/lion.jpg">data-population="120"find() - pierwszy pasujący element1from 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 elementy1# 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)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żoneSelektory CSS:
.species - klasa#lion - IDdiv.species - tag + klasadiv > p - bezpośrednie dzieckodiv p - dowolne zagnieżdżenie[data-population] - atrybut1html = '<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>)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'}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')}")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 21import 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")Problem: Niektóre strony ładują dane przez JavaScript - BeautifulSoup widzi pusty HTML!
Rozwiązanie: Selenium - automatyzuje prawdziwą przeglądarkę!
1pip install selenium
2# Pobierz ChromeDriver: https://chromedriver.chromium.org/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()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żej1import 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 zapytaniami1import 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 losowo1headers = {
2 "User-Agent": "MyResearchBot/1.0 (+https://mysite.com/bot-info; contact@mysite.com)"
3}
4response = requests.get(url, headers=headers)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 None1import 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 htmlStrony 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)BeautifulSoup widzi tylko HTML wyjściowy, nie wynik JS!
Rozwiązanie: Selenium, lub analiza API (sprawdź Network w DevTools)
Rozwiązanie: Przestrzegaj zasad, nie atakuj, rozważ API
W tej lekcji nauczyłeś/aś się:
find(), find_all(), select()Przed przejściem dalej:
find(), select()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! 🗄️💾