Python i web scraping: jak automatycznie zbierać dane ze stron internetowych

python Web scraping

Spis treści:

Ile czasu zajmie przepisanie 15 ofert samochodów do Excela? Kwadrans, jeśli nic się nie pomyli.

Oferty samochodów są na trzech podstronach. Przy każdej ofercie interesują nas cena, rocznik, przebieg, napęd i miasto. Ręczne przepisywanie zajmuje czas i łatwo wtedy o pomyłkę. Skrypt w Pythonie może zebrać te informacje automatycznie, a przy kolejnej aktualizacji wykonać tę samą pracę w kilka chwil.

W tym artykule krok po kroku zbudujemy scraper dla katalogu AutoScope — strony przygotowanej do ćwiczeń. Pokażę, jak pobrać HTML, odczytać dane z kart ofert, przejść przez kolejne podstrony i zapisać wyniki do CSV. Pełny kod znajduje się w artykule.

Jak to działa

Skrypt wysyła do serwera żądanie HTTP i odbiera odpowiedź, która w tym przykładzie zawiera kod HTML. Przeglądarka interpretuje HTML i wyświetla stronę, a skrypt wyszukuje w kodzie wybrane elementy, na przykład nazwy, ceny i linki. Zebrane informacje można następnie zapisać do pliku CSV, otworzyć w Excelu i wykorzystać do dalszej analizy.

Zanim zaczniesz

Przed pobieraniem danych sprawdź regulamin serwisu i jego zasady dotyczące automatycznego dostępu. Warto też zajrzeć do pliku robots.txt (np. adres-strony.pl/robots.txt), który informuje roboty, do jakich części serwisu właściciel nie chce ich dopuszczać. Sam plik robots.txt nie jest jednak zgodą prawną na pobieranie danych. Jeśli dostępne jest oficjalne API (Application Programming Interface), zwykle warto zacząć właśnie od niego. Nie wysyłaj dużej liczby żądań jedno po drugim i nie zbieraj danych osobowych bez odpowiedniej podstawy. Publicznie widoczny numer telefonu również może być daną osobową.

Strona do ćwiczeń

Katalog AutoScope znajduje się pod adresem https://beje.pl/python/oferty_demo.php. Strona przypomina serwis z ogłoszeniami: ma wyszukiwarkę, karty samochodów, ceny, raty i wyróżnioną ofertę. Dane podzielono na trzy podstrony, po pięć ofert na każdej.

Strona startowa katalogu AutoScope

Kliknij „2” pod listą ofert i sprawdź pasek adresu. Pojawi się parametr ?page=2. Po wybraniu napędu lub maksymalnej ceny mogą pojawić się kolejne parametry, np. ?fuel=Hybryda albo ?price_max=150000. Ponieważ parametry są widoczne w adresie, w tym ćwiczeniu nie trzeba automatyzować klikania — wystarczy wysłać żądanie z odpowiednimi parametrami.

Zaglądamy do HTML-a

Kliknij prawym przyciskiem myszy cenę dowolnego auta i wybierz „Zbadaj” (lub naciśnij Ctrl+Shift+C). W narzędziach deweloperskich zobaczysz fragment HTML odpowiadający za wskazany element.

Karta oferty i elementy HTML, z których skrypt pobiera dane

Każda oferta znajduje się w elemencie article z klasą offer-card. Poniżej uproszczony fragment HTML jednej karty:


<article class="offer-card promoted" data-id="O00217">

  <div class="offer-meta">
    <span><span class="verified">✓</span> Centrum Aut</span>
    <span>Poznań</span>
  </div>

  <h2 class="offer-title">Volkswagen Arteon</h2>

  <p class="model-note">2.0 TDI 4Motion · R-Line</p>

  <span class="price" data-value="117900">117 900 zł</span>

  <ul class="parameters">
    <li data-name="year">2020</li>
    <li data-name="mileage">121 000 km</li>
    <li data-name="fuel">Diesel</li>
  </ul>

  <a class="details" href="/oferty/O00217">Zobacz szczegóły</a>

</article>

Warto zwrócić uwagę na sposób zapisania ceny. Tekst „117 900 zł” jest przeznaczony dla osoby czytającej stronę, a atrybut data-value zawiera wartość 117900, którą łatwiej wykorzystać w obliczeniach. Rok, przebieg i rodzaj paliwa mają atrybut data-name. Dzięki temu można je rozpoznać po nazwie, zamiast zakładać, że zawsze występują w tej samej kolejności. Klasa promoted oznacza ofertę wyróżnioną.

Elementy wskazuje się selektorami CSS, tymi samymi, których używa się w arkuszach stylów:

Selektor

Co oznacza

.price

element z klasą price (kropka = klasa)

#offers

element o identyfikatorze offers (# = id)

article.offer-card

znacznik article z klasą offer-card

ul.parameters li

każdy li wewnątrz listy z klasą parameters

.offer-meta > span

span leżący bezpośrednio w .offer-meta

li[data-name="year"]

li z atrybutem data-name równym year

Pierwszy skrypt

Do przykładu wykorzystamy dwie biblioteki: requests do wysłania żądania HTTP oraz BeautifulSoup do wyszukania elementów w otrzymanym HTML-u.

terminal
pip install requests beautifulsoup4

Na początek pobierzemy pierwszą podstronę i wyświetlimy nazwy modeli wraz z cenami.

python
import requests
from bs4 import BeautifulSoup

URL = "https://beje.pl/python/oferty_demo.php"

response = requests.get(URL, timeout=10)
response.raise_for_status()  # błąd 4xx/5xx przerywa skrypt

soup = BeautifulSoup(response.text, "html.parser")

cards = soup.select("article.offer-card")

print("Ofert na stronie:", len(cards))

for card in cards:
    title = card.select_one(".offer-title").get_text(strip=True)
    price = card.select_one(".price").get_text(strip=True)
    print(f"{title:<25} {price}")
wynik
Ofert na stronie: 5

Mercedes-Benz Klasa E     181 600 zł
BMW Seria 5               152 000 zł
Volkswagen Arteon         117 900 zł
Toyota Camry              139 900 zł
BYD Seal                  164 300 zł

Parametr timeout=10 ogranicza czas oczekiwania na odpowiedź serwera. Metoda raise_for_status() zgłasza błąd, jeśli serwer zwróci status HTTP z zakresu 4xx lub 5xx. Bez tej kontroli skrypt mógłby próbować analizować HTML strony błędu, a wynik wyglądałby po prostu na brak ofert. Metoda select() zwraca wszystkie pasujące elementy, a select_one() — pierwszy z nich. get_text(strip=True) pobiera tekst i usuwa zbędne białe znaki z jego początku i końca.

Dwie pułapki

Na pierwszej podstronie jest pięć ofert. Zanim pobierzemy cały katalog, trzeba obsłużyć dwa przypadki: brak jednego z parametrów w karcie oraz sposób ustalania liczby podstron. Oba problemy często pojawiają się przy pracy z rzeczywistymi stronami.

Pierwsza: Toyota Camry nie ma podanego przebiegu.

W karcie Toyoty Camry brakuje elementu z przebiegiem

Jeśli szukany element nie istnieje, select_one() zwróci None. Wywołanie .get_text() na takiej wartości spowoduje błąd AttributeError. W tym przykładzie parametry zapisujemy więc w słowniku, a następnie pobieramy metodą get(). Jeśli danego klucza nie ma, metoda zwróci None i możemy odpowiednio obsłużyć brak danych.

Drugi przypadek dotyczy paginacji, czyli podziału wyników na kolejne strony.

 Kolejne podstrony to ten sam adres z innym parametrem page

Można próbować zwiększać numer strony, aż lista ofert będzie pusta. W tym katalogu takie podejście nie zadziała: żądanie z parametrem ?page=4 ponownie wyświetla trzecią stronę. Pętla mogłaby wtedy przetwarzać te same oferty bez końca. Dlatego skrypt odczytuje numer ostatniej strony z elementów paginacji.

Cały skrypt

Poniższy skrypt pobiera kolejne podstrony, odczytuje dane z każdej karty i zapisuje zebrane rekordy do pliku CSV.

python – scraper.py
import csv
import re
import time
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

# 1. Ustawienia

URL = "https://beje.pl/python/oferty_demo.php"

FILTRY = {}  # np. {"price_max": 150000} albo {"fuel": "Hybryda"}

session = requests.Session()
session.headers["User-Agent"] = "Mozilla/5.0 (kurs Python - web scraping)"

oferty = []
strona = 1
ostatnia = 1  # prawdziwą liczbę stron poznamy po pierwszym pobraniu

# 2. Pętla po podstronach

while strona <= ostatnia:
    parametry_url = {**FILTRY, "page": strona}

    response = session.get(URL, params=parametry_url, timeout=10)
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "html.parser")

    numery = [int(a.get_text()) for a in soup.select("nav.pagination a")]
    ostatnia = max(numery, default=1)

    # 3. Dane z każdej karty oferty

    karty = soup.select("article.offer-card")

    for karta in karty:
        parametry = {li["data-name"]: li.get_text(strip=True)
                     for li in karta.select("ul.parameters li")}

        przebieg = parametry.get("mileage")  # Toyota Camry go nie ma

        if przebieg:
            przebieg = int(re.sub(r"\D", "", przebieg))

        meta = karta.select(".offer-meta > span")

        oferty.append({
            "id": karta["data-id"],
            "model": karta.select_one(".offer-title").get_text(strip=True),
            "wersja": karta.select_one(".model-note").get_text(strip=True),
            "cena_zl": int(karta.select_one(".price")["data-value"]),
            "rok": int(parametry["year"]),
            "przebieg_km": przebieg,
            "naped": parametry.get("fuel"),
            "sprzedawca": meta[0].get_text(strip=True).lstrip("✓ "),
            "miasto": meta[1].get_text(strip=True),
            "promowana": "promoted" in karta["class"],
            "link": urljoin(URL, karta.select_one("a.details")["href"]),
        })

    print(f"Strona {strona}/{ostatnia}: pobrano {len(karty)}")

    strona += 1
    time.sleep(1)  # przerwa, żeby nie obciążać serwera

# 4. Zapis do pliku CSV

with open("oferty.csv", "w", newline="", encoding="utf-8-sig") as plik:
    writer = csv.DictWriter(plik, fieldnames=oferty[0].keys(), delimiter=";")
    writer.writeheader()
    writer.writerows(oferty)

print("Zapisano", len(oferty), "ofert do pliku oferty.csv")
wynik
Strona 1/3: pobrano 5
Strona 2/3: pobrano 5
Strona 3/3: pobrano 5
Zapisano 15 ofert do pliku oferty.csv

Pętla while działa, dopóki numer bieżącej strony nie przekroczy wartości ostatnia. Początkowo ustawiamy ostatnia = 1, ponieważ przed pierwszym żądaniem nie znamy liczby stron. Po pobraniu strony skrypt odczytuje numery z paginacji i aktualizuje tę wartość. Jeśli paginacji nie ma, max(numery, default=1) zwróci 1.

Obiekt Session pozwala korzystać z jednej sesji przy kolejnych żądaniach. Ustawiamy w niej również nagłówek User-Agent. Słownik parametry_url biblioteka requests zamienia na parametry adresu, więc nie trzeba ręcznie łączyć tekstu, np. ’?page=’ + str(strona).

Cena jest pobierana z atrybutu data-value, więc od razu otrzymujemy liczbę. Przebieg zapisano jako tekst, np. „63 400 km”, dlatego wyrażenie regularne \D usuwa znaki inne niż cyfry i pozwala uzyskać wartość 63400. Link w karcie jest względny (np. /oferty/O00217), a urljoin() łączy go z adresem serwisu.

Filtry bez dodatkowego kodu

Aby pobrać tylko samochody w cenie do 150 000 zł, zmień wartość FILTRY na początku skryptu:

python
FILTRY = {"price_max": 150000}
wynik
Strona 1/2: pobrano 5
Strona 2/2: pobrano 1
Zapisano 6 ofert do pliku oferty.csv

W tym katalogu odnośniki paginacji uwzględniają wybrany filtr, więc po jego zastosowaniu skrypt odczyta nową liczbę stron. Nazwy parametrów można sprawdzić w pasku adresu po użyciu formularza wyszukiwania.

CSV, Excel i polskie znaki

Excel nie zawsze poprawnie rozpoznaje polskie znaki w pliku CSV zapisanym jako UTF-8. Kodowanie utf-8-sig dodaje na początku pliku znacznik BOM, który pomaga Excelowi rozpoznać kodowanie. Używamy też średnika jako separatora kolumn, co często lepiej pasuje do polskich ustawień regionalnych Excela.

Plik można następnie wczytać do pandas (instalacja: pip install pandas) i policzyć podstawowe statystyki cen dla poszczególnych rodzajów napędu:

python
import pandas as pd

df = pd.read_csv("oferty.csv", sep=";", encoding="utf-8-sig")

ceny = df.groupby("naped")["cena_zl"].agg(["count", "mean", "min", "max"])
print(ceny.round(0))
wynik
             count      mean     min     max
naped
Diesel           5  151560.0  117900  181600
Elektryczny      5  158020.0  143500  167900
Hybryda          2  144900.0  139900  149900
PHEV             3  150600.0  124900  174900

Gdy coś nie działa

Niektóre strony pobierają dane dopiero za pomocą JavaScriptu. W takim przypadku requests może otrzymać HTML, w którym nie ma jeszcze ofert. Zanim użyjesz narzędzia takiego jak Playwright, sprawdź w narzędziach deweloperskich kartę Network. Dane mogą być pobierane osobnym żądaniem, czasem w formacie JSON.

Status HTTP 403 oznacza, że serwer odmawia dostępu do zasobu. Czasem serwer wymaga odpowiednich nagłówków, ale nie należy próbować omijać świadomych zabezpieczeń ani zasad dostępu serwisu.

Jeśli skrypt działał wcześniej, a teraz nie znajduje ofert, sprawdź, czy zmieniła się struktura HTML lub nazwy klas CSS. Otwórz narzędzia „Zbadaj” i porównaj aktualny kod strony z selektorami używanymi w skrypcie.

Zadanie na koniec

Na koniec spróbuj samodzielnie rozbudować skrypt. Możesz na przykład:

  • pobierz tylko auta elektryczne (fuel=Elektryczny) i policz ich średni przebieg,
  • dodaj kolumnę z ratą miesięczną, którą strona trzyma w elemencie z klasą finance,
  • zapisz wyniki dodatkowo do pliku Excela metodą to_excel() z pandas.

Wszystkie potrzebne elementy zostały pokazane w przykładach powyżej.

Podobne artykuły

Wszystkie artykuły