HOWTO получение интернет-ресурсов с использованием пакета urllib
Автор: | Майкл Фоорд |
---|
Примечание
Есть французский перевод более ранней ревизии этого HOWTO, доступного на сайте urllib2 — Отсутствующее руководство.
Вступление
urllib.request — модуль Python для получения URL-адресов (унифицированных указателей ресурсов). Он предлагает очень простой интерфейс в виде функции urlopen. Это позволяет получать URL-адреса с использованием множества различных протоколов. Он также предлагает немного более сложный интерфейс для обработки обычных ситуаций, таких как базовая аутентификация, файлы cookie, прокси и т. д. Они предоставляются объектами, называемыми обработчиками и открывателями.
urllib.request поддерживает получение URL-адресов для многих «схем URL-адресов»
(идентифицируемых строкой перед ":"
в URL-адресе, например, "ftp"
—
это схема URL-адреса "ftp://digitology.tech/"
) с использованием связанных с ними
сетевых протоколов (например, FTP, HTTP). В этом руководстве основное внимание
уделяется наиболее распространенному случаю — HTTP.
В простых ситуациях urlopen очень прост в использовании. Но как только вы
столкнетесь с ошибками или нетривиальными случаями при открытии URL-адресов
HTTP, вам потребуется некоторое понимание протокола передачи гипертекста (HTTP).
Наиболее полная и авторитетная ссылка на HTTP - RFC 2616. Это технический
документ, который нелегко читать. Цель этого HOWTO — проиллюстрировать
использование urllib с достаточно подробным описанием HTTP, чтобы помочь вам в
этом. Он не предназначен для замены документов urllib.request
, но
является их дополнением.
Запрос URL
Самый простой способ использовать urllib.request заключается в следующем:
import urllib.request
with urllib.request.urlopen('https://digitology.tech/') as response:
html = response.read()
Если вы хотите получить ресурс по URL-адресу и сохранить его во временном
месте, вы можете сделать это с помощью функций shutil.copyfileobj()
и
tempfile.NamedTemporaryFile()
:
import shutil
import tempfile
import urllib.request
with urllib.request.urlopen('https://digitology.tech/') 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
с этим объектом запроса возвращает объект ответа
для запрошенного URL-адреса. Этот ответ представляет собой объект, подобный
файлу, что означает, что вы можете вызвать .read()
в ответе:
import urllib.request
req = urllib.request.Request('https://digitology.tech/')
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-форму, которую
вы заполнили в интернете. Не все 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 вызывает URLError
, когда не может обработать ответ (хотя, как
обычно с API Python, также могут вызываться встроенные исключения, такие как
ValueError
, TypeError
и т. д.).
HTTPError
— это подкласс URLError
, вызываемый в
случае URL-адресов HTTP.
Классы исключений экспортируются из модуля urllib.error
.
URLError
Часто URLError вызывается из-за того, что нет сетевого подключения (нет маршрута к указанному серверу) или указанный сервер не существует. В этом случае возникшее исключение будет иметь атрибут «reason», который представляет собой кортеж, содержащий код ошибки и текстовое сообщение об ошибке.
Например
>>> 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. Словарь
воспроизведен здесь для удобства:
# Таблица сопоставления кодов ответов с сообщениями; записи имеют следующие значения:
# форма {code: (краткое_сообщение, длинное_сообщение)}.
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('https://digitology.tech/index.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:
# всё хорошо
Примечание
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:
# всё хорошо
info и geturl
Ответ, возвращаемый urlopen (или экземпляром HTTPError
), содержит два
полезных метода info()
и geturl()
и определён в модуле
urllib.response
.
geturl — возвращает реальный URL-адрес выбранной страницы. Это полезно, потому
что urlopen
(или используемый объект открывания) мог следовать за
перенаправлением. URL-адрес полученной страницы может не совпадать с
запрошенным URL-адресом.
info - возвращает объект, подобный словарю, который определяет выбранную
страницу, в частности, заголовки, отправленные сервером. В настоящее время это
экземпляр http.client.HTTPMessage
.
Типичные заголовки включают «Content-length», «Content-type» и так далее. См. Краткий справочник по HTTP заголовкам для полезного списка заголовков HTTP с краткими объяснениями их значения и использования.
Открывальщики и обработчики
Когда вы получаете URL-адрес, используется открыватель (экземпляр, возможно,
с запутанным названием urllib.request.OpenerDirector
). Обычно мы
использовали открыватель по умолчанию — через urlopen
, но вы можете
создавать собственные открыватели. Открыватели используют обработчики. Вся
«тяжелая работа» выполняется обработчиками. Каждый обработчик знает, как открывать
URL-адреса для определенной схемы URL (http, ftp и т. д.) или как обрабатывать
аспект открытия URL, например перенаправления HTTP или файлы cookie HTTP.
Вам нужно создать открыватели, если вы хотите получать URL-адреса с установленными определёнными обработчиками, например, чтобы получить средство открытия, которое обрабатывает файлы cookie, или чтобы получить не обрабатывающие перенаправления средство открытия.
Чтобы создать открыватель, создайте экземпляр OpenerDirector
, а затем
повторно вызовите .add_handler(some_handler_instance)
.
В качестве альтернативы вы можете использовать build_opener
, удобную
функцию для создания открывающих объектов с помощью одного вызова функции.
build_opener
по умолчанию добавляет несколько обработчиков, но
предоставляет быстрый способ добавить и/или переопределить обработчики по
умолчанию.
Другие виды обработчиков, которые могут вам понадобиться, могут обрабатывать прокси, аутентификацию и другие общие, но немного специализированные ситуации.
install_opener
можно использовать, чтобы сделать объект opener
(глобальным) средством открытия по умолчанию. Это означает, что вызовы
urlopen
будут использовать установленную вами программу открытия.
У объектов открывания есть метод open
, который можно вызвать напрямую для
получения URL-адресов таким же образом, как и у функции urlopen
: нет
необходимости вызывать install_opener
, кроме как для удобства.
Базовая аутентификация
Чтобы проиллюстрировать создание и установку обработчика, мы будем использовать
HTTPBasicAuthHandler
. Для более подробного обсуждения этого вопроса,
включая объяснение того, как работает базовая аутентификация, см.
Учебник по базовой аутентификации.
Когда требуется аутентификация, сервер отправляет заголовок (а также код ошибки
401) с запросом аутентификации. Это определяет схему аутентификации и
«realm». Заголовок выглядит так: WWW-Authenticate: SCHEME realm="REALM"
.
Например:
WWW-Authenticate: Basic realm="cPanel Users"
Затем клиент должен повторить запрос, указав соответствующее имя и пароль для
realm, включенные в запрос в качестве заголовка. Это «базовая проверка
подлинности». Чтобы упростить этот процесс, мы можем создать экземпляр
HTTPBasicAuthHandler
и средство открытия для использования этого
обработчика.
HTTPBasicAuthHandler
использует объект, называемый диспетчером паролей, для
обработки сопоставления URL-адресов и областей с паролями и именами
пользователей. Если вы знаете, что это за realm (из заголовка аутентификации,
отправленного сервером), вы можете использовать HTTPPasswordMgr
. Часто
никого не волнует, что это за realm. В этом случае удобно использовать
HTTPPasswordMgrWithDefaultRealm
. Позволяет вам указать имя пользователя
и пароль по умолчанию для URL-адреса. Будет предоставлено, если вы не
укажете альтернативную комбинацию для realm. Мы указываем это,
предоставляя None
в качестве аргумента realm для метода add_password
.
URL-адрес верхнего уровня — это первый URL-адрес, требующий аутентификации. URL-адреса «глубже», чем URL-адрес, который вы передаёте в .add_password(), также будут совпадать.
# создать менеджер паролей
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
# Добавьте имя пользователя и пароль.
# Если бы мы знали царство, мы могли бы использовать его вместо None.
top_level_url = "http://example.com/foo/"
password_mgr.add_password(None, top_level_url, username, password)
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
# создать "opener" (сущность OpenerDirector)
opener = urllib.request.build_opener(handler)
# использовать opener для получения URL-адреса
opener.open(a_url)
# Установить opener. Теперь все вызовы urllib.request.urlopen используют
# наш 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
без определенных прокси. Это делается с
помощью шагов, аналогичных настройке обработчика Базовая аутентификация::
>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)
Примечание
В настоящее время urllib.request
не поддерживает запрос https
ресурса через прокси. Однако это можно сделать путём
расширения urlib.request, как показано в рецепте [6].
Примечание
HTTP_PROXY
игнорируется, если установлена переменная REQUEST_METHOD
; см.
документацию относительно getproxies()
.
Сокеты и слои
Поддержка Python для извлечения ресурсов из интернета является многоуровневой.
urllib использует библиотеку http.client
, которая, в свою очередь,
использует библиотеку сокетов.
Начиная с Python 2.3, вы можете указать, как долго сокет должен ждать ответа до истечения времени ожидания. Это может быть полезно в приложениях, которые должны получать веб-страницы. По умолчанию у модуля socket нет тайм-аута и может зависнуть. В настоящее время тайм-аут socket не отображается на уровнях http.client или urllib.request. Однако вы можете установить тайм-аут по умолчанию глобально для всех сокетов, использующих:
import socket
import urllib.request
# timeout в секундах
timeout = 10
socket.setdefaulttimeout(timeout)
# этот вызов к urllib.request.urlopen теперь используется тайм-аут по умолчанию
# мы установили в модуль socket
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)
Сноски
Документ был рассмотрен и пересмотрен Джоном Ли.
[1] | Google например. |
[2] | Браузер сниффинг является очень плохой практикой для создания дизайна веб — создавать сайты, использующие веб-стандарты, гораздо разумнее. К сожалению, многие сайты по-прежнему отправляют разные версии в разные браузеры. |
[3] | Пользовательским агентом MSIE 6 является „Mozilla/4.0 (совместимый; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)“ |
[4] | Дополнительные сведения о заголовках HTTP-запросов см. в разделе Краткий справочник по HTTP заголовкам. |
[5] | В моем случае я должен использовать прокси для доступа к интернету на работе. При попытке получить URL-адреса localhost через этот прокси они блокируются. IE настроен на использование прокси, который urlib захватывает. Чтобы тестировать сценарии с сервером localhost, я должен запретить urllib использовать прокси. |
[6] | Средство открытия urllib для SSL-прокси (метод CONNECT): Рецепт поваренной книги ASPN. |