Как получить ресурсы из интернета с помощью пакета urllib
- Автор:
Введение
urllib.request — это модуль Python для получения URL (Uniform Resource Locators). Он предоставляет очень простой интерфейс в виде функции urlopen. Эта функция способна получать URL-адреса с использованием различных протоколов. Она также предоставляет несколько более сложный интерфейс для обработки распространённых ситуаций — например, базовой аутентификации, файлов cookie, прокси-серверов и так далее. Эти возможности реализованы с помощью объектов, называемых обработчиками и открывателями.
urllib.request поддерживает получение URL для многих «схем URL» (определяемых строкой перед ":" в URL — например, "ftp" — это схема URL "ftp://python.org/") с использованием соответствующих сетевых протоколов (например, FTP, HTTP). В этом руководстве мы сосредоточимся на наиболее распространённом случае — HTTP.
В простых ситуациях функция urlopen очень удобна в использовании. Но как только вы столкнётесь с ошибками или нетривиальными случаями при открытии HTTP-URL, вам потребуется некоторое понимание протокола HyperText Transfer Protocol. Наиболее полное и авторитетное руководство по 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-запрос, который вы отправляете. В самом простом виде вы создаёте объект 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 вызывает URLError, когда не может обработать ответ (хотя, как обычно в Python API, могут быть вызваны встроенные исключения, такие как 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» (требуется аутентификация).
См. раздел 10 RFC 2616 для справки по всем кодам ошибок HTTP.
Возвращаемый экземпляр HTTPError будет иметь целочисленный атрибут «код», который соответствует ошибке, отправленной сервером.
Коды ошибок
Поскольку стандартные обработчики обрабатывают перенаправления (коды в диапазоне 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 как ответ на возвращённой странице. Это означает, что помимо атрибута «код», он также имеет методы 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(или используемый объект открывателя) мог выполнить перенаправление. URL полученной страницы может отличаться от запрошенного URL. -
info — возвращает похожий на словарь объект, описывающий полученную страницу, в частности, заголовки, отправленные сервером. В настоящее время это экземпляр
http.client.HTTPMessage.
Типичные заголовки включают «Content-length», «Content-type» и т. д. См. Краткий справочник по HTTP-заголовкам для полезного списка HTTP-заголовков с кратким описанием их значения и использования.
Открыватели и обработчики
При получении URL вы используете открывателя (экземпляр, возможно, неудачно названного urllib.request.OpenerDirector). Обычно мы используем стандартный открывателя — через urlopen — но вы можете создать пользовательские открыватели. Открыватели используют обработчики. Вся «тяжёлая работа» выполняется обработчиками. Каждый обработчик знает, как открыть URL с определённой схемой URL (http, ftp и т. д.), или как обработать какой-либо аспект открытия URL, например, HTTP-перенаправления или HTTP-куки.
Вам нужно создавать открыватели, если вы хотите получать URL с определёнными установками обработчиков, например, для получения открывателя, который обрабатывает куки, или для получения открывателя, который не обрабатывает перенаправления.
Для создания открывателя необходимо создать экземпляр 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 в качестве аргумента области методу 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, без указания прокси. Это делается аналогично настройке обработчика базовой аутентификации:
>>> 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)
Примечания
Этот документ был проверен и пересмотрен Джоном Ли.
© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/urllib2.html