HOWTO получение интернет-ресурсов с использованием пакета urllib

Автор:Michael Foord

Примечание

Есть французский перевод более ранней ревизии этого HOWTO, доступного в urllib2 - Le Manuel manquant.

Введение

urllib.request - модуль Python для выборки URL-адресов унифицированные
локаторы ресурсов). Он предлагает очень простой интерфейс, в виде функции он urlopen. Это позволяет получать URL-адреса с помощью различных различных протоколов. Он также предлагает несколько более сложный интерфейс для обработки обычных ситуаций - таких как базовая аутентификация, куки, прокси и т. д. Они предоставляются объектами, называемыми обработчиками и открывателями.

urllib.request поддерживает выборку URL для многих «схем URL» (идентифицируемых строка перед ":" в URL - например, "ftp" - схема URL "ftp://python.org/") с использованием связанных с ними сетевых протоколов (например, FTP, HTTP). В данном HOWTO рассматривается наиболее распространенный вариант HTTP.

Для простых ситуаций urlopen очень прост в использовании. Но как только при открытии URL-адресов HTTP возникают ошибки или нетривиальные случаи, потребуется понимание протокола Протокол Передачи Гипертекста (HTTP). Наиболее полная и авторитетная ссылка на HTTP - RFC 2616. Это технический документ, который нелегко прочитать. Этот HOWTO предназначен для иллюстрации использования 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 будут настолько просты (обратите внимание что вместо „http“: URL у нас могло быть используемый URL, начинающийся с „ftp:“ „file:“ и т.д.). Однако цель данного учебного пособия - объяснить более сложные случаи, сосредоточившись на HTTP.

HTTP основан на запросах и ответах - клиент делает запросы, а серверы отправляют ответы. urllib.request отражает это с объектом Request, который представляет создаваемый HTTP-запрос. В простейшей форме создается объект Request, указывающий URL-адрес для выборки. Запрос urlopen с этим объектом запроса возвращает объект ответа для 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()

Обратите внимание, что urlib.request использует один и тот же интерфейс Request для обработки всех схем URL. Например, можно создать такой запрос FTP:

req = urllib.request.Request('ftp://example.com/')

В случае HTTP есть две дополнительные вещи, которые позволяют делать объекты Request: во-первых, вы можете передавать данные для отправки на сервер. Во- вторых, вы можете передать на сервер дополнительную информацию («метаданные») about данных или о самом запросе - эта информация отправляется в виде «заголовков» 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') # данные должны быть байтами
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 URL.

Исключение классы экспортируется от модуля urllib.error.

URLError

Часто URLError возникает из-за отсутствия сетевого подключения (нет маршрута к указанному серверу) или из-за отсутствия указанного сервера. В этом случае исключение подняло, будет иметь „причину“ признаком, который является кортежем, содержащим ошибку код и текстовое сообщение об ошибке.

Например:

>>> 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 будет обрабатывать его для вас). Для тех, с кем он не справится, урлопен поднимет HTTPError. Типичные ошибки включают «404» (страница не найдена), «403» (запрос запрещен) и «401» (требуется аутентификация).

Посмотрите раздел 10 RFC 2616 для справки на всей ошибке HTTP коды.

HTTPError сущность поднял, будет иметь целочисленный „кодовый“ признак, который соответствует ошибке, посланной сервером.

Коды ошибок

Поскольку обработчики по умолчанию обращаются с перенаправлениями (коды в этих 300 диапазонах), и коды в этих 100 - 299 диапазонах указывают на успех, вы будете обычно только видеть ошибку коды в этих 400 - 599 диапазонах.

http.server.BaseHTTPRequestHandler.responses - полезный словарь ответов коды в котором показаны все ответы коды используемый RFC 2616. Словарь воспроизводится здесь для удобства:

# Таблица сопоставления кодов ответов с сообщениями; записи имеют следующие значения:
# форма {code: (краткоесообщение, длинноесообщение)}.
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 сущность. Это означает, что как и атрибут код, он также имеет чтение, geturl и информацию, методы как возвращено модулем 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:
    # все хорошо

Примечание

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:
    # все хорошо

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 cookies.

Если требуется выбрать URL-адреса с установленными обработчиками, например, чтобы получить средство открытия, обрабатывающее cookies, или средство открытия, не обрабатывающее перенаправления, необходимо создать средство открытия.

Чтобы создать начало, иллюстрируйте примерами OpenerDirector и затем называйте .add_handler(some_handler_instance) неоднократно.

Кроме того, можно использовать функцию build_opener, которая является удобной функцией для создания объектов открытия с помощью одного вызова функции. build_opener добавляет несколько обработчиков по умолчанию, но обеспечивает быстрый способ добавления и/или переопределения обработчиков по умолчанию.

Другие типы обработчиков, которые могут потребоваться для обработки прокси, аутентификации и других распространенных, но слегка специализированных ситуаций.

install_opener может быть используемый, чтобы заставить opener возразить (глобальному) началу по умолчанию. Это означает, что при вызовах на urlopen будет использоваться установленная программа открытия.

Объекты Opener имеют метод open, который можно вызвать непосредственно для выборки urls так же, как и функцию 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 (), также будут совпадать.:

# создать менеджер паролей
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()

# Добавьте имя пользователя и пароль.
# Если бы мы знали реалм, мы могли бы использовать его вместо 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/" или «авторитетно» (т.е. имя хоста, произвольно включая номер порта), например, "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 ресурса через прокси. Однако это можно сделать путем расширения urlib.request, как показано в рецепте [6].

Примечание

HTTP_PROXY игнорируется, если установлена переменная REQUEST_METHOD; см. документацию относительно getproxies().

Сокеты и слои

Поддержка Python для извлечения ресурсов из интернета многоуровневая. urlib использует библиотеку http.client, которая, в свою очередь, использует библиотеку socket. Начиная с Python 2.3 можно указать, как долго сокет должен ждать ответа перед тайм-аутом. Это может быть полезно в приложениях, которые должны получать веб-страницы. По умолчанию модуль socket имеет no timeout и может зависать. В настоящее время время ожидания сокет не отображается на уровнях http.client или urlib.request. Однако можно установить время ожидания по умолчанию глобально для всех сокетов с помощью:

import socket
import urllib.request

# timeout в секундах
timeout = 10
socket.setdefaulttimeout(timeout)

# этот вызов к urllib.request.urlopen теперь используется тайм-аут по умолчанию
# мы установили в модуль socket
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)

Сноски

Этот документ был рассмотрен и пересмотрен Джоном Ли.

[1]Google например.
[2]Браузер сниффинг - очень плохая практика для создания дизайна веб - создавать сайты, использующие веб-стандарты, гораздо разумнее. К сожалению, многие сайты по-прежнему отправляют разные версии в разные браузеры.
[3]Пользовательским агентом MSIE 6 является „Mozilla/4.0 (совместимый; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)“
[4]Дополнительные сведения о заголовках HTTP-запросов см. в разделе Краткий справочник по HTTP заголовкам.
[5]В моем случае я должен использовать прокси для доступа к интернету на работе. При попытке получить URL-адреса localhost через этот прокси они блокируются. IE настроен на использование прокси, который urlib захватывает. Чтобы тестировать сценарии с сервером localhost, я должен запретить urllib использовать прокси.
[6]Средство открытия urllib для SSL-прокси (метод CONNECT): ASPN Cookbook Recipe.