使用工作人员可以向

时间:2015-06-15 04:15:25

标签: python concurrency multiprocessing python-multiprocessing

我对python很新(我主要用Java编写代码)。我有一个python脚本,它本质上是一个爬虫。它调用phantomjs,它加载页面,返回它的源,以及它在页面中找到的URL列表。

我一直在尝试使用Python 3的multiprocessing模块来执行此操作,但我无法弄清楚如何使用工作人员也可以添加的共享队列。我不断得到不可预知的结果。

我以前的方法使用了一个全局URL列表,我从中提取了一个块并使用map_async发送给工作人员。最后,我将收集所有返回的URL并将它们附加到全局列表中。问题是每个" chunk"需要与最慢的工人一样长。我试图对其进行修改,以便每当工作完成时,它都可以获取下一个URL。但是,我不认为我正确地做到了。这就是我到目前为止所拥有的:

def worker(url, urls):
    print(multiprocessing.current_process().name + "." + str(multiprocessing.current_process().pid) + " loading " + url)
    returned_urls = phantomjs(url)
    print(multiprocessing.current_process().name + "." + str(multiprocessing.current_process().pid) + " returning " + str(len(returned_urls)) + " URLs")

    for returned_url in returned_urls:
        urls.put(returned_url, block=True)

    print("There are " + str(urls.qsize()) + " URLs in total.\n")

if __name__ == '__main__':    
    manager = multiprocessing.Manager()
    urls = manager.Queue()
    urls.append(<some-url>)

    pool = Pool()
    while True:
        url = urls.get(block=True)
        pool.apply_async(worker, (url, urls))

    pool.close()
    pool.join()

如果有更好的方法,请告诉我。我正在抓取已知网站,最终终止条件是没有要处理的网址。但是现在看起来我将永远保持奔跑状态。我不确定是否会使用queue.empty(),因为它确实说它不可靠。

2 个答案:

答案 0 :(得分:0)

这就是我解决问题的方法。我最初使用this answer中发布的设计,但bj0提到它滥用了初始化函数。所以我决定使用apply_async,以类似于我的问题中发布的代码的方式进行。

由于我的工作人员修改了队列,他们正在读取(他们添加到它)的URL,我认为我可以像这样简单地运行我的循环:

while not urls.empty():
   pool.apply_async(worker, (urls.get(), urls))

我预计这会有效,因为工作人员会添加到队列中,如果所有工作人员都忙,apply_async会等待。 这没有按照我的预期工作,并且循环提前终止。问题是,如果所有工人都很忙,apply_async 不会阻止,这一点并不明确。相反,它会排队提交的任务,这意味着urls最终将变为空,循环将终止。当您尝试执行urls.get()时,如果队列为空,则循环阻止的唯一时间。此时,它将等待更多项目在队列中可用。但我仍然需要找出终止循环的方法。条件是当没有工作人员返回新URL时,循环应该终止。为此,我使用一个共享的dict,如果进程没有返回任何URL,则将与process-name关联的值设置为0,否则为1。我在循环的每次迭代中检查键的总和,如果它是0,我知道我已经完成了。

基本结构最终是这样的:

def worker(url, url_queue, proc_user_urls_queue, proc_empty_urls_queue):

    returned_urls = phantomjs(url) # calls phantomjs and waits for output
    if len(returned_urls) > 0:
        proc_empty_urls_queue.put(
            [multiprocessing.current_process().name, 1]
        )
    else:
        proc_empty_urls_queue.put(
            [multiprocessing.current_process().name, 0]
        )

    for returned_url in returned_urls:
        url_queue.put(returned_url)

def empty_url_tallier(proc_empty_urls_queue, proc_empty_urls_dict):
    while 1:
        # This may not be necessary. I don't know if this worker is run
        # by the same process every time. If not, it is possible that
        # the worker was assigned the task of fetching URLs, and returned
        # some. So let's make sure that we set its entry to zero anyway.
        # If this worker is run by the same process every time, then this
        # stuff is not necessary.
        id = multiprocessing.current_process().name
        proc_empty_urls_dict[id] = 0

        proc_empty_urls = proc_empty_urls_queue.get()
        if proc_empty_urls == "done": # poison pill
            break

        proc_id = proc_empty_urls[0]
        proc_empty_url = proc_empty_urls[1]
        proc_empty_urls_dict[proc_id] = proc_empty_url

manager = Manager()

urls = manager.Queue()
proc_empty_urls_queue = manager.Queue()
proc_empty_urls_dict = manager.dict()

pool = Pool(33)

pool.apply_async(writer, (proc_user_urls_queue,))
pool.apply_async(empty_url_tallier, (proc_empty_urls_queue, proc_empty_urls_dict))

# Run the first apply synchronously 
urls.put("<some-url>")
pool.apply(worker, (urls.get(), urls, proc_empty_urls_queue))
while sum(proc_empty_urls_dict.values()) > 0:
    pool.apply_async(worker, (urls.get(), urls, proc_empty_urls_queue))

proc_empty_urls_queue.put("done") # poison pill
pool.close()
pool.join()

答案 1 :(得分:0)

以下是我可能会做的事情:

def worker(url, urls):
    print(multiprocessing.current_process().name + "." + str(multiprocessing.current_process().pid) + " loading " + url)
    returned_urls = phantomjs(url)
    print(multiprocessing.current_process().name + "." + str(multiprocessing.current_process().pid) + " returning " + str(len(returned_urls)) + " URLs")

      for returned_url in returned_urls:
          urls.put(returned_url, block=True)

      # signal finished processing this url
      urls.put('no-url')

    print("There are " + str(urls.qsize()) + " URLs in total.\n")

if __name__ == '__main__':    
    manager = multiprocessing.Manager()
    pool = Pool()
    urls = manager.Queue()

    # start first url before entering loop
    counter = 1
    pool.apply_async(worker, (<some-url>, urls))

    while counter > 0:
        url = urls.get(block=True)
        if url == 'no-url':
            # a url has finished processing
            counter -= 1
        else:
            # a new url needs to be processed
            counter += 1
            pool.apply_async(worker, (url, urls))

    pool.close()
    pool.join()

每当从队列中弹出一个url时,递增计数器。将其视为“当前正在处理的URL”计数器。当从队列中弹出“no-url”时,“当前正在处理的URL”已经完成,因此递减计数器。只要计数器大于0,就会有一些网址没有完成处理并返回'no-url'。

修改

正如我在评论中所说的那样(在这里为其他任何人阅读),在使用multiprocessing.Pool时,而不是将其视为单个进程,最好将其视为执行的单个结构每次获取数据时的功能(在可能的情况下同时执行)。这对于数据驱动的问题非常有用,在这些问题中,您不会仅跟踪或关注正在处理的数据的单个工作进程。