from twisted.internet import reactor #事件循环 相当于selecet作用 监听是否有连接成功(终止条件,所有的socket对象都被移除。)
from twisted.web.client import getPage #socket对象(如果下载完成,自动从事件循环中移除)
from twisted.internet import defer #defer.Deferred 特殊的socket对象(不会发请求,目的是不让事件循环结束,可以手动移除。)
from queue import Queue
class Request:#请求类
def __init__(self, url, callback):
self.url = url
self.callback = callback
class HttpResponse:#响应类
def __init__(self, content, request):
self.content = content
self.request = request
class Command:
def run(self):#开始运行
crawl_process = CrawlerProcess()#创建crawl_process对象
spider_cls_path_list = ["chouti.ChoutiSpider", "baidu.BaiduSpider"]
for spider_cls_path in spider_cls_path_list:
crawl_process.crawl(spider_cls_path)#创建爬虫 开启引擎
crawl_process.start()#开启事件循环
class CrawlerProcess:#用于开启事件循环
def __init__(self):
self._active = set()
def crawl(self, spider_cls_path):
crawler = Crawler()
d = crawler.crawl(spider_cls_path)#通过spider_cls_path建立socket对象
self._active.add(d)
def start(self):#开启事件循环
dd = defer.DeferredList(self._active)#添加到集合中方便defer.DeferredList(用来监听的事件循环列表)监听
dd.addBoth(lambda _: reactor.stop())#如果事件循环中的socket对象都被移除则循环停止
reactor.run()#开启事件循环
class Crawler:#用于封装引擎和调度器
@defer.inlineCallbacks#异步处理 此装饰器下的函数必须返回yield 不想返回东西可以返回None
def crawl(self, spider_cls_path):
engine = self.create_engine()#创建引擎
spider = self.create_spider(spider_cls_path)#创建爬虫
start_requests = iter(spider.start_requests())#通过爬虫的start_requests()方法获取请求,
# 因为start_requests()返回的是yield,所以要从生成器转成迭代器然后用next获取请求
yield engine.open_spider(start_requests)#引擎开始
yield engine.start()#引擎开启
def create_engine(self):
return ExecutionEngine()#返回ExecutionEngine对象 -- 引擎
def create_spider(self, spider_cls_path):#通过路径创建爬虫的对象
module_path, cls_name = spider_cls_path.rsplit(".", maxsplit=1)
import importlib
m = importlib.import_module(module_path)
cls = getattr(m, cls_name)#获取爬虫的类名
return cls()#返回爬虫的对象
class ExecutionEngine:#引擎:所有的调度
def __init__(self):#引擎初始化
self._close = None#特殊的socket类 不发请求
self.scheduler = None
self.max = 5#正在运行的请求的限制数量
self.crawlling = []#正在运行的请求
def get_response_callback(self, content, request):#获得响应
self.crawlling.remove(request)#先把获得响应的请求从正在运行的请求列表中删除
response = HttpResponse(content, request)#获得响应的对象 里面封装了响应内容和之前发送的请求
result = request.callback(response)#其实就是调用Request的parse 来判断是否还有其他请求
import types
if isinstance(result, types.GeneratorType):#如果result是迭代器 说明又返回了其他请求
for req in result:
self.scheduler.enqueue_request(req)#把新的请求加入到调度队列中
def _next_request(self):
if self.scheduler.size() == 0 and len(self.crawlling) == 0:#事件循环的终止条件既调度队列为空且正在运行的请求列表也为空
self._close.callback(None)#从事件循环中移除,关闭事件循环,进而关闭爬虫
return
while len(self.crawlling) < self.max:#当正在运行的请求小于5时
req = self.scheduler.next_request()#获取新的请求
if not req:#如果请求不为None
return
self.crawlling.append(req)#把不为None的请求放入到正在运行的请求列表中
d = getPage(req.url.encode("utf-8"))
d.addCallback(self.get_response_callback, req)#用get_response_callback获得响应的内容
d.addCallback(lambda _:reactor.callLater(0, self._next_request))
@defer.inlineCallbacks
def open_spider(self, start_requests):#开启爬虫
self.scheduler = Scheduler()#创建调度器
yield self.scheduler.open()#self.scheduler.open()为返回值为None 这里一定要yield 因为@defer.inlineCallbacks规定的
while True:
try:
req = next(start_requests)#通过迭代器获取请求
except StopIteration:#如果迭代器为空 跳出循环
break
self.scheduler.enqueue_request(req)#不断地把获得的请求加入调度队列中,直到迭代器取空
reactor.callLater(0, self._next_request)#调用回调函数 self._next_request来把调度队列中的请求发送出去
@defer.inlineCallbacks
def start(self):
self._close = defer.Deferred()#创建特殊的socket对象,此对象不发请求,加入到事件循环中,
# 等socket请求都得到响应,再从事件循环中删除
yield self._close
class Scheduler:#任务调度器
def __init__(self):
self.q = Queue()#建立调度的队列
def enqueue_request(self, req):#从队列中放入请求
self.q.put(req)
def next_request(self):#从队列中取请求
try:
req = self.q.get(block=False)#非阻塞
except Exception as e:#如果队列为空则会发生异常 抓住这个异常
req = None
return req#返回请求 可能为None
def size(self):#队列里的数量
return self.q.qsize()
def open(self):
pass
if __name__ == '__main__':
cmd = Command()
cmd.run()
ChoutiSpider
from engine import Request
class ChoutiSpider:
def start_requests(self):
start_urls = ["https://www.chouti.com","https://www.taobao.com"]
for url in start_urls:
yield Request(url=url, callback=self.parse)
def parse(self, response):
print(response.request.url)
# yield Request("https://www.zhihu.com",callback=self.parse)
BaiduSpider
from engine import Request
class BaiduSpider:
def start_requests(self):
start_urls = ["https://www.baidu.com","https://www.bilibili.com"]
for url in start_urls:
yield Request(url=url, callback=self.parse)
def parse(self, response):
print(response.request.url)
结果:
获得网页代码太大,不方便放在csdn上,所以我就打印下获得的响应里封装的url,parse里可以继续发请求。
D:\untitled\spider\venv\Scripts\python.exe D:/spider/engine.py
https://www.baidu.com
https://www.bilibili.com
https://www.taobao.com
https://www.chouti.com
Process finished with exit code 0