HOWTO Отримати Інтернет-ресурси за допомогою пакета urllib

Автор:

Michael Foord

вступ

urllib.request — це модуль Python для отримання URL-адрес (уніфікованих покажчиків ресурсів). Він пропонує дуже простий інтерфейс у формі функції urlopen. Це здатне отримувати URL-адреси за допомогою різних протоколів. Він також пропонує дещо складніший інтерфейс для обробки поширених ситуацій, таких як базова автентифікація, файли cookie, проксі тощо. Вони забезпечуються об’єктами, які називаються обробниками та відкривачами.

urllib.request підтримує отримання URL-адрес для багатьох «схем URL-адрес» (визначених рядком перед ":" в URL-адресі - наприклад, "ftp" є схемою URL-адреси "ftp:// python.org/"), використовуючи відповідні мережеві протоколи (наприклад, FTP, HTTP). Цей підручник присвячено найпоширенішому випадку, HTTP.

Для простих ситуацій urlopen дуже простий у використанні. Але як тільки ви зіткнетеся з помилками або нетривіальними випадками під час відкриття URL-адрес HTTP, вам знадобиться деяке розуміння протоколу передачі гіпертексту. Найбільш повним і авторитетним посиланням на HTTP є RFC 2616. Це технічний документ, який не призначений для легкого читання. Цей HOWTO має на меті проілюструвати використання urllib з достатньою кількістю деталей про HTTP, щоб допомогти вам у цьому. Він не призначений для заміни документів urllib.request, а є доповненням до них.

Отримання URL-адрес

Найпростіший спосіб використання urllib.request такий:

import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
   html = response.read()

Якщо ви бажаєте отримати ресурс через URL-адресу та зберегти його у тимчасовому місці, ви можете зробити це за допомогою функцій shutil.copyfileobj() і 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

У багатьох випадках використання urllib буде таким простим (зауважте, що замість URL-адреси «http:» ми могли б використовувати URL-адресу, яка починається з «ftp:», «file:» тощо). Однак мета цього підручника — пояснити складніші випадки, зосереджуючись на HTTP.

HTTP базується на запитах і відповідях - клієнт робить запити, а сервери надсилають відповіді. urllib.request відображає це за допомогою об’єкта Request, який представляє HTTP-запит, який ви робите. У найпростішій формі ви створюєте об’єкт Request, який визначає URL-адресу, яку ви хочете отримати. Виклик urlopen із цим об’єктом Request повертає об’єкт відповіді для запитуваної URL-адреси. Ця відповідь є файлоподібним об’єктом, що означає, що ви можете, наприклад, викликати .read() у відповіді:

import urllib.request

req = urllib.request.Request('http://python.org/')
with urllib.request.urlopen(req) as response:
   the_page = response.read()

Зауважте, що urllib.request використовує той самий інтерфейс Request для обробки всіх схем URL-адрес. Наприклад, ви можете зробити FTP-запит так:

req = urllib.request.Request('ftp://example.com/')

У випадку HTTP об’єкти Request дозволяють робити дві додаткові речі: по-перше, ви можете передавати дані для надсилання на сервер. По-друге, ви можете передати на сервер додаткову інформацію («метадані») про дані або про сам запит — ця інформація надсилається як «заголовки» HTTP. Давайте по черзі розглянемо кожен із них.

Дані

Іноді потрібно надіслати дані за URL-адресою (часто URL-адреса посилатиметься на сценарій CGI (Common Gateway Interface) або іншу веб-програму). За допомогою HTTP це часто робиться за допомогою так званого запиту POST. Це часто робить ваш браузер, коли ви надсилаєте форму HTML, яку ви заповнили в Інтернеті. Не всі повідомлення POST мають надходити з форм: ви можете використовувати POST для передачі довільних даних у власну програму. У звичайному випадку форм HTML дані потрібно закодувати стандартним способом, а потім передати в об’єкт Request як аргумент data. Кодування виконується за допомогою функції з бібліотеки urllib.parse.

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()

Зауважте, що іноді потрібні інші кодування (наприклад, для завантаження файлів із форм HTML – див. Специфікація HTML, Подання форми для отримання додаткової інформації).

Якщо ви не передаєте аргумент data, urllib використовує запит GET. Запити GET і POST відрізняються тим, що запити POST часто мають «побічні ефекти»: вони певним чином змінюють стан системи (наприклад, розміщуючи на веб-сайті замовлення на доставку сотень консервованого спаму). до ваших дверей). Хоча стандарт HTTP чітко визначає, що POST призначені завжди спричиняти побічні ефекти, а запити GET ніколи не спричиняти побічних ефектів, ніщо не заважає запитам GET мати побічні ефекти, а запитам POST — не мати побічних ефектів. побічні ефекти. Дані також можна передати в запиті HTTP GET, закодувавши їх у самій URL-адресі.

Це робиться наступним чином:

>>> 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)

Зауважте, що повна URL-адреса створюється шляхом додавання ? до URL-адреси, а потім закодованих значень.

Заголовки

Ми обговоримо тут один конкретний HTTP-заголовок, щоб проілюструвати, як додати заголовки до вашого HTTP-запиту.

Деяким веб-сайтам [1] не подобається, коли їх переглядають програми, або вони надсилають різні версії в різні браузери [2]. За замовчуванням urllib ідентифікує себе як Python-urllib/x.y (де x і y — номер основної та другорядної версій випуску Python, наприклад Python-urllib/2.5 ), що може заплутати сайт або просто не працювати. Браузер ідентифікує себе через заголовок User-Agent [3]. Коли ви створюєте об’єкт Request, ви можете передати словник заголовків. Наступний приклад робить той самий запит, що й вище, але ідентифікує себе як версію 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()

Відповідь також має два корисні методи. Перегляньте розділ про info та geturl, який йде після того, як ми подивимося, що відбувається, коли щось йде не так.

Обробка винятків

urlopen raises URLError when it cannot handle a response (though as usual with Python APIs, built-in exceptions such as ValueError, TypeError etc. may also be raised).

HTTPError is the subclass of URLError raised in the specific case of HTTP URLs.

Класи винятків експортуються з модуля urllib.error.

URLError

Часто URLError виникає через відсутність підключення до мережі (немає маршруту до вказаного сервера) або вказаний сервер не існує. У цьому випадку викликаний виняток матиме атрибут «причина», який є кортежем, що містить код помилки та текстове повідомлення про помилку.

напр.

>>> 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

Every HTTP response from the server contains a numeric «status code». Sometimes the status code indicates that the server is unable to fulfil the request. The default handlers will handle some of these responses for you (for example, if the response is a «redirection» that requests the client fetch the document from a different URL, urllib will handle that for you). For those it can’t handle, urlopen will raise an HTTPError. Typical errors include „404“ (page not found), „403“ (request forbidden), and „401“ (authentication required).

Перегляньте розділ 10 RFC 2616 для довідки про всі коди помилок HTTP.

The HTTPError instance raised will have an integer „code“ attribute, which corresponds to the error sent by the server.

Коди помилок

Оскільки обробники за замовчуванням обробляють переспрямування (коди в діапазоні 300), а коди в діапазоні 100–299 вказують на успіх, зазвичай ви побачите коди помилок лише в діапазоні 400–599.

http.server.BaseHTTPRequestHandler.responses — це корисний словник кодів відповідей, який показує всі коди відповідей, які використовує RFC 2616. Для зручності словник наведено тут

# Table mapping response codes to messages; entries have the
# form {code: (shortmessage, longmessage)}.
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 Content', '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 Timeout', '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 Server 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 Unavailable',
          '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.'),
    }

When an error is raised the server responds by returning an HTTP error code and an error page. You can use the HTTPError instance as a response on the page returned. This means that as well as the code attribute, it also has read, geturl, and info, methods as returned by the urllib.response module:

>>> 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
  ...

Загортання

So if you want to be prepared for HTTPError or URLError there are two basic approaches. I prefer the second approach.

Номер 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

Примітка

The except HTTPError must come first, otherwise except URLError will also catch an HTTPError.

Номер 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

інформація та geturl

The response returned by urlopen (or the HTTPError instance) has two useful methods info() and geturl() and is defined in the module urllib.response.

  • geturl - повертає справжню URL-адресу отриманої сторінки. Це корисно, оскільки urlopen (або використаний об’єкт відкриття) міг слідувати за перенаправленням. URL-адреса отриманої сторінки може не збігатися з запитуваною URL-адресою.

  • info - це повертає об’єкт, схожий на словник, який описує отриману сторінку, зокрема заголовки, надіслані сервером. Зараз це екземпляр http.client.HTTPMessage.

Typical headers include „Content-length“, „Content-type“, and so on. See the Quick Reference to HTTP Headers for a useful listing of HTTP headers with brief explanations of their meaning and use.

Відкривачки та обробники

When you fetch a URL you use an opener (an instance of the perhaps confusingly named urllib.request.OpenerDirector). Normally we have been using the default opener - via urlopen - but you can create custom openers. Openers use handlers. All the «heavy lifting» is done by the handlers. Each handler knows how to open URLs for a particular URL scheme (http, ftp, etc.), or how to handle an aspect of URL opening, for example HTTP redirections or HTTP cookies.

Ви захочете створити відкривачі, якщо хочете отримати URL-адреси з установленими певними обробниками, наприклад, щоб отримати відкривач, який обробляє файли cookie, або щоб отримати відкривач, який не обробляє переспрямування.

Щоб створити відкривач, створіть екземпляр OpenerDirector, а потім кілька разів викличте .add_handler(some_handler_instance).

Крім того, ви можете використовувати build_opener, яка є зручною функцією для створення відкриваючих об’єктів за допомогою одного виклику функції. build_opener додає кілька обробників за замовчуванням, але забезпечує швидкий спосіб додати більше та/або замінити обробники за замовчуванням.

Інші типи обробників, які вам можуть знадобитися, можуть обробляти проксі, автентифікацію та інші типові, але трохи спеціалізовані ситуації.

install_opener можна використовувати, щоб зробити об’єкт opener (глобальним) відкривачем за замовчуванням. Це означає, що виклики urlopen використовуватимуть встановлений вами відкривач.

Об’єкти Opener мають метод open, який можна викликати безпосередньо для отримання URL-адрес так само, як і функцію urlopen: немає необхідності викликати install_opener, окрім як для зручності.

Базова автентифікація

To illustrate creating and installing a handler we will use the HTTPBasicAuthHandler. For a more detailed discussion of this subject – including an explanation of how Basic Authentication works - see the Basic Authentication Tutorial.

Коли потрібна автентифікація, сервер надсилає заголовок (а також код помилки 401) із запитом на автентифікацію. Це визначає схему автентифікації та «сферу». Заголовок виглядає так: WWW-Authenticate: SCHEME realm="REALM".

напр.

WWW-Authenticate: Basic realm="cPanel Users"

Потім клієнт повинен повторити запит із відповідним іменем і паролем для області, включеними як заголовок запиту. Це «базова автентифікація». Щоб спростити цей процес, ми можемо створити екземпляр HTTPBasicAuthHandler і засіб відкриття для використання цього обробника.

HTTPBasicAuthHandler використовує об’єкт під назвою менеджер паролів для обробки зіставлення URL-адрес і областей з паролями та іменами користувачів. Якщо ви знаєте, що таке область (із заголовка автентифікації, надісланого сервером), ви можете використовувати HTTPPasswordMgr. Часто байдуже, що таке царство. У такому випадку зручно використовувати HTTPPasswordMgrWithDefaultRealm. Це дозволяє вказати ім’я користувача та пароль за умовчанням для URL-адреси. Це буде надано, якщо ви не надасте альтернативну комбінацію для певного царства. Ми вказуємо на це, надаючи None як аргумент області для методу add_password.

URL-адреса верхнього рівня – це перша URL-адреса, яка вимагає автентифікації. URL-адреси, «глибші» за URL-адресу, яку ви передаєте в .add_password(), також відповідатимуть.

# 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)

Примітка

In the above example we only supplied our HTTPBasicAuthHandler to build_opener. By default openers have the handlers for normal situations – ProxyHandler (if a proxy setting such as an http_proxy environment variable is set), UnknownHandler, HTTPHandler, HTTPDefaultErrorHandler, HTTPRedirectHandler, FTPHandler, FileHandler, DataHandler, HTTPErrorProcessor.

top_level_url насправді є або повною URL-адресою (включно з компонентом схеми „http:“ та ім’ям хоста та, необов’язково, номером порту), напр. "http://example.com/" або «орган» (тобто ім’я хоста, необов’язково включаючи номер порту), наприклад. "example.com" або "example.com:8080" (останній приклад містить номер порту). Повноваження, якщо вони присутні, НЕ повинні містити компонент «userinfo» - наприклад, "joe:password@example.com" є неправильним.

Проксі

urllib автоматично визначить ваші налаштування проксі та використає їх. Це відбувається через ProxyHandler, який є частиною звичайного ланцюжка обробників, коли виявляється налаштування проксі. Зазвичай це добре, але бувають випадки, коли це може бути некорисним [5]. Один із способів зробити це — налаштувати наш власний ProxyHandler без визначених проксі. Це робиться за допомогою подібних кроків до налаштування обробника Basic Authentication:

>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)

Примітка

Наразі urllib.request не підтримує отримання адрес https через проксі. Однак це можна ввімкнути, розширивши urllib.request, як показано в рецепті [6].

Примітка

HTTP_PROXY ігноруватиметься, якщо встановлено змінну REQUEST_METHOD; перегляньте документацію на getproxies().

Розетки та шари

Підтримка Python для отримання ресурсів з Інтернету є багаторівневою. urllib використовує бібліотеку http.client, яка, у свою чергу, використовує бібліотеку сокетів.

Починаючи з Python 2.3, ви можете вказати, як довго сокет повинен чекати відповіді перед закінченням часу очікування. Це може бути корисним у програмах, які мають отримати веб-сторінки. За замовчуванням модуль сокета не має часу очікування і може зависати. Наразі час очікування сокета не розкривається на рівнях http.client або urllib.request. Однак ви можете глобально встановити тайм-аут за замовчуванням для всіх сокетів за допомогою

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)

Виноски

Цей документ переглянув і відредагував Джон Лі.