多个进程之间的速率限制下载

时间:2019-03-21 13:38:35

标签: python python-multiprocessing multiprocessing-manager

我想从网站上下载并处理很多文件。网站的服务条款限制了每秒允许您下载的文件数量。

处理文件所需的时间实际上是瓶颈,因此我希望能够并行处理多个文件。但是我不希望将不同的过程结合起来违反下载限制。因此,我需要一些可以限制请求率过高的内容。我在想类似以下的内容,但是我并不是multiprocessing模块的专家。

import multiprocessing
from multiprocessing.managers import BaseManager
import time

class DownloadLimiter(object):

    def __init__(self, time):
        self.time = time
        self.lock = multiprocessing.Lock()

    def get(self, url):
        self.lock.acquire()
        time.sleep(self.time)
        self.lock.release()
        return url


class DownloadManager(BaseManager):
    pass

DownloadManager.register('downloader', DownloadLimiter)


class Worker(multiprocessing.Process):

    def __init__(self, downloader, queue, file_name):
        super().__init__()
        self.downloader = downloader
        self.file_name = file_name
        self.queue = queue

    def run(self):
        while not self.queue.empty():
            url = self.queue.get()
            content = self.downloader.get(url)
            with open(self.file_name, "a+") as fh:
                fh.write(str(content) + "\n")

然后在其他地方使用

manager = DownloadManager()
manager.start()
downloader = manager.downloader(0.5)
queue = multiprocessing.Queue()

urls = range(50)
for url in urls:
    queue.put(url)

job1 = Worker(downloader, queue, r"foo.txt")
job2 = Worker(downloader, queue, r"bar.txt")
jobs = [job1, job2]

for job in jobs:
    job.start()

for job in jobs:
    job.join()

这似乎在小范围内完成了这项工作,但是我对是否真的正确完成了锁定有些谨慎。

此外,如果有更好的模式可以实现相同的目标,我很想听听。

5 个答案:

答案 0 :(得分:1)

这可以通过Ray干净地完成,enter image description here是用于并行和分布式Python的库。

Ray中的资源

启动Ray时,可以告诉它该计算机上有哪些资源。 Ray会自动尝试确定CPU内核的数量和GPU的数量,但是可以指定这些内核,实际上,也可以传入任意用户定义的资源,例如,通过调用< / p>

ray.init(num_cpus=4, resources={'Network': 2})

这告诉Ray,该计算机具有4个CPU内核和2个用户定义的资源,称为Network

每个Ray“任务”都是可调度的工作单元,具有一定的资源需求。默认情况下,一项任务将需要1个CPU内核,而不需要其他任何内核。但是,可以通过使用以下命令声明相应的功能来指定任意资源要求:

@ray.remote(resources={'Network': 1})
def f():
    pass

这告诉Ray,为了使f在“工人”进程上执行,必须有1个CPU内核(默认值)和1个Network资源可用。

由于该机器具有Network资源中的2个和4个CPU内核,因此最多可以同时执行f的2个副本。另一方面,如果还有另一个用{p>声明的函数g

@ray.remote
def g():
    pass

然后可以同时执行四份g,或者可以同时执行两份f和两份g

示例

这里是一个示例,其中包含用于下载内容和处理内容的实际功能的占位符。

import ray
import time

max_concurrent_downloads = 2

ray.init(num_cpus=4, resources={'Network': max_concurrent_downloads})

@ray.remote(resources={'Network': 1})
def download_content(url):
    # Download the file.
    time.sleep(1)
    return 'result from ' + url

@ray.remote
def process_result(result):
    # Process the result.
    time.sleep(1)
    return 'processed ' + result

urls = ['url1', 'url2', 'url3', 'url4']

result_ids = [download_content.remote(url) for url in urls]

processed_ids = [process_result.remote(result_id) for result_id in result_ids]

# Wait until the tasks have finished and retrieve the results.
processed_results = ray.get(processed_ids)

以下是时间轴描述(您可以通过在命令行中运行ray timeline并在Chrome网络浏览器中以chrome:// tracing打开生成的JSON文件来生成)。

在上面的脚本中,我们提交了4个download_content任务。这些是我们通过指定它们需要Network资源(除了默认的1 CPU资源)来进行等级限制的对象。然后,我们提交4个process_result任务,每个任务都需要默认的1个CPU资源。任务分三个阶段执行(只需查看蓝色框)。

  1. 我们首先执行2个download_content任务,这一次可以执行的任务数量最多(由于速率限制)。我们还无法执行任何process_result任务,因为它们取决于download_content任务的输出。
  2. 完成这些任务后,我们开始执行其余两个download_content任务以及两个process_result任务,因为我们没有对process_result任务进行速率限制。
  3. 我们执行其余process_result个任务。

每个“行”都是一个工作进程。时间从左到右。

Ray documentation

您可以在go modules上详细了解如何执行此操作。

答案 1 :(得分:0)

有一个完全满足您需求的库,名为ratelimit

主页上的示例

此功能将无法在15分钟的时间内进行15次以上的API调用。

from ratelimit import limits

import requests

FIFTEEN_MINUTES = 900

@limits(calls=15, period=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

顺便说一句,在I / O密集型任务(例如Web爬网)中,可以使用多线程而不是多处理。使用多重处理时,您必须创建另一个控制过程,并编排所有工作。在多线程方法的情况下,所有线程本质上都将有权访问主进程内存,因此发信号变得更加容易(因为e在线程之间共享)

import logging
import threading
import time

logging.basicConfig(level=logging.DEBUG,
                    format='(%(threadName)-10s) %(message)s',
                    )

def wait_for_event(e):
    """Wait for the event to be set before doing anything"""
    logging.debug('wait_for_event starting')
    event_is_set = e.wait()
    logging.debug('event set: %s', event_is_set)

def wait_for_event_timeout(e, t):
    """Wait t seconds and then timeout"""
    while not e.isSet():
        logging.debug('wait_for_event_timeout starting')
        event_is_set = e.wait(t)
        logging.debug('event set: %s', event_is_set)
        if event_is_set:
            logging.debug('processing event')
        else:
            logging.debug('doing other work')


e = threading.Event()
t1 = threading.Thread(name='block', 
                      target=wait_for_event,
                      args=(e,))
t1.start()

t2 = threading.Thread(name='non-block', 
                      target=wait_for_event_timeout, 
                      args=(e, 2))
t2.start()

logging.debug('Waiting before calling Event.set()')
time.sleep(3)
e.set()
logging.debug('Event is set')

答案 2 :(得分:0)

最简单的方法是在主线程上下载文件并将其馈送到工作池。

在我自己的实现中,我走了使用celery处理文档和使用gevent下载的路线。这样做只会增加复杂性。

这是一个简单的例子。

import multiprocessing
from multiprocessing import Pool
import time
import typing

def work(doc: str) -> str:
    # do some processing here....
    return doc + " processed"

def download(url: str) -> str:
    return url  # a hack for demo, use e.g. `requests.get()`

def run_pipeline(
    urls: typing.List[str],
    session_request_limit: int = 10,
    session_length: int = 60,
) -> None:
    """
    Download and process each url in `urls` at a max. rate limit
    given by `session_request_limit / session_length`
    """
    workers = Pool(multiprocessing.cpu_count())
    results = []

    n_requests = 0
    session_start = time.time()

    for url in urls:
        doc = download(url)
        results.append(
            workers.apply_async(work, (doc,))
        )
        n_requests += 1

        if n_requests >= session_request_limit:
            time_to_next_session = session_length - time.time() - session_start
            time.sleep(time_to_next_session)

        if time.time() - session_start >= session_length:
            session_start = time.time()
            n_requests = 0

    # Collect results
    for result in results:
        print(result.get())

if __name__ == "__main__":
    urls = ["www.google.com", "www.stackoverflow.com"]
    run_pipeline(urls)

答案 3 :(得分:0)

目前尚不清楚您在“速率限制下载”下的含义。在这种情况下,有很多并发下载,这是一个经常使用的情况,我认为简单的解决方案是将信号量与进程池一起使用:

#!/usr/bin/env python3
import os
import time
import random
from functools import partial
from multiprocessing import Pool, Manager


CPU_NUM = 4
CONCURRENT_DOWNLOADS = 2


def download(url, semaphore):
    pid = os.getpid()

    with semaphore:
        print('Process {p} is downloading from {u}'.format(p=pid, u=url))
        time.sleep(random.randint(1, 5))

    # Process the obtained resource:
    time.sleep(random.randint(1, 5))

    return 'Successfully processed {}'.format(url)


def main():
    manager = Manager()

    semaphore = manager.Semaphore(CONCURRENT_DOWNLOADS)
    target = partial(download, semaphore=semaphore)

    urls = ['https://link/to/resource/{i}'.format(i=i) for i in range(10)]

    with Pool(processes=CPU_NUM) as pool:
        results = pool.map(target, urls)

    print(results)


if __name__ == '__main__':
    main()

如您所见,一次只有CONCURRENT_DONWLOADS个进程正在下载,而其他进程正忙于处理获得的资源。

答案 4 :(得分:0)

好的,在OP中进行了以下澄清之后

  

“每秒下载”是指在全球范围内,每秒开始下载的次数不超过

我决定发布另一个答案,因为我认为第一个答案也可能对希望限制多个并发运行进程的人很感兴趣。

我认为没有必要使用其他框架来解决此问题。想法是使用为每个资源链接,资源队列和固定数量的进程(而不是线程)处理工作者生成的下载线程:

#!/usr/bin/env python3
import os
import time
import random
from threading import Thread
from multiprocessing import Process, JoinableQueue


WORKERS = 4
DOWNLOADS_PER_SECOND = 2


def download_resource(url, resource_queue):
    pid = os.getpid()

    t = time.strftime('%H:%M:%S')
    print('Thread {p} is downloading from {u} ({t})'.format(p=pid, u=url, t=t),
          flush=True)
    time.sleep(random.randint(1, 10))

    results = '[resource {}]'.format(url)
    resource_queue.put(results)


def process_resource(resource_queue):
    pid = os.getpid()

    while True:
        res = resource_queue.get()

        print('Process {p} is processing {r}'.format(p=pid, r=res),
              flush=True)
        time.sleep(random.randint(1, 10))

        resource_queue.task_done()


def main():
    resource_queue = JoinableQueue()

    # Start process workers:
    for _ in range(WORKERS):
        worker = Process(target=process_resource,
                         args=(resource_queue,),
                         daemon=True)
        worker.start()

    urls = ['https://link/to/resource/{i}'.format(i=i) for i in range(10)]

    while urls:
        target_urls = urls[:DOWNLOADS_PER_SECOND]
        urls = urls[DOWNLOADS_PER_SECOND:]

        # Start downloader threads:
        for url in target_urls:
            downloader = Thread(target=download_resource,
                                args=(url, resource_queue),
                                daemon=True)
            downloader.start()

        time.sleep(1)

    resource_queue.join()


if __name__ == '__main__':
    main()

结果看起来像这样:

$ ./limit_download_rate.py
Thread 32482 is downloading from https://link/to/resource/0 (10:14:08)
Thread 32482 is downloading from https://link/to/resource/1 (10:14:08)
Thread 32482 is downloading from https://link/to/resource/2 (10:14:09)
Thread 32482 is downloading from https://link/to/resource/3 (10:14:09)
Thread 32482 is downloading from https://link/to/resource/4 (10:14:10)
Thread 32482 is downloading from https://link/to/resource/5 (10:14:10)
Process 32483 is processing [resource https://link/to/resource/2]
Process 32484 is processing [resource https://link/to/resource/0]
Thread 32482 is downloading from https://link/to/resource/6 (10:14:11)
Thread 32482 is downloading from https://link/to/resource/7 (10:14:11)
Process 32485 is processing [resource https://link/to/resource/1]
Process 32486 is processing [resource https://link/to/resource/3]
Thread 32482 is downloading from https://link/to/resource/8 (10:14:12)
Thread 32482 is downloading from https://link/to/resource/9 (10:14:12)
Process 32484 is processing [resource https://link/to/resource/6]
Process 32485 is processing [resource https://link/to/resource/9]
Process 32483 is processing [resource https://link/to/resource/8]
Process 32486 is processing [resource https://link/to/resource/4]
Process 32485 is processing [resource https://link/to/resource/7]
Process 32483 is processing [resource https://link/to/resource/5]

这里,每隔DOWNLOADS_PER_SECOND个线程都在启动,在本示例中为两个,然后将其下载并将资源放入队列中。 WORKERS是许多从队列中获取资源以进行进一步处理的进程。使用此设置,您将能够限制每秒开始的下载数量,并使工作人员并行处理获得的资源。

相关问题