Ограничение / регулирование скорости HTTP-запросов в GRequests
Я пишу небольшой скрипт на Python 2.7.3 с GRequests и lxml, которые позволят мне собрать некоторые коллекционные цены на карты с различных веб-сайтов и сравнить их. Проблема в том, что один из веб-сайтов ограничивает количество запросов и отправляет обратно HTTP-ошибку 429, если я ее превышаю.
есть ли способ добавить дросселирование количества запросов в GRequestes, чтобы я не превышал количество запросов в секунду, которое я указываю? Также - как я могу сделать GRequestes повторить через некоторое время, если HTTP 429 происходит?
на боковой ноте-их предел смехотворно низок. Что-то вроде 8 запросов за 15 секунд. Я нарушил его с помощью своего браузера несколько раз, просто обновляя страницу, ожидая изменения цен.
4 ответов
собираюсь ответить на свой вопрос, так как я должен был понять это сам, и, похоже, очень мало информации об этом.
идея такова. Каждый объект запроса, используемый с GRequests, может принимать объект сеанса в качестве параметра при создании. Объекты сеанса, с другой стороны, могут иметь подключенные http-адаптеры, которые используются при выполнении запросов. Создавая наш собственный адаптер, мы можем перехватывать запросы и ограничивать их скорость, чтобы найти лучшее для нашего приложения. В моем случай I закончился с кодом ниже.
объект, используемый для регулирования:
DEFAULT_BURST_WINDOW = datetime.timedelta(seconds=5)
DEFAULT_WAIT_WINDOW = datetime.timedelta(seconds=15)
class BurstThrottle(object):
max_hits = None
hits = None
burst_window = None
total_window = None
timestamp = None
def __init__(self, max_hits, burst_window, wait_window):
self.max_hits = max_hits
self.hits = 0
self.burst_window = burst_window
self.total_window = burst_window + wait_window
self.timestamp = datetime.datetime.min
def throttle(self):
now = datetime.datetime.utcnow()
if now < self.timestamp + self.total_window:
if (now < self.timestamp + self.burst_window) and (self.hits < self.max_hits):
self.hits += 1
return datetime.timedelta(0)
else:
return self.timestamp + self.total_window - now
else:
self.timestamp = now
self.hits = 1
return datetime.timedelta(0)
адаптер HTTP:
class MyHttpAdapter(requests.adapters.HTTPAdapter):
throttle = None
def __init__(self, pool_connections=requests.adapters.DEFAULT_POOLSIZE,
pool_maxsize=requests.adapters.DEFAULT_POOLSIZE, max_retries=requests.adapters.DEFAULT_RETRIES,
pool_block=requests.adapters.DEFAULT_POOLBLOCK, burst_window=DEFAULT_BURST_WINDOW,
wait_window=DEFAULT_WAIT_WINDOW):
self.throttle = BurstThrottle(pool_maxsize, burst_window, wait_window)
super(MyHttpAdapter, self).__init__(pool_connections=pool_connections, pool_maxsize=pool_maxsize,
max_retries=max_retries, pool_block=pool_block)
def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
request_successful = False
response = None
while not request_successful:
wait_time = self.throttle.throttle()
while wait_time > datetime.timedelta(0):
gevent.sleep(wait_time.total_seconds(), ref=True)
wait_time = self.throttle.throttle()
response = super(MyHttpAdapter, self).send(request, stream=stream, timeout=timeout,
verify=verify, cert=cert, proxies=proxies)
if response.status_code != 429:
request_successful = True
return response
настройка:
requests_adapter = adapter.MyHttpAdapter(
pool_connections=__CONCURRENT_LIMIT__,
pool_maxsize=__CONCURRENT_LIMIT__,
max_retries=0,
pool_block=False,
burst_window=datetime.timedelta(seconds=5),
wait_window=datetime.timedelta(seconds=20))
requests_session = requests.session()
requests_session.mount('http://', requests_adapter)
requests_session.mount('https://', requests_adapter)
unsent_requests = (grequests.get(url,
hooks={'response': handle_response},
session=requests_session) for url in urls)
grequests.map(unsent_requests, size=__CONCURRENT_LIMIT__)
взгляните на это для автоматического регулирования запросов: https://pypi.python.org/pypi/RequestsThrottler/0.2.2
вы можете установить фиксированную задержку между каждым запросом или установить количество запросов для отправки в фиксированном количестве секунд (что в основном одно и то же):
import requests
from requests_throttler import BaseThrottler
request = requests.Request(method='GET', url='http://www.google.com')
reqs = [request for i in range(0, 5)] # An example list of requests
with BaseThrottler(name='base-throttler', delay=1.5) as bt:
throttled_requests = bt.multi_submit(reqs)
где функция multi_submit
возвращает список ThrottledRequest
(см. doc: ссылка в конце).
вы тогда можете доступ к ответам:
for tr in throttled_requests:
print tr.response
в качестве альтернативы вы можете достичь того же, указав номер или запросы для отправки в фиксированное время (например, 15 запросов каждые 60 секунд):
import requests
from requests_throttler import BaseThrottler
request = requests.Request(method='GET', url='http://www.google.com')
reqs = [request for i in range(0, 5)] # An example list of requests
with BaseThrottler(name='base-throttler', reqs_over_time=(15, 60)) as bt:
throttled_requests = bt.multi_submit(reqs)
оба решения могут быть реализованы без использования with
о себе:
import requests
from requests_throttler import BaseThrottler
request = requests.Request(method='GET', url='http://www.google.com')
reqs = [request for i in range(0, 5)] # An example list of requests
bt = BaseThrottler(name='base-throttler', delay=1.5)
bt.start()
throttled_requests = bt.multi_submit(reqs)
bt.shutdown()
для более подробной информации: http://pythonhosted.org/RequestsThrottler/index.html
не похоже, что есть какой-либо простой механизм для обработки этой сборки в запросах или коде grequests. Единственный крючок, который, кажется, вокруг, - это ответы.
вот супер хакерская работа, чтобы, по крайней мере, доказать, что это возможно - я изменил grequests, чтобы сохранить список времени, когда был выдан запрос, и спать создание AsyncRequest, пока запросы в секунду не были ниже максимума.
class AsyncRequest(object):
def __init__(self, method, url, **kwargs):
print self,'init'
waiting=True
while waiting:
if len([x for x in q if x > time.time()-15]) < 8:
q.append(time.time())
waiting=False
else:
print self,'snoozing'
gevent.sleep(1)
вы можете использовать grequests.imap () для просмотра это интерактивно
import time
import rg
urls = [
'http://www.heroku.com',
'http://python-tablib.org',
'http://httpbin.org',
'http://python-requests.org',
'http://kennethreitz.com',
'http://www.cnn.com',
]
def print_url(r, *args, **kwargs):
print(r.url),time.time()
hook_dict=dict(response=print_url)
rs = (rg.get(u, hooks=hook_dict) for u in urls)
for r in rg.imap(rs):
print r
Я хотел было более элегантное решение, но пока я не могу найти. Осмотрелся в сеансах и адаптерах. Может быть, poolmanager может быть дополнен вместо этого?
кроме того, я бы не поставил этот код в производство - список " q " никогда не обрезается и в конечном итоге станет довольно большим. Кроме того, я не знаю, действительно ли это работает, как рекламируется. Это просто выглядит так, когда я смотрю на вывод консоли.
тьфу. Просто смотрю этот код я могу сказать, что это 3 утра. Время, чтобы перейти к постели.
у меня была похожая проблема. Вот мое решение. В вашем случае, я бы сделал:
def worker():
with rate_limit('slow.domain.com', 2):
response = requests.get('https://slow.domain.com/path')
text = response.text
# Use `text`
предполагая, что у вас есть несколько доменов, из которых вы отбираете, я бы настроил сопоставление словаря (domain, delay)
таким образом, вы не попали в свои пределы скорости.
этот код предполагает, что вы собираетесь использовать gevent и monkey patch.
from contextlib import contextmanager
from gevent.event import Event
from gevent.queue import Queue
from time import time
def rate_limit(resource, delay, _queues={}):
"""Delay use of `resource` until after `delay` seconds have passed.
Example usage:
def worker():
with rate_limit('foo.bar.com', 1):
response = requests.get('https://foo.bar.com/path')
text = response.text
# use `text`
This will serialize and delay requests from multiple workers for resource
'foo.bar.com' by 1 second.
"""
if resource not in _queues:
queue = Queue()
gevent.spawn(_watch, queue)
_queues[resource] = queue
return _resource_manager(_queues[resource], delay)
def _watch(queue):
"Watch `queue` and wake event listeners after delay."
last = 0
while True:
event, delay = queue.get()
now = time()
if (now - last) < delay:
gevent.sleep(delay - (now - last))
event.set() # Wake worker but keep control.
event.clear()
event.wait() # Yield control until woken.
last = time()
@contextmanager
def _resource_manager(queue, delay):
"`with` statement support for `rate_limit`."
event = Event()
queue.put((event, delay))
event.wait() # Wait for queue watcher to wake us.
yield
event.set() # Wake queue watcher.