Руководство: получение интернет-ресурсов с помощью пакета 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:» мы могли бы использовать адрес с префиксом «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-адрес (часто он ведёт к скрипту 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, который вызывается в конкретном случае с 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» (требуется аутентификация).
Справочную информацию обо всех кодах ошибок HTTP см. в разделе 10 документа RFC 2616.
У вызванного экземпляра 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
info и geturl
Ответ, возвращаемый urlopen (или экземпляр HTTPError), имеет два полезных метода info() и geturl() и определён в модуле urllib.response.
-
geturl — возвращает фактический URL-адрес полученной страницы. Это полезно, поскольку
urlopen(или используемый объект opener) мог выполнить перенаправление. URL-адрес полученной страницы может отличаться от запрошенного. -
info — возвращает объект, похожий на словарь, который описывает полученную страницу, в частности заголовки, отправленные сервером. В настоящее время это экземпляр
http.client.HTTPMessage.
К типичным заголовкам относятся «Content-length», «Content-type» и так далее. Полезный список HTTP-заголовков с краткими пояснениями их значения и назначения см. в кратком справочнике по HTTP-заголовкам.
Открыватели и обработчики
Для получения URL-адреса используется открыватель (экземпляр, возможно, сбивающего с толку по названию класса urllib.request.OpenerDirector). Обычно мы использовали открыватель по умолчанию — через urlopen, — но можно создавать собственные открыватели. Открыватели используют обработчики. Всю основную работу выполняют обработчики. Каждый обработчик знает, как открывать 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) с запросом на аутентификацию. В нём указываются схема аутентификации и «область». Заголовок выглядит так: WWW-Authenticate: SCHEME
realm="REALM".
Например:
WWW-Authenticate: Basic realm="cPanel Users"
Затем клиенту следует повторить запрос, включив в него в качестве заголовка соответствующие имя пользователя и пароль для этой области. Это и есть «базовая аутентификация». Чтобы упростить этот процесс, можно создать экземпляр HTTPBasicAuthHandler и открыватель, использующий этот обработчик.
В HTTPBasicAuthHandler для сопоставления URL-адресов и областей с паролями и именами пользователей используется объект, называемый менеджером паролей. Если область вам известна (из заголовка аутентификации, отправленного сервером), можно использовать HTTPPasswordMgr. Часто область не имеет значения. В этом случае удобно использовать HTTPPasswordMgrWithDefaultRealm. Он позволяет указать имя пользователя и пароль по умолчанию для URL-адреса. Эти данные будут использоваться, если вы не предоставите другую комбинацию для конкретной области. Это указывается передачей None в качестве аргумента realm методу 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)
Примечание
В приведённом выше примере мы передали в build_opener только наш HTTPBasicAuthHandler. По умолчанию открыватели включают обработчики для обычных ситуаций: 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 через прокси-сервер. Однако эту возможность можно включить, расширив 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)
Сноски
Документ был проверен и отредактирован John Lee.
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/howto/urllib2.html