爬虫--scrapy框架
twisted介绍
Twisted是用Python实现的基于事件驱动的网络引擎框架,scrapy正是依赖于twisted,从而基于事件循环机制实现爬虫的并发。
scrapy的项目结构目录
project_name/ scrapy.cfg project_name/ __init__.py items.py pipelines.py settings.py spiders/ __init__.py 爬虫1.py 爬虫2.py 爬虫3.py
文件说明:
- scrapy.cfg 项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)
- items.py 设置数据存储模板,用于结构化数据,如:Django的Model
- pipelines 数据处理行为,如:一般结构化的数据持久化
- settings.py 配置文件,如:递归的层数、并发数,延迟下载等
- spiders 爬虫目录,如:创建文件,编写爬虫规则
scrapy的pipeline文件和items文件
# -*- coding: utf-8 -*- import scrapy class ChoutiSpider(scrapy.Spider): ''' 爬去抽屉网的帖子信息 ''' name = 'chouti' allowed_domains = ['chouti.com'] start_urls = ['http://chouti.com/'] def parse(self, response): # 获取帖子列表的父级div content_div = response.xpath('//div[@id="content-list"]') # 获取帖子item的列表 items_list = content_div.xpath('.//div[@class="item"]') # 打开一个文件句柄,目的是为了将获取的东西写入文件 with open('articles.log','a+',encoding='utf-8') as f: # 循环item_list for item in items_list: # 获取每个item的第一个a标签的文本和url链接 text = item.xpath('.//a/text()').extract_first() href = item.xpath('.//a/@href').extract_first() # print(href, text.strip()) # print('-'*100) f.write(href+'\n') f.write(text.strip()+'\n') f.write('-'*100+'\n') # 获取分页的页码,然后让程序循环爬去每个链接 # 页码标签对象列表 page_list = response.xpath('//div[@id="dig_lcpage"]') # 循环列表 for page in page_list: # 获取每个标签下的a标签的url,即每页的链接 page_a_url = page.xpath('.//a/@href').extract() # 将域名和url拼接起来 page_url = 'https://dig.chouti.com' + page_a_url # 重要的一步!!!! # 导入Request模块,然后实例化一个Request对象,然后yield它 # 就会自动执行Request对象的callback方法,爬去的是url参数中的链接 from scrapy.http import Request yield Request(url=page_url,callback=self.parse)
在这个示例中,我们通过chouti.py一个文件的parse方法实现了爬抽屉网的新闻并将之保存在文件中的功能,但是会发现有两个问题:
1、在循环爬去每一页的时候,每次都需要重新打开然后再关闭文件
2、我们将解析和数据持久化都放在了同一个文件的同一个方法中,没有做到分工明确
解决这两个问题就需要用到pipeline文件和items文件
用法:
如果我们要使用这两个文件从而解决问题,则需要有四部操作:
a.编写pipeline文件中的类,格式如下:
class XXXPipeline(object): def process_item(self, item, spider): return item
b.编写items文件中的类,格式如下:
class XXXItem(scrapy.Item): href = scrapy.Field() title = scrapy.Field()
c.配置settings文件
ITEM_PIPELINES = { 'xxx.pipelines.XXXPipeline': 300, # 'xxx.pipelines.XXXPipeline2': 600, # 后面的数字为优先级,数字越大,优先级月底 }
d.在parse方法中yield一个Item对象
from xxx.items import XXXItem def parse(self, response): ... yield XXXItem(text=text,href=href)
执行流程为:
当我们在执行爬虫的parse方法的时候,scrapy一旦解析到有yield XXXitem的语句,就会到配置文件中找ITEM_PIPELINES的配置项,进而找到XXXPipeline类,然后执行其中的方法,我们就可以在方法中做很多操作,当然pipeline中不止process_item一个方法。
Pipeline中的方法详解
class FilePipeline(object): def __init__(self,path): self.f = None self.path = path @classmethod def from_crawler(cls, crawler): """ 初始化时候,用于创建pipeline对象 :param crawler: :return: """ # 从配置文件中获取配置好的文件存放目录 path = crawler.settings.get('HREF_FILE_PATH') return cls(path) def open_spider(self,spider): """ 爬虫开始执行时,调用 :param spider: :return: """ self.f = open(self.path,'a+') def process_item(self, item, spider): # 在这里做持久化 self.f.write(item['href']+'\n') return item # 交给下一个pipeline的process_item方法 # raise DropItem()# 如果写上这一句,后续的 pipeline的process_item方法不再执行 def close_spider(self,spider): """ 爬虫关闭时,被调用 :param spider: :return: """ self.f.close()
去重(两种方法)
方法一:scrapy内部实现的去重方法
其实scrapy内部在循环爬页码的时候,已经帮我们做了去重功能,
因为我们在首页可以看到1,2,3,4,5,6,7,8,9,10页的页码以及连接,当爬虫爬到第二页的时候,
还是可以看到这10个页面及连接,然后它并没有再重新把第一页爬一遍。
它内部实现去重的原理是,将已爬去的网址存入一个set集合里,每次爬取新页面的时候就先看一下是否在集合里面
如果在,就不再爬去,如果不在就爬取,然后再添加入到set里。当然,这个集合存放的不是原网址,
而是将链接通过request_fingerprint()方法将它变成一个类似于md5的值,这样可以节省存储空间
自定义去重
1.编写DupeFilter类
from scrapy.dupefilter import BaseDupeFilter from scrapy.utils.request import request_fingerprint class XXXDupeFilter(BaseDupeFilter): def __init__(self): '''初始化一个集合,用来存放爬去过的网址''' self.visited_fd = set() @classmethod def from_settings(cls, settings): ''' 如果我们自定义了DupeFilter类并且重写了父类的该方法, scrapy会首先执行该方法,获取DupeFilter对象, 如果没有定义,则会执行init方法来获取对象 ''' return cls() def request_seen(self, request): '''在此方法中做操作,判断以及添加网址到set里''' # 将request里的url转换下,然后判断是否在set里 fd = request_fingerprint(request=request) # 循环set集合,如果已经在集合里,则返回True,爬虫将不会继续爬取该网址 if fd in self.visited_fd: return True self.visited_fd.add(fd) def open(self): # can return deferred '''开始前执行此方法''' print('开始') def close(self, reason): # can return a deferred '''结束后执行此方法''' print('结束') def log(self, request, spider): # log that a request has been filtered '''在此方法中可以做日志操作''' print('日志')
2.配置settings文件
# 修改默认的去重规则 # DUPEFILTER_CLASS = 'scrapy.dupefilter.RFPDupeFilter' DUPEFILTER_CLASS = 'xxx.dupefilters.XXXDupeFilter'
深度
深度就是爬虫所要爬取的层级,限制深度需要配置一下settings
# 限制深度 DEPTH_LIMIT = 3
获取cookie
获取上一次请求之后获得的cookie
from scrapy.http.cookies import CookieJar class ChoutiSpider(scrapy.Spider): name = 'chouti' allowed_domains = ['chouti.com'] start_urls = ['https://dig.chouti.com/'] cookie_dict = {} def parse(self, response): # 去响应头中获取cookie,cookie保存在cookie_jar对象 cookie_jar = CookieJar() cookie_jar.extract_cookies(response, response.request) # 去对象中将cookie解析到字典 for k, v in cookie_jar._cookies.items(): for i, j in v.items(): for m, n in j.items(): self.cookie_dict[m] = n.value
再次请求的时候携带cookie
yield Request( url='https://dig.chouti.com/login', method='POST', body="phone=861300000000&password=12345678&oneMonth=1",# cookies=self.cookie_dict, headers={ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, callback=self.check_login )
中间件
class SpiderMiddleware(object): def process_spider_input(self,response, spider): """ 下载完成,执行,然后交给parse处理 :param response: :param spider: :return: """ pass def process_spider_output(self,response, result, spider): """ spider处理完成,返回时调用 :param response: :param result: :param spider: :return: 必须返回包含 Request 或 Item 对象的可迭代对象(iterable) """ return result def process_spider_exception(self,response, exception, spider): """ 异常调用 :param response: :param exception: :param spider: :return: None,继续交给后续中间件处理异常;含 Response 或 Item 的可迭代对象(iterable),交给调度器或pipeline """ return None def process_start_requests(self,start_requests, spider): """ 爬虫启动时调用 :param start_requests: :param spider: :return: 包含 Request 对象的可迭代对象 """ return start_requests
class DownMiddleware1(object): def process_request(self, request, spider): """ 请求需要被下载时,经过所有下载器中间件的process_request调用 :param request: :param spider: :return: None,继续后续中间件去下载; Response对象,停止process_request的执行,开始执行process_response Request对象,停止中间件的执行,将Request重新调度器 raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception """ pass def process_response(self, request, response, spider): """ spider处理完成,返回时调用 :param response: :param result: :param spider: :return: Response 对象:转交给其他中间件process_response Request 对象:停止中间件,request会被重新调度下载 raise IgnoreRequest 异常:调用Request.errback """ print('response1') return response def process_exception(self, request, exception, spider): """ 当下载处理器(download handler)或 process_request() (下载中间件)抛出异常 :param response: :param exception: :param spider: :return: None:继续交给后续中间件处理异常; Response对象:停止后续process_exception方法 Request对象:停止中间件,request将会被重新调用下载 """ return None
自定义启动项目文件
多爬虫项目:
在spiders同级创建任意目录, 例如:目录名commands
from scrapy.commands import ScrapyCommand class Command(ScrapyCommand): requires_project = True def syntax(self): return '[options]' def short_desc(self): return 'Runs all of the spiders' def run(self, args, opts): spider_list = self.crawler_process.spiders.list() for name in spider_list: self.crawler_process.crawl(name, **opts.__dict__) self.crawler_process.start()
在其中创建crawlall.py文件(文件名就是自定义的命令名)
在settings.py中添加配置COMMANDS_MODULE = '项目名称.目录名称'
在项目目录(和.cfg同一级)创建start文件
import sys from scrapy.cmdline import execute if __name__ == '__main__': execute(["scrapy","crawlall","--nolog"])
启动时,可直接执行start.py文件即可
单爬虫项目:
import sys from scrapy.cmdline import execute if __name__ == '__main__': execute(["scrapy","github","--nolog"])
信号:类
from scrapy import signals class MyExtend(object): def __init__(self): pass @classmethod def from_crawler(cls, crawler): self = cls() crawler.signals.connect(self.x1, signal=signals.spider_opened) crawler.signals.connect(self.x2, signal=signals.spider_closed) return self def x1(self, spider): print('open') def x2(self, spider): print('close')
在settings里配置
EXTENSIONS = {'xdb.ext.MyExtend': 666}