Spec-Zone.ru › Python 3.12

HOWTO: получение интернет-ресурсов с помощью пакета 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 URL-адресов вы столкнётесь с ошибками или нетривиальными случаями, вам потребуется некоторое понимание протокола передачи гипертекста. Самый полный и авторитетный источник сведений о 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 возвращает объект ответа для запрошенного 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-адресу (часто этот URL-адрес указывает на CGI-сценарий (Common Gateway Interface) или другое веб-приложение). В HTTP это часто делается с помощью так называемого запроса POST. Именно так обычно действует ваш браузер, когда вы отправляете заполненную веб-форму HTML. Однако запросы 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. Для удобства этот словарь приведён здесь:

# 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-адреса для определённой схемы URL (http, ftp и т. д.) либо обрабатывать какой-либо аспект открытия URL-адресов, например перенаправления HTTP или файлы cookie HTTP.

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

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

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

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

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

У объектов opener есть метод 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)

Примечание

В приведённом выше примере мы передали 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 через прокси. Однако эту возможность можно добавить, расширив urllib.request, как показано в рецепте [6].

Примечание

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

Сокеты и уровни

Поддержка получения ресурсов из Интернета в Python организована по уровням. urllib использует библиотеку http.client, которая, в свою очередь, использует библиотеку socket.

Начиная с 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.

[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 обнаруживает эту настройку. Чтобы тестировать сценарии с локальным сервером, мне приходится запрещать urllib использовать прокси.

[6]

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

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

Spec-Zone.ru

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