Py3.x异步网络爬虫浅涉
异步的概念
举个例子,A正在玩游戏,B去叫A一起吃饭,这个时候B有两种选择,一是等A玩完游戏一起去吃饭,二是去干其他事情,并告诉A玩完通知他。是的,前一种选择就是单线程,后一种是多线程,但是,如果使用多线程做这件事就会出现B为了得到A的通知不得不隔一段时间停下手中的活看看A是否通知,而使用while循环似乎又不太恰当,降低程序性能,这个时候就可用异步处理。
异步示例
异步是py3.4+的版本引入的,在py3.5+中正式定义其操作符async与await等,以下我将使用py3.4+的语法写一些异步程序,这些程序在之后的版本中也是可以运行的。
import asyncio
# 获取事件循环
loop = asyncio.get_event_loop()
@asyncio.coroutine
def a_persion():
for i in range(5):
yield from asyncio.sleep(3)
print('我是A,我在玩游戏')
return "A游戏玩完了!"
def a_statu(say):
print(say.result())
@asyncio.coroutine
def b_person(x):
for i in range(7):
yield from asyncio.sleep(3)
print('我是B,我在等A,顺便看书', x)
a_task = asyncio.ensure_future(a_persion()) # 新建任务
a_task.add_done_callback(a_statu) # 设置回调函数
b_task = asyncio.ensure_future(b_person('无聊'))
tasks = [a_task, b_task] # 任务列表
loop.run_until_complete(asyncio.wait(tasks))
print('A和B高高兴兴的吃饭去了!')
执行结果:
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
A游戏玩完了!
我是B,我在等A,顺便看书 无聊
我是B,我在等A,顺便看书 无聊
A和B高高兴兴的吃饭去了!
很清楚的,我们看到,A与B的事情都同时在做,并且最后他们都可以同时出去吃饭,这就是异步,如果让你用多线程方式去做是否比较麻烦?
用异步用来写一个爬虫框架
网络爬虫的决速步骤在下载网页源码步骤上,此步耗时最大,因此,我将用异步的方式对其优化,实现并行地下载页面。我们会用到aiohttp
这个目前还是第三方的库,可通过pip
安装之。同时,我将使用Scrapy的风格写出这个框架,也是向Scrapy团队致敬。我们暂且叫它py3spider吧!
引入相应的包
import asyncio
import aiohttp
import warnings
定义请求类
class Request:
def __init__(self, url, callback, meta=None):
self.url = url
self.callback = callback
self.meta = meta
用于封装Request请求,实现对url及其解析函数的暂存
定义响应类
class Response:
def __init__(self, body, url, meta=None, callback=None):
self.body = body
self.url = url
self.meta = meta
self.callback = callback
用于对下载完毕的数据进行暂存
定义中间件
class Middleware:
def __init__(self):
self.web = aiohttp.ClientSession()
def __del__(self):
self.web.close()
@asyncio.coroutine
def get(self, req):
response = yield from self.web.get(req.url)
body = yield from response.read()
response.close()
return Response(body, req.url, req.meta, req.callback)
def save(self, data):
print(data)
用于下载页面与保存数据等
定义爬虫接口
class Spider:
start_urls = []
def parse(self, response):
pass
所有爬虫都需实现此接口,start_urls
是入口列表,parse
函数是第一个调用的解析函数
定义调度器
class Scheduler:
def __init__(self, spider, middleware=Middleware()):
self.__spider = spider
self.__midw = middleware
self.__request = [] # 请求队列
self.__response = [] # 回应队列
self.thread_num = 16 # 线程数
def __start(self):
for url in self.__spider.start_urls:
self.__request.append(Request(url, self.__spider.parse))
def __putRequest(self):
loop = asyncio.get_event_loop()
def putRes(fur):
self.__response.append(fur.result())
tasks = []
while self.__request:
try:
num = 0
if len(self.__request) < self.thread_num:
num = len(self.__request)
else:
num = self.thread_num
for i in range(num):
task = asyncio.ensure_future(
self.__midw.get(self.__request.pop()))
task.add_done_callback(putRes)
tasks.append(task)
if tasks:
loop.run_until_complete(asyncio.wait(tasks))
self.__doResponse()
tasks.clear()
except Exception as e:
warnings.warn(e)
loop.close()
def __doResponse(self):
while self.__response:
res = self.__response.pop()
try:
for req in res.callback(res):
if type(req) == Request:
self.__request.append(req)
else:
self.__midw.save(req)
except Exception as e:
warnings.warn(e)
def start(self):
self.__start()
self.__putRequest()
print("The end ...")
执行爬虫的调度任务,爬虫、中间件、线程都可以自己设置。将上面的代码封装成py3spider
,使用方法与Scrapy类似,只需要修改极少代码甚至不需要修改Scrapy的爬虫源码即可使用。
调用示例
以古诗文网的名句项目为例说明,想了解古诗文网的详情可以用浏览器打开http://so.gushiwen.org/mingju/
这个网址查看。
from py3spider import *
from bs4 import BeautifulSoup
import re
class MingSpider(Spider):
start_urls = ['http://so.gushiwen.org/mingju/']
def parse(self, response):
soup = BeautifulSoup(response.body.decode('utf-8', 'ignore'), 'lxml')
div = soup.find('div', attrs = {'class': 'sright'})
for a in div.find_all('a', attrs={'href': re.compile('^/mingju/Default.aspx.*')}):
yield Request('http://so.gushiwen.org/'+a.attrs['href'], callback=self.parseDetail, meta={'title': a.text})
def parseDetail(self, response):
soup = BeautifulSoup(response.body.decode('utf-8', 'ignore'), 'lxml')
# <a style=" float:left;" target="_blank" href="/mingju/ju_162.aspx">山有木兮木有枝,心悦君兮君不知。</a>
for a in soup.find_all('a', attrs={'href': re.compile('^/mingju/ju_.*')}):
if a and a.text:
yield {'title': response.meta['title'], 'text': a.text}
nt = soup.find('a', text='下一页')
if nt and nt.attrs and 'href' in nt.attrs:
yield Request('http://so.gushiwen.org/mingju/'+nt.attrs['href'], callback=self.parseDetail, meta=response.meta)
class MyMiddleware(Middleware):
def __init__(self):
super(MyMiddleware, self).__init__()
def save(self, data):
with open(data['title']+'.txt', 'a') as f:
f.write(data['text']+'\n')
scheduler = Scheduler(MingSpider(), middleware = MyMiddleware())
scheduler.start()
执行上面的代码即可很快的爬行名句项目。关于py3spider这个项目我已上传pypi
,可以通过pip
安装之。
相比于Scrapy,这个安装更容易,第三方库仅aiohttp
一个。