首页 > 解决方案 > 在 Asyncio Web Scraping Application 中放置 BeautifulSoup 代码的位置

问题描述

我需要抓取并获取许多(每天 5-10k)新闻文章的正文段落的原始文本。我已经编写了一些线程代码,但考虑到这个项目的高度 I/O 绑定性质,我正在涉足asyncio. 下面的代码片段并不比 1 线程版本快,而且比我的线程版本差得多。谁能告诉我我做错了什么?谢谢!

async def fetch(session,url):
    async with session.get(url) as response:
        return await response.text()

async def scrape_urls(urls):
    results = []
    tasks = []
    async with aiohttp.ClientSession() as session:
        for url in urls:
            html = await fetch(session,url)
            soup = BeautifulSoup(html,'html.parser')
            body = soup.find('div', attrs={'class':'entry-content'})
            paras = [normalize('NFKD',para.get_text()) for para in body.find_all('p')]
            results.append(paras)
    return results

标签: pythonasynchronousbeautifulsouppython-asyncioaiohttp

解决方案


await意思是“等到结果准备好”,所以当你在每次循环迭代中等待获取时,你请求(并获取)顺序执行。要并行获取,您需要fetch使用类似的东西将每个任务生成到后台任务中asyncio.create_task(fetch(...))然后等待它们,类似于使用线程执行此操作的方式。或者更简单地说,您可以让asyncio.gather便利功能为您完成。例如(未经测试):

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

def parse(html):
    soup = BeautifulSoup(html,'html.parser')
    body = soup.find('div', attrs={'class':'entry-content'})
    return [normalize('NFKD',para.get_text())
            for para in body.find_all('p')]

async def fetch_and_parse(session, url):
    html = await fetch(session, url)
    paras = parse(html)
    return paras

async def scrape_urls(urls):
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(
            *(fetch_and_parse(session, url) for url in urls)
        )

如果你发现这仍然比多线程版本运行慢,可能是 HTML 的解析拖慢了 IO 相关的工作。(默认情况下,Asyncio 在单个线程中运行所有内容。)为了防止 CPU 绑定代码干扰 asyncio,您可以使用以下命令将解析移至单独的线程run_in_executor

async def fetch_and_parse(session, url):
    html = await fetch(session, url)
    loop = asyncio.get_event_loop()
    # run parse(html) in a separate thread, and
    # resume this coroutine when it completes
    paras = await loop.run_in_executor(None, parse, html)
    return paras

请注意,run_in_executor必须等待,因为当后台线程完成给定分配时,它会返回一个“唤醒”的可等待对象。由于此版本使用异步 IO 和线程进行解析,它的运行速度应该与您的线程版本一样快,但可以扩展到更大数量的并行下载。

最后,如果您希望解析实际并行运行,使用多个内核,您可以使用多处理来代替:

_pool = concurrent.futures.ProcessPoolExecutor()

async def fetch_and_parse(session, url):
    html = await fetch(session, url)
    loop = asyncio.get_event_loop()
    # run parse(html) in a separate process, and
    # resume this coroutine when it completes
    paras = await loop.run_in_executor(pool, parse, html)
    return paras

推荐阅读