Ограничение / регулирование скорости HTTP-запросов в GRequests

Я пишу небольшой скрипт на Python 2.7.3 с GRequests и lxml, который позволит мне собрать некоторые цены коллекционных карт с разных сайтов и сравнить их. Проблема в том, что один из веб-сайтов ограничивает количество запросов и отправляет обратно ошибку HTTP 429, если я превышаю ее.

Есть ли способ добавить регулирование количества запросов в GRequestes, чтобы я не превышал указанное мной количество запросов в секунду? Также - как я могу заставить GRequestes повторить попытку через некоторое время, если происходит HTTP 429?

Кстати, их лимит смехотворно низок. Примерно 8 запросов за 15 секунд. Я несколько раз нарушал его в своем браузере, просто обновляя страницу в ожидании изменения цен.


person Bartłomiej Siwek    schedule 27.11.2013    source источник
comment
вы регистрируете несколько URL-адресов на своем сайте с помощью grequests? Если это так, было бы лучше сделать запросы для их сайта синхронно, чтобы вы могли легко контролировать повторные попытки.   -  person dm03514    schedule 27.11.2013
comment
Согласен с @ dm03514 - для этого вам не нужны греки. Я недавно прочитал алгоритм, который оптимизирует решение такого рода проблем.   -  person Wayne Werner    schedule 27.11.2013
comment
Я пытаюсь получить доступ к нескольким URL-адресам с одного и того же сайта одновременно, потому что я хочу одновременно отслеживать стоимость нескольких карт.   -  person Bartłomiej Siwek    schedule 27.11.2013
comment
@WayneWerner Звучит интересно. Не могли бы вы поделиться своим синхронным подходом?   -  person Bartłomiej Siwek    schedule 27.11.2013


Ответы (5)


Собираюсь ответить на свой вопрос, так как я должен был понять это сам, и, похоже, очень мало информации об этом.

Идея в следующем. Каждый объект запроса, используемый с GRequests, может принимать объект сеанса в качестве параметра при создании. С другой стороны, у объектов сеанса могут быть смонтированы адаптеры HTTP, которые используются при выполнении запросов. Создав собственный адаптер, мы можем перехватывать запросы и ограничивать их скорость так, как мы сочтем нужным для нашего приложения. В моем случае я получил код ниже.

Объект, используемый для дросселирования:

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__)
person Bartłomiej Siwek    schedule 04.12.2013

Взгляните на это для автоматического регулирования запросов: 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.

person se7entyse7en    schedule 05.02.2014

Не похоже, что существует какой-либо простой механизм для обработки этой сборки в коде запросов или 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

Хотелось бы найти более элегантное решение, но пока не могу его найти. Посмотрел в сессиях и переходниках. Может быть, вместо этого можно было бы увеличить пулменеджер?

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

Фу. Просто взглянув на этот код, я могу сказать, что сейчас 3 часа ночи. Время ложиться спать.

person synthesizerpatel    schedule 28.11.2013

У меня была аналогичная проблема. Вот мое решение. В вашем случае я бы сделал:

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.
person GrantJ    schedule 17.11.2015

Те, кто хочет лучше контролировать регулирование / ограничение скорости, могут использовать следующую библиотеку Python: ratelimit 2.2.1: https://pypi.org/project/ratelimit/

ratelimit 2.2.1: в этих пакетах представлен декоратор функций, предотвращающий вызов функции чаще, чем это разрешено поставщиком API. Это должно помешать поставщикам API блокировать ваши приложения в соответствии с их лимитами скорости.

от ratelimit лимитов импорта

запросы на импорт

FIFTEEN_MINUTES = 900

@limits (звонки = 15, период = FIFTEEN_MINUTES) def call_api (url): response = requests.get (url)

if response.status_code != 200:
    raise Exception('API response: {}'.format(response.status_code))
return response
person user8361858    schedule 07.07.2021