簡體   English   中英

限制/限制GRequests中的HTTP請求速率

[英]Limiting/throttling the rate of HTTP requests in GRequests

我正在使用GRequests和lxml在Python 2.7.3中編寫一個小腳本,這將允許我從各個網站收集一些可收集的卡片價格並進行比較。 問題是其中一個網站限制了請求數量,如果我超過它,則發回HTTP錯誤429。

有沒有辦法在GRequestes中添加限制請求數量,這樣我就不會超過我指定的每秒請求數? 另外 - 如果發生HTTP 429,我怎樣才能使GRequestes在一段時間后重試?

在旁注 - 他們的限制是非常低的。 每15秒就有8個請求。 我在瀏覽器中多次破壞它只是刷新頁面等待價格變化。

要回答我自己的問題,因為我必須自己解決這個問題,而且似乎很少有關於此問題的信息。

這個想法如下。 與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__)

看看這個自動請求限制: 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的列表(參見文檔末尾的鏈接)。

然后,您可以訪問響應:

for tr in throttled_requests:
    print tr.response

或者,您可以通過指定在固定時間內發送的數量或請求(例如,每60秒15個請求)來實現相同的目標:

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代碼中的此內置。 似乎唯一的鈎子是響應。

這是一個超級hacky解決方案,至少證明它是可能的 - 我修改了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點。 是時候去睡覺了。

我遇到了類似的問題。 這是我的解決方案。 在你的情況下,我會這樣做:

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補丁。

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.

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM