Не знаю почему, но я старался пользоваться родным модулем из Python. Возможно потому, что его использование позволяет обойтись без дополнительных зависимостей. Возможно также, что я пытался пользоваться родным модулем, потому что ранее в скриптах на Perl пользовался родным для него модулем LWP и у меня с ним не возникало никаких проблем. Я ожидал, что с родным модулем из Python тоже не должно возникнуть никаких проблем. Мне было трудно, но, к счастью, разбираться с модулем мне пришлось поэтапно, так что со временем в моих скриптах и программах набралось достаточное количество примеров на самые разные случаи с обработкой всех встречавшихся мне исключений. Я решил собрать все эти примеры в одном месте и поделиться ими в этой статье.
1. GET-запрос
Начнём с самого простого - с выполнения GET-запроса. Нужно просто скачать страницу.import warnings, urllib, urllib2, socket, ssl warnings.filterwarnings('ignore', category=UserWarning, module='urllib2') def http_get(str_url, dict_params, dict_headers, float_timeout): query = urllib.urlencode(dict_params) req = urllib2.Request(str_url + '?' + query, headers=dict_headers) opener = urllib2.build_opener() try: res = opener.open(req, timeout=float_timeout) except urllib2.HTTPError as e: res = e except urllib2.URLError as e: return None, None except socket.timeout: return None, None except ssl.SSLError: return None, None return res.code, res.read()Здесь можно увидеть функцию http_get, которая принимает параметры:
- str_url - строка с URL страницы, которую нужно скачать,
- dict_params - словарь с параметрами и их значениями, из которых будет сформирована строка запроса. Эта строка запроса потом будет добавлена к строке URL после знака вопроса. Ключи словаря являются именами параметров, а значения - соответственно значениями,
- dict_headers - словарь с дополнительными заголовками, которые будут добавлены в GET-запрос. Ключи словаря являются именами заголовков, а значения - их значениями,
- float_timeout - число с плавающей запятой, задающее таймаут ожидания запроса.
Если вам нужно узнать заголовки из ответа, то имейте в виду, что объект res принадлежит классу httplib.HTTPResponse. У объектов этого класса есть методы getheader и getheaders. Метод getheader вернёт значение заголовка с указанным именем или значение по умолчанию. Метод getheaders вернёт список кортежей с именами заголовков и их значениями. Например, словарь из заголовков и их значений (исключая заголовки с несколькими значениями) можно было бы получить следующим образом:
headers = dict(res.getheaders())
2. POST-запрос enctype=application/x-www-form-urlencoded
Можно сказать, что это "обычный" POST-запрос. В таких POST-запросах применяется кодирование параметров, аналогичное тому, которое используется для кодирования параметров в GET-запросах. В случае с GET-запросами строка с параметрами добавляется после вопросительного знака к строке URL запрашиваемого ресурса. В случае с POST-запросом эта строка с параметрами помещается в тело запроса. В запросах такого рода нельзя передать на сервер файл.import warnings, urllib, urllib2, socket, ssl warnings.filterwarnings('ignore', category=UserWarning, module='urllib2') def http_post(str_url, dict_params, dict_headers, float_timeout): query = urllib.urlencode(dict_params) dict_headers['Content-type'] = 'application/x-www-form-urlencoded' req = urllib2.Request(str_url, data=query, headers=dict_headers) opener = urllib2.build_opener() try: res = opener.open(req, timeout=float_timeout) except urllib2.HTTPError as e: res = e except urllib2.URLError as e: return None, None except socket.timeout: return None, None except ssl.SSLError: return None, None return res.code, res.read()Функция http_post по входным и выходным параметрам полностью аналогична функции http_get, только выполняет POST-запрос.
3. POST-запрос enctype=multipart/form-data
Если в запросе нужно отправить много данных, то они могут оказаться слишком большими, чтобы уместиться в одной строке. В таких случаях, например если нужно приложить файл, в веб-приложениях используются формы с типом "multipart/form-data". Для отправки подобных запросов переделаем предыдущую функцию, воспользовавшись модулем poster.import warnings, urllib2, socket, ssl warnings.filterwarnings('ignore', category=UserWarning, module='urllib2') from poster.encode import multipart_encode def http_post_multipart(str_url, dict_params, dict_headers, float_timeout): body, headers = multipart_encode(dict_params) dict_headers.update(headers) req = urllib2.Request(str_url, data=body, headers=dict_headers) opener = urllib2.build_opener() try: res = opener.open(req, timeout=float_timeout) except urllib2.HTTPError as e: res = e except urllib2.URLError as e: return None, None except socket.timeout: return None, None except ssl.SSLError: return None, None return res.code, res.read()Функция http_post_multipart по входным и выходным параметрам полностью аналогична функциям http_get и http_post, только выполняет POST-запрос, закодировав параметры способом, пригодным для передачи данных большого объёма, в том числе - файлов.
4. GET-запрос с аутентификацией
Продемонстрирую аутентификацию на примере с GET-запросом:import warnings, urllib, urllib2, socket, ssl warnings.filterwarnings('ignore', category=UserWarning, module='urllib2') def http_get_auth(str_url, dict_params, dict_headers, float_timeout, str_user, str_password): query = urllib.urlencode(dict_params) req = urllib2.Request(str_url + '?' + query, headers=dict_headers) passman = urllib2.HTTPPasswordMgrWithDefaultRealm() passman.add_password(None, str_url, str_user, str_password) authhandler = urllib2.HTTPBasicAuthHandler(passman) opener = urllib2.build_opener(authhandler) try: res = opener.open(req, timeout=float_timeout) except urllib2.HTTPError as e: res = e except urllib2.URLError as e: return None, None except socket.timeout: return None, None except ssl.SSLError: return None, None return res.code, res.read()Как видно, функция http_get_auth по выходным параметрам совпадает со всеми предыдущими функциями, но среди входных параметров появилось два новых:
- str_user - строка, содержащая имя пользователя,
- str_password - строка, содержащая пароль пользователя.
Менеджер паролей может предоставлять обработчику разные пароли, в зависимости от области доступа (часто называемой realm'ом), которую сообщит веб-сервер или в зависимости от URL запрашиваемого ресурса.
5. GET-запрос с безусловной аутентификацией
Обработчик HTTPBasicAuthHandler устроен таким образом, что сначала всегда делает запрос без передачи логина и пароля. И только если получен ответ с кодом 401, уже пытается выполнить запрос с аутентификацией.При попытке выполнить запрос к API, реализованному на основе библиотеки swagger, возникают некоторые трудности с аутентификацией. При обращении к API без аутентификации, API возвращает HTML-справку по использованию API с кодом ответа 403, поэтому дополнительных попыток получить страницу с аутентификацией не предпринимается.
Чтобы исправить эту ситуацию, я воспользовался ответом, подсмотренным здесь: does urllib2 support preemptive authentication authentication?
import warnings, urllib, urllib2, socket, ssl, base64 warnings.filterwarnings('ignore', category=UserWarning, module='urllib2') class PreemptiveBasicAuthHandler(urllib2.HTTPBasicAuthHandler): '''Preemptive basic auth. Instead of waiting for a 403 to then retry with the credentials, send the credentials if the url is handled by the password manager. Note: please use realm=None when calling add_password.''' def http_request(self, req): url = req.get_full_url() realm = None # this is very similar to the code from retry_http_basic_auth() # but returns a request object. user, pw = self.passwd.find_user_password(realm, url) if pw: raw = "%s:%s" % (user, pw) auth = 'Basic %s' % base64.b64encode(raw).strip() req.add_unredirected_header(self.auth_header, auth) return req https_request = http_request def http_get_preemptive_auth(str_url, dict_params, dict_headers, float_timeout, str_user, str_password): query = urllib.urlencode(dict_params) req = urllib2.Request(str_url + '?' + query, headers=dict_headers) passman = urllib2.HTTPPasswordMgrWithDefaultRealm() passman.add_password(None, str_url, str_user, str_password) authhandler = PreemptiveBasicAuthHandler(passman) opener = urllib2.build_opener(authhandler) try: res = opener.open(req, timeout=float_timeout) except urllib2.HTTPError as e: res = e except urllib2.URLError as e: return None, None except socket.timeout: return None, None except ssl.SSLError: return None, None return res.code, res.read()Функция http_get_preemptive_auth по входным и выходным параметрам полностью аналогична функции http_get_auth, но в ней используется другой дополнительный обработчик - PreemptiveBasicAuthHandler, который отличается от обработчика HTTPBasicAuthHandler тем, что не выполняет запрос без аутентификации, ожидая получить ошибку 401, а сразу отправляет логин и пароль, соответствующие запрашиваемому URL.
6. GET-запрос через прокси
Смотрим пример:import warnings, urllib, urllib2, socket, ssl warnings.filterwarnings('ignore', category=UserWarning, module='urllib2') def http_get_proxy(str_url, dict_params, dict_headers, float_timeout, str_proxy): query = urllib.urlencode(dict_params) req = urllib2.Request(url + '?' + query, headers=dict_headers) proxyhandler = urllib2.ProxyHandler(str_proxy) opener = urllib2.build_opener(proxyhandler) try: res = opener.open(req, timeout=float_timeout) except urllib2.HTTPError as e: res = e except urllib2.URLError as e: return None, None except socket.timeout: return None, None except ssl.SSLError: return None, None return res.code, res.read()Функция http_get_proxy по выходным параметрам полностью соответствует функции http_get, а среди входных параметров имеется один дополнительный:
- str_proxy - строка с адресом веб-прокси. Имеет вид http://proxy.domain.tld:3128
{ 'http': 'http://proxy-http.domain.tld:3128', 'https': 'http://proxy-https.domain.tld:3128', 'ftp': 'http://proxy-ftp.domain.tld:3128' }
7. Использование нескольких обработчиков запросов одновременно
Когда в запросе нужно использовать несколько обработчиков одновременно, то можно растеряться, т.к. в примерах выше каждый раз используется только один обработчик для аутентификации или для прокси. Но использовать несколько обработчиков совсем не трудно. Это можно сделать, указав их через запятую, вот так:opener = urllib2.build_opener(authhandler, proxyhandler)Но и в этом случае можно растеряться, если список обработчиков нужно формировать динамически, так что каждый конкретный обработчик может отсутствовать. Тут тоже всё просто. Можно добавлять обработчики в массив, а затем использовать этот массив как список аргументов:
# В начале список обработчиков пуст handlers = [] # Если нужна аутентификация, добавляем обработчик в список if ...: ... authhandler = ... handlers.append(authhandler) # Если запрос нужно выполнить через прокси, добавляем обработчик в список if ...: ... proxyhandler = ... handlers.append(proxyhandler) # Выполняем запрос, используя все обработчики из списка opener = urllib2.build_opener(*handlers)Если нужно пройти аутентификацию и на прокси и на веб-ресурсе, то для аутентификации на прокси можно использовать обработчик ProxyBasicAuthHandler, так что всего будет использоваться аж сразу три обработчика запросов.
Этих примеров достаточно для того, чтобы научиться пользоваться модулем urllib2 и понимать, в каком направлении надо копать, чтобы сделать что-то такое, что здесь не описано. Если у вас есть чем дополнить эту статью, прошу написать мне об этом в комментариях.
Дополнение от 4 сентября 2017 года: Исправил место указания таймаута. Судя по тому, что за почти полгода никто не указал на ошибку, либо статья не очень востребована, либо людям лень указывать на ошибку и они просто переходят к другой статье на ту же тему :)
Дополнение от 21 декабря 2017 года: Добавил импорт пропущенных модулей, в которых определяются объекты исключений, перехватываемых в примерах.
Комментариев нет:
Отправить комментарий