Spec-Zone.ru › Python 3.11

Руководство: получение интернет-ресурсов с помощью пакета urllib

Автор:

Michael Foord

Введение

Связанные статьи

Вам также может быть полезна следующая статья о получении веб-ресурсов с помощью Python:

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

    Руководство по базовой аутентификации с примерами на Python.

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

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

В простых ситуациях urlopen очень проста в использовании. Однако, столкнувшись с ошибками или нестандартными случаями при открытии HTTP-адресов, вам понадобится некоторое понимание протокола передачи гипертекста. Самый полный и авторитетный источник сведений об 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-адресами.

Классы исключений экспортируются из модуля 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. Для удобства этот словарь приведён здесь:

# 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-адрес полученной страницы может отличаться от запрошенного.
  • 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 в качестве аргумента 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)

Примечание

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

[1]

Например, Google.

[2]

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

[3]

Строка user agent для MSIE 6: ‘Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)’

[4]

Подробнее о других заголовках HTTP-запросов см. в кратком справочнике по HTTP-заголовкам.

[5]

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

[6]

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

© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/howto/urllib2.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API