web-dev-qa-db-ja.com

Python aiohttp / asyncio-返されたデータを処理する方法

Aiohttpを使用して同期コードをasyncioに移動する過程にあります。同期コードの実行には15分かかっていたので、これを改善したいと思っています。

いくつかのURLからデータを取得し、それぞれの本文を返す作業コードがあります。しかし、これは1つのラボサイトに反するものであり、70以上の実際のサイトがあります。

したがって、すべてのサイトのすべてのURLのリストを作成するループが発生した場合、リスト内の700のURLが処理されます。今それらを処理することは問題ではないと思いますか?

しかし、結果を「処理」すると、プログラミング方法がわかりません。返される各結果に対して「処理」を行うコードはすでにありますが、正しいタイプの結果に対してプログラムする方法がわかりません。

コードが実行されると、すべてのURLが処理され、実行時間に応じて、不明な順序が返されますか?

あらゆるタイプの結果を処理する関数が必要ですか?

import asyncio, aiohttp, ssl
from bs4 import BeautifulSoup

def page_content(page):
    return BeautifulSoup(page, 'html.parser')


async def fetch(session, url):
    with aiohttp.Timeout(15, loop=session.loop):
        async with session.get(url) as response:
            return page_content(await response.text())

async def get_url_data(urls, username, password):
    tasks = []
    # Fetch all responses within one Client session,
    # keep connection alive for all requests.
    async with aiohttp.ClientSession(auth=aiohttp.BasicAuth(username, password)) as session:
        for i in urls:
            task = asyncio.ensure_future(fetch(session, i))
            tasks.append(task)

        responses = await asyncio.gather(*tasks)
        # you now have all response bodies in this variable
        for i in responses:
            print(i.title.text)
        return responses


def main():
    username = 'monitoring'
    password = '*********'
    ip = '10.10.10.2'
    urls = [
        'http://{0}:8444/level/15/exec/-/ping/{1}/timeout/1/source/vlan/5/CR'.format(ip,'10.10.0.1'),
        'http://{0}:8444/level/15/exec/-/traceroute/{1}/source/vlan/5/probe/2/timeout/1/ttl/0/10/CR'.format(ip,'10.10.0.1'),
        'http://{0}:8444/level/15/exec/-/traceroute/{1}/source/vlan/5/probe/2/timeout/1/ttl/0/10/CR'.format(ip,'frontend.domain.com'),
        'http://{0}:8444/level/15/exec/-/traceroute/{1}/source/vlan/5/probe/2/timeout/1/ttl/0/10/CR'.format(ip,'planner.domain.com'),
        'http://{0}:8444/level/15/exec/-/traceroute/{1}/source/vlan/5/probe/2/timeout/1/ttl/0/10/CR'.format(ip,'10.10.10.1'),
        'http://{0}:8444/level/15/exec/-/traceroute/{1}/source/vlan/5/probe/2/timeout/1/ttl/0/10/CR'.format(ip,'10.11.11.1'),
        'http://{0}:8444/level/15/exec/-/ping/{1}/timeout/1/source/vlan/5/CR'.format(ip,'10.12.12.60'),
        'http://{0}:8444/level/15/exec/-/traceroute/{1}/source/vlan/5/probe/2/timeout/1/ttl/0/10/CR'.format(ip,'10.12.12.60'),
        'http://{0}:8444/level/15/exec/-/ping/{1}/timeout/1/source/vlan/5/CR'.format(ip,'lon-dc-01.domain.com'),
        'http://{0}:8444/level/15/exec/-/traceroute/{1}/source/vlan/5/probe/2/timeout/1/ttl/0/10/CR'.format(ip,'lon-dc-01.domain.com'),
        ]
    loop = asyncio.get_event_loop()
    future = asyncio.ensure_future(get_url_data(urls,username,password))
    data = loop.run_until_complete(future)
    print(data)

if __name__ == "__main__":
    main()
16
AlexW

concurrent.futures.ProcessPoolExecutor の例を次に示します。 max_workersを指定せずに作成された場合、実装は代わりにos.cpu_countを使用します。 asyncio.wrap_future は公開されていますが、文書化されていないことにも注意してください。または、 AbstractEventLoop.run_in_executor があります。

import asyncio
from concurrent.futures import ProcessPoolExecutor

import aiohttp
import lxml.html


def process_page(html):
    '''Meant for CPU-bound workload'''
    tree = lxml.html.fromstring(html)
    return tree.find('.//title').text


async def fetch_page(url, session):
    '''Meant for IO-bound workload'''
    async with session.get(url, timeout = 15) as res:
      return await res.text()


async def process(url, session, pool):
    html = await fetch_page(url, session)
    return await asyncio.wrap_future(pool.submit(process_page, html))


async def dispatch(urls):
    pool = ProcessPoolExecutor()
    async with aiohttp.ClientSession() as session:
        coros = (process(url, session, pool) for url in urls)
        return await asyncio.gather(*coros)


def main():
    urls = [
      'https://stackoverflow.com/',
      'https://serverfault.com/',
      'https://askubuntu.com/',
      'https://unix.stackexchange.com/'
    ]
    result = asyncio.get_event_loop().run_until_complete(dispatch(urls))
    print(result)

if __name__ == '__main__':
    main()
9
saaj

あなたのコードはマークからそれほど遠くありません。 _asyncio.gather_は引数の順序で結果を返すため、ここでは順序は保持されますが、_page_content_は順番に呼び出されません。

いくつかの調整:

まず、ここでは_ensure_future_は必要ありません。タスクの作成が必要になるのは、コルーチンをその親よりも長持ちさせようとしている場合、つまり、タスクを作成した関数が実行されてもタスクを実行し続ける必要がある場合のみです。ここで必要なのは、代わりにコルーチンを使用して_asyncio.gather_を直接呼び出すことです。

_async def get_url_data(urls, username, password):
    async with aiohttp.ClientSession(...) as session:
        responses = await asyncio.gather(*(fetch(session, i) for i in urls))
    for i in responses:
        print(i.title.text)
    return responses
_

しかしこれを呼び出すと、すべてのフェッチが同時にスケジュールされ、URLの数が多いため、これは最適とは言えません。代わりに、最大同時実行性を選択し、常に最大X個のフェッチが実行されていることを確認する必要があります。これを実装するには、asyncio.Semaphore(20)を使用できます。このセマフォは最大20個のコルーチンでしか取得できないため、他のセマフォはスポットが利用可能になるまで取得を待機します。

_CONCURRENCY = 20
TIMEOUT = 15

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

async def get_url_data(urls, username, password):
    sem = asyncio.Semaphore(CONCURRENCY)
    async with aiohttp.ClientSession(...) as session:
        responses = await asyncio.gather(*(
            asyncio.wait_for(fetch(session, sem, i), TIMEOUT)
            for i in urls
        ))
    for i in responses:
        print(i.title.text)
    return responses
_

このようにして、すべてのフェッチがすぐに開始されますが、セマフォを取得できるのはそのうちの20個だけです。他のものは最初の_async with_命令でブロックし、別のフェッチが完了するまで待機します。

また、ここでaiohttp.Timeoutを公式のasyncioに置き換えました。

最後に、データの実際の処理では、CPU時間によって制限されている場合、asyncioはおそらくあまり役​​に立ちません。ここでProcessPoolExecutorを使用して、実際の作業を別のCPUに並列化する必要があります。 _run_in_executor_はおそらくに役立つでしょう。

5
Arthur