【问题标题】:How to speed up requests module while scraping?如何在抓取时加速请求模块?
【发布时间】:2018-10-02 22:57:30
【问题描述】:

首先,我问这个问题不是因为我找不到答案,而是因为我无法理解我找到的答案。

人们很容易回答一个问题,认为“我回答了你的问题,如果你不明白那是你自己的错”,所以现在我需要一些帮助来理解,或者只是简化过程。

我有一个使用 pythons requests 模块访问的大约 300,000 个 url 的列表。获取/加载 url 所花费的时间非常痛苦,我认为这是因为 url 上的内容量很大。每个请求我可能需要 15-20 秒。我正在想办法大大减少这段时间。

我的第一个想法是我是否可以禁用/过滤掉图像以及我提前知道我不需要使用请求的任何其他内容。我不确定如何实现,或者是否可以做到。

我的第二个想法是发送“批量请求”,这在我看来就像同时发送多个请求。我真的不确定这是否真的更快,我无法得到我的请求的准确响应,因为我无法让我的代码工作。我的假设是我可以一次性发送 X 个请求,获得 X 个响应作为回报,然后单独处理每个请求。下面是我尝试使用的解决方案。

def getpage(list_urls):
    for url in list_urls:
        r = requests.get(url)
        dostuffwithresponse()

for file in list_files:
    list_links = open(file).readlines()
    pool = multiprocessing.Pool(processes = 10)
    pool_outputs = pool.map(getpage(), list_links)
    pool.close()
    pool.join()
    print('*')
    print(pool_outputs)

在减少我的响应大小(如果可能)和发送多个请求之间。我的目标是将 15 秒以上的等待时间缩短到 5 秒及以下(或尽可能好)。

有人对更简单、更直接的方法有好的建议吗?

【问题讨论】:

  • 您对多处理方法的想法是正确的。但是,您仍然会受到使用多处理的进程数的任意限制。你研究过 asyncio 和 aiohttp 吗?它肯定会快得多(使用多处理可以进一步加快速度)
  • 我完全同意@NinjaKitty。例如,您可以查看此链接 pawelmhm.github.io/asyncio/python/aiohttp/2016/04/22/…,其中包含有关如何使用的非常详细的说明。

标签: python multithreading web-scraping python-requests


【解决方案1】:

@OleksandrDashkov 提供了一个非常有用的指南的链接,以便能够使用 aiohttpasyncio 相当有效地发送数百万个请求

我将尝试将这些信息浓缩为可以帮助您解决问题的内容。

我强烈建议您查看asyncio 文档和其他博客文章,以便在使用它进行编程之前对它有一个很好的了解(或者阅读代码并尝试了解它在做什么)。

我们将从aiohttp 中的基本提取工作原理开始。和requests很像。

import asyncio

import aiohttp

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            dostuffwithresponse()  # To mimic your code.

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

# If you're on Python 3.7 :o
asyncio.run(main())

相当简单。如果您使用请求的session 对象,除了async 语法之外,它应该几乎相同。

现在,我们想要获取大量 URL。我们也不希望每次都重新创建会话对象。

async def fetch(session, url):
    async with session.get(url) as response:
        dostuffwithresponse()

async def main():
    async with aiohttp.ClientSession() as session:
        for file in list_files:
            for link in open(file).readlines():
                await fetch(session, url)

现在我们正在获取所有 URL。它仍然是相同的行为,仍然是同步的,因为我们正在等待 fetch() 完成,然后再转到下一个链接。

async def fetch(session, url):
    ...

async def main():
    tasks = []
    async with aiohttp.ClientSession() as session:
        for file in list_files:
            for link in open(file).readlines():
                task = asyncio.ensure_future(fetch(session, url))
                tasks.append(fut)
        results = await asyncio.gather(*tasks)
    # results is a list of everything that returned from fetch().
    # do whatever you need to do with the results of your fetch function

在这里,我建议您尝试了解asyncio.ensure_future()asyncio.gather() 的作用。 Python 3.7 有一个新的修改文档,并且有很多关于这个的博客文章。

最后,您不能同时获取 300,000 个链接。你的操作系统很可能会给你错误,告诉你如何无法打开那么多文件描述符或类似的东西。

因此,您可以通过使用信号量来解决这个问题。对于这种情况,您需要使用asyncio.Semaphore(max_size)asyncio.BoundedSemaphore(max_size)

async def fetch(session, url):
    ...

async def bounded_fetch(sem, url, session):
    async with sem:
        await fetch(url, session)

async def main():
    tasks = []
    sem = asyncio.Semaphore(1000)  # Generally, most OS's don't allow you to make more than 1024 sockets unless you personally fine-tuned your system. 
    async with aiohttp.ClientSession() as session:
        for file in list_files:
            for link in open(file).readlines():
                # Notice that I use bounded_fetch() now instead of fetch()
                task = asyncio.ensure_future(bounded_fetch(sem, session, url))
                tasks.append(fut)
        results = await asyncio.gather(*tasks)
    # do whatever you need to do with the results of your fetch function

为什么这一切都更快?

因此,asyncio 的工作原理主要是当您向 Web 服务器发送请求时,您不想浪费时间等待响应。相反,当响应到达时,会创建一个事件来告诉事件循环。在等待 1 个响应发生时,您继续发出另一个请求(也就是向事件循环询问下一个任务),然后继续。

我绝对不是最擅长解释这一切的,但我希望这能帮助您基本了解如何加快网页抓取速度。祝你好运!

编辑:回顾一下,您可能必须添加 asyncio.sleep() 才能在循环时实际开始。但是这段代码也使用了open().readlines(),这可能会阻塞事件循环。

【讨论】:

    【解决方案2】:

    发送一堆异步请求是要走的路。正如@NinjaKitty 提到的,您可以考虑使用aiohttp。我最近不得不做一些类似的事情,发现使用requests_futures 对我来说更容易。您可以设置一个循环来发出 N 个异步请求,每个请求都有一个回调函数。然后等待所有N完成,继续下一个N。

    【讨论】:

      猜你喜欢
      • 2020-04-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-07
      • 1970-01-01
      • 1970-01-01
      • 2018-08-04
      相关资源
      最近更新 更多