Esempio di Scraper con Python e Request-HTML

In questo articolo vedremo come creare un semplice scraper che ci permetterà di estrarre i alcuni date da un sito web/portale per la ricerca di attività commerciali in base alla località ed a uno o più termini di ricerca.

Per effettuare la nostra estrazione andremo ad utilizzare la libreria Requests-HTML, una versione riadattata e più Human Friendly della storica libreria Requests (è possibile installarla con pip usando il comando: pip install requests-html).

Andiamo ad importare le librerie che ci servono:

from requests_html import HTMLSession 
import argparse

Per rendere il nostro script più interattivo abbiamo importato anche la libreria argparse, che ci permette di impostare i nostri parametri di ricerca.

Andiamo quindi ad impostare i parametri di argparse:

parser = argparse.ArgumentParser(description="Simple Yellow Pages data scraper")
parser.add_argument("search_term", type=str, help="Search Term")
parser.add_argument("location_term", type=str, help="Geo Location Term")
args = parser.parse_args()

Andiamo ad inserire il nostro punto di ingresso, che sarà da mettere al fondo del nostro script:

if __name__ == "__main__":    
scrape(args.search_term, args.location_term)

Come potete vedere viene chiamata la funzione scrape che richiede due parametri: uno rappresenta il nostro/i termine/i di ricerca e l’altro indica la posizione geografica di dove effettuare la ricerca.

Ci sono molti siti che funzionano in base a questi due semplici parametri come ad esempio le pagine bianche, per il nostro esempio andremo a vedere la versione inglese delle pagine gialle (yellow pages).

Proviamo a cercare una pizzeria a New York ed andiamo a vedere l’url che viene prodotto dal sito nella pagina dei risultati:

 
https://www.yellowpages.com/search?search_terms= pizza + &geo_location_terms=New+York

Possiamo facilmente immaginare che cambiando questi due termini possiamo ottenere diversi risultati, questo ci permette di creare la nostra funzione scrape():

def scrape(search_term, location_term):    
session = HTMLSession()
pages = session.get('https://www.yellowpages.com/search?search_terms='+search_term+'&geo_location_terms='+location_term)

for page in pages.html:
try:
organic_results = page.find('.organic', first=True)
results = organic_results.find('.result')
for result in results:
business_name = result.find('.business-name')
address = result.find('.adr')
print(f'Business Name: {business_name[0].text}')
print(f'Street Address: {address[0].text}')
print(f'\n')
except:
print('No more results.')
return

La libreria Requests-HTML ha una interessante funzione di paginazione (Requests_HTML Pagination) che ci permette di scorrere facilmente le pagine di risultati con una semplice riga di codice:

for page in pages.html: 

Successivamente andiamo semplicemente ad individuare con la funzione find() gli elementi che ci interessano utilizzando gli identificatori CSS, in questo specifico caso andiamo ad individuare l’elendo dei risultati (‘.result’) ed al loro interno andiamo ad estrarre il nome dell’attività (‘.business-name’) e l’indirizzo (‘.adr’).

Abbiamo così creato un semplice crawler che scorre l’elenco dei risultati per estrarre i dati che ci interessano.

Di seguito riporto il codice completo ed il link per poterlo scaricare da GitHub:

from requests_html
import HTMLSessionimport argparse
parser = argparse.ArgumentParser(description="Simple Yellow Pages data scraper")
parser.add_argument("search_term", type=str, help="Search Term")
parser.add_argument("location_term", type=str, help="Geo Location Term")
args = parser.parse_args()

def scrape(search_term, location_term):
session = HTMLSession()
pages = session.get('https://www.yellowpages.com/search?search_terms='+search_term+'&geo_location_terms='+location_term)
for page in pages.html:
try:
organic_results = page.find('.organic', first=True)
results = organic_results.find('.result') for
result in results:
business_name = result.find('.business-name')
address = result.find('.adr')
print(f'Business Name: {business_name[0].text}')
print(f'Street Address: {address[0].text}')
print(f'\n')
except:
print('No more results.')
return

if __name__ == "__main__":
scrape(args.search_term, args.location_term)

Servizio automatico di traduzione delle Keywords

Supponiamo che abbiate un sito web con un negozio online per il mercato Italiano, gli affari vanno bene e dopo la prima fase di avvio intendete allargare i vostri orizzonti commerciali verso gli altri paesi Europei.

Il vostro sito di scarpe ed abbigliamento sportivo ‘MR Calcio’ è ottimizzato per i motori di ricerca e tra le varie attività di SEO on page avete impostato un’accurata selezione di keywords nell’apposito meta tag.

<meta name="keywords" content="scarpe, MR-Calcio, offerte, saldi">

Ora queste keywords devono essere tradotte in altre quattro lingue per adattarsi alle nuove pagine del sito realizzate per i mercati: Inglese, Francese, Spagnolo e Tedesco.

Sul web esistono diversi siti che si occupano di fornire un servizio di traduzione:

  • https://context.reverso.net
  • http://www.wordreference.com
  • https://en.pons.com/translate

In questo esempio le parole sono poche, me volessimo tradurre le keywords di ogni pagina del sito il lavoro diventerebbe enorme, meglio creare una script per automatizzare le traduzioni.

Prendiamo in considerazione il primo sito dell’elenco ed effettuiamo una ricerca per la traduzione della parola ‘scarpe’ dall’Italiano all’Inglese. Quello che comparirà sulla barra di ricerca sara questa url:

https://context.reverso.net/translation/italian-english/scarpe

La composizione della url è abbastanza semplice possiamo quindi intuire che se volessimo tradurre la parola ‘casa’ dall’Italiano all’Inglese possiamo semplicemente scrivere sulla barra del nostro browser:

https://context.reverso.net/translation/italian-english/scarpe

Se volessimo tradurre nuovamente la parola ‘scarpe’, questa volta però in lingua Francese possiamo usare la url:

https://context.reverso.net/translation/italian-french/scarpe

Con due semplici cicli for possiamo ottenere tutte le combinazioni che ci interessano per poi effettuare in modo automatico le chiamate al sito scorrendo gli elementi della lista delle nostre keywords:

keywords=["scarpe", "MR Calcio","offerte", "saldi"]
languages = ['english', 'french','dutch','spanish']
for l in languages:
    for k in keywords:
        url='https://context.reverso.net/translation/italian-'+l+'/'+k
        print(url)

Questo semplice script ci fornirà come risultato l’elenco delle chiamata che dobbiamo effettuare. Utilizzando la libreria ‘Request’ possiamo effettuare rapidamente tutte le chiamate al sito.

Per ottenere la parola tradotta possiamo ricercare il nodo che ci interessa con lo ‘strumento per sviluppatori’ del browser, accessibile su Chrome e Firefox premendo semplicemente il tasto F12 della tastiera.

Copiando l’indirizzo xpath del nodo possiamo andare ad estrarre la traduzione della parola con l’aiuto della libreria ‘lxml’:

import requests

keywords=["scarpe", "MR Calcio","offerte", "saldi"]
languages = ['english', 'french','dutch','spanish']
for l in languages:
    print(l.upper())
    for k in keywords:
        url='https://context.reverso.net/translation/italian-'+l+'/'+k
        headers = {'User-Agent': random.choice(user_agent)}
        page = requests.get(url,headers=headers)
            try:
                parser = etree.HTML(page.content)
                translation = parser.xpath('//*[@id="translations-content"]/a[1]/text()')[0]
                clean_translation=translation
                print(clean_translation)
            except:
                print(f'Traduzione di {k} non disponibile in {l}!' )
            

Se lanciamo il programma otterremo un risultato come il seguente:

ENGLISH
shoes
Traduzione di MR Calcio non disponibile in english!
offers
balances
FRENCH
chaussures
Traduzione di MR Calcio non disponibile in french!
offres
soldes
DUTCH
schoenen
Traduzione di MR Calcio non disponibile in dutch!
offertes
saldi
SPANISH
zapatos
Traduzione di MR Calcio non disponibile in spanish!
ofertas
saldos

Abbiamo visto come ottenere un semplice servizio di traduzione per ottenere rapidamente delle keywords in più lingue.

Utilizzo di indirizzi IP multipli di server proxy.

Un problema comune che si incontra durante la scansione di un sito è quello di venir identificati come un agente malevolo e venir bloccati attraverso l’iscrizione del proprio IP in una black-list del sito.

Ci sono varie tecniche per evitare questo problema, in questo articolo vedremo come proteggerci con l’uso di un proxies che cambiano ad ogni chiamata che effettuiamo al server che stiamo interrogando.

L’utilizzo di proxy e di indirizzi IP rotanti in combinazione con user-agents rotanti può aiutarti a superare la maggior parte delle misure anti-scraping e impedire di essere rilevato come un bot.

proxy rotation www.ferromauro.it

Il concetto di rotazione degli indirizzi IP durante lo scraping è semplice: si cerca far credere al sito web oggetto dell’analisi che non sei un singolo “bot” o una persona che accede con più richieste, ma più utenti che accedono al sito da più postazioni. Se questa tecnica viene utilizzata in modo corretto le possibilità di essere bloccati sono minime.

L’utilizzo di un proxy con Python 3 e la libreria Requests è molto semplice:

import requests
url= 'https://httpbin.org/ip'
proxies = {
  'http': 'http://10.10.1.10:3128',
  'https': 'http://10.10.1.10:1080',
}
requests.get(url, proxies=proxies)
print(response.json())

Nell’esempio sopra riportato viene indicato un indirizzo IP di un proxy gratuito, che molto probabilmente non sarà più in funzione nel momento in cui state leggendo quest’articolo.

Per individuare una lista di proxies gratuiti ci si può affidare a diversi siti web come ad esempio:

Dato che i proxies di queste liste cambiano piuttosto di frequente dobbiamo trovare un modo per automatizzare il controllo dei proxies disponibili, fortunatamente ci piace lo scraping e possiamo fare affidamento su Python!

Partiamo dall’ultimo sito dell’elenco, possiamo una funzione che effettui una chiamata al sito per scaricare la pagina con la libreria ‘requests‘ e successivamente con la libreria ‘lxml‘ andare a cercare le informazioni che ci interessano: l’indirizzo IP e la porta dei proxies con livello di sicurezza ‘elite’.

def proxy_list():
    url = 'https://free-proxy-list.net/'
    response = requests.get(url)
    parser = etree.HTML(response.content)
    proxies = set()
    for i in parser.xpath('//*[@id="proxylisttable"]/tbody/tr')[:10]:
        if i.xpath('.//td[5][contains(text(),"elite proxy")]'):
            proxy = ":".join([i.xpath('.//td[1]/text()')[0], i.xpath('.//td[2]/text()')[0]])
            proxies.add(proxy)
    return(proxies)

Questo è il risultato che ottengo mentre scrivo, mentre leggete l’articolo la pagina potrebbe avere subito delle variazioni e potrebbe essere necessario modificare il percorso xpath di alcuni elementi:


{'1.20.101.175:38263', '190.248.136.18:46090', '125.25.45.120:49887', '140.227.204.8:3128', '109.172.65.134:58147', '190.90.193.96:32455', '170.247.31.159:44669', '88.116.12.182:32871', '118.175.93.185:47007', '86.120.78.21:33666'}

Potete provare a fare lo stesso lavoro con gli altri siti elencati: noterete che per via di alcuni accorgimenti adottati il compito sarà più complicato (ma non impossibile!).

Ora che abbiamo un metodo per avere una lista sempre aggiornata di indirizzi IP di server proxy possiamo creare un programma che utilizzi in modo ciclico gli indirizzi IP della lista simulando che le nostre richieste provengano da diverse località.

Possiamo fare una prova verificando l’indirizzo IP con il sito httpbin.org (cliccando sul link vedrete il vostro indirizzo IP attualmente in uso):

import requests
from lxml import etree
from itertools import cycle

def main():
    proxies = proxy_list()
    proxy_pool = cycle(proxies)
    
    url = 'https://httpbin.org/ip'
    for i in range(1,16):
        proxy = next(proxy_pool)
        print(f'Request {i}')
        try:
            response = requests.get(url,proxies={"http": proxy, "https": proxy})
            print(response.json())
        except:
            print("Skipping. Proxy Not Responding")    
 
    
def proxy_list():
    url = 'https://free-proxy-list.net/'
    response = requests.get(url)
    parser = etree.HTML(response.content)
    proxies = set()
    for i in parser.xpath('//*[@id="proxylisttable"]/tbody/tr')[:10]:
        if i.xpath('.//td[5][contains(text(),"elite proxy")]'):
            proxy = ":".join([i.xpath('.//td[1]/text()')[0], i.xpath('.//td[2]/text()')[0]])
            proxies.add(proxy)
    return(proxies)

if __name__ == '__main__':
    main()

Il risultato ottenuto sarà simile al seguente:

Request 1
{'origin': '202.93.128.98'}
Request 2
{'origin': '206.189.148.118'}
Request 3
{'origin': '129.205.160.160'}
Request 4
{'origin': '109.207.112.212'}
Request 5
{'origin': '77.82.88.249'}
Request 6
{'origin': '125.25.165.127'}
Request 7
{'origin': '36.89.181.155'}
Request 8
{'origin': '5.9.239.164'}
Request 9
{'origin': '202.93.128.98'}
Request 10
{'origin': '206.189.148.118'}
Request 11
{'origin': '129.205.160.160'}
Request 12
{'origin': '109.207.112.212'}
Request 13
{'origin': '77.82.88.249'}
Request 14
{'origin': '125.25.165.127'}
Request 15
{'origin': '36.89.181.155'}

In alcuni casi la connessione con un proxy può fallire per questo motivo è bene creare una chiamata try except per verificare che tutto vada bene.

Questo semplice script permette di usufruire di proxy server sempre aggiornati utile durante un lavoro di scansione, se unito ad un metodo di rotazione dell’user-agent costituisce una buona difesa contro il blocco da parte di un sito web.

Si consiglia di utilizzare solo proxy con livello di protezione alto (elite proxy), solo questi permettono di essere simulare la chiamata come se si trattasse di un utente reale che non stia utilizzando alcun proxy.

Si segnala che l’utilizzo di un proxy gratuito in alcuni casi è inefficace e sarebbe bene utilizzare un proxy a pagamento. I proxy gratuiti a volte vengono inseriti in black-list e subito respinti dai siti con una valida protezione.

Come creare uno scraper con User-Agent Rotation

L’informazione ‘user-agent’ è una stringa che un web browser o una app invia ad ogni sito web visitato. Una tipica stringa di ‘user-agent’ contiene dettagli come: il tipo di applicazione, il sistema operativo, il fornitore del software o la versione software utilizzato dal client che interroga il Web Server di un sito. I Web Server utilizzano questi dati per valutare le funzionalità del computer, ottimizzando le prestazioni e la visualizzazione di una pagina.

Di seguito un esempio di stringa user-agent dell’header:

user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36

Ogni richiesta effettuata da un browser Web contiene un’intestazione user-agent. Quando si analizzano molte pagine di un sito Web con uno scraper, l’uso dello stesso user-agent porta coerentemente al rilevamento della presenza di un bot. Un modo per bypassare tale rilevamento è fingere il proprio user-agent e modificarlo con ogni richiesta effettuata su un sito web.

In questo tutorial, vedremo come modificare e rendere casuale il proprio user-agent per evitare di essere bloccati durante lo scraping di un sito web.

Come modificare il proprio user-agent con Python 3

Utilizzando la libreria ‘requests’ possiamo modificare il nostro user-agent passando un parametro nell’header della chiamata GET. Per testare il codice possiamo utilizzare il sito httpbin.org:

import requests 
url = 'https://httpbin.org/user-agent'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0'
headers = {'User-Agent': user_agent}
response = requests.get(url,headers=headers)
html = response.content
print(response.content)

Questo è quello che otterremo come risultato, una stringa contenente i dati dell’user-agent che abbiamo impostato in modo manuale:

'{\n  "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0"\n}'

Per rendere casuale la stringa dell’user-agent possiamo impostare una lista contenente una serie di 10 user-agent diversi. Una volta generata la lista possiamo pescare in modo casuale un elemento della lista grazie alla funzione choice della libreria ‘random‘. Aggiungiamo anche un ciclo while per ripetere la richiesta 10 volte. Vediamo il codice modificato:

import requests
import random
i=0
url = 'https://httpbin.org/user-agent'
user_agent = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0',
    'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
    'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
    'Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)',
    'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko',
    'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
    'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko',
    'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)',
    'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko']

while i<10:
    headers = {'User-Agent': random.choice(user_agent)}
    response = requests.get(url,headers=headers)
    html = response.content
    print(response.content)
    i=i+1

Se lanciamo lo script vederemo un risultato simile al seguente:

b'{\n  "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0"\n}\n'
b'{\n  "user-agent": "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"\n}\n'
b'{\n  "user-agent": "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)"\n}\n'

Come si può notare questa semplice tecnica ci permette di fingere che le nostre chiamate tramite uno script Python vengano considerate come chiamate effettuate da normali web browser, ogni chiamata sembrerà effettuata da un browser scelto in modo casuale da una lista appositamente preimpostata.

Con questa tecnica è possibile ridurre il rischio di essere bloccati durante un lavoro di scraping dalle misure solitamente presenti nei web server per la limitazione delle chiamate al server.