Содержание страницы
HOWTO получение интернет-ресурсов с помощью пакета urllib¶HOWTO Fetch Internet Resources Using The urllib Package
- Автор
Примечание
Существует французский перевод более ранней версии этого руководства, доступный по адресу urllib2 - Le Manuel manquant.
Введение¶Introduction
urllib.request – это модуль Python для получения URL (Uniform Resource Locators). Он предоставляет очень простой интерфейс в виде функции urlopen. Она позволяет получать URL с использованием различных протоколов. Также доступен несколько более сложный интерфейс для обработки типовых ситуаций, таких как базовая аутентификация, куки, прокси и т. д. Они реализованы через объекты, называемые обработчиками и открывателями.
urllib.request поддерживает получение URL для множества «схем URL» (определяемых строкой
перед ":" в URL – например, "ftp" – это схема URL для
"ftp://python.org/") с использованием соответствующих сетевых протоколов (например, FTP, HTTP).
Данное руководство сосредоточено на наиболее распространённом случае – HTTP.
Для простых случаев urlopen очень прост в использовании. Но как только при открытии HTTP-URL возникают ошибки или нестандартные ситуации, потребуется некоторое понимание протокола передачи гипертекста (HTTP). Наиболее полным и авторитетным справочником по HTTP является RFC 2616. Это технический документ, не предназначенный для лёгкого чтения. Данное HOWTO призвано проиллюстрировать использование urllib с достаточным количеством деталей по HTTP, чтобы помочь разобраться. Оно не заменяет документацию urllib.request, а дополняет её.
Загрузка URL¶Fetching URLs
Простейший способ использования 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://www.voidspace.org.uk')
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-заголовков. Рассмотрим каждую из них по очереди.
Данные¶Data
Иногда требуется отправить данные на URL (часто URL будет ссылаться на CGI-скрипт или другое веб-приложение). В 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 должна быть типа 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) # Порядок может отличаться от указанного ниже.
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, после которого следуют закодированные значения.
Заголовки¶Headers
Здесь будет рассмотрен один конкретный HTTP-заголовок, чтобы проиллюстрировать, как добавлять заголовки в HTTP-запросы.
Некоторые сайты 1 не любят, когда их просматривают программы, либо отправляют разные версии разным браузерам 2. По умолчанию urllib идентифицирует себя как Python-urllib/x.y (где x и y – номера старшей и младшей версии релиза Python, например Python-urllib/2.5), что может сбить сайт с толку или просто не работать. Браузер идентифицирует себя через заголовок User-Agent3. При создании объекта 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, который следует после того, как мы рассмотрим, что происходит, когда что-то идёт не так.
Обработка исключений¶Handling Exceptions
urlopen возбуждает URLError, когда не может обработать ответ (хотя, как обычно в Python API, могут также возбуждаться встроенные исключения, такие как 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» (требуется аутентификация).
См. раздел 10 в RFC 2616 для справки по всем кодам ошибок HTTP.
Возбуждённый экземпляр HTTPError будет иметь целочисленный атрибут 'code', который соответствует ошибке, отправленной сервером.
Коды ошибок¶Error Codes
Поскольку обработчики по умолчанию обрабатывают перенаправления (коды в диапазоне 300), а коды в диапазоне 100–299 указывают на успех, обычно вы будете видеть только коды ошибок в диапазоне 400–599.
http.server.BaseHTTPRequestHandler.responses – это полезный словарь кодов ответа, который показывает все коды ответа, используемые RFC 2616. Словарь
воспроизведён здесь для удобства
# Таблица, сопоставляющая коды ответов с сообщениями; записи имеют
# вид {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
...
Резюме¶Wrapping it Up
Итак, если вы хотите быть готовым к HTTPError или URLError, есть два основных подхода. Второй подход предпочтительнее.
Способ 1¶Number 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¶Number 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¶info and geturl
Ответ, возвращаемый urlopen (или экземпляром HTTPError), имеет два
полезных метода: info() и geturl(), и определён в модуле
urllib.response..
geturl – возвращает реальный URL полученной страницы. Это полезно,
поскольку urlopen (или используемый объект opener) мог выполнить
перенаправление. URL полученной страницы может не совпадать с запрошенным URL.
info – возвращает объект, подобный словарю, который описывает полученную
страницу, в частности заголовки, отправленные сервером. В настоящее время это
экземпляр http.client.HTTPMessage.
Типичные заголовки включают ‘Content-length’, ‘Content-type’ и т.д. Смотрите Краткий справочник HTTP-заголовков для полезного списка HTTP-заголовков с краткими пояснениями их значения и использования.
Открыватели и обработчики¶Openers and Handlers
При получении URL используется открыватель – экземпляр urllib.request.OpenerDirector, название которого, возможно, сбивает с толку. Обычно используется открыватель по умолчанию – через urlopen, – но можно создавать собственные открыватели. Открыватели используют обработчики. Вся «тяжёлая работа» выполняется обработчиками. Каждый обработчик знает, как открывать URL для определённой схемы (http, ftp и т.д.) или как обрабатывать аспект открытия URL, например перенаправления HTTP или HTTP-куки.
Стоит создавать собственные opener'ы, если нужно получать URL с установленными определёнными обработчиками, например чтобы получить opener, обрабатывающий cookies, или opener, не обрабатывающий перенаправления.
Чтобы создать opener, создайте экземпляр OpenerDirector, а затем вызывайте
.add_handler(some_handler_instance) несколько раз.
В качестве альтернативы можно использовать build_opener – удобную функцию для
создания объектов opener одним вызовом. build_opener по умолчанию добавляет
несколько обработчиков, но предоставляет быстрый способ добавить ещё и/или
переопределить стандартные обработчики.
Другие типы обработчиков могут обрабатывать прокси, аутентификацию и другие распространённые, но слегка специализированные ситуации.
install_opener можно использовать, чтобы сделать объект opener (глобальным) стандартным
opener'ом. Это означает, что вызовы urlopen будут использовать установленный
opener.
Объекты opener имеют метод open, который можно вызывать напрямую для получения
URL так же, как функцию urlopen: нет необходимости вызывать
install_opener, кроме как для удобства.
Базовая аутентификация¶Basic Authentication
Для иллюстрации создания и установки обработчика мы будем использовать
HTTPBasicAuthHandler. Для более подробного обсуждения этой темы –
включая объяснение того, как работает базовая аутентификация, – смотрите Руководство
по базовой аутентификации.
Когда требуется аутентификация, сервер отправляет заголовок (а также код ошибки 401)
с запросом аутентификации. В нём указывается схема аутентификации
и «realm». Заголовок выглядит так: WWW-Authenticate: SCHEME
realm="REALM".
например
WWW-Authenticate: Basic realm="cPanel Users"
Затем клиент должен повторить запрос, включив в него соответствующие имя и пароль
для данной области в качестве заголовка. Это и есть «базовая
аутентификация». Чтобы упростить этот процесс, можно создать экземпляр
HTTPBasicAuthHandler и opener для использования этого обработчика.
HTTPBasicAuthHandler использует объект, называемый менеджером паролей, для обработки
соответствия URL и областей паролям и именам пользователей. Если известно,
какая область (из заголовка аутентификации, отправленного сервером), то можно использовать
HTTPPasswordMgr. Часто область не важна. В этом
случае удобно использовать HTTPPasswordMgrWithDefaultRealm. Это позволяет
указать имя пользователя и пароль по умолчанию для URL. Они будут использованы
при отсутствии альтернативной комбинации для конкретной
области. Это указывается передачей None в качестве аргумента realm методу
add_password.
URL верхнего уровня – это первый URL, требующий аутентификации. URL «глубже», чем переданный в .add_password(), также будут соответствовать.
# создать менеджер паролей
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
# Добавить имя пользователя и пароль.
# Если бы мы знали realm, мы могли бы использовать его вместо 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/", либо «authority» (т.е. имя хоста,
опционально включая номер порта), например "example.com" или "example.com:8080"
(последний пример включает номер порта). Authority, если присутствует, НЕ
должен содержать компонент «userinfo» – например "joe:password@example.com" некорректно.
Прокси¶Proxies
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().
Сокеты и уровни¶Sockets and Layers
Поддержка Python для получения ресурсов из сети является многоуровневой. urllib использует
библиотеку http.client, которая, в свою очередь, использует библиотеку socket.
Начиная с Python 2.3 можно указать, как долго сокет должен ждать ответа до истечения тайм-аута. Это может быть полезно в приложениях, которым приходится получать веб-страницы. По умолчанию модуль socket не имеет тайм-аута и может зависнуть. В настоящее время тайм-аут сокета не доступен на уровнях http.client или urllib.request. Однако можно установить глобальный тайм-аут по умолчанию для всех сокетов с помощью
import socket
import urllib.request
# тайм-аут в секундах
timeout = 10
socket.setdefaulttimeout(timeout)
# этот вызов urllib.request.urlopen теперь использует тайм-аут по умолчанию
# который мы установили в модуле socket
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)
Примечания¶Footnotes
Этот документ был проверен и доработан Джоном Ли.
- 1
Google, например.
- 2
Определение браузера (browser sniffing) – очень плохая практика для веб-дизайна: создание сайтов с использованием веб-стандартов гораздо разумнее. К сожалению, многие сайты до сих пор отправляют разные версии разным браузерам.
- 3
Пользовательский агент для MSIE 6 – 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)'
- 4
Подробнее о HTTP-заголовках запросов см. Quick Reference to HTTP Headers.
- 5
В моём случае на работе приходится использовать прокси для доступа в интернет. Если через этот прокси пытаться получить localhost-URL, он их блокирует. В IE настроено использование прокси, и urllib это подхватывает. Чтобы тестировать скрипты с локальным сервером, приходится запрещать urllib использовать прокси.
- 6
Открыватель urllib для SSL-прокси (метод CONNECT): ASPN Cookbook Recipe.