框架图(高清官方) https://doc.scrapy.org/en/latest/_images/scrapy_architecture_02.png
![b4901b3b40dd3c57f9b6fa16ca886cb8.png](https://i-blog.csdnimg.cn/blog_migrate/2fe9874e0644296ef4345ee1957ef2b7.jpeg)
图上没有的 class Crawler
很多类创建的时候依赖 Crawler。重要的有ExecutionEngine、downloader、所以上面启动流程很难绕过 Crawler 创建。
class ExecutionEngine
上图中间的engine。Crawler 像管理者,ExecutionEngine像执行经理,联系着downloader、spiders、scheduler、pipelines、
其他先看 downloader
爬虫就是不停的爬,每个请求封装成Request(对应地也封装Response)。好处是每个请求变成了一个个对象,方便处理。
所以downloader如果需要简单实现,是可以的。scrapy的downloader核心代码在core.downloader.handles,其中其实只关心http的handle即可
downloader和其他模块的耦合不大,甚至可以自己实现,或者提取这部分模块用作其他项目。所以不需要了解细节。要留意的是downloader的工作是异步的,利用了 twisted defer reactor,这部分比较有意思。
因为耦合小,我们实现downloader来实践一下。利用一般人熟悉的 requests (使用gevent异步)
from twisted.internet import reactor, defer
import requests
import gevent
from gevent import monkey
from scrapy.http import TextResponse
monkey.patch_socket()
def request_worker(request, callback=None):
callback(requests.get(request.url))
def download_request(request, _=None):
df = defer.Deferred()
def callback(res):
response = TextResponse(url=res.url, status=res.status_code, headers=res.headers, body=res.text, encoding='utf8')
df.callback(response)
gevent.spawn(request_worker, request, callback).join()
return df
class Downloader(object):
def __init__(self, crawler):
self.settings = crawler.settings
self.active = set()
def fetch(self, request, spider):
return download_request(request, spider)
def needs_backout(self):
return False
def close(self): pass
没看错,最简单的实现就是这样,scrapy之所以感觉复杂是因为做得完备,大项目的副作用。
替换测试下,发现确实可以处理一般的请求了。
中间件处理现在没了,但其实想支持也很简单,代码稍微改下。
from twisted.internet import reactor, defer
import requests
import gevent
from gevent import monkey
from scrapy.http import TextResponse
from scrapy.core.downloader.middleware import DownloaderMiddlewareManager
monkey.patch_socket()
def request_worker(request, callback=None): callback(requests.get(request.url))
def download_request(request, spider=None):
df = defer.Deferred()
def callback(res):
if 'Content-Encoding' in res.headers: del res.headers['Content-Encoding']
df.callback(TextResponse(url=res.url, status=res.status_code, headers=res.headers, body=res.text, encoding='utf8'))
gevent.spawn(request_worker, request, callback).join()
return df
class Downloader(object):
def __init__(self, crawler):
self.settings = crawler.settings
self.active = set()
self.middleware = DownloaderMiddlewareManager.from_crawler(crawler)
def fetch(self, request, spider):
return self.middleware.download(download_request, request, spider)
# return download_request(request, spider)
def needs_backout(self): return False
def close(self): pass
所以downloader启动流程其实就是初始化好 middleware
然后回到 scheduler,把他精简下,看看核心代码逻辑
import os
import json
import logging
from scrapy.utils.misc import load_object, create_instance
from scrapy.utils.job import job_dir
logger = logging.getLogger(__name__)
class Scheduler(object):
def __init__(self, mqclass=None, pqclass=None):
self.pqclass = pqclass
self.mqclass = mqclass
@classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
pqclass = load_object(settings['SCHEDULER_PRIORITY_QUEUE'])
mqclass = load_object(settings['SCHEDULER_MEMORY_QUEUE'])
return cls(pqclass=pqclass, mqclass=mqclass)
def has_pending_requests(self):
return len(self) > 0
def open(self, spider):
self.spider = spider
self.mqs = self.pqclass(self._newmq)
def close(self, reason):
pass
def enqueue_request(self, request):
self._mqpush(request)
return True
def next_request(self):
request = self.mqs.pop()
print('next_request', request)
return request
def __len__(self):
return len(self.mqs)
def _mqpush(self, request):
self.mqs.push(request, -request.priority)
def _newmq(self, priority):
return self.mqclass()
其实删掉的代码是持久化队列之类,一般用不上。
Spider 模块的基类
import logging
import warnings
from scrapy.http import Request
class Spider(object):
name = None
def __init__(self, name=None, **kwargs):
print('hi~~~~~~~~~~~~~~~~')
self.name = name or 'xxx'
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = cls(*args, **kwargs)
spider._set_crawler(crawler)
return spider
def _set_crawler(self, crawler):
self.crawler = crawler
self.settings = crawler.settings
def start_requests(self):
for url in self.start_urls:
yield Request(url, dont_filter=True)
def parse(self, response):
raise NotImplementedError('{}.parse callback is not defined'.format(self.__class__.__name__))
@classmethod
def update_settings(cls, settings):
pass
@staticmethod
def close(spider, reason):
pass
其实就是不断的 yield Request (执行交给 engine模块),由于具体流程需要子类实现,spider的难点是 yield 的使用。
下面代码
def test_yield():
def func():
yield '电影详情页'
for x in range(0,2):
yield '电影每一集播放页'
for e in func():
print(e)
输出如下
电影详情页
电影每一集播放页
电影每一集播放页
yield在scrapy的理解需要结合整个流程,先暂停起来。
再回到 Crawler 类
很多类都有个 from_crawler 风格的创建函数 crawler 作为必要参数。代码可以精简到(而且有不少是log和统计的非核心代码)
class Crawler(object):
def __init__(self, spidercls, settings=None):
self.spidercls = spidercls
self.settings = settings.copy()
# self.spidercls.update_settings(self.settings)
self.signals = SignalManager(self)
self.stats = load_object(self.settings['STATS_CLASS'])(self)
handler = LogCounterHandler(self, level=self.settings.get('LOG_LEVEL'))
logging.root.addHandler(handler)
lf_cls = load_object(self.settings['LOG_FORMATTER'])
self.logformatter = lf_cls.from_crawler(self)
self.settings.freeze()
self.spider = None
self.engine = None
@defer.inlineCallbacks
def crawl(self, *args, **kwargs):
try:
self.spider = self._create_spider(*args, **kwargs)
self.engine = self._create_engine()
start_requests = iter(self.spider.start_requests())
yield self.engine.open_spider(self.spider, start_requests)
yield defer.maybeDeferred(self.engine.start)
except Exception:
if self.engine is not None:
yield self.engine.close()
raise
def _create_spider(self, *args, **kwargs):
return self.spidercls.from_crawler(self, *args, **kwargs)
def _create_engine(self):
return ExecutionEngine(self, lambda _: self.stop())
@defer.inlineCallbacks
def stop(self):
yield defer.maybeDeferred(self.engine.stop)
可以看到,实际工作的是
![5a05c556e214a37445f1ed852abb2ed4.png](https://i-blog.csdnimg.cn/blog_migrate/9a2496e9becac6d6193e3c276ce1f3d7.png)
再回到 ExecutionEngine 类
首先,有个 slot 记录正在请求的Request,slot 还引用 nextcall scheduler。nextcall 就是 CallLaterOnce(self._next_request, spider),顾名思义。调用 slot.nextcall.schedule() 开始定时。实际上,直接调用 self._next_request(spider) 开始也行,不过需要改写下 _needs_backout。
由于不熟悉 yield 和 reactor,_next_request 是难点。
spider 都有 start_requests,这个没问题
![e7e8f389a8e8766a22617bf82cb88379.png](https://i-blog.csdnimg.cn/blog_migrate/736e443f46c77238d7707591be434437.png)
这个一般会找不到下一个 请求
![897e4abd5cf9813417b423e89e1716eb.png](https://i-blog.csdnimg.cn/blog_migrate/95871b3cb19a7d3da88a61a6acf4e4ab.png)
但 _next_request 会不停被调用,问题来了,_next_request怎么不停进入的?是定时检查?用户自己触发?都不是。_next_request写得比较绕,理解上有坑。
第一次是
![f4e6792ad09d7806788213ddb336cba2.png](https://i-blog.csdnimg.cn/blog_migrate/5e72a4b5ab7f7dd28c2d4c0c7321dc9c.png)
然后 self.crawl(request, spider)里面关键的是又调用了nextcall.schedule(),说白了,只要调用一次nextcall.schedule() 就会触发一次_next_request。而且看CallLaterOnce的实现会发现,可以发起多次,但只会到点触发一次。
那如果不跑self.crawl,_next_request_from_scheduler符合条件,开始了第一个请求,下一次怎么触发 _next_request?
![720f08390ff1428a790388b11d2747bd.png](https://i-blog.csdnimg.cn/blog_migrate/104657cb9c6a496b6108708ab42897d0.png)
![67ad381d90849ef9804a6aa65643d5f5.png](https://i-blog.csdnimg.cn/blog_migrate/10add4f5424e1e8a0f9b48039d16e2e8.png)
但还有个终极的保证,
![967c6dc4acae59bbcd057937d9982a6a.png](https://i-blog.csdnimg.cn/blog_migrate/65aeaedd38e793d692c9a6bc00e9e98e.png)
自己启动定时器任务检查!
![20dbda3c828288aa5142edd0afe1cf91.png](https://i-blog.csdnimg.cn/blog_migrate/556f683b9a12bfa26c743bb143daaeca.png)
彻底搞清楚循环后,就可以理解 parse 回调中的 yield 了
然后是 engine._handle_downloader_output,engine的每个请求的会掉都会跑这里
![c4172acd4bf26e98143dac433d707074.png](https://i-blog.csdnimg.cn/blog_migrate/65f2a24ab4f97df380ced40f3ee9745e.png)
![72d16056623172cc58ec411f6570894c.png](https://i-blog.csdnimg.cn/blog_migrate/58ba65b9c327320d1a774481b9370c98.png)
跟踪代码一直到
![546b7003d36b0f5bdd61b67dd4b4f054.png](https://i-blog.csdnimg.cn/blog_migrate/2e31d1183bdef89672f0ba2b9d949170.png)
所以为啥spider第一个入口叫 parse,为啥request定义要加callback
![5a9495e78a5d1c1308d7d35c986f246b.png](https://i-blog.csdnimg.cn/blog_migrate/ea2e5d1c46512b17e53d75a7995eb2fd.png)
现在,scrapy已经入门了。
然后用 scrapy的几乎都会在这里产生疑问。正常人的思维应该如下图。
![3a98ae332e39ab016c21763af53ed8e3.png](https://i-blog.csdnimg.cn/blog_migrate/88a8e86db0c7493bce7478a673a12ee9.png)
但实际例子却是用 yield
找到源码
![a4314dab10fbae8f5f6dbb21ddd6f106.png](https://i-blog.csdnimg.cn/blog_migrate/38118fb79eb99c43bb602ba33bf63463.png)
(这里要熟悉 refer 这个库,简单说就是一个回调链,建议看其他专门的文章)。
![ad87fa5dcb1fe0572b89c696a91835af.png](https://i-blog.csdnimg.cn/blog_migrate/e24539d3c95cb71e13aafa2569e9bb6a.png)
最终,就算你不用 yield,直接return,系统也会帮你变成iter(迭代器)
![ca7736ef277a7698412726383caec3c3.png](https://i-blog.csdnimg.cn/blog_migrate/06d06eb519bf1e0ee7cd225fefaa3936.png)
用 return 写其实没问题的,但推荐用yield,比如如下逻辑
![3766dda141142d9401c90986cbc1b8eb.png](https://i-blog.csdnimg.cn/blog_migrate/7ac22c99c0baf031252094615eda54f4.png)
用 return 就不优雅了。
理解启动流程中多次出现的 yield的意义。
![ab3024a419aeca9e00a6c813fce94980.png](https://i-blog.csdnimg.cn/blog_migrate/b1fcafba50bc8857cf88aef7fa2dbeb0.png)
![d46c70ffbf9e5eeba15313127704bb73.png](https://i-blog.csdnimg.cn/blog_migrate/462cb5cfaaa8fb0eca99ca2b78236e62.png)
我发现这些 yield 有些去掉就跑不起来。首先 yield 嵌套是深度优先的
![6ef527a8b126ccad9f231e7b4f48bf6a.png](https://i-blog.csdnimg.cn/blog_migrate/37c8e16a4652b4973c1cea30266e2e2a.png)
所以 crawler.crawl 中 engine.open_spider 是先全部迭代执行完后才到 engine.start 内容。但 engine open_spider 中又有 yield。启动的关键代码看不出有触发这些迭代器的样子。
![2460d7e1ea4d8bc43ead7aecab3364be.png](https://i-blog.csdnimg.cn/blog_migrate/c8aa6d9cb49902657dc9bcb01e32b256.png)
其实关键是这个
![dcdca1c8f7dc4f4fff432f3687a5a3cd.png](https://i-blog.csdnimg.cn/blog_migrate/b5c5be45ef8ca11f75ff7f63d936500d.png)
具体需要另外说
linx:scrapy 启动时的 inlineCallbackszhuanlan.zhihu.com最后的 item pipelines
在 Scraper 类中,
![c29872c099f47ee9b2353edc82f88aee.png](https://i-blog.csdnimg.cn/blog_migrate/db3f2c8d1fd3ef9fa8dd7215664c10c9.png)
触发在
![0d07497b2a24d5a2998258112b3becf8.png](https://i-blog.csdnimg.cn/blog_migrate/67f7778cf2df4616cc94d95fa7a7b799.png)
![e5a7da4ef100588d08679a06f297131a.png](https://i-blog.csdnimg.cn/blog_migrate/c0a950b8731e481e3ef6d5026a4e1a16.png)
对于数据的保存 完全可以自己写一套服务和scrapy通信就行,所以item pipelines并不是scrapy框架中必要的角色。
到此,我们自己也可以写一个类似scrapy的简陋的demo框架了,
可以基于twisted,也可以到github找独立的 defer 库