在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 ljust rjust center输出
Sep 06 Python
Python运算符重载详解及实例代码
Mar 07 Python
Django 忘记管理员或忘记管理员密码 重设登录密码的方法
May 30 Python
Python关于excel和shp的使用在matplotlib
Jan 03 Python
Python3网络爬虫中的requests高级用法详解
Jun 18 Python
使用django的ORM框架按月统计近一年内的数据方法
Jul 18 Python
Python autoescape标签用法解析
Jan 17 Python
浅谈Pytorch中的自动求导函数backward()所需参数的含义
Feb 29 Python
python为Django项目上的每个应用程序创建不同的自定义404页面(最佳答案)
Mar 09 Python
Pycharm安装python库的方法
Nov 24 Python
学会Python数据可视化必须尝试这7个库
Jun 16 Python
opencv深入浅出了解机器学习和深度学习
Mar 17 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
php产生随机数的两种方法实例代码 输出随机IP
2011/04/08 PHP
PHP表单递交控件名称含有点号(.)会被转化为下划线(_)的处理方法
2013/01/06 PHP
destoon整合ucenter后注册页面不跳转的解决方法
2014/06/21 PHP
php 微信开发获取用户信息如何实现
2016/12/13 PHP
实现laravel 插入操作日志到数据库的方法
2019/10/11 PHP
php 实现简单的登录功能示例【基于thinkPHP框架】
2019/12/02 PHP
jQuery对表单的操作代码集合
2011/04/06 Javascript
js阻止默认事件与js阻止事件冒泡示例分享 js阻止冒泡事件
2014/01/27 Javascript
使用js实现的简单拖拽效果
2015/03/18 Javascript
JS实现获取键盘按下的按键并显示在页面上的方法
2015/11/04 Javascript
Angularjs中的页面访问权限怎么设置
2016/11/11 Javascript
使用jQuery实现动态添加小广告
2017/07/11 jQuery
jQuery EasyUI开发技巧总结
2017/09/26 jQuery
JS简单实现数组去重的方法分析
2017/10/14 Javascript
浅谈vue项目用到的mock数据接口的两种方式
2019/10/09 Javascript
vue中改变滚动条样式的方法
2020/03/03 Javascript
整理Python 常用string函数(收藏)
2016/05/30 Python
利用aardio给python编写图形界面
2017/08/21 Python
Python中的CSV文件使用"with"语句的方式详解
2018/10/16 Python
利用Python小工具实现3秒钟将视频转换为音频
2019/10/29 Python
奥地利顶级内衣丝袜品牌英国站:Wolford英国
2016/08/29 全球购物
雅诗兰黛(Estee Lauder)英国官方网站:世界顶级化妆品牌
2016/12/29 全球购物
英国计算机产品零售商:Novatech(定制个人电脑、笔记本电脑、工作站和服务器)
2018/01/28 全球购物
买卖正宗运动鞋:GOAT
2019/12/06 全球购物
BannerBuzz加拿大:在线定制横幅印刷、广告和标志
2020/03/10 全球购物
Delphi CS笔试题
2014/01/04 面试题
会计电算化专业个人的自我评价
2013/11/24 职场文书
企业精细化管理实施方案
2014/03/23 职场文书
公司门卫岗位职责范本
2014/07/08 职场文书
党员教师群众路线对照检查材料思想汇报
2014/09/29 职场文书
大学生求职自荐信
2015/03/24 职场文书
信用卡工资证明范本
2015/06/19 职场文书
2015年暑期见闻
2015/07/14 职场文书
2019运动会广播加油稿汇总
2019/08/21 职场文书
导游词之镜泊湖
2019/12/09 职场文书
Windows下载并安装MySQL8.0.x 版本的完整教程
2022/04/10 MySQL