Nella "guida completa di Python" pinnata in homepage abbiamo già visto alcuni esempi di web scraping in particolare sul mio vecchio portfolio.
Oggi andremo a vedere un caso un po' diverso ossia il web crawling, che su certi aspetti si basa sullo stesso principio.
Il nostro tool sarà proprio il crawler che visita automaticamente le pagine web e scarica il contenuto (nel nostro caso immagini).
Iniziamo a scrivere il tool, per prima cosa importiamo le librerie necessarie. Ecco un elenco:
os
per la gestione dei file e directory.
time
e random
usati per generare numeri casuali (in questo caso per dare nomi unici ai file) e per pause.
requests
per fare le richieste HTTP.
BeautifulSoup
libreria per estrarre le foto.
urlparse
utile per manipolare gli URL.
OptionParser
gestisce i parametri che passiamo al programma dal terminale.
Definiamo un indice CONFIG
per memorizzare le variabili globali.
m_url
sarà l'URL da cui partiamo per raccogliere le immagini, tdir
la cartella dove salveremo le immagini e
cpages
è il numero di pagine da esplorare.
imgs
, furls
e purls
sono set per memorizzare immagini, link da visitare e quelli già visitati.
Scriviamo una funzione che garantisce che la cartella dove vogliamo salvare le immagini esista (ensure_directory_exists(directory)
).
Facciamo parsing con BeautifulSoup per estrarre tutte le immagini e i link da una pagina.
Ci servirà anche una funzione per scaricare le foto (download_images()
) dove: per ogni url (immagine) proviamo a scaricarla, se troviamo un immagine con lo stesso nome aggiungiamo un numero casuale per evitare conflitti e in caso di errore verrà tutto registrato sull'apposito file.
Per fare il crawling vero e proprio avremo bisogno di una funzione che esplora le pagine web e raccoglie altri link e immagini, questo facendo decidere all'utente in numero di pagine da analizzare e ripete il processo per ogni pagina.
Nel main richiamiamo le funzioni principali:
OptionParser
che gestisce le opzioni della riga di comando che nel nostro caso ci permettono di configurare l'URL di partenza, la directory, numero di pagine da esplorare, file di log (errori) ecc.
BeautifulSoup
: utilizzato per fare il parsing della pagina di partenza, estrarre i link e immagini dalla pagina iniziale.
crawl_pages()
per le pagine linkate, raccogliendo immagini e link.
download_images()
: se l'utente conferma partirà il download di tutte le immagini trovate.
import os
import time
import json
import random
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse
from optparse import OptionParser
from colorama import init, Fore
init()
# variabili
CONFIG = {
"m_url": "",
"tdir": "immagini_grabber",
"p_name": "",
"cpages": 0,
"error_file": "error.log",
"s_page": True,
"imgs": set(),
"furls": set(),
"purls": set(),
"http_s": "https:"
}
def p2url(p, base_url):
return base_url + p
def sslash(url):
return url.startswith("//")
def ensure_directory_exists(directory):
if not os.path.isdir(directory):
os.makedirs(directory)
def save_urls_to_logfile(urls, logfile):
with open(logfile, "w") as f:
for url in urls:
f.write(url + "\n")
def download_images():
config = CONFIG
tdir = config["tdir"]
error_file = config["error_file"]
ensure_directory_exists(tdir)
tdir = os.path.join(tdir, "")
save_urls_to_logfile(config["imgs"], os.path.join(tdir, "urls.log"))
print(" [*] Download immagini ...")
for count, url in enumerate(config["imgs"]):
print(f" \t[*] Download immagine ({count + 1}/{len(config['imgs'])}) ... ")
try:
filename = os.path.basename(url)
while os.path.exists(os.path.join(tdir, filename)):
filename = f"{random.randint(0, 999999)}_{filename}"
if not os.path.splitext(filename)[1]:
filename += ".png"
with open(os.path.join(tdir, filename), "wb") as f:
f.write(requests.get(url).content)
except Exception:
print(Fore.LIGHTRED_EX + " \t[-] Errore download. Immagine saltata..." + Fore.RESET)
with open(error_file, "a") as f:
f.write(f"[ERROR] Errore download per URL: {url}\n")
print(Fore.LIGHTGREEN_EX + "[+] Download completato!" + Fore.RESET)
def crawl_pages(page_limit):
config = CONFIG
for _ in range(page_limit):
if not config["furls"]:
print(Fore.LIGHTYELLOW_EX + " [.] URL terminate." + Fore.RESET)
break
current_url = config["furls"].pop()
config["purls"].add(current_url)
print(f" [*] Crawling URL: {current_url}")
try:
soup = BeautifulSoup(requests.get(current_url).text, "html.parser")
extract_links_and_images(soup, current_url)
except Exception:
print(Fore.LIGHTRED_EX + " [-] Connessione URL fallita." + Fore.RESET)
def extract_links_and_images(soup, base_url):
config = CONFIG
for img in soup.find_all("img"):
src = img.get("src") or img.get("data-lazy")
if not src:
continue
if src.startswith("http"):
config["imgs"].add(src)
elif sslash(src):
config["imgs"].add(config["http_s"] + src)
else:
config["imgs"].add(p2url(src, base_url))
for a in soup.find_all("a"):
href = a.get("href")
if not href:
continue
if href.startswith("http"):
config["furls"].add(href)
elif sslash(href):
config["furls"].add(config["http_s"] + href)
else:
config["furls"].add(p2url(href, base_url))
def main():
parser = OptionParser()
parser.add_option("-u", "--url", dest="url", help="Target URL.")
parser.add_option("-d", "--dir", dest="dir", default="immagini_grabber", help="Target Directory.")
parser.add_option("-p", "--page", dest="page", help="Nomi delle pagine.")
parser.add_option("-c", "--crawl", type="int", dest="crawl", help="Pagine da grabbare.")
parser.add_option("-l", "--log", dest="elog", default="error.log", help="Errore di destinazione.")
parser.add_option("-s", "--spage", dest="spage", action="store_true", help="Vuoi rimanere nella stessa pagina?")
(options, args) = parser.parse_args()
config = CONFIG
config.update({
"m_url": options.url or input("Target URL: "),
"tdir": options.dir.strip('/'),
"error_file": options.elog,
"cpages": options.crawl or int(input("Pagine da grabbare: ")),
"s_page": options.spage,
})
config["http_s"] = "https:" if "https://" in config["m_url"] else "http:"
soup = BeautifulSoup(requests.get(config["m_url"]).text, "html.parser")
extract_links_and_images(soup, config["m_url"])
crawl_pages(config["cpages"])
yn = input(f"[.] Trovato {len(config['imgs'])} immagini. Vuoi scaricarle? [y/n] ").lower()
if yn == "y":
download_images()
if __name__ == "__main__":
main()