【问题标题】:Best way of parallelising this webcrawling loop?并行化此网络爬虫循环的最佳方法?
【发布时间】:2016-11-20 17:54:40
【问题描述】:

我正在制作一个网络爬虫,并且我有一些“睡眠”功能可以使爬网时间很长。 现在我正在做:

for speciality in range(1,25):
    for year in range(1997, 2017):
        for quarter in [1,2]:
            deal_with (driver, year, quarter, speciality, ok)

deal_with 函数正在打开多个网页,等待几秒钟以完成 html 下载,然后再继续。执行时间很长:有 25 * 10 * 2 = 500 个循环,一个循环不少于一分钟。

我想使用我的 4 个物理核心(8 个线程)来享受并行性。 我阅读了有关龙卷风、多处理、joblib 的信息……但我真的无法想出一个简单的解决方案来适应我的代码。

欢迎任何见解:-)

【问题讨论】:

  • 您实际上并不需要很多 CPU 来处理这种工作负载,因为 Web 抓取主要受 I/O 限制。尝试使用 Tornado
  • 好吧,他可能正在做文本挖掘,这可能会受到 CPU 的限制。
  • 现在我的解决方案是打开几个笔记本,每个笔记本都处理专业范围的一个子集...
  • 最简单的可能是使用thread 模块同时下载多个网页(假设您的网络连接有带宽)。
  • 你在哪里睡觉?在deal_with 方法里面?移动到一个循环的解决方案会有帮助吗?

标签: python multiprocessing


【解决方案1】:

tl;dr 在不完全了解您面临的瓶颈的情况下投资任何选择都无济于事。

归根结底,只有两种基本方法可以像这样扩展任务:

多处理

您启动了多个 Python 进程,并将任务分配给每个进程。这是您认为现在可以帮助您的方法。

一些示例代码说明其工作原理,但您可以使用任何适当的包装器:

import multiprocessing

# general rule of thumb: launch twice as many processes as cores

process_pool = multiprocessing.Pool(8) # launches 8 processes

# generate a list of all inputs you wish to feed to this pool

inputs = []

for speciality in range(1,25):
    for year in range(1997, 2017):
        for quarter in [1,2]:
            inputs.append((driver, year, quarter, speciality, ok))

# feed your list of inputs to your process_pool and print it when done
print(process_pool.map(deal_with, inputs))

如果这就是你想要的,你现在可以停止阅读。

异步执行

在这里,您对单个线程或进程感到满意,但您不希望它闲置等待诸如网络读取或磁盘搜索之类的东西回来 - 您希望它继续执行其他更多操作等待中的重要事项。

真正的原生异步 I/O 支持在 Python 3 中提供,但在 Twisted 网络库之外的 Python 2.7 中不存在。

import concurrent.futures

# generate a list of all inputs you wish to feed to this pool

inputs = []

for speciality in range(1,25):
    for year in range(1997, 2017):
        for quarter in [1,2]:
            inputs.append((driver, year, quarter, speciality, ok))

# produce a pool of processes, and make sure they don't block each other
# - get back an object representing something yet to be resolved, that will
# only be updated when data comes in.

with concurrent.futures.ProcessPoolExecutor() as executor:
    outputs = [executor.submit(input_tuple) for input_tuple in inputs]

    # wait for all of them to finish - not ideal, since it defeats the purpose
    # in production, but sufficient for an example

    for future_object in concurrent.futures.as_completed(outputs):
         # do something with future_object.result()

那么有什么区别呢?

我的主要观点是强调从技术列表中进行选择并不像找出真正的瓶颈在哪里那么难。

在上面的示例中,没有任何区别。两者都遵循一个简单的模式:

  1. 有很多工人
  2. 允许这些工作人员立即从任务队列中挑选一些东西
  3. 当一个人有空时,让他们立即处理下一个人。

因此,如果您逐字逐句地按照这些示例进行操作,您将不会完全获得概念上的差异,即使它们使用完全不同的技术并声称使用完全不同的技术。

如果您以这种模式编写,您选择的任何技术都将是徒劳的 - 即使您会获得一些加速,但如果您期望大幅提升性能,您将会非常失望。

为什么这种模式不好?因为它不能解决你的问题。

你的问题很简单:你有等待。当你的进程在等待某些东西回来时,它不能做任何其他事情!它不能为您调用更多页面。它无法处理传入的任务。它所能做的就是等待。

拥有更多最终等待的进程并不是真正的解决方案。如果你将一支必须进军滑铁卢的军队分成几个团,它不会更快——每个团最终都必须睡觉,尽管他们可能会在不同的时间和不同的时间睡觉,而将会发生的事情是他们所有人都会几乎在同一时间到达。

需要的是一支不眠不休的军队。

那么你应该怎么做?

所有 I/O 绑定任务抽象为非阻塞。这是你真正的瓶颈。如果您正在等待网络响应,请不要让糟糕的进程坐在那里 - 让它有事可做。

您的任务有些困难,因为默认情况下从套接字读取是阻塞的。这就是操作系统的方式。幸运的是,您不需要 需要 Python 3 来解决它(尽管这始终是首选解决方案) - Python 2.7 中已经存在 asyncore 库 (though Twisted is comparably superior in every way) 来进行网络读取并在后台真正写入。

只有一种情况需要在 Python 中使用真正的多处理,那就是您正在执行 CPU 密集型或 CPU 密集型工作。从你的描述来看,好像不是这样的。

简而言之,您应该编辑您的deal_with 函数以避免初期等待。如果需要,使用来自 Twisted 或 asyncore 的适当抽象,在后台等待。但是不要让它完全消耗你的过程。

【讨论】:

    【解决方案2】:

    如果您使用的是 python3,我会查看 asycio module。我相信你可以用@asyncio.coroutine 装饰deal_with。您可能还需要调整 deal_with 所做的工作以正确处理事件循环。

    【讨论】:

    • 我不得不说我在 python 2 中:-/
    • 你可以随时port你的代码到3。
    猜你喜欢
    • 1970-01-01
    • 2011-03-09
    • 2013-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-16
    • 2011-12-11
    相关资源
    最近更新 更多