Last active
February 12, 2025 17:01
-
-
Save zedr/c6db3798f9cb0b8c1dd104063e40e5d5 to your computer and use it in GitHub Desktop.
Trivial crawler for books.toscrape.com
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import time | |
import json | |
from pathlib import Path | |
import pandas as pd | |
import requests | |
from bs4 import BeautifulSoup | |
# Creo un set: https://docs.python.org/3/tutorial/datastructures.html#sets | |
# Questo mi permette di aggiungere in tempo O1 (nei casi pratici) | |
# e verificare la presenza di una chiave sempre in tempo 01. | |
# Posso fare lo stesso usando i dict, mentre le liste sono | |
# molto meno efficienti (On per la verifica) | |
ignored = { | |
"index.html", | |
"https://books.toscrape.com/index.html", | |
} | |
# L'URL da cui partire | |
base_url = "https://books.toscrape.com/" | |
financial_keys = ( | |
'Price (excl. tax)', | |
'Price (incl. tax)', | |
'Tax' | |
) | |
numerical_keys = ( | |
"Number of reviews", | |
) | |
data = [] | |
def get_parent_url(url): | |
"""Get the parent of the given URL | |
e.g. https://www.example.com/abc/cde.html -> https://www.example.com/abc/ | |
""" | |
*parent_parts, _ = url.split("/") | |
return "/".join(parent_parts) | |
def rows_to_dict(rows): | |
"""Convert a sequence of rows of an HTML table to a dict""" | |
rows_data = {el.th.text: el.td.text for el in rows} | |
for key, val in rows_data.items(): | |
if key in financial_keys: | |
# Correggi il dato e lo trasformo in decimale | |
# '£51.77' -> 51.77 | |
# Per farlo escludo i primi due caratteri | |
rows_data[key] = float(val[2:]) | |
if key in numerical_keys: | |
# Converti il dato in un numero intero | |
rows_data[key] = int(val) | |
return rows_data | |
def crawl(url): | |
"""Crawl the page at the given URL | |
Any tables will be added as dicts to the | |
module-level name "data". | |
Perform the crawl using the breadth-first | |
algorithm (BFS). | |
""" | |
# Prendo la URL base | |
parent_url = get_parent_url(url) | |
# Invio una chiamata GET usando l'URL | |
response = requests.get(url) | |
status_code = response.status_code | |
if status_code != 200: | |
raise Exception(f"Il server ha risposto con uno status code inaspettato: {status_code}") | |
# Faccio il parsing del HTML della pagina | |
soup = BeautifulSoup(response.text, features="html.parser") | |
# Cerco tutti gli element tr (table row) nella pagina (se ci sono) | |
rows = soup.find_all("tr") | |
# Se li trovo, li converto in un dict e li aggiungo ai dati | |
row_data = rows_to_dict(rows) | |
if row_data: | |
data.append(row_data) | |
# Aspetto un secondo prima di proseguire | |
# in modo da non caricare troppo il server | |
time.sleep(1) | |
# Ora cerco tutti i link all'interno della prima sezione della pagina | |
links = soup.find_all("section")[0].find_all("a") | |
print(f"Crawled: {url} and found {len(links)} new links and {len(rows)} table rows.", file=sys.stderr) | |
for link in links: | |
# Prendo il link (nell'attributo HREF) | |
link_rel = link.attrs["href"] | |
# Ignora i link che tornano indietro | |
if ".." in link_rel: | |
continue | |
# Se non è un link assoluto (che comincia con "http")... | |
if not link_rel.startswith("http"): | |
# ... lo rendo assoluto usando la URL base | |
url = "/".join([parent_url, link_rel]) | |
else: | |
url = link_rel | |
# I link esterni vanno ignorati | |
if not url.startswith(base_url): | |
continue | |
# Se ho già incontrato il link, lo ignoro | |
if url in ignored: | |
continue | |
# Aggiungo la URL alla lista da ignorare | |
ignored.add(url) | |
# Chiamata ricorsiva | |
crawl(url) | |
# Faccio partire il crawl | |
try: | |
crawl(base_url) | |
except KeyboardInterrupt: | |
pass | |
# Creo e stampo il dataframe | |
df = pd.DataFrame(data) | |
print(df) | |
# Salvo i dati JSON in un file per poterli utilizzare dopo | |
with open("books.json", "w") as fd: | |
fd.write(json.dumps(data)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment