在Python3中使用asyncio库进行快速数据抓取的教程


Posted in Python onApril 02, 2015

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来调用它们。除了这些,下面的代码看起来相当直观:
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

结论
好了,现在我们来到了这个部分。你有了一个异步工作的小抓取器。这意味着多个页面可以同时被下载,所以这个例子要比使用请求的相同代码快3倍。现在你应该可以用相同的方法写出你自己的抓取器了。

你可以在这里找到生成的代码,也包括一些额外的建议。

你一旦熟悉了这一切,我建议你看一看asyncio的文档和aiohttp的范例,这些都能告诉你 asyncio拥有怎样的潜力。

这种方法(事实上是所有手动的方法)的一个局限在于,没有一个独立的库可以用来处理表单。机械化的方法拥有很多辅助工具,这使得提交表单变得十分简单,但是如果你不使用它们,你将不得不自己去处理这些事情。这可能会导致一些bug的出现,所以同时我可能会写一个这样的库(不过目前为止无需为此担心)。

额外的建议:不要敲打服务器
同时做3个请求很酷,但是同时做5000个就不那么好玩了。如果你打算同时做太多的请求,链接有可能会断掉。你甚至有可能会被禁止链接网络。

为了避免这些,你可以使用semaphore。这是一个可以被用来限制同时工作的协同程序数量的同步工具。我们只需要在建立循环之前创建一个semaphore ,同时把我们希望允许的同时请求的数量作为参数传给它既可:
 

sem = asyncio.Semaphore(5)

然后,我们只需要将下面
 

page = yield from get(url, compress=True)

替换成被semaphore 保护的同样的东西。
 

with (yield from sem):
  page = yield from get(url, compress=True)

这就可以保证同时最多有5个请求会被处理。

额外建议:进度条
这个东东是免费的哦:tqdm是一个用来生成进度条的优秀的库。这个协同程序就像asyncio.wait一样工作,不过会显示一个代表完成度的进度条。
 

@asyncio.coroutine
def wait_with_progress(coros):
  for f in tqdm.tqdm(asyncio.as_completed(coros), total=len(coros)):
    yield from f
Python 相关文章推荐
用Python的Django框架编写从Google Adsense中获得报表的应用
Apr 17 Python
在Python中操作文件之seek()方法的使用教程
May 24 Python
Python实现时钟显示效果思路详解
Apr 11 Python
python 实现数字字符串左侧补零的方法
Dec 04 Python
详解python中的time和datetime的常用方法
Jul 08 Python
布隆过滤器的概述及Python实现方法
Dec 08 Python
10行Python代码实现Web自动化管控的示例代码
Aug 14 Python
如何使用python socket模块实现简单的文件下载
Sep 04 Python
python pymysql库的常用操作
Oct 16 Python
浅析python连接数据库的重要事项
Feb 22 Python
如何利用Matlab制作一款真正的拼图小游戏
May 11 Python
Python使用socket去实现TCP客户端和TCP服务端
Apr 12 Python
Python中的Classes和Metaclasses详解
Apr 02 #Python
详解Python中的装饰器、闭包和functools的教程
Apr 02 #Python
详解Python的迭代器、生成器以及相关的itertools包
Apr 02 #Python
用Python实现通过哈希算法检测图片重复的教程
Apr 02 #Python
仅用500行Python代码实现一个英文解析器的教程
Apr 02 #Python
python下载文件时显示下载进度的方法
Apr 02 #Python
Python使用正则匹配实现抓图代码分享
Apr 02 #Python
You might like
短波收音机简介
2021/03/01 无线电
PHP中Header使用的HTTP协议及常用方法小结
2014/11/04 PHP
thinkPHP3.1验证码的简单实现方法
2016/04/22 PHP
php处理单文件、多文件上传代码分享
2016/08/24 PHP
JavaScript 利用Cookie记录用户登录信息
2009/12/08 Javascript
javascript+mapbar实现地图定位
2010/04/09 Javascript
jQuery函数的第二个参数获取指定上下文中的DOM元素
2014/05/19 Javascript
js动态创建标签示例代码
2014/06/09 Javascript
freemarker判断对象是否为空的方法
2015/08/13 Javascript
Bootstrap按钮功能之查询按钮和重置按钮
2016/10/26 Javascript
web.js.字符串与正则表达式操作
2017/05/13 Javascript
Vue计算属性的使用
2017/08/04 Javascript
JavaScript设计模式之职责链模式应用示例
2018/08/07 Javascript
Vue 路由切换时页面内容没有重新加载的解决方法
2018/09/01 Javascript
原生JS实现随机点名项目的实例代码
2019/04/30 Javascript
jQuery+ajax实现批量删除功能完整示例
2019/06/06 jQuery
Python中的匿名函数使用简介
2015/04/27 Python
python实现简单登陆系统
2018/10/18 Python
Python Django2 model 查询介绍(条件、范围、模糊查询)
2020/03/16 Python
基于python 将列表作为参数传入函数时的测试与理解
2020/06/05 Python
详解在Python中使用Torchmoji将文本转换为表情符号
2020/07/27 Python
python/golang 删除链表中的元素
2020/09/14 Python
jupyter使用自动补全和切换默认浏览器的方法
2020/11/18 Python
html5如何在Canvas中实现自定义路径动画示例
2017/09/18 HTML / CSS
Proenza Schouler官方网站:纽约女装和配饰品牌
2019/01/03 全球购物
EJB的激活机制
2013/10/25 面试题
六一儿童节活动策划方案
2014/01/27 职场文书
2014年医务科工作总结
2014/12/18 职场文书
地道战观后感300字
2015/06/04 职场文书
教师节作文之小学四年级
2019/09/03 职场文书
2019年世界儿童日宣传标语
2019/11/22 职场文书
Oracle以逗号分隔的字符串拆分为多行数据实例详解
2021/07/16 Oracle
MySQL 1130异常,无法远程登录解决方案详解
2021/08/23 MySQL
golang中的struct操作
2021/11/11 Golang
解决redis批量删除key值的问题
2022/03/23 Redis
Java实现扫雷游戏详细代码讲解
2022/05/25 Java/Android