urllib2 - zaginiony podręcznik

Jest to tłumaczenie artykułu urllib2 - The Missing Manual uzupełnione o kilka przykładów.

Wprowadzenie

urllib2 jest modułem Pythona do pobierania zasobów z sieci za pośrednictwem URL-i (ang. Uniform Resource Locators). Oferuje z jednej strony uproszczony interfejs w formie funkcji urlopen, która pozwala na pobranie zasobów przy użyciu różnych protokołów. Z drugiej strony urllib2 oferuje nieco bardziej złożony interfejs do obsługi różnych, powszechnych sytuacji - jak uwierzytelnianie, cookies, proxy itp. Realizowany on jest przy pomocy obiektów nazywanych handlerami i openerami [1].

urllib2 umożliwia pobieranie zasobów za pośrednictwem wielu protokołów (identyfikowanych przez ciąg znaków przed znakiem ":" w URL-u - np. ciąg "ftp" jest protokołem URL-a ftp://python.org/), jak np.: FTP, HTTP, FILE, GOPHER. Ten artykuł koncentruje się na najbardziej powszechnym przypadku: HTTP.

Dla prostych przypadków funkcja urlopen jest bardzo łatwa i wygodna w użyciu. Jednak w momencie, gdy zostaną napotkane błędy lub dla nietrywialnych przypadków pobierania zasobów, potrzebne będzie bardziej dogłębne zrozumienie samego protokołu HTTP (ang. HyperText Transfer Protocol). Najpełniejszy i najbardziej miarodajny opis tego protokołu znajduje się w dokumencie RFC 2616. Poniższy artykuł ma na celu zilustrowanie użycia modułu urllib2 z jednoczesnym wyjaśnieniem niezbędnych szczegółów protokołu HTTP. Nie ma on na celu zastąpienia oficjalnej dokumentacji urllib2, a jedynie jej uzupełnienie.

Pobieranie zasobów

Najprostsze użycie urllib2 wygląda tak:

import urllib2
response = urllib2.urlopen('http://python.kofeina.net/')
html = response.read()

Przeważnie użycie urllib2 będzie właśnie tak proste (zauważ, iż zamiast protokołu 'http:' moglibyśmy użyć 'ftp:', 'file:', itd.). Jednak celem tego artykułu jest wyjaśnienie bardziej złożonych przypadków, koncentrując się na HTTP.

HTTP bazuje na żądaniach i odpowiedziach - klient zgłasza żądanie, a serwer wysyła odpowiedź. urllib2 odzwierciedla to przy pomocy obiektu Request, który reprezentuje zgłaszane żądanie HTTP. W najprostszej formie tworzymy obiekt Request podając tylko URL zasobu, który chcemy pobrać. Wywołanie funkcji urlopen dla takiego obiektu Request zwraca obiekt odpowiedzi dla żądanego URL-a. Ten obiekt jest obiektem pseudoplikowym (ang. file-like), co oznacza, że posiada on np. metodę read:

import urllib2

request = urllib2.Request('http://python.kofeina.net/')
response = urllib2.urlopen(request)
html = response.read()

Zauważ, iż urllib2 wykorzystuje ten sam obiekt Request do obsłużenia wszystkich protokołów. Na przykład żądanie FTP można wykonać w ten sposób:

request = urllib2.Request('ftp://ftp.oreilly.com/')

W przypadku HTTP, są jeszcze dwie dodatkowe rzeczy, które możemy zrobić przy pomocy obiektu Request. Po pierwsze możemy przesłać dane do serwera. Po drugie możemy przesłać też serwerowi ekstra informacje ("metadane") na temat przesyłanych danych lub na temat samego żądania. Te dodatkowe informacje wysyłane są w postaci nagłówków HTTP. Spójrzmy po kolei jak to wygląda.

Dane

Czasami chcemy wysłać jakieś dane do zasobu sieciowego (URL często odnosi się do skryptu CGI (ang. Common Gateway Interface) [2] jakiejś aplikacji webowej). Za pośrednictwem HTTP wykonuje się to przy pomocy tzw. żądania POST. To właśnie często wykonuje przeglądarka WWW, gdy zatwierdzamy HTML-owy formularz po wypełnieniu. Nie wszystkie żądania POST muszą pochodzić z formularzy. Można użyć żądania POST do przesłania dowolnych danych do naszej aplikacji. W najczęstszym przypadku, czyli użycia formularza HTML, dane muszą zostać zakodowane w standardowy sposób, a następnie przekazane do obiektu Request jako argument data (czyli drugi argument lub argument nazwany data). Kodowanie jest wykonywane przy użyciu funkcji urlencode, ale z modułu urllib, a nie urllib2:

import urllib
import urllib2

url = 'http://www.jakisserwer.com/cgi-bin/rejestruj.cgi'
values = {'nazwisko'      : 'Jan Kowalski',
          'miejscowosc'   : 'Glucha Dolna',
          'jezyk'         : 'Python' }

data = urllib.urlencode(values)
request = urllib2.Request(url, data) # lub urllib2.Request(url, data=data)
response = urllib2.urlopen(request)
html = response.read()

Zauważ, iż czasami wymagane są inne kodowania (np. do przesłania pliku poprzez formularz HTML - po szczegóły odsyłam do specyfikacji HTML-a).

Jeśli nie zostanie przekazany argument data, urllib2 używa żądania GET, a w przeciwnym wypadku żądania POST.

Możliwe jest także przekazywanie danych w żądaniu GET poprzez zakodowanie ich w samym URL-u, np.:

>>> import urllib2
>>> import urllib
>>> dane = {}
>>> dane['nazwisko'] = 'Jan Kowalski'
>>> dane['miejscowosc'] = 'Glucha Dolna'
>>> dane['jezyk'] = 'Python'
>>> wartosci = urllib.urlencode(dane)
>>> print wartosci
nazwisko=Jan+Kowalski&miejscowosc=Glucha+Dolna&jezyk=Python
>>> url = 'http://www.jakisserwer.com/rejestruj.cgi'
>>> pelny_url = url + '?' + wartosci
>>> dane = urllib2.open(pelny_url)

Zauważ, iż pełny URL tworzony jest poprzez dodanie znaku '?' oraz ciągu zakodowanych wartości.

Nagłówki

Omówimy tu jeden konkretny nagłówek HTTP, aby zilustrować w jaki sposób można dodawać nagłówki do żądań HTTP.

Niektóre strony internetowe [3] nie lubią być przeglądane przez programy lub prezentują się w różny sposób różnym przeglądarkom [4]. Domyślnie urllib2 przedstawia się jako Python-urllib/x.y (gdzie x i y tworzą wersję Pythona, np.: Python-urllib/2.5), co może wprawić w zakłopotanie daną witrynę lub nawet po prostu nie zadziałać w ogóle. Przeglądarka przedstawia się przy pomocy nagłówka User-Agent [5]. Podczas tworzenia obiektu Request możemy przekazać mu słownik nagłówków. Poniższy przykład wykonuje takie samo żądanie jak powyżej, ale przedstawia się jako wersja Internet Explorera [6].

import urllib
import urllib2

url = 'http://www.jakisserwer.com/cgi-bin/rejestruj.cgi'
values = {'nazwisko'    : 'Jan Kowalski',
          'miejscowosc' : 'Glucha Dolna',
          'jezyk'       : 'Python' }
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
naglowki = {'User-Agent' : user_agent}

data = urllib.urlencode(values)
request = urllib2.Request(url, data, naglowki)
response = urllib2.urlopen(request)
html = response.read()

Obiekt odpowiedzi ma dwie użyteczne metody. Omówione są one w sekcji info i geturl, do której przejdziemy zaraz po omówieniu co się dzieje, gdy wystąpią jakieś błędy.

Obsługa wyjątków

urlopen rzuca wyjątek URLError, jeśli nie jest w stanie obsłużyć odpowiedzi (chociaż, jak to zwykle w przypadku API Pythona, mogą być także rzucane wbudowane wyjątki typu ValueError, TypeError itd.).

Wyjątek HTTPError jest klasą potomną klasy URLError i rzucany jest w specyficznych sytuacjach związanych z zasobami HTTP.

URLError

URLError jest często zgłaszany z powodu braku połączenia sieciowego (brak ścieżki do podanego serwera) lub z powodu nieistnienia podanego serwera. W tym przypadku rzucony wyjątek będzie posiadał atrybut reason, który jest tuplą zawierającą kod i opis błędu, np.:

>>> request = urllib2.Request('http://www.bledny-serwer.org')
>>> try:
...     urllib2.urlopen(request)
... except urllib2.URLError, ex:
...     print ex.reason
(4, 'getaddrinfo failed')

HTTPError

Każda odpowiedź serwera zawiera numeryczny "kod statusu". Czasami ten kod statusu wskazuje na to, iż serwer nie był w stanie wykonać żądania. Domyślne handlery obsłużą niektóre z tych odpowiedzi automatycznie (np. jeśli odpowiedzią będzie "przekierowanie", które informuje klienta o możliwości pobrania zasobu spod innego adresu, urllib2 obsłuży to dla nas). Dla przypadków, których nie może obsłużyć, urlopen rzuci wyjątek HTTPError. Typowe błędy to: '404' (strona nieznaleziona), '403' (żądanie niedozwolone) i '401' (wymagane uwierzytelnienie).

Zajrzyj do sekcji 10 dokumentu RFC 2616, jeśli chcesz zobaczyć wszystkie kody błędów HTTP.

Rzucony wyjątek klasy HTTPError zawiera całkowitoliczbowy atrybut code, który odpowiada błędowi przesłanemu przez serwer.

Kody błędów

Ponieważ domyślne handlery obsługują przekierowania (kody błędów w zakresie 300-399) i kody w zakresie 100-299 oznaczające udane żądanie, będziemy mieli do czynienia zwykle z błędami z zakresu 400-599.

BaseHTTPServer.BaseHTTPRequestHandler.responses jest użytecznym słownikiem kodów odpowiedzi zdefiniowanych w RFC 2616. Słownik ten zaprezentujemy tutaj dla wygody:

# Tabela mapujaca kody odpowiedzi na komunikaty; pozycje maja
# postac {kod: (krotki_komunikat, dlugi_komunikat)}.
# Zobacz: http://www.w3.org/hypertext/WWW/Protocols/HTTP/HTRESP.html
responses = {
    100: ('Continue', 'Request received, please continue'),
    101: ('Switching Protocols',
          'Switching to new protocol; obey Upgrade header'),

    200: ('OK', 'Request fulfilled, document follows'),
    201: ('Created', 'Document created, URL follows'),
    202: ('Accepted',
          'Request accepted, processing continues off-line'),
    203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
    204: ('No response', 'Request fulfilled, nothing follows'),
    205: ('Reset Content', 'Clear input form for further input.'),
    206: ('Partial Content', 'Partial content follows.'),

    300: ('Multiple Choices',
          'Object has several resources -- see URI list'),
    301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
    302: ('Found', 'Object moved temporarily -- see URI list'),
    303: ('See Other', 'Object moved -- see Method and URL list'),
    304: ('Not modified',
          'Document has not changed since given time'),
    305: ('Use Proxy',
          'You must use proxy specified in Location to access this '
          'resource.'),
    307: ('Temporary Redirect',
          'Object moved temporarily -- see URI list'),

    400: ('Bad request',
          'Bad request syntax or unsupported method'),
    401: ('Unauthorized',
          'No permission -- see authorization schemes'),
    402: ('Payment required',
          'No payment -- see charging schemes'),
    403: ('Forbidden',
          'Request forbidden -- authorization will not help'),
    404: ('Not Found', 'Nothing matches the given URI'),
    405: ('Method Not Allowed',
          'Specified method is invalid for this server.'),
    406: ('Not Acceptable', 'URI not available in preferred format.'),
    407: ('Proxy Authentication Required', 'You must authenticate with '
          'this proxy before proceeding.'),
    408: ('Request Time-out', 'Request timed out; try again later.'),
    409: ('Conflict', 'Request conflict.'),
    410: ('Gone',
          'URI no longer exists and has been permanently removed.'),
    411: ('Length Required', 'Client must specify Content-Length.'),
    412: ('Precondition Failed', 'Precondition in headers is false.'),
    413: ('Request Entity Too Large', 'Entity is too large.'),
    414: ('Request-URI Too Long', 'URI is too long.'),
    415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
    416: ('Requested Range Not Satisfiable',
          'Cannot satisfy request range.'),
    417: ('Expectation Failed',
          'Expect condition could not be satisfied.'),

    500: ('Internal error', 'Server got itself in trouble'),
    501: ('Not Implemented',
          'Server does not support this operation'),
    502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
    503: ('Service temporarily overloaded',
          'The server cannot process the request due to a high load'),
    504: ('Gateway timeout',
          'The gateway server did not receive a timely response'),
    505: ('HTTP Version not supported', 'Cannot fulfill request.'),
    }

Gdy pojawi się błąd, serwer odpowiada zwracając kod błędu HTTP i stronę z komunikatem błędu. Możemy użyć obiektu HTTPError tak samo jak odpowiedzi zwracającej konkretną stronę. To oznacza, że poza atrybutem code możemy także skorzystać z metod read, geturl i info.

>>> request = urllib2.Request('http://www.python.org/nieistnieje.html')
>>> try:
...     urllib2.urlopen(request)
... except urllib2.URLError, ex:
...     print ex.code
...     print ex.read()
404
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
........... itd. ...........

Właściwa obsługa błędów

Jeśli chcemy być przygotowani na HTTPError lub URLError, to mamy do wyboru dwa podejścia. Preferuję to drugie.

Podejście nr 1

from urllib2 import Request, urlopen, URLError, HTTPError
request = Request(jakisurl)
try:
    response = urlopen(request)
except HTTPError, ex:
    print 'Serwer nie mogl wykonac zadania.'
    print 'Kod bledu: ', ex.code
except URLError, ex:
    print 'Serwer jest niedostepny.'
    print 'Powod: ', ex.reason
else:
    # wszystko w porzadu

Uwaga!

Klauzula except HTTPError musi pojawić się jako pierwsza, gdyż HTTPError jest klasą potomną URLError i w związku z tym klauzula except URLError także przechwytuje wyjątki HTTPError.

Podejście nr 2

from urllib2 import Request, urlopen, URLError
request = Request(jakisurl)
try:
    response = urlopen(request)
except URLError, ex:
    if hasattr(ex, 'reason'):
        print 'Serwer jest niedostepny.'
        print 'Powod: ', ex.reason
    elif hasattr(ex, 'code'):
        print 'Serwer nie mogl wykonac zadania.'
        print 'Kod bledu: ', ex.code
else:
    # wszystko w porzadku

Uwaga!

URLError jest klasą potomną wbudowanego wyjątku IOError. To oznacza, że można uniknąć importowania URLError:

from urllib2 import Request, urlopen
request = Request(jakisurl)
try:
    response = urlopen(request)
except IOError, ex:
    if hasattr(ex, 'reason'):
        print 'Serwer jest niedostepny.'
        print 'Powod: ', ex.reason
    elif hasattr(ex, 'code'):
        print 'Serwer nie mogl wykonac zadania.'
        print 'Kod bledu: ', ex.code
else:
    # wszystko w porzadku

W rzadkich przypadkach urllib2 może rzucić wyjątek socket.error.

info i geturl

Odpowiedź zwrócona przez urlopen (lub obiekt HTTPError) ma dwie użyteczne metody info i geturl.

geturl - zwraca rzeczywisty URL pobranego zasobu. Jest to użyteczne,
ponieważ urlopen (lub użyty obiekt openera) obsługując żądanie mogła podążyć za przekierowaniem wskazanym przez serwer. W związku z tym URL pobranego zasobu nie musi być taki sam jak URL żądania.
info - zwraca obiekt pseudosłownikowy (ang. dictionary-like), który zawiera
informacje na temat pobranego zasobu, a w szczególności nagłówki przesłane przez serwer. Obecnie jest to instancja klasy httplib.HTTPMessage.

Do typowych nagłówków należą 'Content-length', 'Content-type' itd. Zobacz to zestawienie nagłówków HTTP z krótkim wyjaśnieniem ich znaczenia i sposobu użycia.

Openery i Handlery

Gdy pobieramy jakiś zasób sieciowy podając URL-a, to tak naprawdę wykorzystujemy obiekt openera (instancja może nienajszczęśliwiej nazwanej klasy urllib2.OpenerDirector). Zwykle używamy domyślnego openera - poprzez urlopen - ale możemy też tworzyć własne openery. Openery używają handlerów. Cała "czarna robota" wykonywana jest przez handlery. Każdy handler wie jak otworzyć URL dla konkretnego protokołu (http, ftp, itd.) lub jak obsłużyć jakiś aspekt procesu pobierania zasobu, np. przekierowanie HTTP lub ciasteczka (ang. cookies) HTTP.

Własne openery tworzymy, jeśli chcemy pobierać zasoby przy pomocy specyficznych handlerów, np. potrzebny będzie opener obsługujący ciasteczka lub opener nie wykonujący automatycznych przekierowań.

Aby utworzyć opener powołujemy instancję klasy OpenerDirector, a następnie odpowiednio wywołujemy metodę add_handler(obiekt_jakiegos_handlera).

Alternatywnie można użyć funkcji build_opener, która jest pomocniczą funkcją do tworzenia obiektów openera. build_opener domyślnie dodaje kilka handlerów, ale dostarcza też łatwego sposobu na dodanie ich więcej i/lub nadpisanie handlerów domyślnych. Wystarczy w wywołaniu tej funkcji podać w kolejnych parametrach obiekty handlerów, które mają zostać dodane do naszego openera, np.:

>>> http_handler = urllib2.HTTPHandler(debuglevel=1)
>>> proxy_handler = urllib2.ProxyHandler({'http': 'http://localhost:8080'})
>>> opener = urllib2.build_opener(http_handler, proxy_handler)

Inne rodzaje handlerów, których moglibyśmy potrzebować, potrafią obsłużyć np. serwery proxy, uwierzytelnianie i inne popularne, ale nieco bardziej specyficzne sytuacje.

Funkcja install_opener może być użyta do uczynienia jakiegoś openera domyślnym (globalnym) openerem. To oznacza, iż kolejne wywołania funkcji urlopen będą korzystać z tego właśnie zainstalowanego openera.

Obiekt openera posiada metodę open, która może być wywoływana bezpośrednio do pobierania zasobów w takim sam sposób jak funkcja urlopen. Zwykle nie ma potrzeby wywoływania funkcji install_opener. Czasami użycie jej po prostu ułatwia korzystanie z własnego openera.

Uwierzytelnianie Proste

Aby zilustrować utworzenie i zainstalowanie handlera użyjemy klasy HTTPBasicAuthHandler. Po szczegółowe omówienie tego tematu - włącznie z wyjaśnieniem jak działa Uwierzytelnianie Proste - odsyłam do tego artykułu.

Gdy wymagane jest uwierzytelnianie, serwer wysyła nagłówek żądający uwierzytelnienia (jak również kod błędu 401). Określa on schemat oraz domenę (ang. realm) uwierzytelniania. Nagłówek ten wygląda tak: Www-authenticate: SCHEMAT realm="DOMENA", np. Www-authenticate: Basic realm="cPanel Users"

Klient powinien wówczas ponowić żądanie z dołączonymi w nagłówku odpowiednimi wartościami dla nazwy użytkownika i hasła dla danej domeny. To jest właśnie "uwierzytelnianie proste". Aby uprościć ten proces możemy utworzyć instancję klasy HTTPBasicAuthHandler i opener, który użyje tego handlera.

HTTPBasicAuthHandler korzysta z obiektu zwanego menadżerem haseł (ang. password manager), aby poradzić sobie z mapowaniem URL-i i domen na poszczególne pary haseł i nazw użytkowników. Jeśli już wiemy o jaką domenę chodzi (z nagłówka uwierzytelniania wysłanego przez serwer), to możemy użyć klasy HTTPPasswordMgr. Najczęściej jednak domena nie ma znaczenia i w takim wypadku dogodnie jest użyć klasy HTTPPasswordMgrWithDefaultRealm. Pozwala ona na podanie domyślnego hasła i nazwy użytkownika dla danego URL-a. Zostanie to wykorzystane w sytuacji, w której nie podamy jawnie pary hasło-login dla jakiejś domeny. Na taką sytuację wskazujemy podając None jako argument realm (czyli pierwszy argument lub argument nazwany realm) metody add_password.

URL najwyższego poziomu w danej witrynie wymaga uwierzytelnienia jako pierwszy. URL-e leżące "głębiej", niż ten który podaliśmy metodzie add_password zostaną również dopasowane.

# tworzymy manager hasel
hasla_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()

# Dodajemy nazwe uzytkownika i haslo.
# Jesli znamy domene (realm) mozemy jej uzyc zamiast ``None``.
url_najwyz_poziomu = "http://example.com/foo/"
hasla_mgr.add_password(None, url_najwyz_poziomu, uzytkownik, haslo)

handler = urllib2.HTTPBasicAuthHandler(hasla_mgr)

# tworzymy "opener" (instancję klasy OpenerDirector)
opener = urllib2.build_opener(handler)

# uzywamy openera do pobrania zasobu
opener.open(jakis_url)

# Instalujemy opener.
# Od teraz wszystkie wywolania ``urllib2.urlopen`` beda uzywac naszego openera.
urllib2.install_opener(opener)

Uwaga!

W powyższym przykładzie dostarczyliśmy tylko jednego handlera do funkcji build_opener, a mianowicie: HTTPBasicAuthHandler. Domyślnie openery dysponują kilkoma handlerami do obsługi najpowszechniejszych sytuacji: ProxyHandler, UnknownHandler, HTTPHandler, HTTPDefaultErrorHandler, HTTPRedirectHandler, FTPHandler, FileHandler, HTTPErrorProcessor.

url_najwyz_poziomu może być albo pełnym URL-em (włącznie z określeniem protokołu "http:", nazwą hosta i opcjonalnym numerem portu), np. "http://example.com/" albo tylko nazwą hosta z opcjonalnym numerem portu, np. "example.com" lub "example.com:8080" (ten ostatni przykład zawiera numer portu). Wersja tylko z nawą hosta i opcjonalnym portem nie może zawierać żadnych informacji na temat użytkownika, czy hasła, np. taki zapis jest niepoprawny: "janek@haslo:example.com".

Serwery Proxy

urllib2 jest w stanie wykryć samodzielnie nasze ustawienia proxy i je wykorzystać. Używa do tego celu klasy ProxyHandler, obiekt której jest jednym z domyślnych handlerów. Normalnie to jest bardzo korzystne, ale zdarzają się sytuacje, w których to nie jest pomocne [7]. Jednym ze sposobów, aby rozwiązać ten problem, jest utworzenie własnego handlera klasy ProxyHandler bez definiowania w nim żadnego serwera proxy. Możemy to wykonać w podobny sposób jak przy użyciu handlera uwierzytelniania prostego:

>>> proxy_handler = urllib2.ProxyHandler({})
>>> opener = urllib2.build_opener(proxy_handler)
>>> urllib2.install_opener(opener)

Z kolei, gdy musimy zdefiniować ręcznie korzystanie z proxy możemy to zrobić w ten sposób:

PROXY_IP, PROXY_PORT = '127.0.0.1', 8080
PROXY_USER, PROXY_PASSWORD = None, None
if PROXY_USER:
    proxy_url = 'http://%s:%s@%s:%d' % (PROXY_USER, PROXY_PASSWORD,
                                        PROXY_IP, PROXY_PORT)
else:
    proxy_url = 'http://%s:%d' % (PROXY_IP, PROXY_PORT)

proxy_support = urllib2.ProxyHandler({'http': proxy_url})
opener = urllib2.build_opener(proxy_support)
urllib2.install_opener(opener)

print urllib2.urlopen(urllib2.Request('http://python.org/')).read()
print urllib2.urlopen(urllib2.Request('http://python.kofeina.net/')).read()

Gniazda i warstwy

Wsparcie Pythona dla pobierania zasobów sieciowych jest wielowarstwowe. urllib2 korzysta z biblioteki httplib, która z kolei używa biblioteki socket.

Od Pythona 2.3 możemy określić jak długo gniazdo ma oczekiwać na odpowiedź, czyli innymi słowy możemy określić jego timeout. To może być użyteczne w przypadku aplikacji, które muszą pobierać strony internetowe. Domyślnie moduł socket nie ma określonego czasu oczekiwania i może zostać zablokowany na stałe. Aktualnie timeout gniazda nie jest dostępny z poziomu bibliotek httplib lub urllib2. Jednak możemy ustawić globalnie wartość timeoutu dla wszystkich gniazd:

import socket
import urllib2

# timeout w sekundach
timeout = 10
socket.setdefaulttimeout(timeout)

# to wywolanie ``urllib2.urlopen`` teraz korzysta z domyslnego timeoutu,
# ktory ustawilismy w module ``socket``
request = urllib2.Request('http://www.voidspace.org.uk')
response = urllib2.urlopen(request)

Debugowanie

W razie problemów urllib2 pozwala na włączenie trybu debugowania. Możemy go włączyć przy pomocy metody set_http_debuglevel handlera (lub przekazując parametr debuglevel konstruktorowi handlera). Wywołanie tej metody z wartością większą od zera powoduje wyświetlania dużej porcji dodatkowych informacji na konsoli, np.:

>>> http_handler = urllib2.HTTPHandler()
>>> http_handler.set_http_debuglevel(1)
>>> opener = urllib2.build_opener(http_handler)
>>> opener.open(urllib2.Request('http://python.kofeina.net'))
connect: (python.kofeina.net, 80)
send: 'GET / HTTP/1.1\r\nAccept-Encoding: identity\r\nHost: python.kofeina.net\r\n
    Connection: close\r\nUser-agent: Python-urllib/2.4\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
........... itd. ............

lub alternatywnie:

>>> http_handler = urllib2.HTTPHandler(debuglevel=1)
>>> opener = urllib2.build_opener(http_handler)
>>> opener.open(urllib2.Request('http://python.kofeina.net'))

Wszystko razem

Teraz połączymy wszystkie wiadomości w jeden działający program. Wykorzystam do tego celu formularz dostępny w serwisie money.pl. Celem tego programu jest pobranie pliku CSV z notowaniami danej spółki za podany okres.

import urllib
import urllib2

def pobierz_plik(stock, day_from, month_from, year_from,
                 day_to, month_to, year_to, period, format,
                 debug=0, proxy=None, loguj=False):

    url = 'http://www.money.pl/gielda/archiwum/spolki/'

    # zakodowanie wartosci formularza, ktory chcemy wypelnic
    values = {'stock': stock, 'day_from': day_from,
              'month_from': month_from, 'year_from': year_from,
              'day_to': day_to, 'month_to': month_to,
              'year_to': year_to, 'period': period,
              'format': format, 'periodarchive': 'Szukaj'}
    data = urllib.urlencode(values)

    # handler http z ustawionym poziomem debugowania
    http_handler = urllib2.HTTPHandler(debuglevel=debug)

    if proxy:
        # ustawienie obslugi proxy
        proxy_handler = urllib2.ProxyHandler({'http': proxy})
    else:
        # bez proxy
        proxy_handler = urllib2.ProxyHandler({})

    # ``HTTPHandler`` i ``ProxyHandler`` naleza do handlerow domyslnych,
    # tzn. dodaje je tez funkcja ``build_opener``. Z tego wzgledu, nie mozemy
    # dodac tych handlerow przy pomocy metody ``add_handler``, lecz musimy
    # podac je od razu przy wywolaniu funkcji ``build_opener``

    # utworzenie openera
    opener = urllib2.build_opener(http_handler, proxy_handler)

    #if loguj:
        ## ten serwis nie wymaga uwierzytelniania, ale gdyby tak bylo
        ## to zrobilibysmy to tak
        #hasla_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
        #hasla_mgr.add_password(None, url, 'uzytkownik', 'haslo')
        #opener.add_handler(urllib2.HTTPBasicAuthHandler(hasla_mgr))

    # serwisy WWW nie lubia nieznanych przegladarek :)
    headers = {'User-agent': 'Mozilla/5.0'}

    # w tym miejscu wypelniamy formularz i wysylamy go do zdalnego serwera
    request = urllib2.Request(url, data, headers)
    page = opener.open(request)

    fname = 'dane.csv'  # domyslna nazwa pliku
    try:
        # jesli zostal zwrocony naglowek ``Content-Disposition``,
        # to wykorzystujemy jego wartosc jako nazwe pliku,
        # w ktorym zachowamy pobrane dane
        fname = page.headers['Content-Disposition']
        fname = fname.split(";")[1].split("=")[1]
        fname = fname.strip('"')
    except KeyError:
        # naglowek ``Content-Disposition`` nie zostal zwrocony,
        # a wiec tworzymy nazwe pliku samodzielnie
        fname = 'default_%s_%s%s%s_%s%s%s.%s' % (stock, year_from, month_from,
                                                 day_from, year_to, month_to,
                                                 day_to, format)
    print "Zapisuje plik: %s" % fname
    f = open(fname, 'w')
    f.write(page.read())
    page.close()
    f.close()

pobierz_plik('PLX', '09', '01', '2007', '20', '02', '2007', '0', 'csv')
pobierz_plik('PLX', '10', '01', '2007', '20', '02', '2007', '0', 'csv', 1)
pobierz_plik('PLX', '11', '01', '2007', '20', '02', '2007', '0', 'csv', 0,
             'http://localhost:8080')

I to by było na tyle.


Przypisy

[1]Nie mam pomysłu na przetłumaczenie słów "handler" i "opener", więc je brutalnie spolszczam ("otwieracz" i "posługacz" nie wchodzą w grę :) ).
[2]Po szczegóły o protokole CGI odsyłam do Tworzenie aplikacji webowych w Pythonie.
[3]Jak np. Google. Właściwym sposobem na użycie google z programu jest wykorzystanie PyGoogle oczywiście. Zobacz Voidspace Google
[4]Sprawdzanie rodzaju i wersji przeglądarki jest bardzo złą praktyką w projektowaniu witryny - tworzenie witryn w oparciu o standardy jest dużo bardziej sensowne. Niestety wiele witryn nadal wysyła różne wersje stron różnym przeglądarkom.
[5]Ciąg "user agent" dla MSIE 6 to 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)'
[6]Po szczegóły na temat nagłówków HTTP odsyłam do zestawienia nagłówków HTTP.
[7]W moim przypadku muszę używać w pracy serwera proxy do dostępu do internetu. Gdy próbuję pobrać jakiś lokalny zasób z localhosta poprzez tego proxy, następuje zablokowanie. IE ma ustawiony serwer proxy, który urllib2 odnajduje i wykorzystuje. Aby testować skrypty z lokalnym serwerem, muszę powstrzymać urllib2 od używania serwera proxy.