摄影:产品经理切面薄而整齐,到底是用什么刀切的?
在昨天的文章《Callback ——从同步思维切换到异步思维》,我们举的例子似乎还不能很好地说明 Callback 的优势。今天我们再来看另外一个场景。
例如有下面这个代码片段:
async def parse(html): selector = fromstring(html) print('...解析 HTML 的数据...') next_page = selector.xpath('//a[@class="next_page"]/@href') if next_page: next_page_url = next_page[0] next_page_html = await request(next_page_url) await parse(next_page_html)
这种场景常常发生在需要翻页的时候,不同页面的处理逻辑是完全一样的,但是每次获取到了当前页面才能获取下一页。于是不少人使用递归的办法来解决问题。
如果页数非常多,那么你就会面临一个问题:超出最大递归深度,导致报错。
并且,在定义这个parse
函数的时候,我使用了async def
把它定义为一个异步函数。但实际上,解析 HTML 是一个 CPU 密集型的工作,它没有 IO 等待,根本就没有必要异步!但为了在里面调用await request(next_page_url)
,却必须要使用async def
。
为了解决这个问题,我们可以把递归改成循环。于是可能有人会这样写代码:
...无关代码... url_list = ['初始 URL'] while url_list: url = url_list.pop() html = await request(url) data = parse(html) next_page_url = data['next_page_url'] if next_page_url: url_list.append(next_page_url)
通过这种写法,parse
函数可以直接定义成普通函数,并且每次只会调用1层,不会递归调用。这就同时解决了两个问题。
看到这里,大家可能发现了,实际上我们只有在涉及到 IO 请求的地方,才需要使用async/await
。在解析网页的地方,只需要使用普通函数就可以了。
而对于aiohttp
请求网页来说,它的逻辑非常简单,你告诉它url
、headers
、method
、body
。它返回源代码给你。它不需要关心你传入的这一批URL 是不是对应同一个类型的页面,甚至不需要关心你请求的是不是同一个网站!
在这种情况下,如果我们使用 Callback,那么优势就凸现出来了。我们来看下面这个例子:
def parse_1(html): print('处理页面1的源代码') def parse_2(html): print('处理页面2的源代码') def parse_3(html): print('处理另外一个网站的源代码') class RequestObj: def __init__(self, url, headers=None, method='get', body=None, callback): self.url = url self.headers = headers self.method = method self.body = body self.callback = callback async def request(req_obj): async with aiohttp.ClientSession() as session: if req_obj.method == 'get': resp = await session.get(req_obj.url, headers=req_obj.headers) else: resp = await session.post(req_obj.url, headers=req_obj.headers, json=req_obj.body) html = await resp.text(encoding='utf-8') req_obj.callback(html) async main(): req_obj_list = [ RequestObj('页面1的 url', headers1, callback=parse_1), RequestObj('页面2的 url', headers2, method='post', body={'xx': 1}, callback=parse_2), RequstObj('另一个网站的 url', headers3, callback=parse_3) ] tasks = [request(x) for x in req_obj_list] await asyncio.gather(*tasks)
在这个代码片段中,不同的 parse 函数处理不同的 url。我们在创建 RequestObj 对象的时候,把不同的 parse 函数通过 callback 参数与 url 关联起来。那么下载器在请求完成 url 以后,要做的仅仅是调用这个 callback 函数。
这样一来,假设有一个网站,我们先访问列表页,然后从列表页中拿到每一个详情页的 URL 去访问详情页。列表页可以翻页,详情页也可以翻页。通过维护一个全局的队列,我们可以实现,列表页要翻页的时候,把RequestObj 对象放到队列中,详情页要翻页的时候,把 RequestObj 对象也放到队列中。而负责请求网站的代码,不关心它自己请求的是哪个页面,它只管请求,然后调用 callback 传入 html 即可。这样就是实现了,列表页和详情页同时请求。速度大大提升。
下一篇文章,我们来实现这个全局的队列。