使用Python的asyncio按顺序获取数据

时间:2014-06-16 15:05:34

标签: python asynchronous python-asyncio

我有一个Python 2.7程序,它从网站提取数据并将结果转储到数据库。它遵循消费者生产者模型,并使用线程模块编写。

为了好玩,我想用新的 asyncio 模块(从3.4)重写这个程序,但我无法弄清楚如何正确地做到这一点。

最关键的要求是程序必须按顺序从同一网站获取数据。例如,对于网址' http://a-restaurant.com' ,首先应该' http://a-restaurant.com/menu/0' ,然后' http://a-restaurant.com/menu/1' 然后' http://a-restaurant.com/menu/2' ,...... 如果没有按顺序取出它们,网站就会完全停止发送页面,你必须从0开始。

然而,另一个网站(' http://another-restaurant.com' )的另一次抓取可以(并且应该)同时运行(其他网站也有sequantial限制) )。

线程模块非常适合这种情况,因为我可以为每个网站创建单独的线程,并且在每个线程中它可以等到一个页面完成加载,然后再获取另一个页面。

这是一个来自线程版本(Python 2.7)的极为简化的代码片段:

class FetchThread(threading.Threading)
    def __init__(self, queue, url)
        self.queue = queue
        self.baseurl = url
    ...
    def run(self)
        # Get 10 menu pages in a sequantial order
        for food in range(10):
            url = self.baseurl + '/' + str(food)
            text = urllib2.urlopen(url).read()
            self.queue.put(text)
            ...
def main()
    queue = Queue.Queue()
    urls = ('http://a-restaurant.com/menu', 'http://another-restaurant.com/menu')
    for url in urls:
        fetcher = FetchThread(queue, url)
        fetcher.start()
        ...

以下是我尝试使用asyncio(在3.4.1中)的方法:

@asyncio.coroutine
def fetch(url):
    response = yield from aiohttp.request('GET', url)
    response = yield from response.read_and_close()
    return response.decode('utf-8')

@asyncio.coroutine
def print_page(url):
    page = yield from fetch(url)
    print(page)


l = []
urls = ('http://a-restaurant.com/menu', 'http://another-restaurant.com/menu')
for url in urls:
    for food in range(10):
        menu_url = url + '/' + str(food)
        l.append(print_page(menu_url))

loop.run_until_complete(asyncio.wait(l))

它以非连续的顺序提取和打印所有内容。嗯,我想这就是那些协同程序的全部想法。我应该不使用aiohttp并只使用urllib获取?但是第一家餐馆的食物是否阻止了其他餐馆的提取?我只是觉得这完全错了吗? (这只是一个尝试按顺序获取东西的测试。但是还没有进入队列部分。)

2 个答案:

答案 0 :(得分:5)

您当前的代码适用于不关心请求顺序排序的餐厅。菜单的所有十个请求将同时运行,并且一旦完成就会打印到stdout。

显然,这对需要连续请求的餐厅不起作用。你需要重构一下才能发挥作用:

@asyncio.coroutine
def fetch(url):
    response = yield from aiohttp.request('GET', url)
    response = yield from response.read_and_close()
    return response.decode('utf-8')

@asyncio.coroutine
def print_page(url):
    page = yield from fetch(url)
    print(page)

@syncio.coroutine
def print_pages_sequential(url, num_pages):
    for food in range(num_pages):
        menu_url = url + '/' + str(food)
        yield from print_page(menu_url)

l = [print_pages_sequential('http://a-restaurant.com/menu', 10)]

conc_url = 'http://another-restaurant.com/menu'
for food in range(10):
    menu_url = conc_url + '/' + str(food)
    l.append(print_page(menu_url))

loop.run_until_complete(asyncio.wait(l))

我们不会将连续餐厅的所有十个请求添加到列表中,而是将一个协同程序添加到列表中,该列表将按顺序迭代所有十个页面。这样做的方式是yield from print_page将停止print_pages_sequential的执行,直到print_page请求完成,但它会这样做而不会阻止任何其他同时运行的协同程序(如所有print_page来自l的{​​{1}}来电。

通过这种方式,您所有的“另一个餐厅”请求可以完全同时运行,就像您想要的那样,并且您的“餐厅”请求将按顺序运行,但不会阻止任何“另一个餐厅” “请求。

修改

如果所有站点都具有相同的顺序提取要求,则可以更简化逻辑:

l = []
urls = ["http://a-restaurant.com/menu", "http://another-restaurant.com/menu"]
for url in urls:
    menu_url = url + '/' + str(food)
    l.append(print_page_sequential(menu_url, 10))

loop.run_until_complete(asyncio.wait(l))

答案 1 :(得分:2)

asyncio.Task替换 asyncio 世界中的threading.Threadasyncio.async也会创建新任务。

asyncio.gather是等待几个协程的非常方便的方式,我更喜欢它而不是asyncio.wait

@asyncio.coroutine
def fetch(url):
    response = yield from aiohttp.request('GET', url)
    response = yield from response.read_and_close()
    return response.decode('utf-8')

@asyncio.coroutine
def print_page(url):
    page = yield from fetch(url)
    print(page)

@asyncio.coroutine
def process_restaurant(url):
    for food in range(10):
        menu_url = url + '/' + str(food)
        yield from print_page(menu_url)

urls = ('http://a-restaurant.com/menu', 'http://another-restaurant.com/menu')
coros = []
for url in urls:
    coros.append(asyncio.Task(process_restaurant(url)))

loop.run_until_complete(asyncio.gather(*coros))