我想同时从网站上抓取数据,但我发现以下程序不会同时执行。
async def return_soup(url):
r = requests.get(url)
r.encoding = "utf-8"
soup = BeautifulSoup(r.text, "html.parser")
future = asyncio.Future()
future.set_result(soup)
return future
async def parseURL_async(url):
print("Started to download {0}".format(url))
soup = await return_soup(url)
print("Finished downloading {0}".format(url))
return soup
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
t = [parseURL_async(url_1), parseURL_async(url_2)]
loop.run_until_complete(asyncio.gather(*t))
然而,该程序仅在第一个内容完成后才开始下载第二个内容。如果我的理解是正确的,await
上的await return_soup(url)
关键字等待函数完成,并在等待完成时,它将控件返回到事件循环,这使循环能够开始第二次下载。
一旦函数最终完成执行,其中的未来实例将获得结果值。
但为什么这不兼容呢?我在这里缺少什么?
答案 0 :(得分:2)
其他答案中提到的原因是缺乏对协程的库支持。
不过,从 Python 3.9 开始,您可以使用函数 to_thread 作为 I/O 并发的替代方法。
显然这并不完全等效,因为顾名思义,它在单独的线程中运行您的函数,而不是在事件循环中的单个线程中运行,但它可以是一种无需依赖适当的异步支持即可实现 I/O 并发的方法来自图书馆。
在您的示例中,代码为:
def return_soup(url):
r = requests.get(url)
r.encoding = "utf-8"
return BeautifulSoup(r.text, "html.parser")
def parseURL_async(url):
print("Started to download {0}".format(url))
soup = return_soup(url)
print("Finished downloading {0}".format(url))
return soup
async def main():
result_url_1, result_url_2 = await asyncio.gather(
asyncio.to_thread(lambda: parseURL_async(url_1)),
asyncio.to_thread(lambda: parseURL_async(url_2)),
)
asyncio.run(main())
答案 1 :(得分:1)
使用asyncio与使用线程不同,因为您无法将其添加到现有代码库以使其并发。具体而言,在asyncio事件循环中运行的代码不得阻止 - 所有阻塞调用必须替换为能够控制事件循环的非阻塞版本。在您的情况下,requests.get
会阻止并破坏asyncio实现的并行性。
要避免此问题,您需要使用以asyncio编写的http库,例如aiohttp
。
答案 2 :(得分:1)
我还会向用户4815162342的回复添加更多内容。 asyncio框架使用协同程序,它们在执行长操作时必须放弃对线程的控制。请参阅this section末尾的图表,以获得精美的图形表示。正如用户4815162342所述,请求库不支持asyncio。我知道有两种方法可以同时完成这项工作。首先,是执行user4815162342建议的操作并切换到具有异步请求的本机支持的库。第二种是在单独的线程或进程中运行此同步代码。由于run_in_executor
函数,后者很容易。
loop = asyncio.get_event_loop()
async def return_soup(url):
r = await loop.run_in_executor(None, requests.get, url)
r.encoding = "utf-8"
return BeautifulSoup(r.text, "html.parser")
async def parseURL_async(url):
print("Started to download {0}".format(url))
soup = await return_soup(url)
print("Finished downloading {0}".format(url))
return soup
t = [parseURL_async(url_1), parseURL_async(url_2)]
loop.run_until_complete(asyncio.gather(*t))
此解决方案消除了使用asyncio的一些好处,因为长操作仍可能从固定大小的线程池执行,但它也更容易开始。