HOWTO: получение интернет-ресурсов с помощью пакета 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-адресов возникают ошибки или нетривиальные ситуации, вам потребуется некоторое понимание протокола передачи гипертекста. Наиболее полным и авторитетным справочником по 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, если не может обработать ответ (хотя, как обычно в API Python, также могут быть вызваны встроенные исключения, например ValueError, TypeError и т. д.).
HTTPError — подкласс URLError, вызываемый в особых случаях, связанных с HTTP URL-адресами.
Классы исключений экспортируются модулем 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. Ниже приведён фрагмент этого словаря.
responses = {
...
<HTTPStatus.OK: 200>: ('OK', 'Request fulfilled, document follows'),
...
<HTTPStatus.FORBIDDEN: 403>: ('Forbidden',
'Request forbidden -- authorization will '
'not help'),
<HTTPStatus.NOT_FOUND: 404>: ('Not Found',
'Nothing matches the given URI'),
...
<HTTPStatus.IM_A_TEAPOT: 418>: ("I'm a Teapot",
'Server refuses to brew coffee because '
'it is a teapot'),
...
<HTTPStatus.SERVICE_UNAVAILABLE: 503>: ('Service Unavailable',
'The server cannot process the '
'request due to a high load'),
...
}
При возникновении ошибки сервер возвращает 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 в качестве аргумента области методу 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 можно указать, как долго сокет должен ждать ответа до истечения времени ожидания. Это может быть полезно в приложениях, которым необходимо получать веб-страницы. По умолчанию в модуле socket не задано время ожидания, и ожидание может затянуться. В настоящее время тайм-аут сокета нельзя задать на уровне 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–2025 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.13/howto/urllib2.html