КАК получить доступ к Интернет-ресурсам с помощью Пакета urllib¶
- Автор:
Вступление¶
urllib.request - это модуль Python для получения URL-адресов (унифицированных указателей ресурсов). Он предлагает очень простой интерфейс в виде функции urlopen. Он позволяет получать URL-адреса с использованием множества различных протоколов. Он также предлагает несколько более сложный интерфейс для обработки распространенных ситуаций, таких как обычная аутентификация, файлы cookie, прокси-серверы и т.д. Они предоставляются объектами, называемыми обработчиками и средствами открытия.
urllib.request поддерживает выборку URL-адресов для многих «схем URL» (обозначаемых строкой перед ":"
в URL-адресе - например, "ftp"
является схемой URL для "ftp://python.org/"
) с использованием связанных с ними сетевых протоколов (например, FTP, HTTP). В этом руководстве основное внимание уделяется наиболее распространенной причине - HTTP.
Для простых ситуаций urlopen очень прост в использовании. Но как только вы столкнетесь с ошибками или нетривиальными случаями при открытии HTTP-адресов, вам потребуется некоторое представление о протоколе передачи гипертекста. Наиболее полной и авторитетной ссылкой на HTTP является RFC 2616. Это технический документ, и он не предназначен для удобства чтения. Цель этого руководства - проиллюстрировать использование 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-запрос. В простейшей форме вы создаете объект запроса, который указывает URL, который вы хотите получить. Вызов urlopen
с помощью этого объекта Request возвращает объект response для запрошенного 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 использует один и тот же интерфейс запроса для обработки всех схем URL. Например, вы можете отправить FTP-запрос следующим образом:
req = urllib.request.Request('ftp://example.com/')
В случае HTTP есть две дополнительные возможности, которые позволяют вам выполнять объекты Request: во-первых, вы можете передавать данные для отправки на сервер. Во-вторых, вы можете передавать дополнительную информацию («метаданные») о данных или о самом запросе на сервер - эта информация отправляется в виде HTTP-заголовков. Давайте рассмотрим каждый из них по очереди.
Данные¶
Иногда требуется отправить данные по URL-адресу (часто URL-адрес ссылается на сценарий CGI (Common Gateway Interface) или другое веб-приложение). В случае HTTP это часто делается с помощью так называемого POST-запроса. Часто именно это делает ваш браузер, когда вы отправляете HTML-форму, которую вы заполнили в Интернете. Не все сообщения должны поступать из форм: вы можете использовать СООБЩЕНИЕ для передачи произвольных данных в ваше собственное приложение. В обычном случае HTML-форм данные должны быть закодированы стандартным образом, а затем переданы объекту запроса в качестве аргумента 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 Specification, Form Submission).
Если вы не передадите аргумент data
, urllib использует запрос GET. Одно из отличий запросов GET от запросов POST заключается в том, что запросы POST часто имеют «побочные эффекты»: они каким-то образом изменяют состояние системы (например, при размещении заказа на веб-сайте на доставку центнера консервированного спама к вашей двери). Хотя стандарт HTTP ясно дает понять, что сообщения предназначены для того, чтобы всегда вызывать побочные эффекты, а запросы 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]. Когда вы создаете объект запроса, вы можете передать в него словарь заголовков. В следующем примере выполняется тот же запрос, что и выше, но он идентифицируется как версия 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 and geturl, который приводится после того, как мы рассмотрим, что происходит, когда что-то идет не так.
Обработка исключений¶
urlopen вызывает URLError
, когда он не может обработать ответ (хотя, как обычно в API-интерфейсах Python, также могут возникать встроенные исключения, такие как ValueError
, TypeError
и т.д.).
HTTPError
- это подкласс URLError
, созданный в конкретном случае HTTP-URL-адресов.
Классы исключений экспортируются из модуля urllib.error
.
Ошибка URL-адреса¶
Часто ошибка URL-адреса возникает из-за отсутствия сетевого подключения (нет маршрута к указанному серверу) или из-за того, что указанный сервер не существует. В этом случае возникшее исключение будет иметь атрибут «причина», который представляет собой кортеж, содержащий код ошибки и текстовое сообщение об ошибке.
напр.
>>> 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 (ошибка)¶
Каждый HTTP-ответ от сервера содержит числовой «код состояния». Иногда код состояния указывает на то, что сервер не может выполнить запрос. Обработчики по умолчанию обработают некоторые из этих ответов за вас (например, если ответом является «перенаправление», которое запрашивает у клиента получение документа с другого URL-адреса, urllib обработает это за вас). Для тех, с кем urlopen не может справиться, он выдает ошибку HTTPError
. Типичные ошибки включают «404» (страница не найдена), «403» (запрос запрещен) и «401» (требуется аутентификация).
Смотрите раздел 10 раздела RFC 2616 для получения информации обо всех кодах ошибок HTTP.
Созданный экземпляр HTTPError
будет иметь целочисленный атрибут „code“, который соответствует ошибке, отправленной сервером.
Коды ошибок¶
Поскольку обработчики по умолчанию обрабатывают перенаправления (коды в диапазоне 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.'),
}
При возникновении ошибки сервер возвращает код ошибки HTTP * и страницу с ошибкой. Вы можете использовать экземпляр HTTPError
в качестве ответа на возвращаемой странице. Это означает, что помимо атрибута code, у него также есть методы read, geturl и info, возвращаемые модулем 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
...
Сворачивая все это¶
Итак, если вы хотите быть готовым к HTTPError
или URLError
, есть два основных подхода. Я предпочитаю второй подход.
Число 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
Примечание
except HTTPError
должен быть первым, в противном случае except URLError
также * перехватит 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¶
Ответ, возвращаемый urlopen (или экземпляром HTTPError
), содержит два полезных метода info()
и geturl()
и определен в модуле urllib.response
.
geturl - возвращает реальный URL-адрес выбранной страницы. Это полезно, потому что
urlopen
(или используемый объект открывания) мог быть перенаправлен. URL-адрес выбранной страницы может не совпадать с запрошенным URL-адресом.info - возвращает объект, подобный словарю, который описывает выбранную страницу, в частности заголовки, отправленные сервером. В настоящее время это экземпляр
http.client.HTTPMessage
.
Типичные заголовки включают «Content-length», «Content-type» и т.д. Полезный список HTTP-заголовков с краткими объяснениями их значения и использования приведен в Quick Reference to HTTP Headers.
Открыватели и манипуляторы¶
Когда вы извлекаете URL-адрес, вы используете средство открытия (экземпляр, возможно, с непонятным названием urllib.request.OpenerDirector
). Обычно мы используем средство открытия по умолчанию - через urlopen
, но вы можете создавать собственные средства открытия. Средства открытия используют обработчики. Вся «тяжелая работа» выполняется обработчиками. Каждый обработчик знает, как открывать URL-адреса для определенной схемы URL-адресов (http, ftp и т.д.) или как обрабатывать некоторые аспекты открытия URL-адресов, например, перенаправления HTTP или файлы cookie HTTP.
Вам понадобится создать средства открытия, если вы хотите получать URL-адреса с установленными определенными обработчиками, например, чтобы получить средство открытия, которое обрабатывает файлы cookie, или средство открытия, которое не обрабатывает перенаправления.
Чтобы создать открывающий элемент, создайте экземпляр OpenerDirector
, а затем повторно вызовите .add_handler(some_handler_instance)
.
В качестве альтернативы вы можете использовать build_opener
, которая представляет собой удобную функцию для создания объектов opener с помощью одного вызова функции. build_opener
добавляет несколько обработчиков по умолчанию, но предоставляет быстрый способ добавить больше и/или переопределить обработчики по умолчанию.
Другие типы обработчиков, которые вам могут понадобиться, могут обрабатывать прокси-серверы, аутентификацию и другие распространенные, но слегка специализированные ситуации.
install_opener
можно использовать для того, чтобы сделать объект opener
(глобальным) открывателем по умолчанию. Это означает, что вызовы urlopen
будут использовать установленный вами открыватель.
У объектов Opener есть метод open
, который может быть вызван напрямую для получения URL-адресов таким же образом, как и функция urlopen
: нет необходимости вызывать install_opener
, кроме как для удобства.
Базовая аутентификация¶
Чтобы проиллюстрировать создание и установку обработчика, мы будем использовать HTTPBasicAuthHandler
. Более подробное обсуждение этого вопроса, включая объяснение того, как работает базовая аутентификация, смотрите в 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)
Примечание
В приведенном выше примере мы указали только значение от HTTPBasicAuthHandler
до build_opener
. По умолчанию средства запуска содержат обработчики для обычных ситуаций - ProxyHandler
(если задан параметр прокси, такой как переменная среды http_proxy
), 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)
Сноски¶
Этот документ был просмотрен и переработан Джоном Ли.