Package: easy-spider
Easy Spiderrelease note1.0.13: 添加自动保存功能1.1.2: 引入中间件处理，优化 easy_spider.core.Spider 中对请求进行处理的逻辑。quick startfromeasy_spiderimportasync_env,AsyncSpider,Request,HTMLResponseclassMySpider(AsyncSpider):definit(self):super().__init__()self.start_targets=["https://github.blog/"]defhandle(self,response:HTMLResponse):titles=response.bs.select(".post-list__item a")print([title.textfortitleintitles])async_env.run(MySpider())这段代码定义了一个spider对象，并爬取了github-blog中文章的标题。其中主要包含了init以及handle两个方法。在init方法中可以对爬虫的参数进行设置。例如self.start_targets = ["https://github.blog/"]设置了初始爬取目标。而handle(self, response: HTMLResponse)方法主要用于对服务器返回的内容进行处理。response.bs表示一个由返回的网页构造的Beautiful Soup4对象，你可以用它来提取需要的内容。发现新链接handle方法除了处理服务器返回的响应对象Response以外，还有一个重要的功能则是发现新的链接。在easy_spider中，一个新的链接需要组装成一个请求对象Request。请求对象除了包含需要爬取的链接外，还包含爬取这个链接的参数或Cookies等内容，这些内容大部分具有默认值。一个跟随链接的示例为:fromeasy_spiderimportasync_env,AsyncSpider,Request,HTMLResponseclassMySpider(AsyncSpider):definit(self):self.start_targets=["https://github.blog/"]defhandle(self,response:HTMLResponse):forainresponse.bs.select("a"):if"href"ina.attrs:yieldRequest.of(response.url_join(a.attrs["href"]))response.url_join方法将网页中的相对链接变为绝对链接，例如从page/2变为https://github.blog/2。Request.of方法利用url创建具有默认参数的Request对象。handle方法可以是一个生成器或者也可以直接返回可迭代Request对象集合。所有handle产生的新Request对象都将放入待请求队列中，逐个进行请求。当某个请求完成后，将继续调用handle方法进行处理。easy_spider中已经实现了一个简单灵活的发现新链接的方法，你可以直接:fromeasy_spiderimportasync_env,AsyncSpider,Request,HTMLResponseclassMySpider(AsyncSpider):definit(self):self.start_targets=["https://github.blog/"]defhandle(self,response:HTMLResponse):yield fromsuper().handle(response)# 默认发现新连接的方法发现response中的所有链接。进阶使用使用过滤器在默认的情况下，除初始请求外的所有请求都将使用过滤器Filter进行过滤。过滤器接收一个参数，并返回布尔值，表示接受或是拒绝这个参数。在定义spider对象时可以指定self.filter，表示不被该filter接收的请求都不会放入请求队列中。例如:classMySpider(AsyncSpider):definit(self):self.start_targets=["https://github.blog/"]self.filter=URLRegFilter(r"^https://github\.blog.+")defhandle(self,response:HTMLResponse):forainresponse.bs.select("a"):if"href"ina.attrs:yieldRequest.of(response.url_join(a.attrs["href"]))在handle方法中虽然提取了所有的 URL，但采用URLRegFilter对其进行了过滤。只有URL满足r"^https://github\.blog.+"的请求会被放入队列，否则将被丢弃。在easy_spider中主要包含普通过滤器以及去重过滤器两种类型的过滤器，普通过滤器有如下几种:RegexFilter: 正则表达式过滤器static_filter: 静态文件过滤器(避免爬取到 jpg|css 等类型文件)url_filter: 合法 URL 过滤器html_filter: 响应类型为 html 的 URL 过滤器(通过后缀判定，有一定的误判几率)all_pass_filter: 全部接收过滤器all_reject_filter: 全部拒绝过滤器GenerationFilter: 拒绝深度大于某个值的请求过滤器支持某些运算符操作，例如static_filter虽然表示接受静态文件，但在如果-static_filter则变为拒绝静态文件。这里设有两个过滤器为f1 f2，则有:f1+f2:f1 f2同时接收则接受，否则拒绝f1-f2：f1接受同时f2拒绝则接受，否则拒绝f1|f2：f1接受或者f2拒绝则接受，否则拒绝-f1:f1拒绝则接受，否则拒绝所有的运算符支持多个过滤器级联使用。self.filter默认为html_filter，实际上html_filter = url_filter - static_filter， 即提取接受所有具有合法URL[1]且不为静态文件的请求。GenerationFilter并不对请求的 URL 进行检查，而是检查请求的深度(Request.generation)，每一个初始请求的深度为0，初始请求产生的请求深度为1，深度为1的请求产生的请求深度为2。easy_spider爬取的方法为广度优先。去重过滤器还包含了BloomFilter以及HashFilter对已完成的请求进行过滤，即URL去重的功能。easy_spider默认采用HashFilter进行去重，可以将self.crawled_filter[2] 修改为BloomFilter以节约内存。某些时候如<a href="javascript: callback">click</a>提取的URL为javascript: callback，不是合法的URL 。这里必须设置self.crawled_filter而不是使用self.filter。这去重过滤器也无法和普通的过滤器进行运算，否则无法实现去重的功能。请求对象使用默认参数的Request对象能满足大部分的需求，但是Request也支持许多自定义的参数:method: str = 请求方法 GET|POST|PUT|DELETE 
timeout: int = 超时时间
headers: dict = 请求头，默认{}
cookies: dict = cookies，默认{}
encoding: str = 编码方式，默认utf-8
params: dict = URL参数，默认{}
data: dict = 数据，取决于 data_format
data_format: str = 数据格式 FORM|JSON，若为 FORM 数据通过 application/x-www-form-urlencoded 格式传递，若为 JSON 则通过 application/json。
proxy: str = 代理，默认为空
handler: str = 处理该 Request 的回调函数其中handler属性用于定义该请求完成时的回调处理函数。默认情况下，将采用Spider对象中的handle方法。当然你也可以设置任何自己的方法进行处理。响应对象一个请求对象经由AsyncClient[1]，发起请求后将返回一个响应对象，即在 handle 方法中处理的对象。easy_spider默认根据content_type生成三种不同的响应对象，如果content_type包含text/html则HtmlResponse，如果content_type包含除text/html以外的text/*对象，则返回TextResponse，否则返回Response。其中不同的响应对象有不同的属性，从Response -> TextResponse -> HtmlResponse逐渐增多。Response对象具有的属性:headers: HTTP 响应头body: HTTP 响应体，为未解码的 bytes 类型url: 响应对象的URLrequest: 产生该响应对应的请求TextResponse在Response基础上增加了:text: 解码后的文本内容，TextResponse会自动猜测body的编码类型进行解码，尽量避免乱码产生HtmlResponse在TextResponse基础上增加了:bs:  利用 text 属性构建的Beautiful Soup4对象默认请求参数很多时候需要定义一系列默认参数，而不是在每个请求中都声明，在easy_spider中所有定义在spider中与请求对象相同的属性都将复制给每一个新产生的请求，包括初始请求:classMySpider(AsyncSpider):definit(self):self.start_targets=["https://github.blog/"]self.cookies={"key":"value"}# 用于设置默认请求参数defhandle(self,response:HTMLResponse):urls=[response.url_join(a.attrs["href"])forainresponse.bs.select("a")]yieldself.from_url(urls)# 所有请求的 cookies 都将设置与 self.cookies 相同自定义处理方法可以通过设置请求对象的handler属性来设置处理该请求返回的响应:fromeasy_spiderimportasync_env,AsyncSpider,Request,HTMLResponseclassMySpider(AsyncSpider):definit(self):self.cookies={"key":"value"}# 用于设置默认请求参数self.start_targets=["https://github.blog/"]defhandle(self,response:HTMLResponse):urls=[response.url_join(a.attrs["href"])forainresponse.bs.select("a")]forurlinurls:ifurl.endswith("xxx"):yieldRequest(url,handler=self.handle_other)# 该请求返回的 response 将由 handle_other 方法处理else:yieldRequest(url,handler=self.handle)defhandle_other(self,response):# do somethingpassasync_env.run(MySpider())请求中间件在easy_spider中，所有的请求都将经过请求中间件的处理。请求中间件是一系列对请求进行变换的类，其都继承于RequestMiddleware，其定义如下:classRequestMiddleware(ABC):@abstractmethoddeftransform(self,requests:Iterable[Request],response:Optional[Response])->Iterable[Request]:pass其核心方法transform接受一个可迭代的请求集合，以及产生这些请求的Response对象如果为初始请求，则Response对象为None。transform根据这些参数产生新的可迭代的请求集合。easy_spider许多功能都通过中间件实现，如过滤器、去重过滤器以及默认参数。例如ExtractorFilterMiddleware(self.filter)接受self.filter为参数，将所有self.filter拒绝的请求从输入的请求集合中除去。 默认采用了以下中间件:defmiddlewares(self):returnChainMiddleware(GenerationMiddleware(),# 设置请求为第 n 代，支持GenerationFilter 过滤器ExtractorFilterMiddleware(self.filter),# 用于支持过滤器FilterMiddleware(self.crawled_filter),# 支持去重过滤器SetAttrMiddleware(self))# 用于设置请求默认参数其中ChainMiddleware用于将多个中间件合并为一个中间件进行执行[1]。用于可以编写自己的请求中间并重写spider对象的middlewares方法以扩展easy_spider的功能。例如某些时候需要将初始爬取目标保存在文件中，使用时读取:fromtypingimportIterable,Optionalfromeasy_spiderimportasync_env,AsyncSpider,Request,HTMLResponse,Responsefromeasy_spider.middlewares.build_inimportRequestMiddleware,ChainMiddlewarefromeasy_spider.toolimportget_abs_pathfromeasy_spiderimportGenerationFilterfromos.pathimportjoinclassLoadFileMiddleware(RequestMiddleware):deftransform(self,requests:Iterable[Request],response:Optional[Response])->Iterable[Request]:ifnotresponse:# 如果为初始请求，则从文件中构造请求url=list(requests)[0].urlwithopen(join(get_abs_path(__file__),url),encoding='utf-8')asfd:forlineinfd.readlines():yieldRequest.of(line.strip("\n"))else:# 否则不做任何事情yield fromrequestsclassMySpider(AsyncSpider):definit(self):self.start_targets=["urls.txt"]defhandle(self,response:HTMLResponse):titles=response.bs.select(".post-list__item a")print([title.textfortitleintitles])defmiddlewares(self):returnChainMiddleware(LoadFileMiddleware()).extend(super().middlewares())async_env.run(MySpider())其中，middlewares中采用ChainMiddleware(LoadFileMiddleware()).extend(super().middlewares())将新的中间件以及默认的中间件结合在一起[2]，形成新的中间件并返回。ChainMiddleware将所有中间件按照传入的顺序进行执行如果你不是十分了解默认中间件执行的功能以及去掉它所产生的影响，建议在自定义中间件时一定要保留默认中间件，并使得自定义中间件在默认中间件前执行。恢复爬虫easy_spider实现了两种可恢复的爬虫，一种是当用户主动结束爬虫，一种是被意外关闭的爬虫。对于第一种，easy_spider捕获了ctrl + c，当你按下ctrl + c时会进行一系列询问，你可以选择是否保存你的爬虫[1]。如果第二次运行该爬虫，如果检测已到保存的记录，会询问你是否继续。对于第二种，easy_spider提供了自动保存的功能。你可以继承RecoverableSpider来实现可恢复的爬虫。该爬虫对象与AsyncSpider使用方法相同，只是多了需要设置的参数:name: 爬虫的名称auto_save_frequence: 自动保存频率name将决定爬虫保存的路径，而auto_save_frequence将决定爬虫自动保存的频率。auto_save_frequence = 1000表示每进行1000请求则保存一次爬虫。auto_save_frequence = 0则不进行自动保存。爬虫保存的内存包括已爬取的请求以及为爬取的请求两大部分，其中保存的文件位置为代码执行目录/.easy_spider/{name}其中{name}爬虫的属性self.name。请求溢出请求溢出是指当请求达到一定数量时将请求溢出到磁盘中已节约内存，easy_spider默认已经开启了此功能，可以设置self.num_of_spill = 0来关闭。同样self.num_of_spill也指定了溢出的门限值，即若请求达到self.num_of_spill * 2则将其中的self.num_of_spill请求溢出到磁盘中。详细的溢出逻辑请参考SpillRequestQueueProxy。