КАК получить интернет-ресурсы с помощью пакета urllib

Автор

Michael Foord

Примечание

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

Введение

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

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

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

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

Данные

Иногда вы хотите отправить данные на 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 Specification, Form Submission).

Если вы не передаете аргумент 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 and geturl, который появится после того, как мы рассмотрим, что происходит, когда что-то идет не так.

Обработка исключений

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“, который соответствует ошибке, отправленной сервером.

Коды ошибок

Поскольку обработчики по умолчанию обрабатывают перенаправления (коды в диапазоне 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 в качестве ответа на возвращаемой странице. Это означает, что помимо атрибута 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 полученной страницы может не совпадать с запрошенным URL.

info - возвращает словареподобный объект, описывающий полученную страницу, в частности, заголовки, отправленные сервером. В настоящее время это экземпляр http.client.HTTPMessage.

Типичные заголовки включают „Content-length“, „Content-type“ и так далее. Смотрите Quick Reference to HTTP Headers для полезного списка заголовков HTTP с краткими объяснениями их значения и использования.

Открыватели и манипуляторы

Когда вы получаете URL, вы используете открыватель (экземпляр, возможно, сбивающего с толку названия urllib.request.OpenerDirector). Обычно мы используем открывалку по умолчанию - через urlopen, но вы можете создавать собственные открывалки. Открыватели используют обработчики. Всю «тяжелую работу» выполняют обработчики. Каждый обработчик знает, как открывать URL для определенной схемы URL (http, ftp и т.д.), или как обрабатывать тот или иной аспект открытия URL, например, HTTP-перенаправления или HTTP-куки.

Вы захотите создать открыватели, если вам нужно получить URL с установленными определенными обработчиками, например, чтобы получить открыватель, который обрабатывает cookies, или чтобы получить открыватель, который не обрабатывает перенаправления.

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

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

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

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

Объекты Opener имеют метод open, который можно вызвать непосредственно для получения урлов таким же образом, как и функцию urlopen: нет необходимости вызывать install_opener, кроме как для удобства.

Базовая аутентификация

Для иллюстрации создания и установки обработчика мы будем использовать HTTPBasicAuthHandler. Для более подробного обсуждения этой темы - включая объяснение того, как работает базовая аутентификация - смотрите Basic Authentication Tutorial.

Когда требуется аутентификация, сервер отправляет заголовок (а также код ошибки 401) с запросом на аутентификацию. В нем указывается схема аутентификации и «область». Заголовок выглядит следующим образом: WWW-Authenticate: SCHEME realm="REALM".

например.

WWW-Authenticate: Basic realm="cPanel Users"

Затем клиент должен повторить запрос с соответствующими именем и паролем для сферы, включенными в качестве заголовка в запрос. Это и есть «базовая аутентификация». Чтобы упростить этот процесс, мы можем создать экземпляр HTTPBasicAuthHandler и открывающее устройство для использования этого обработчика.

HTTPBasicAuthHandler использует объект, называемый менеджером паролей, для обработки сопоставления URL и царств с паролями и именами пользователей. Если вы знаете, что такое царство (из заголовка аутентификации, отправленного сервером), то вы можете использовать HTTPPasswordMgr. Часто бывает так, что человеку не важно, что такое царство. В этом случае удобно использовать HTTPPasswordMgrWithDefaultRealm. Это позволяет указать имя пользователя и пароль по умолчанию для URL. Они будут заданы в том случае, если вы не укажете альтернативную комбинацию для конкретного царства. Мы указываем это, предоставляя None в качестве аргумента realm для метода 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, без определенных прокси. Это делается с помощью шагов, аналогичных настройке обработчика Basic Authentication:

>>> 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)

Сноски

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

1

Например, Google.

2

Браузерный сниффинг - это очень плохая практика для дизайна сайтов - создание сайтов с использованием веб-стандартов гораздо более разумно. К сожалению, многие сайты все еще отправляют разные версии в разные браузеры.

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

В моем случае я вынужден использовать прокси-сервер для доступа в Интернет на работе. Если вы пытаетесь получить URL localhost через этот прокси, он блокирует их. IE настроен на использование прокси, и urllib это замечает. Чтобы протестировать скрипты с сервером localhost, я должен запретить urllib использовать прокси.

6

urllib-открывалка для SSL-прокси (метод CONNECT): ASPN Cookbook Recipe.

Вернуться на верх