# HOWTO Отримати Інтернет-ресурси за допомогою пакета urllib

Автор:

  Michael Foord

## вступ

urllib.request — це модуль Python для отримання URL-адрес (уніфікованих покажчиків ресурсів). Він пропонує дуже простий інтерфейс у формі функції urlopen. Це здатне отримувати URL-адреси за допомогою різних протоколів. Він також пропонує дещо складніший інтерфейс для обробки поширених ситуацій, таких як базова автентифікація, файли cookie, проксі тощо. Вони забезпечуються об’єктами, які називаються обробниками та відкривачами.

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

Для простих ситуацій urlopen дуже простий у використанні. Але як тільки ви зіткнетеся з помилками або нетривіальними випадками під час відкриття URL-адрес 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 буде таким простим (зауважте, що замість 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 мають надходити з форм: ви можете використовувати 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 raises `URLError` when it cannot handle a response (though as usual with Python APIs, built-in exceptions such as `ValueError`, `TypeError` etc. may also be raised).

`HTTPError` is the subclass of `URLError` raised in the specific case of HTTP URLs.

Класи винятків експортуються з модуля `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

Every HTTP response from the server contains a numeric «status code». Sometimes the status code indicates that the server is unable to fulfil the request. The default handlers will handle some of these responses for you (for example, if the response is a «redirection» that requests the client fetch the document from a different URL, urllib will handle that for you). For those it can’t handle, urlopen will raise an `HTTPError`. Typical errors include „404“ (page not found), „403“ (request forbidden), and „401“ (authentication required).

Перегляньте розділ 10 RFC 2616 для довідки про всі коди помилок HTTP.

The `HTTPError` instance raised will have an integer „code“ attribute, which corresponds to the error sent by the server.

#### Коди помилок

Оскільки обробники за замовчуванням обробляють переспрямування (коди в діапазоні 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.'),
        }

When an error is raised the server responds by returning an HTTP error code and an error page. You can use the `HTTPError` instance as a response on the page returned. This means that as well as the code attribute, it also has read, geturl, and info, methods as returned by the `urllib.response` module:

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

### Загортання

So if you want to be prepared for `HTTPError` or `URLError` there are two basic approaches. I prefer the second approach.

#### Номер 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

Примітка

The `except HTTPError` must come first, otherwise `except URLError` will also catch an `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

## інформація та geturl

The response returned by urlopen (or the `HTTPError` instance) has two useful methods `info()` and `geturl()` and is defined in the module `urllib.response`.

- geturl - повертає справжню URL-адресу отриманої сторінки. Це корисно, оскільки `urlopen` (або використаний об’єкт відкриття) міг слідувати за перенаправленням. URL-адреса отриманої сторінки може не збігатися з запитуваною URL-адресою.

- info - це повертає об’єкт, схожий на словник, який описує отриману сторінку, зокрема заголовки, надіслані сервером. Зараз це екземпляр `http.client.HTTPMessage`.

Typical headers include „Content-length“, „Content-type“, and so on. See the Quick Reference to HTTP Headers for a useful listing of HTTP headers with brief explanations of their meaning and use.

## Відкривачки та обробники

When you fetch a URL you use an opener (an instance of the perhaps confusingly named `urllib.request.OpenerDirector`). Normally we have been using the default opener - via `urlopen` - but you can create custom openers. Openers use handlers. All the «heavy lifting» is done by the handlers. Each handler knows how to open URLs for a particular URL scheme (http, ftp, etc.), or how to handle an aspect of URL opening, for example HTTP redirections or HTTP cookies.

Ви захочете створити відкривачі, якщо хочете отримати URL-адреси з установленими певними обробниками, наприклад, щоб отримати відкривач, який обробляє файли cookie, або щоб отримати відкривач, який не обробляє переспрямування.

Щоб створити відкривач, створіть екземпляр `OpenerDirector`, а потім кілька разів викличте `.add_handler(some_handler_instance)`.

Крім того, ви можете використовувати `build_opener`, яка є зручною функцією для створення відкриваючих об’єктів за допомогою одного виклику функції. `build_opener` додає кілька обробників за замовчуванням, але забезпечує швидкий спосіб додати більше та/або замінити обробники за замовчуванням.

Інші типи обробників, які вам можуть знадобитися, можуть обробляти проксі, автентифікацію та інші типові, але трохи спеціалізовані ситуації.

`install_opener` можна використовувати, щоб зробити об’єкт `opener` (глобальним) відкривачем за замовчуванням. Це означає, що виклики `urlopen` використовуватимуть встановлений вами відкривач.

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

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

To illustrate creating and installing a handler we will use the `HTTPBasicAuthHandler`. For a more detailed discussion of this subject – including an explanation of how Basic Authentication works - see the Basic Authentication Tutorial.

Коли потрібна автентифікація, сервер надсилає заголовок (а також код помилки 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(), також відповідатимуть.

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

Примітка

In the above example we only supplied our `HTTPBasicAuthHandler` to `build_opener`. By default openers have the handlers for normal situations – `ProxyHandler` (if a proxy setting such as an `http_proxy` environment variable is set), `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 (сумісний; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)»

[4]

Додаткову інформацію про заголовки HTTP-запитів див. у розділі Короткий довідник із заголовками HTTP.

[5]

У моєму випадку мені доводиться використовувати проксі для доступу до Інтернету на роботі. Якщо ви намагаєтесь отримати URL-адреси localhost через цей проксі, він блокує їх. IE налаштовано на використання проксі, який підбирає urllib. Щоб перевірити сценарії на локальному сервері, я повинен заборонити urllib використовувати проксі.

[6]

urllib opener for SSL proxy (CONNECT method): ASPN Cookbook Recipe.