Как исправить BeautifulSoup, нарушающий асинхронное выполнение asyncio и aiohttp? - PullRequest
0 голосов
/ 19 сентября 2019

Я использую asyncio с run_until_complete и asyncio.gather для асинхронного выполнения запросов к нескольким URL-адресам.Для ограничения общего запроса я также использую semaphore.Я проверяю результаты результата aiohttp GET-запроса затем с помощью BeautifulSoup.

Вот код:

async def fetch(session, url):
    try:
        async with session.get(url) as response:
            return await response.text()
    except Exception as e:
        print(url, str(e))
        return False

async def validate_page(session, url):
    res = await fetch(session, url)
    if res:
        try:
            soup = BeautifulSoup(res, 'lxml')
        except Exception:
            print(traceback.format_exc())
            return False


async def validate_page_bounded(sem, session, url):
    async with sem:
        return await validate_page(session, url)

.... 

Странно, пока я закомментируюстрока

soup = BeautifulSoup(res, 'lxml')

, код работает без сбоев и выполняет GET-запросы асинхронно.Но как только он не комментируется, кажется, что aiohttp просто отправляет GET-запросы один за другим, ожидая завершения каждого запроса.

Есть идеи почему?

1 Ответ

1 голос
/ 19 сентября 2019

Но как только [BeautifulSoup] не закомментирован, создается впечатление, что aiohttp просто отправляет GET-запросы один за другим, ожидая завершения каждого запроса.Любая идея почему?

Потому что asyncio является однопоточным.Если анализ BeautifulSoup требует много ресурсов ЦП, это время нельзя использовать для обслуживания HTTP-запросов и ответов, поэтому ваши загрузки будут эффективно сериализованы или, по крайней мере, строго ограничены.К счастью, есть простой способ исправить это, просто замените:

soup = BeautifulSoup(res, 'lxml')

на:

loop = asyncio.get_event_loop()
soup = loop.run_in_executor(None, BeautifulSoup, res, 'lxml')

Это будет указывать asyncio отправлять анализатор в отдельный пул потоков, позволяя другимсопрограммы продолжаются, пока BeautifulSoup работает, и возобновляют текущую сопрограмму, как только это будет сделано.

С помощью небольшой дополнительной настройки вы можете расширить этот подход для использования многопроцессорной обработки и фактически использовать преимущества нескольких ядер:

from concurrent.futures import ProcessPoolExecutor

executor = ProcessPoolExecutor(8)

# Place the actual validation in a sync function that accepts a simple
# bytes object, and returns a bool (as opposed to a BeautifulSoup
# instance or such). Since these simple types are efficient to
# serialize, the function can run in a different process.
def do_validate(data):
    try:
        soup = BeautifulSoup(data, 'lxml')
    except Exception:
        print(traceback.format_exc())
        return False
    # the rest of your validation code goes here
    # ...
    return True

async def validate_page(session, url):
    res = await fetch(session, url)
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(executor, do_validate, res)

# fetch and validate_page_bounded are unchanged
...