Jeśli dotarłeś do tego wykładu, to znaczy, że dobrze
rozumiesz, jak działa web scraping i jak używać
świetnego BeautifulSoup
do wyciągania potrzebnych
danych z HTML. Jednak w świecie web scrapingu nie wszystko jest
takie różowe, jak piszą w podręcznikach. Czasami nasze marzenie
o zbieraniu danych zamienia się w walkę z błędami, więc porozmawiajmy
o tym, jak ominąć pułapki i sprawić, by nasz scraper był jak
najbardziej odporny.
1. Typowe błędy w web scrapingu
Błąd 404 i inne błędy HTTP
Klasyczny problem: próbujesz uzyskać stronę, a zamiast treści dostajesz dumne "404 Not Found". Może się to zdarzyć, ponieważ strona została usunięta lub przeniesiona. Inne powszechne błędy HTTP to 403 (Forbidden), 500 (Internal Server Error) i 503 (Service Unavailable).
Zmiana struktury HTML
Spędziłeś mnóstwo czasu na pisaniu kodu do wyciągania danych, a następnego dnia strona postanowiła trochę "zabłysnąć" i zmieniła strukturę HTML. O nie, znowu trzeba wszystko przepisywać!
Ograniczenia ilości zapytań
Niektóre strony zaczynają podejrzewać coś nie tak, gdy skrypty scraperów cały dzień je "oblizują". W najlepszym przypadku dostaniesz blokadę na jakiś czas, w najgorszym - na zawsze.
Czas oczekiwania i timeouty
Czasami strony ładują się długo, a twój skrypt może się wywalić, jeśli czekanie przekroczy standardowy czas timeoutu.
2. Metody obsługi błędów
Użycie try-except
Twoje skrypty nie powinny się wykrzaczać przez każdą
niespodziankę. Dodanie bloków try-except
pomaga złapać błędy i sprawić, by twój scraper działał
dalej, jakby nic się nie stało.
import requests
from bs4 import BeautifulSoup
url = 'http://example.com/some-nonexistent-page'
try:
response = requests.get(url)
response.raise_for_status() # Wywołuje HTTPError dla złych odpowiedzi
except requests.exceptions.HTTPError as errh:
print("Błąd HTTP:", errh)
except requests.exceptions.ConnectionError as errc:
print("Błąd połączenia:", errc)
except requests.exceptions.Timeout as errt:
print("Błąd timeoutu:", errt)
except requests.exceptions.RequestException as err:
print("Ups: coś innego", err)
Dobry skrypt nie tylko przechwytuje wyjątek - ma odpowiedź na każdy rodzaj błędu. Zostałeś zbanowany na IP - przechodzisz do następnego proxy, strona padła - na razie parsujesz inną. Jeśli na stronie nie znaleziono jakichś elementów, które tam powinny być, można powiadomić właściciela parsera, że należy zaktualizować skrypt parsujący i wysłać mu email.
Logowanie
"Po co te logi?" — zapytasz. A po to, że logi to twoje drugie oczy. Pomogą ci zrozumieć, co poszło nie tak i naprawić błąd jak najszybciej.
import logging
logging.basicConfig(filename='scraper.log', level=logging.INFO)
try:
# Twój kod scrapowania
pass
except Exception as e:
logging.error("Wystąpił wyjątek", exc_info=True)
Użycie timeoutów i retry
Czasem wszystko, czego potrzeba, to trochę poczekać i spróbować ponownie. Do tych celów doskonale nadają się timeouty i retry.
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
except requests.exceptions.Timeout:
print("Wystąpił timeout. Próbuję ponownie...")
# Powtórz zapytanie lub wykonaj inne działanie
3. Przykłady odpornego scrapowania
Prosty scraper z obsługą błędów
Stwórzmy mały, ale niezawodny scraper, który potrafi obsługiwać niektóre typowe błędy.
import requests
from bs4 import BeautifulSoup
import time
import logging
logging.basicConfig(filename='scraper.log', level=logging.INFO)
url = 'http://example.com/some-nonexistent-page'
def fetch_html(url):
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
return response.text
except requests.exceptions.HTTPError as errh:
logging.error("Błąd HTTP: %s", errh)
except requests.exceptions.ConnectionError as errc:
logging.error("Błąd połączenia: %s", errc)
except requests.exceptions.Timeout as errt:
logging.warning("Wystąpił timeout. Próbuję ponownie...")
time.sleep(1) # Poczekaj chwilę i spróbuj ponownie
return fetch_html(url)
except requests.exceptions.RequestException as err:
logging.error("Ups: coś innego %s", err)
return None
html_content = fetch_html(url)
if html_content:
soup = BeautifulSoup(html_content, 'html.parser')
# Twój kod do wyciągania danych
else:
print("Nie udało się pobrać zawartości strony.")
Zapisywanie danych w częściach
Aby nie stracić już wyciągniętych danych w przypadku awarii, zapisuj je w częściach. Na przykład, jeśli wyciągasz informacje z listy stron, zapisując wyniki w miarę postępu, minimalizujesz ryzyko utraty danych.
import csv
def save_to_csv(data, filename='data.csv'):
with open(filename, mode='a', newline='') as file:
writer = csv.writer(file)
writer.writerow(data)
Dzięki temu, nawet jeśli twój skrypt rozbije się w połowie pracy, nie stracisz wszystkich danych i będziesz mógł kontynuować od ostatniego zapisanego punktu.
Strona, którą parsujesz, może zmienić się częściowo, a większość danych nadal będzie dostępna. Byłoby źle anulować parsowanie i stracić cenne dane, jeśli brakuje tylko niewielkiego procentu z nich.
GO TO FULL VERSION