urllib.request - Accesso a Risorse in Rete
Scopo: Una libreria per aprire URL che può esserer estesa per definire gestori di protocollo personalizzati.
Il modulo urllib.request fornisce una API per utilizzare le risorse Internet identificate da URL. E' progettato per essere esteso da singole applicazioni per supportare nuovi protocolli o per aggiungere varianti a protocolli esistenti (tipo la gestione dell'autenticazione base HTTP).
HTTP GET
# http_server_GET.py
from http.server import BaseHTTPRequestHandler
from urllib import parse
class GetHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed_path = parse.urlparse(self.path)
message_parts = [
'VALORI DEL CLIENT:',
'indirizzi client={} ({})'.format(
self.client_address,
self.address_string()),
'comando={}'.format(self.command),
'percorso={}'.format(self.path),
'percorso reale={}'.format(parsed_path.path),
'query={}'.format(parsed_path.query),
'versione richiesta={}'.format(self.request_version),
'',
'VALORI DEL SERVER:',
'versione server={}'.format(self.server_version),
'versione sys={}'.format(self.sys_version),
'versione protocollo={}'.format(self.protocol_version),
'',
'INTESTAZIONI RICEVUTE:',
]
for name, value in sorted(self.headers.items()):
message_parts.append(
'{}={}'.format(name, value.rstrip())
)
message_parts.append('')
message = '\r\n'.join(message_parts)
self.send_response(200)
self.send_header('Content-Type',
'text/plain; charset=utf-8')
self.end_headers()
self.wfile.write(message.encode('utf-8'))
if __name__ == '__main__':
from http.server import HTTPServer
server = HTTPServer(('localhost', 8080), GetHandler)
print('Avvio del server, usare <Ctrl-C> per interrompere')
server.serve_forever()
Una operazione HTTP GET costituisce l'uso più semplice di urllib.request
. Si passi un URL a urlopen()
per ottenere un handle "tipo file" dei dati remoti.
# urllib_request_urlopen.py
from urllib import request
response = request.urlopen('http://localhost:8080/')
print('RISPOSTA :', response)
print('URL :', response.geturl())
headers = response.info()
print('DATA :', headers['date'])
print('INTESTAZIONI :')
print('--------------')
print(headers)
data = response.read().decode('utf-8')
print('LUNGHEZZA :', len(data))
print('DATI :')
print('---------------')
print(data)
Il server di esempio accetta valori in arrivo e formatta una risposta in testo semplice da restituire. Il valore di ritorno da urlopen()
fornisce accesso alle intestazioni (headers) dal server HTTP tramite il metodo info()
e i dati per le risorse remote con metodi come read()
e readlines()
.
$ python3 urllib_request_urlopen.py RISPOSTA : <http.client.HTTPResponse object at 0x7f8000a08e80> URL : http://localhost:8080/ DATA : Sun, 30 Apr 2017 13:25:10 GMT INTESTAZIONI : -------------- Server: BaseHTTP/0.6 Python/3.5.2+ Date: Sun, 30 Apr 2017 13:25:10 GMT Content-Type: text/plain; charset=utf-8 LUNGHEZZA : 382 DATI : --------------- VALORI DEL CLIENT: indirizzi client=('127.0.0.1', 32990) (127.0.0.1) comando=GET percorso=/ percorso reale=/ query= versione richiesta=HTTP/1.1 VALORI DEL SERVER: versione server=BaseHTTP/0.6 versione sys=Python/3.5.2+ versione protocollo=HTTP/1.0 INTESTAZIONI RICEVUTE: Accept-Encoding=identity Connection=close Host=localhost:8080 User-Agent=Python-urllib/3.5
L'oggetto "tipo file" ritornato da urlopen()
è iterabile.
# urllib_request_urlopen_iterator.py
from urllib import request
response = request.urlopen('http://localhost:8080/')
for line in response:
print(line.decode('utf-8').rstrip())
Questo esempio elimina i ritorni a campo e gli avanti linea prima di stampare l'output.
$ python3 urllib_request_urlopen_iterator.py VALORI DEL CLIENT: indirizzi client=('127.0.0.1', 37030) (127.0.0.1) comando=GET percorso=/ percorso reale=/ query= versione richiesta=HTTP/1.1 VALORI DEL SERVER: versione server=BaseHTTP/0.6 versione sys=Python/3.5.2+ versione protocollo=HTTP/1.0 INTESTAZIONI RICEVUTE: Accept-Encoding=identity Connection=close Host=localhost:8080 User-Agent=Python-urllib/3.5
Codificare Argomenti
Gli argomenti possono essere passati al server codificandoli con urllib.parse.urlencode()
e accodati all'URL.
# urllib_request_http_get_args.py
from urllib import parse
from urllib import request
query_args = {'q': 'query string', 'foo': 'bar'}
encoded_args = parse.urlencode(query_args)
print('Codificato:', encoded_args)
url = 'http://localhost:8080/?' + encoded_args
print(request.urlopen(url).read().decode('utf-8'))
La lista dei valori del client ritornati nell'output di esempio contiene gli argomenti di query codificati.
$ python3 urllib_request_http_get_args.py Codificato: q=query+string&foo=bar VALORI DEL CLIENT: indirizzi client=('127.0.0.1', 39624) (127.0.0.1) comando=GET percorso=/?q=query+string&foo=bar percorso reale=/ query=q=query+string&foo=bar versione richiesta=HTTP/1.1 VALORI DEL SERVER: versione server=BaseHTTP/0.6 versione sys=Python/3.5.2+ versione protocollo=HTTP/1.0 INTESTAZIONI RICEVUTE: Accept-Encoding=identity Connection=close Host=localhost:8080 User-Agent=Python-urllib/3.5
HTTP POST
# http_server_POST.py
import cgi
from http.server import BaseHTTPRequestHandler
import io
class PostHandler(BaseHTTPRequestHandler):
def do_POST(self):
# Parse the form data posted
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={
'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': self.headers['Content-Type'],
}
)
# Inizio della risposta
self.send_response(200)
self.send_header('Content-Type',
'text/plain; charset=utf-8')
self.end_headers()
out = io.TextIOWrapper(
self.wfile,
encoding='utf-8',
line_buffering=False,
write_through=True,
)
out.write('Client: {}\n'.format(self.client_address))
out.write('User-agent: {}\n'.format(
self.headers['user-agent']))
out.write('Percorso: {}\n'.format(self.path))
out.write('Dati del form:\n')
# Ritorna le informazioni di ciò che era stato inviato nel form
for field in form.keys():
field_item = form[field]
if field_item.filename:
# Il campo contiene un file inviato
file_data = field_item.file.read()
file_len = len(file_data)
del file_data
out.write(
'\tUploaded {} as {!r} ({} bytes)\n'.format(
field, field_item.filename, file_len)
)
else:
# Valori del normali
out.write('\t{}={}\n'.format(
field, form[field].value))
# Disconnette il wrapper di codifica dal buffer sottostante
# in modo che l'eliminazione del wrapper non chiude
# il socket, che è ancora in uso dal server.
out.detach()
if __name__ == '__main__':
from http.server import HTTPServer
server = HTTPServer(('localhost', 8080), PostHandler)
print('Server in esecuzione, usare <Ctrl-C> per terminare')
server.serve_forever()
Per inviare dati codificati da un form al server remoto utilizzare POST in luogo di GET, si passino gli argomenti di ricerca codificati come dati per urlopen()
.
# urllib_request_urlopen_post.py
from urllib import parse
from urllib import request
query_args = {'q': 'query string', 'foo': 'bar'}
encoded_args = parse.urlencode(query_args).encode('utf-8')
url = 'http://localhost:8080/'
print(request.urlopen(url, encoded_args).read().decode('utf-8'))
Il server può decodificare i dati del form e accedere ai singoli valori per nome.
$ python3 urllib_request_urlopen_post.py Client: ('127.0.0.1', 37606) User-agent: Python-urllib/3.5 Percorso: / Dati del form: q=query string foo=bar
Aggiungere Intestazioni In Uscita
urlopen()
è una funzione di comodo che nasconde alcuni dettagli di come la richiesta sia composta e gestita. Un controllo più preciso è possibile usando direttamente una istanza di Request
. Ad esempio intestazioni personalizzate potrebbero essere aggiunte alla richiesta in uscita per controllare il formato dei dati restituiti, specificare la versione di un documento in cache locale, e dire al server remoto il nome del software del client con il quale sta comunicando.
Come mostra l'output degli esempi precedenti, il valore predefinito dell'intestazione User-agent è composto dalla costante Python-urllib
seguito dalla versione dell'interprete Python. Quando si sta creando una applicazione che avrà accesso a risorse web in possesso di qualcun altro, è cortesia includere informazioni reali sull'user agent nelle richieste, in modo che sia possibile identificare la sorgente delle chiamate più facilmente. L'uso di un user agent personalizzato consente anche di controllare i crawler usando un file robots.txt
(vedere il modulo urllib.robotparser).
Utilizzare http_server_GET.py
come server per questo esempio (n.d.t.).
# urllib_request_request_header.py
from urllib import request
r = request.Request('http://localhost:8080/')
r.add_header(
'User-agent',
'PyMOTW (https://pymotw.com/)',
)
response = request.urlopen(r)
data = response.read().decode('utf-8')
print(data)
Dopo la creazione dell'oggetto Request
, si usi add_header()
per aggiungere il valore User-agent prima di aprire la richiesta. L'ultima riga nell'output mostra il valore personalizzato.
$ python3 urllib_request_request_header.py VALORI DEL CLIENT: indirizzi client=('127.0.0.1', 48146) (127.0.0.1) comando=GET percorso=/ percorso reale=/ query= versione richiesta=HTTP/1.1 VALORI DEL SERVER: versione server=BaseHTTP/0.6 versione sys=Python/3.5.2+ versione protocollo=HTTP/1.0 INTESTAZIONI RICEVUTE: Accept-Encoding=identity Connection=close Host=localhost:8080 User-Agent=PyMOTW (https://pymotw.com/)
Inviare Dati da un Form da una Richiesta
I dati in uscita possono essere specificati quando si costruisce un oggetto Request
per inviare i dati al server.
# urllib_request_request_post.py
from urllib import parse
from urllib import request
query_args = {'q': 'query string', 'foo': 'bar'}
r = request.Request(
url='http://localhost:8080/',
data=parse.urlencode(query_args).encode('utf-8'),
)
print('Metodo di richiesta :', r.get_method())
r.add_header(
'User-agent',
'PyMOTW (https://pymotw.com/)',
)
print()
print('DATI IN USCITA:')
print(r.data)
print()
print('RISPOSTA DEL SERVER:')
print(request.urlopen(r).read().decode('utf-8'))
Il metodo HTTP usato da Request
viene modificato automaticamente da GET a POST quando i dati sono aggiunti.
$ python3 urllib_request_request_post.py Metodo di richiesta : POST DATI IN USCITA: b'foo=bar&q=query+string' RISPOSTA DEL SERVER: Client: ('127.0.0.1', 51962) User-agent: PyMOTW (https://pymotw.com/) Percorso: / Dati del form: q=query string foo=bar
Inviare File
Codificare file per l'invio richiede maggior lavoro rispetto a semplici dati di un form. Occorre costruire nel corpo della richiesta un messaggio MIME completo, in modo che il server possa riconoscere i campi del form dai file inviati.
# urllib_request_upload_files.py
import io
import mimetypes
from urllib import request
import uuid
class MultiPartForm:
"""Raccoglie i dati da usare quando si invia un form."""
def __init__(self):
self.form_fields = []
self.files = []
# Usa una grande string di byte casuali per separere le
# parti dei dati MIME.
self.boundary = uuid.uuid4().hex.encode('utf-8')
return
def get_content_type(self):
return 'multipart/form-data; boundary={}'.format(
self.boundary.decode('utf-8'))
def add_field(self, name, value):
"""Aggiunge un campo semplice ai dati del form."""
self.form_fields.append((name, value))
def add_file(self, fieldname, filename, fileHandle,
mimetype=None):
"""Aggiunte un file da inviare."""
body = fileHandle.read()
if mimetype is None:
mimetype = (
mimetypes.guess_type(filename)[0] or
'application/octet-stream'
)
self.files.append((fieldname, filename, mimetype, body))
return
@staticmethod
def _form_data(name):
return ('Content-Disposition: form-data; '
'name="{}"\r\n').format(name).encode('utf-8')
@staticmethod
def _attached_file(name, filename):
return ('Content-Disposition: file; '
'name="{}"; filename="{}"\r\n').format(
name, filename).encode('utf-8')
@staticmethod
def _content_type(ct):
return 'Content-Type: {}\r\n'.format(ct).encode('utf-8')
def __bytes__(self):
"""Rttorna una stringa di byte che rappresentano i dati del form,
compreso i file allegati
"""
buffer = io.BytesIO()
boundary = b'--' + self.boundary + b'\r\n'
# Aggiunge i campi del form
for name, value in self.form_fields:
buffer.write(boundary)
buffer.write(self._form_data(name))
buffer.write(b'\r\n')
buffer.write(value.encode('utf-8'))
buffer.write(b'\r\n')
# Aggiunge i fiel da inviare
for f_name, filename, f_content_type, body in self.files:
buffer.write(boundary)
buffer.write(self._attached_file(f_name, filename))
buffer.write(self._content_type(f_content_type))
buffer.write(b'\r\n')
buffer.write(body)
buffer.write(b'\r\n')
buffer.write(b'--' + self.boundary + b'--\r\n')
return buffer.getvalue()
if __name__ == '__main__':
# Creat il form con campi semplici
form = MultiPartForm()
form.add_field('firstname', 'Doug')
form.add_field('lastname', 'Hellmann')
# Aggiunge un file fasullo
form.add_file(
'biography', 'bio.txt',
fileHandle=io.BytesIO(b'Python developer and blogger.'))
# Costruisce la richiesta, compresa la stringa di byte
# per i dati da inviare.
data = bytes(form)
r = request.Request('http://localhost:8080/', data=data)
r.add_header(
'User-agent',
'PyMOTW (https://pymotw.com/)',
)
r.add_header('Content-type', form.get_content_type())
r.add_header('Content-length', len(data))
print()
print('DATI IN USCITA:')
for name, value in r.header_items():
print('{}: {}'.format(name, value))
print()
print(r.data.decode('utf-8'))
print()
print('RISPOSTA DEL SERVER:')
print(request.urlopen(r).read().decode('utf-8'))
La classe MultiPartForm
può rappresentare un form arbitrario come un messaggio MIME a più parti con file allegati.
$ python3 urllib_request_upload_files.py DATI IN USCITA: Content-length: 389 Content-type: multipart/form-data; boundary=d54496eaac6e4bf18ea96c827c1b5d36 User-agent: PyMOTW (https://pymotw.com/) --d54496eaac6e4bf18ea96c827c1b5d36 Content-Disposition: form-data; name="firstname" Doug --d54496eaac6e4bf18ea96c827c1b5d36 Content-Disposition: form-data; name="lastname" Hellmann --d54496eaac6e4bf18ea96c827c1b5d36 Content-Disposition: file; name="biography"; filename="bio.txt" Content-Type: text/plain Python developer and blogger. --d54496eaac6e4bf18ea96c827c1b5d36-- RISPOSTA DEL SERVER: Client: ('127.0.0.1', 42158) User-agent: PyMOTW (https://pymotw.com/) Percorso: / Dati del form: lastname=Hellmann firstname=Doug Uploaded biography as 'bio.txt' (29 bytes)
Creare Gestori di Protocollo Personalizzati
urllib.request
ha un supporto built-in per accesso HTTP(S), FTP e locale. Per aggiungere supporto ad altri tipi di URL, occorre registrare un altro gestore di protocollo. Ad esempio per supportare URL che puntano a file che si trovano su server NFS remoti, senza richiedere agli utenti di montare il percorso prima di accedere al file, si crei una classe derivata da BaseHandler
con un metodo nfs_open()
.
Il metodo open()
specifico per il protocollo riceve un singolo argomento, l'istanza di Request
, e dovrebbe ritornare un oggetto con un metodo read()
che possa essere usato per leggere i dati, un metodo info()
per ritornare le intestazioni di risposta, e geturl()
per ritornare l'effettivo URL del file che si sta per leggere. Un semplice modo per farlo è di creare una istanza di urllib.response.addinfourl
, passare le intestazioni, l'URL, e l'handle del file aperto nel costruttore.
# urllib_request_nfs_handler.py
import io
import mimetypes
import os
import tempfile
from urllib import request
from urllib import response
class NFSFile:
def __init__(self, tempdir, filename):
self.tempdir = tempdir
self.filename = filename
with open(os.path.join(tempdir, filename), 'rb') as f:
self.buffer = io.BytesIO(f.read())
def read(self, *args):
return self.buffer.read(*args)
def readline(self, *args):
return self.buffer.readline(*args)
def close(self):
print('\nNFSFile:')
print(' sto smontando {}'.format(
os.path.basename(self.tempdir)))
print(' quando {} è chiuso'.format(
os.path.basename(self.filename)))
class FauxNFSHandler(request.BaseHandler):
def __init__(self, tempdir):
self.tempdir = tempdir
super().__init__()
def nfs_open(self, req):
url = req.full_url
directory_name, file_name = os.path.split(url)
server_name = req.host
print('Simulazione montaggio FauxNFSHandler:')
print(' Percorso remoto : {}'.format(directory_name))
print(' Server : {}'.format(server_name))
print(' Percorso locale : {}'.format(
os.path.basename(tempdir)))
print(' Nome file : {}'.format(file_name))
local_file = os.path.join(tempdir, file_name)
fp = NFSFile(tempdir, file_name)
content_type = (
mimetypes.guess_type(file_name)[0] or
'application/octet-stream'
)
stats = os.stat(local_file)
size = stats.st_size
headers = {
'Content-type': content_type,
'Content-length': size,
}
return response.addinfourl(fp, headers,
req.get_full_url())
if __name__ == '__main__':
with tempfile.TemporaryDirectory() as tempdir:
# Popola il file temporaneo per la simulazione
filename = os.path.join(tempdir, 'file.txt')
with open(filename, 'w', encoding='utf-8') as f:
f.write('Contents of file.txt')
# Costruisce un opener con il nostro handler NFS
# e lo registra come predefinito.
opener = request.build_opener(FauxNFSHandler(tempdir))
request.install_opener(opener)
# Apre il file tramite un URL.
resp = request.urlopen(
'nfs://remote_server/path/to/the/file.txt'
)
print()
print('CONTENUTO LETTO:', resp.read())
print('URL :', resp.geturl())
print('INTESTAZIONI :')
for name, value in sorted(resp.info().items()):
print(' {:<15} = {}'.format(name, value))
resp.close()
Le classi FauxNFSHandler
e NFSFile
stampano messaggi per illustrare dove una vera implementazione avrebbe effettuato le chiamate di montaggio e smontaggio. Visto che si tratta di una simulazione, FauxNFSHandler
viene preparato con il nome di una directory temporanea dove dovrebbe cercare tutti i suoi file.
$ python3 urllib_request_nfs_handler.py Simulazione montaggio FauxNFSHandler: Percorso remoto : nfs://remote_server/path/to/the Server : remote_server Percorso locale : tmpycu8eq3r Nome file : file.txt CONTENUTO LETTO: b'Contents of file.txt' URL : nfs://remote_server/path/to/the/file.txt INTESTAZIONI : Content-length = 20 Content-type = text/plain NFSFile: sto smontando tmpycu8eq3r quando file.txt è chiuso
Vedere anche:
- urllib.request
- La documentazione della libreria standard per questo modulo
- urllib.parse
- Lavora con le stringa URL
- Form content types
- Specifiche WC3 per l'invio di file o grandi volumi di dati tramite form HTTP.
- requests
- Libreria di terze parti con miglior supporto per connessioni sicure e una API più facile da usare. La squadra di sviluppo del core di Python raccomanda che la maggior parte degli sviluppatori utilizzi requests, in parte perchè riceve aggiornamenti di sicurezza più frequentemente della libreria standard.