Så här hämtar du internetresurser med urllib-paketet¶
- Författare:
Introduktion¶
urllib.request är en Python-modul för att hämta URL:er (Uniform Resource Locators). Den erbjuder ett mycket enkelt gränssnitt i form av funktionen urlopen. Denna kan hämta webbadresser med hjälp av en mängd olika protokoll. Det finns också ett lite mer komplext gränssnitt för att hantera vanliga situationer - som grundläggande autentisering, cookies, proxies och så vidare. Dessa tillhandahålls av objekt som kallas handlers och openers.
urllib.request stöder hämtning av webbadresser för många ”URL-scheman” (identifieras av strängen före ":"
i URL - till exempel "ftp"
är URL-schemat för "ftp://python.org/"
) med hjälp av deras associerade nätverksprotokoll (t.ex. FTP, HTTP). Denna handledning fokuserar på det vanligaste fallet, HTTP.
För okomplicerade situationer är urlopen mycket lätt att använda. Men så snart du stöter på fel eller icke-triviala fall när du öppnar HTTP-URL:er behöver du en viss förståelse för HyperText Transfer Protocol. Den mest omfattande och auktoritativa referensen till HTTP är RFC 2616. Detta är ett tekniskt dokument som inte är avsett att vara lättläst. Denna HOWTO syftar till att illustrera användningen av urllib, med tillräckligt med detaljer om HTTP för att hjälpa dig igenom. Den är inte avsedd att ersätta urllib.request
-dokumenten, utan är ett komplement till dem.
Hämta webbadresser¶
Det enklaste sättet att använda urllib.request är enligt följande:
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
html = response.read()
Om du vill hämta en resurs via URL och lagra den på en tillfällig plats kan du göra det med hjälp av funktionerna shutil.copyfileobj()
och tempfile.NamedTemporaryFile()
:
import shutil
import tempfile
import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
shutil.copyfileobj(response, tmp_file)
with open(tmp_file.name) as html:
pass
Många användningar av urllib kommer att vara så enkla (notera att vi i stället för en ”http:”-URL kunde ha använt en URL som börjar med ”ftp:”, ”file:” etc.). Syftet med denna handledning är dock att förklara de mer komplicerade fallen, med fokus på HTTP.
HTTP baseras på förfrågningar och svar – klienten gör förfrågningar och servrarna skickar svar. urllib.request speglar detta med ett Request
-objekt som representerar den HTTP-förfrågan du gör. I sin enklaste form skapar du ett Request-objekt som anger den URL du vill hämta. Om du anropar urlopen
med detta Request-objekt returneras ett svarobjekt för den begärda URL:en. Detta svar är ett fil-liknande objekt, vilket innebär att du till exempel kan anropa .read()
på svaret:
import urllib.request
req = urllib.request.Request('http://python.org/')
with urllib.request.urlopen(req) as response:
the_page = response.read()
Observera att urllib.request använder samma Request-gränssnitt för att hantera alla URL-scheman. Du kan till exempel göra en FTP-begäran så här:
req = urllib.request.Request('ftp://example.com/')
När det gäller HTTP finns det två extra saker som Request-objekten gör det möjligt för dig att göra: För det första kan du skicka data som ska skickas till servern. För det andra kan du skicka extra information (”metadata”) om data eller om själva begäran till servern - denna information skickas som HTTP-”headers”. Låt oss titta på var och en av dessa i tur och ordning.
Data¶
Ibland vill man skicka data till en URL (ofta hänvisar URL:en till ett CGI-skript (Common Gateway Interface) eller en annan webbapplikation). Med HTTP görs detta ofta med hjälp av en så kallad POST-begäran. Det är ofta vad din webbläsare gör när du skickar in ett HTML-formulär som du fyllt i på webben. Alla POSTs behöver inte komma från formulär: du kan använda en POST för att överföra godtyckliga data till din egen applikation. I det vanliga fallet med HTML-formulär måste data kodas på ett standardiserat sätt och sedan skickas till Request-objektet som argumentet data
. Kodningen görs med hjälp av en funktion från urllib.parse
-biblioteket.
import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
values = {'name' : 'Michael Foord',
'location' : 'Northampton',
'language' : 'Python' }
data = urllib.parse.urlencode(values)
data = data.encode('ascii') # data should be bytes
req = urllib.request.Request(url, data)
with urllib.request.urlopen(req) as response:
the_page = response.read()
Observera att andra kodningar ibland krävs (t.ex. för filuppladdning från HTML-formulär - se HTML Specification, Form Submission för mer information).
Om du inte skickar med argumentet data
använder urllib en GET-begäran. Ett sätt som GET- och POST-begäranden skiljer sig åt är att POST-begäranden ofta har ”bieffekter”: de ändrar systemets tillstånd på något sätt (till exempel genom att göra en beställning på webbplatsen för att få hundra kilo konserverad skräppost levererad till din dörr). Även om HTTP-standarden klargör att POST-förfrågningar alltid ska orsaka bieffekter och GET-förfrågningar aldrig ska orsaka bieffekter, finns det inget som hindrar en GET-förfrågan från att ha bieffekter eller en POST-förfrågan från att inte ha några bieffekter. Data kan också skickas i en HTTP GET-begäran genom att koda den i själva URL:en.
Detta görs på följande sätt:
>>> import urllib.request
>>> import urllib.parse
>>> data = {}
>>> data['name'] = 'Somebody Here'
>>> data['location'] = 'Northampton'
>>> data['language'] = 'Python'
>>> url_values = urllib.parse.urlencode(data)
>>> print(url_values) # The order may differ from below.
name=Somebody+Here&language=Python&location=Northampton
>>> url = 'http://www.example.com/example.cgi'
>>> full_url = url + '?' + url_values
>>> data = urllib.request.urlopen(full_url)
Observera att den fullständiga URL:en skapas genom att lägga till en ?
till URL:en, följt av de kodade värdena.
Sidhuvud¶
Vi kommer här att diskutera en särskild HTTP-header för att illustrera hur man lägger till headers i en HTTP-begäran.
Vissa webbplatser [1] ogillar att surfas av program, eller skickar olika versioner till olika webbläsare [2]. Som standard identifierar urllib sig själv som Python-urllib/x.y
(där x
och y
är Python-versionens större och mindre versionsnummer, t.ex. Python-urllib/2.5
), vilket kan förvirra webbplatsen eller helt enkelt inte fungera. Det sätt på vilket en webbläsare identifierar sig är genom User-Agent
-headern [3]. När du skapar ett Request-objekt kan du skicka in en ordbok med rubriker. Följande exempel gör samma förfrågan som ovan, men identifierar sig själv som en version av Internet Explorer [4].
import urllib.parse
import urllib.request
url = 'http://www.someserver.com/cgi-bin/register.cgi'
user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'
values = {'name': 'Michael Foord',
'location': 'Northampton',
'language': 'Python' }
headers = {'User-Agent': user_agent}
data = urllib.parse.urlencode(values)
data = data.encode('ascii')
req = urllib.request.Request(url, data, headers)
with urllib.request.urlopen(req) as response:
the_page = response.read()
Svaret har också två användbara metoder. Se avsnittet om info och geturl som kommer efter att vi har tittat på vad som händer när saker och ting går fel.
Hantering av undantag¶
urlopen ger upphov till URLError
när det inte kan hantera ett svar (men som vanligt med Python API:er kan inbyggda undantag som ValueError
, TypeError
etc. också ges upphov till).
HTTPError
är underklassen till URLError
som uppstår i det specifika fallet med HTTP-URL:er.
Undantagsklasserna exporteras från modulen urllib.error
.
URLError¶
Ofta uppstår URLError på grund av att det inte finns någon nätverksanslutning (ingen väg till den angivna servern) eller att den angivna servern inte existerar. I detta fall kommer det undantag som uppstår att ha ett ”reason”-attribut, som är en tupel som innehåller en felkod och ett textfelmeddelande.
t.ex.
>>> req = urllib.request.Request('http://www.pretend_server.org')
>>> try: urllib.request.urlopen(req)
... except urllib.error.URLError as e:
... print(e.reason)
...
(4, 'getaddrinfo failed')
HTTPError¶
Varje HTTP-svar från servern innehåller en numerisk ”statuskod”. Ibland indikerar statuskoden att servern inte kan uppfylla begäran. Standardhanterarna kommer att hantera vissa av dessa svar åt dig (om svaret till exempel är en ”omdirigering” som begär att klienten hämtar dokumentet från en annan URL, kommer urllib att hantera det åt dig). För de som inte kan hanteras kommer urlopen att skapa ett HTTPError
. Typiska fel är ’404’ (sidan hittades inte), ’403’ (begäran förbjuden) och ’401’ (autentisering krävs).
Se avsnitt 10 i RFC 2616 för en referens till alla HTTP-felkoder.
Den HTTPError
-instans som skapas kommer att ha ett heltalsattribut ’code’, som motsvarar det fel som skickas av servern.
Felkoder¶
Eftersom standardhanterarna hanterar omdirigeringar (koder i intervallet 300), och koder i intervallet 100-299 indikerar framgång, kommer du vanligtvis bara att se felkoder i intervallet 400-599.
http.server.BaseHTTPRequestHandler.responses
är en användbar ordbok över svarskoder som visar alla de svarskoder som används av RFC 2616. Ett utdrag från ordlistan visas nedan
svar = {
...
<HTTPStatus.OK: 200>: ('OK', 'Begäran uppfylld, dokumentet följer'),
...
<HTTPStatus.FORBIDDEN: 403>: ('Förbjuden',
'Begäran förbjuden -- auktorisering kommer '
'inte hjälpa'),
<HTTPStatus.NOT_FOUND: 404>: ('Hittades inte',
"Inget matchar den angivna URI:n"),
...
<HTTPStatus.IM_A_TEAPOT: 418>: ("Jag är en tekanna",
"Servern vägrar att brygga kaffe eftersom '
'den är en tekanna'),
...
<HTTPStatus.SERVICE_UNAVAILABLE: 503>: ('Tjänsten är inte tillgänglig',
'Servern kan inte behandla '
'begäran på grund av hög belastning'),
...
}
När ett fel uppstår svarar servern genom att returnera en HTTP-felkod och en felsida. Du kan använda HTTPError
-instansen som ett svar på den returnerade sidan. Detta innebär att förutom kodattributet har den också metoderna read, geturl och info, som returneras av modulen urllib.response
:
>>> req = urllib.request.Request('http://www.python.org/fish.html')
>>> try:
... urllib.request.urlopen(req)
... except urllib.error.HTTPError as e:
... print(e.code)
... print(e.read())
...
404
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n\n\n<html
...
<title>Page Not Found</title>\n
...
Avslutning¶
Så om du vill vara beredd på HTTPError
eller URLError
finns det två grundläggande tillvägagångssätt. Jag föredrar det andra tillvägagångssättet.
Nummer 1¶
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
req = Request(someurl)
try:
response = urlopen(req)
except HTTPError as e:
print('The server couldn\'t fulfill the request.')
print('Error code: ', e.code)
except URLError as e:
print('We failed to reach a server.')
print('Reason: ', e.reason)
else:
# everything is fine
Anteckning
except HTTPError
måste komma först, annars kommer except URLError
också att fånga ett HTTPError
.
Nummer 2¶
from urllib.request import Request, urlopen
from urllib.error import URLError
req = Request(someurl)
try:
response = urlopen(req)
except URLError as e:
if hasattr(e, 'reason'):
print('We failed to reach a server.')
print('Reason: ', e.reason)
elif hasattr(e, 'code'):
print('The server couldn\'t fulfill the request.')
print('Error code: ', e.code)
else:
# everything is fine
info och geturl¶
Svaret som returneras av urlopen (eller instansen HTTPError
) har två användbara metoder info()
och geturl()
och definieras i modulen urllib.response
.
geturl - returnerar den riktiga URL:en för den hämtade sidan. Detta är användbart eftersom
urlopen
(eller det öppningsobjekt som används) kan ha följt en omdirigering. URL:en för den hämtade sidan kanske inte är densamma som den begärda URL:en.info - detta returnerar ett ordboksliknande objekt som beskriver den hämtade sidan, särskilt de rubriker som skickats av servern. Det är för närvarande en instans av
http.client.HTTPMessage
.
Typiska rubriker är ”Content-length”, ”Content-type” och så vidare. I Quick Reference to HTTP Headers finns en användbar lista över HTTP-rubriker med korta förklaringar av deras betydelse och användning.
Öppnare och hanterare¶
När du hämtar en URL använder du en öppnare (en instans av den kanske förvirrande benämnda urllib.request.OpenerDirector
). Normalt har vi använt standardöppnaren - via urlopen
- men du kan skapa egna öppnare. Öppnare använder hanterare. Alla ”tunga lyft” görs av hanterarna. Varje hanterare vet hur man öppnar URL:er för ett visst URL-schema (http, ftp, etc.), eller hur man hanterar en aspekt av URL-öppning, till exempel HTTP-omdirigeringar eller HTTP-cookies.
Du behöver skapa öppnare om du vill hämta webbadresser med specifika hanterare installerade, t.ex. för att få en öppnare som hanterar cookies eller för att få en öppnare som inte hanterar omdirigeringar.
För att skapa en öppnare instansierar du en OpenerDirector
och anropar sedan .add_handler(some_handler_instance)
upprepade gånger.
Alternativt kan du använda build_opener
, som är en bekvämlighetsfunktion för att skapa öppningsobjekt med ett enda funktionsanrop. build_opener
lägger till flera hanterare som standard, men ger ett snabbt sätt att lägga till fler och/eller åsidosätta standardhanterarna.
Andra typer av hanterare som du kanske vill ha kan hantera proxyservrar, autentisering och andra vanliga men lite specialiserade situationer.
install_opener
kan användas för att göra ett opener
-objekt till den (globala) standardöppnaren. Detta innebär att anrop till urlopen
kommer att använda den öppnare du har installerat.
Öppningsobjekt har en metod som heter open
, som kan anropas direkt för att hämta webbadresser på samma sätt som funktionen urlopen
: det finns ingen anledning att anropa install_opener
, förutom för bekvämlighetens skull.
Grundläggande autentisering¶
För att illustrera hur man skapar och installerar en hanterare kommer vi att använda HTTPBasicAuthHandler
. För en mer detaljerad diskussion om detta ämne - inklusive en förklaring av hur Basic Authentication fungerar - se Basic Authentication Tutorial.
När autentisering krävs skickar servern ett huvud (samt felkoden 401) med en begäran om autentisering. Detta anger autentiseringsschemat och en ”realm”. Headern ser ut som följer: WWW-Authenticate: SCHEME realm="REALM"
.
t.ex.
WWW-Autenticate: Basic realm="cPanel-användare"
Klienten bör sedan försöka igen med det lämpliga namnet och lösenordet för sfären som ingår som en rubrik i begäran. Detta är ”grundläggande autentisering”. För att förenkla denna process kan vi skapa en instans av HTTPBasicAuthHandler
och en öppnare för att använda denna hanterare.
I HTTPBasicAuthHandler
används ett objekt som kallas lösenordshanterare för att hantera mappningen av URL:er och sfärer till lösenord och användarnamn. Om du vet vad sfären är (från autentiseringshuvudet som skickas av servern) kan du använda en HTTPPasswordMgr
. Ofta bryr man sig inte om vad sfären är. I så fall är det bekvämt att använda HTTPPasswordMgrWithDefaultRealm
. Detta gör att du kan ange ett standardanvändarnamn och lösenord för en URL. Detta kommer att anges om du inte tillhandahåller en alternativ kombination för en specifik sfär. Vi anger detta genom att tillhandahålla None
som realm-argument till metoden add_password
.
URL:en på högsta nivån är den första URL:en som kräver autentisering. URL:er som är ”djupare” än den URL du skickar till .add_password() kommer också att matchas.
# create a password manager
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
# Add the username and password.
# If we knew the realm, we could use it instead of None.
top_level_url = "http://example.com/foo/"
password_mgr.add_password(None, top_level_url, username, password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
# create "opener" (OpenerDirector instance)
opener = urllib.request.build_opener(handler)
# use the opener to fetch a URL
opener.open(a_url)
# Install the opener.
# Now all calls to urllib.request.urlopen use our opener.
urllib.request.install_opener(opener)
Anteckning
I exemplet ovan skickade vi bara vår HTTPBasicAuthHandler
till build_opener
. Som standard har öppnare hanterare för normala situationer – ProxyHandler
(om en proxyinställning såsom en http_proxy
är inställd), UnknownHandler
, HTTPHandler
, HTTPDefaultErrorHandler
, HTTPRedirectHandler
, FTPHandler
, FileHandler
, DataHandler
, HTTPErrorProcessor
.
top_level_url
är i själva verket endera en fullständig URL (inklusive ”http:”-komponenten och värdnamnet och eventuellt portnumret), t.ex. "http://example.com/"
eller en ”auktoritet” (dvs. värdnamnet, eventuellt inklusive portnumret), t.ex. "example.com"
eller "example.com:8080"
(det senare exemplet inkluderar ett portnummer). Den eventuella auktoriteten får INTE innehålla komponenten ”userinfo” - t.ex. är ”joe:password@example.com” inte korrekt.
Proxies¶
urllib kommer automatiskt att upptäcka dina proxyinställningar och använda dem. Detta sker genom ProxyHandler
, som är en del av den normala hanteringskedjan när en proxyinställning upptäcks. Normalt är det bra, men det finns tillfällen då det kanske inte är till hjälp [5]. Ett sätt att göra detta är att ställa in vår egen ProxyHandler
, utan några proxyer definierade. Detta görs genom att använda liknande steg som att sätta upp en Basic Authentication handler:
>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)
Anteckning
För närvarande har urllib.request
inte stöd för hämtning av https
-platser via en proxy. Detta kan dock aktiveras genom att utöka urllib.request som visas i receptet [6].
Anteckning
HTTP_PROXY
ignoreras om en variabel REQUEST_METHOD
är inställd; se dokumentationen för getproxies()
.
Socklar och lager¶
Python-stödet för att hämta resurser från webben är uppdelat i flera lager. urllib använder biblioteket http.client
, som i sin tur använder socket-biblioteket.
Från och med Python 2.3 kan du ange hur länge en socket ska vänta på ett svar innan tidsgränsen överskrids. Detta kan vara användbart i applikationer som måste hämta webbsidor. Som standard har socket-modulen ingen timeout och kan hänga sig. För närvarande är socket timeout inte exponerad på http.client eller urllib.request nivåerna. Du kan dock ställa in standardtimeout globalt för alla socklar med
import socket
import urllib.request
# timeout in seconds
timeout = 10
socket.setdefaulttimeout(timeout)
# this call to urllib.request.urlopen now uses the default timeout
# we have set in the socket module
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)
Fotnoter¶
Detta dokument har granskats och reviderats av John Lee.