РУКОВОДСТВО: получение интернет-ресурсов с помощью пакета urllib
- Автор:
Введение
urllib.request — это модуль Python для получения URL-адресов (унифицированных указателей ресурсов). Он предоставляет очень простой интерфейс в виде функции 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:» можно было использовать адрес, начинающийся с «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 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(или используемый объект открывателя) мог выполнить перенаправление. 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-адреса. Эти данные будут использованы, если вы не указали другую пару для конкретной области. Чтобы обозначить это, в качестве аргумента области для метода add_password указывается None.
Верхнеуровневый 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/", либо «authority» (то есть имя хоста, при необходимости включающее номер порта), например "example.com" или "example.com:8080" (в последнем примере указан номер порта). Если authority указан, он НЕ должен содержать компонент «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 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/howto/urllib2.html