python:利用asyncio进行快速抓取(1)


web数据抓取是一个经常在python的讨论中出现的主题。有很多方法可以用来进行web数据抓取,然而其中好像并没有一个最好的办法。有一些如scrapy这样十分成熟的框架,更多的则是像mechanize这样的轻量级库。DIY自己的解决方案同样十分流行:你可以使用requests、beautifulsoup或者pyquery来实现。

方法如此多样的原因在于,数据“抓取”实际上包括很多问题:你不需要使用相同的工具从成千上万的页面中抓取数据,同时使一些Web工作流自动化例如填一些表单然后取回数据)。我喜欢DIY的原因在于其灵活性,但是却不适合用来做大量数据的抓取,因为需要请求同步,所以大量的请求意味着你不得不等待很长时间。

在本文中,我将会为你展示一个基于新的异步库aiohttp)的请求的代替品。我使用它写了一些速度的确很快的小数据抓取器,下面我将会为你演示是如何做到的。

asyncio的基本概念

asyncio是在python3.4中被引进的异步IO库。你也可以通过python3.3的pypi来安装它。它相当的复杂,而且我不会介绍太多的细节。相反,我将会解释你需要知道些什么,以利用它来写异步的代码。

简而言之,有两件事情你需要知道:协同程序和事件循环。协同程序像是方法,但是它们可以在代码中的特定点暂停和继续。当在等待一个IO比如一个HTTP请求),同时执行另一个请求的时候,可以用来暂停一个协同程序。我们使用关键字yield from来设定一个状态,表明我们需要一个协同程序的返回值。而事件循环则被用来安排协同程序的执行。

关于asyncio还有很多很多,但是以上是我们到目前为止需要知道的。可能你还有些不清楚,那么让我们来看一些代码吧。

aiohttp

aiohttp是一个利用asyncio的库,它的API看起来很像请求的API。到目前为止,相关文档还不健全。但是这里有一些非常有用的例子。我们将会演示它的基本用法。

首先,我们会定义一个协同程序用来获取页面,并打印出来。我们使用 asyncio.coroutine将一个方法装饰成一个协同程序。aiohttp.request是一个协同程序,所以它是一个可读方法,我们需要使用yield from来调用它们。除了这些,下面的代码看起来相当直观:

  1. @asyncio.coroutine  
  2. def print_page(url):  
  3.     response = yield from aiohttp.request('GET', url)  
  4.     body = yield from response.read_and_close(decode=True)  
  5.     print(body) 

如你所见,我们可以使用yield from从另一个协同程序中调用一个协同程序。为了从同步代码中调用一个协同程序,我们需要一个事件循环。我们可以通过asyncio.get_event_loop()得到一个标准的事件循环,之后使用它的run_until_complete()方法来运行协同程序。所以,为了使之前的协同程序运行,我们只需要做下面的步骤:

  1. loop = asyncio.get_event_loop()  
  2. loop.run_until_complete(print_page('http://example.com')) 

一个有用的方法是asyncio.wait,通过它可以获取一个协同程序的列表,同时返回一个将它们全包括在内的单独的协同程序,所以我们可以这样写:

  1. loop.run_until_complete(asyncio.wait([print_page('http://example.com/foo'),  
  2.                                       print_page('http://example.com/bar')])) 

另一个是asyncio.as_completed,通过它可以获取一个协同程序的列表,同时返回一个按完成顺序生成协同程序的迭代器,因此当你用它迭代时,会尽快得到每个可用的结果。

数据抓取

现在我们知道了如何做异步HTTP请求,因此我们可以来写一个数据抓取器了。我们仅仅还需要一些工具来读取html页面,我使用了beautifulsoup来做这个事情,其余的像 pyquery或lxml也可以实现。

在这个例子中,我们会写一个小数据抓取器来从海盗湾抓取一些linux distributions的torrent 链路海盗湾英语:The Pirate Bay,缩写:TPB)是一个专门存储、分类及搜索Bittorrent种子文件的网站,并自称“世界最大的BitTorrent trackerBT种子服务器)”,提供的BT种子除了有自由版权的收集外,也有不少被著作人声称拥有版权的音频、视频、应用软件与电子游戏等,为网络分享与下载的重要网站之一–译者注来自维基百科)

首先,需要一个辅助协同程序来获取请求:

  1. @asyncio.coroutine  
  2. def get(*args, **kwargs):  
  3.     response = yield from aiohttp.request('GET', *args, **kwargs)  
  4.     return (yield from response.read_and_close(decode=True)) 

解析部分。本文并非介绍beautifulsoup的,所以这部分我会简写:我们获取了这个页面的第一个磁链。

  1. def first_magnet(page):  
  2.     soup = bs4.BeautifulSoup(page)  
  3.     a = soup.find('a', title='Download this torrent using magnet')  
  4.     return a['href'

在这个协同程序中,url的结果通过种子的数量进行排序,所以排名第一的结果实际上是种子最多的:

  1. @asyncio.coroutine  
  2. def print_magnet(query):  
  3.     url = 'http://thepiratebay.se/search/{}/0/7/0'.format(query)  
  4.     page = yield from get(url, compress=True)  
  5.     magnet = first_magnet(page)  
  6.     print('{}: {}'.format(query, magnet)) 

最后,用下面的代码来调用以上所有的方法。

  1. distros = ['archlinux''ubuntu''debian']  
  2. loop = asyncio.get_event_loop()  
  3. f = asyncio.wait([print_magnet(d) for d in distros])  
  4. loop.run_until_complete(f) 


评论关闭