07-scrapy的高阶用法

发布时间 2024-01-02 11:28:23作者: Way*yy

爬虫中间件和下载中间件

爬虫中间件

# 第一步:写个爬虫中间件类
	class ScrapyDemoSpiderMiddleware:
    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 走架构图第1步,会触发这里
    def process_spider_input(self, response, spider):
        # Called for each response that goes through the spider
        # middleware and into the spider.

        # Should return None or raise an exception.
        return None

    # 架构图,第1,7步走这里
    def process_spider_output(self, response, result, spider):
        # Called with the results returned from the Spider, after
        # it has processed the response.

        # Must return an iterable of Request, or item objects.
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        # Called when a spider or process_spider_input() method
        # (from other spider middleware) raises an exception.

        # Should return either None or an iterable of Request or item objects.
        pass

    # 架构图第一步
    def process_start_requests(self, start_requests, spider):
        # Called with the start requests of the spider, and works
        # similarly to the process_spider_output() method, except
        # that it doesn’t have a response associated.

        # Must return only requests (not items).
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)
        
        
 # 2 配置文件配置
	SPIDER_MIDDLEWARES = {
   "scrapy_demo.middlewares.ScrapyDemoSpiderMiddleware": 543,
}

下载中间件(代理,加请求头,加cookie)

class ScrapyDemoDownloaderMiddleware:

    @classmethod
    def from_crawler(cls, crawler):
        # This method is used by Scrapy to create your spiders.
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    def process_request(self, request, spider):
        # - return None: 继续执行当次请求,继续走下一个中间件--->如果中间件没了--->执行下载
        # - return Response :直接把Response返回给引擎,引擎交给爬虫去解析
        # - return Request :把request返回给引擎,引擎给调度器,等待下一次被爬取
        # - 直接抛异常: 触发process_exception执行
        return None

    def process_response(self, request, response, spider):
        # Must either;
        # - return Response:正常爬取完--->给引擎--->引擎给爬虫去解析
        # - return Request: 爬取失败-->给引擎-->引擎给调度器-->等待下次爬取
        # - 抛异常  :走到process_exception
        return response

    def process_exception(self, request, exception, spider):
        # Called when a download handler or a process_request()
        # (from other downloader middleware) raises an exception.

        # Must either:
        # - return None: continue processing this exception
        # - return a Response object: stops process_exception() chain
        # - return a Request object: stops process_exception() chain
        pass

    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

加请求头(加到请求对象中)

# faker 模块 :随机生成假数据
# pip install fake_useragent:随机生成请求头

###  加referer,加token 加 user-agent
def process_request(self, request, spider):
        #### 加请求头
        print(request.headers)
        request.headers['referer'] = 'http://www.lagou.com'
        request.headers['token'] = 'asdfasdf.asdfads.asfdasfd'
        # user-agent--->写死了--->想随机请求头
        from fake_useragent import UserAgent
        ua = UserAgent()
        request.headers['User-Agent'] = str(ua.random)
        print(request.headers)
        return None
    
    
 

加cookie

    def process_request(self, request, spider):
        print(request.cookies)
        request.cookies['name']='lqz'
        return None

加代理

# 在下载中间件的def process_request(self, request, spider):写代码

# 第一步:
	-在下载中间件写process_request方法
   	 def get_proxy(self):
        import requests
        res = requests.get('http://127.0.0.1:5010/get/').json()
        if res.get('https'):
            return 'https://' + res.get('proxy')
        else:
            return 'http://' + res.get('proxy')
        
        
        
     def process_request(self, request, spider):
            #request.meta['proxy'] = self.get_proxy()
            request.meta['proxy'] = 'http://192.168.11.11:8888'
            return None
        
# 第二步:代理可能不能用,会触发process_exception,在里面写
    def process_exception(self, request, exception, spider):
        print('-----',request.url)  # 这个地址没有爬
        return request

scrapy集成selenium

# 使用scrapy默认下载器--->类似于requests模块发送请求,不能执行js,有的页面拿回来数据不完整

# 想在scrapy中集成selenium,获取数据更完整,获取完后,自己组装成 Response对象,就会进爬虫解析,现在解析的是使用selenium拿回来的页面,数据更完整


# 集成selenium 因为有的页面,是执行完js后才渲染完,必须使用selenium去爬取数据才完整


# 保证整个爬虫中,只有一个浏览器器
# 只要爬取 下一页这种地址,使用selenium,爬取详情,继续使用原来的

# 第一步:在爬虫类中写
from selenium import webdriver
class CnblogsSpider(scrapy.Spider):
    bro = webdriver.Chrome() # 使用无头
    bro.implicitly_wait(10)
    def close(spider, reason):
        spider.bro.close() #浏览器关掉
        
# 第二步:在中间件中
    def process_request(self, request, spider):
        # 爬取下一页这种地址--->用selenium,但是文章详情,就用原来的
        if 'sitehome/p' in request.url:
            spider.bro.get(request.url)
            from scrapy.http.response.html import HtmlResponse
            response = HtmlResponse(url=request.url, body=bytes(spider.bro.page_source, encoding='utf-8'))
            return response
        else:
            return None

源码去重规则(布隆过滤器)

# 如果爬取过的地址,就不会再爬了,scrapy 自带去重

# 调度器可以去重,研究一下,如何去重的--->使用了集合


# 要爬取的Request对象,在进入到scheduler调度器排队之前,先执行enqueue_request,它如果return False,这个Request就丢弃掉,不爬了---->如何判断这个Request要不要丢弃掉,执行了self.df.request_seen(request),它来决定的----->RFPDupeFilter类中的方法---->request_seen--->会返回True或False---->如果这个request在集合中,说明爬过了,就return True,如果不在集合中,就加入到集合中,然后返回False



# 调度器源码
from scrapy.core.scheduler import Scheduler
	# 这个方法如果return True表示这个request要爬取,如果return False表示这个网址就不爬了(已经爬过了)
    def enqueue_request(self, request: Request) -> bool:
        # request当次要爬取的地址对象
        if self.df.request_seen(request):
            # 有的请情况,在爬虫中解析出来的网址,不想爬了,就就可以指定
            # yield Request(url=url, callback=self.detail_parse, meta={'item': item},dont_filter=True)
            # 如果符合这个条件,表示这个网址已经爬过了 
            return False
        return True
    
    
    
# self.df 去重类 是去重类的对象 RFPDupeFilter-->配置文件配置的
    -在配置文件中如果配置了:DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'表示,使用它作为去重类,按照它的规则做去重
    -RFPDupeFilter的request_seen
       def request_seen(self, request: Request) -> bool:
        # request_fingerprint 生成指纹
        fp = self.request_fingerprint(request) #request当次要爬取的地址对象
        #判断 fp 在不在集合中,如果在,return True
        if fp in self.fingerprints:
            return True
        #如果不在,加入到集合,return False
        self.fingerprints.add(fp)
        return False

    
# 传进来是个request对象,生成的是指纹
	-爬取的网址:https://www.cnblogs.com/teach/p/17238610.html?name=lqz&age=19
    -和        https://www.cnblogs.com/teach/p/17238610.html?age=19&name=lqz
    -它俩是一样的,返回的数据都是一样的,就应该是一条url,就只会爬取一次
    -所以 request_fingerprint  就是来把它们做成一样的(核心原理是把查询条件排序,再拼接到后面)
    
   
    -生成指纹,指纹是什么? 生成的指纹放到集合中去重
    	-www.cnblogs.com?name=lqz&age=19
        -www.cnblogs.com?age=19&name=lqz
        -上面的两种地址生成的指纹是一样的
        # 测试指纹
        from scrapy.utils.request import RequestFingerprinter
        from scrapy import Request

        fingerprinter = RequestFingerprinter()
        request1 = Request(url='http://www.cnblogs.com?name=lqz&age=20')
        request2 = Request(url='http://www.cnblogs.com?age=20&name=lqz')

        res1 = fingerprinter.fingerprint(request1).hex()
        res2 = fingerprinter.fingerprint(request2).hex()
        print(res1)
        print(res2)
        
        
# 集合去重,集合中放 
# a一个bytes
# 假设爬了1亿条url,放在内存中,占空间非常大
a6af0a0ffa18a9b2432550e1914361b6bffcff1a
a6af0a0ffa18a9b2432550e191361b6bffc34f1a

# 想一种方式,极小内存实现去重--->布隆过滤器
https://zhuanlan.zhihu.com/p/94668361

布隆过滤器

# 极小内存实现去重:
# 应用场景:爬虫去重,避免缓存穿透,垃圾邮件过滤

# bloomfilter:是一个通过多哈希函数映射到一张表的数据结构,能够快速的判断一个元素在一个集合内是否存在,具有很好的空间和时间效率。(典型例子,爬虫url去重)
#布隆案例

# from pybloom_live import ScalableBloomFilter
# bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)
# url = "www.cnblogs.com"
# url2 = "www.liuqingzheng.top"
# bloom.add(url)
# print(url in bloom)
# print(url2 in bloom)


from pybloom_live import BloomFilter
bf = BloomFilter(capacity=1000)
url='www.baidu.com'
bf.add(url)
print(url in bf)
print("www.liuqingzheng.top" in bf)
from scrapy.dupefilters import BaseDupeFilter
from scrapy.utils.request import RequestFingerprinter
from pybloom_live import ScalableBloomFilter


class MyPDupeFilter(BaseDupeFilter):
    fingerprints = ScalableBloomFilter(initial_capacity=100, error_rate=0.001,
                                       mode=ScalableBloomFilter.LARGE_SET_GROWTH)
    fingerprinter = RequestFingerprinter()

    def request_seen(self, request):
        print('zoule')
        fp = self.request_fingerprint(request)
        if fp in self.fingerprints:
            return True
        self.fingerprints.add(fp)
        return False

    def request_fingerprint(self, request) -> str:
        return self.fingerprinter.fingerprint(request).hex()

分布式爬虫

# 原来scrapy的Scheduler维护的是本机的任务队列(待爬取的地址)+本机的去重队列(放在集合中)--->在本机内存中
# 如果把scrapy项目,部署到多台机器上,多台机器爬取的内容是重复的


# 所以实现分布式爬取的关键就是,找一台专门的主机上运行一个共享的队列比如Redis,
然后重写Scrapy的Scheduler,让新的Scheduler到共享队列存取Request,并且去除重复的Request请求,所以总结下来,实现分布式的关键就是三点:
    #1、多台机器共享队列
    #2、重写Scheduler,让其无论是去重还是任务都去访问共享队列
    #3、为Scheduler定制去重规则(利用redis的集合类型)
    
    
# scrapy-redis实现分布式爬虫
	-公共的去重
    -公共的待爬取地址队列
    
    
    
#  使用步骤
	0 下载:pip2 install scrapy-redis
	1 把之前爬虫类,继承class CnblogsSpider(RedisSpider):
    2 去掉起始爬取的地址,加入一个类属性
    	redis_key = 'myspider:start_urls'  # redis列表的key,后期我们需要手动插入起始地址
        
     3 配置文件中配置
        DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"  # scrapy redis去重类,使用redis的集合去重
        # 不使用原生的调度器了,使用scrapy_redis提供的调度器,它就是使用了redis的列表
        SCHEDULER = "scrapy_redis.scheduler.Scheduler"
        REDIS_HOST = 'localhost'                            # 主机名
        REDIS_PORT = 6379                                   # 端口
        ITEM_PIPELINES = {
       # 'mysfirstscrapy.pipelines.MyCnblogsPipeline': 300,
       'mysfirstscrapy.pipelines.MyCnblogsMySqlPipeline': 301,
        'scrapy_redis.pipelines.RedisPipeline': 400,
    }
        
    4 再不同多台机器上运行scrapy的爬虫,就实现了分布式爬虫
    5 写入到redis的列表中起始爬取的地址:列表key:myspider:start_urls
         rpush myspider:start_urls https://www.cnblogs.com