Scrapy

一 scrapy介绍安装--架构

1.1 安装

Scrapy 是一个爬虫框架(底层代码封装好了,只需要在固定位置写固定代码即可),应用领域比较广泛--->爬虫界的django


# 安装
#Windows平台
    1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
    3、pip3 install lxml
    4、pip3 install pyopenssl
    5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
    6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
    8、pip3 install scrapy

#Linux,mac平台
    1、pip3 install scrapy

1.2 scrapy架构

# 引擎(EGINE):引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。大总管,负责整个爬虫数据的流动

# 调度器(SCHEDULER)用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL的优先级队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址

#下载器(DOWLOADER) 用于下载网页内容, 并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的(效率很高,同时可以发送特别多请求出出)

#爬虫(SPIDERS) SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求

#项目管道(ITEM PIPLINES) 在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作

# 下载器中间件(Downloader Middlewares)位于Scrapy引擎和下载器之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response,你可用该中间件做以下几件事

# 爬虫中间件(Spider Middlewares)位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)


1.3 创建scrapy项目

# 创建scrapy项目--->使用命令cmd
	# 创建项目
	-scrapy startproject 项目名字
	# 创建爬虫 
    -scrapy genspider 爬虫名 爬虫网址
    -scrapy genspider cnblogs www.cnblogs.com
    
    # 启动爬虫
    scrapy crawl cnblogs
	

# 使用脚本运行爬虫
# 小myfirstcrawl/run.py
from scrapy.cmdline import execute

execute(['scrapy', 'crawl', 'cnblogs'])
# execute(['scrapy', 'crawl', 'cnblogs', '--nolog'])  不产生日志

spiders/cnblogs.py

import scrapy


# 爬虫类,继承了scrapy.Spider
class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['www.cnblogs.com']  # 允许爬取的域--->
    start_urls = ['http://www.cnblogs.com/']  # 开始爬取的地址

    def parse(self, response):
        print(response.text)  # http响应包装成了response
        # scrapy 内置了解析库,不需要使用第三方:支持xpath和css

二 scrapy项目目录结构

mysfirstscrapy  # 项目名
    mysfirstscrapy # 包
        spiders    # 包,里面放了自定义的爬虫,类似于app
            __init__.py
            baidu.py  # 百度爬虫
            cnblogs.py#cnblogs爬虫
        items.py      #类似于django的 models表模型,一个个模型类
        middlewares.py # 中间件
        pipelines.py #管道--->写持久化
        settings.py #项目配置文件
    scrapy.cfg  # 项目上线配置

 # 后期我们只关注spiders中得爬虫和pipelines持久化即可

三 scrapy解析数据

1 response对象有css方法和xpath方法
	-css中写css选择器
    -xpath中写xpath选择
2 重点1:
	-xpath取文本内容
	'.//a[contains(@class,"link-title")]/text()'
    -xpath取属性
    './/a[contains(@class,"link-title")]/@href'
    -css取文本
    'a.link-title::text'
    -css取属性
    'img.image-scale::attr(src)'
3 重点2:
	.extract_first()  取一个
    .extract()        取所有
    

3.1 css解析

import scrapy


# 爬虫类,继承了scrapy.Spider
class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['www.cnblogs.com']  # 允许爬取的域--->
    start_urls = ['http://www.cnblogs.com/']  # 开始爬取的地址

    def parse(self, response):
        # 解析出所有文章
        article_list = response.css('article.post-item')  # 列表中放对象
        print(len(article_list))
        # 继续解析文章详情:文章标题,文章摘要,作者图片,作者名字,文章详情地址
        for article in article_list:
            title = article.css('section>div>a.post-item-title::text').extract_first()
            desc = article.css('p.post-item-summary::text').extract()
            real_desc = desc[0].replace('\n', '').replace(' ', '')
            if real_desc:
                desc = real_desc
            else:
                real_desc = desc[1].replace('\n', '').replace(' ', '')
                desc = real_desc
            author_img = article.css('p.post-item-summary img::attr(src)').extract_first()
            author_name = article.css('footer.post-item-foot span::text').extract_first()
            url = article.css('div.post-item-text a::attr(href)').extract_first()
            print('''
            文章名字:%s
            文章摘要:%s
            作者图片:%s
            作者名字:%s
            文章地址:%s
            ''' % (title, desc, author_img, author_name, url))

3.2 xpath选择器

import scrapy


# 爬虫类,继承了scrapy.Spider
class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['www.cnblogs.com']  # 允许爬取的域--->
    start_urls = ['http://www.cnblogs.com/']  # 开始爬取的地址


    def parse(self, response):
        ######xpath选择器#####
        # 解析出所有文章
        article_list = response.xpath('//article[contains(@class,"post-item")]')  # 列表中放对象
        print(len(article_list))
        # 继续解析文章详情:文章标题,文章摘要,作者图片,作者名字,文章详情地址
        for article in article_list:
            # title = article.xpath('./section/div/a/text()').extract_first()
            title = article.xpath('.//a/text()').extract_first()
            desc = article.xpath('.//p[contains(@class,"post-item-summary")]/text()').extract()
            real_desc = desc[0].replace('\n', '').replace(' ', '')
            if real_desc:
                desc = real_desc
            else:
                real_desc = desc[1].replace('\n', '').replace(' ', '')
                desc = real_desc
            author_img = article.xpath('.//p//img/@src').extract_first()
            author_name = article.xpath('.//footer//span/text()').extract_first()
            url = article.xpath('.//div[contains(@class,"post-item-text")]//a/@href').extract_first()
            print('''
            文章名字:%s
            文章摘要:%s
            作者图片:%s
            作者名字:%s
            文章地址:%s
            ''' % (title, desc, author_img, author_name, url))

'''
一启动爬虫:把start_urls地址包装成request对象--->
丢给引擎--->调度器--->排队--->引擎--->下载中间件--->下载器--->下载完成--->引擎--->爬虫---->就回到了parse
'''

四 setting配置

4.1 基础配置

#1 了解
BOT_NAME = "firstscrapy"  #项目名字,整个爬虫名字
#2 爬虫存放位置    了解
SPIDER_MODULES = ["firstscrapy.spiders"]
NEWSPIDER_MODULE = "firstscrapy.spiders"

#3  记住 是否遵循爬虫协议,一般都设为False
ROBOTSTXT_OBEY = False
# 4 记住
USER_AGENT = "firstscrapy (+http://www.yourdomain.com)"

#5  记住  日志级别
LOG_LEVEL='ERROR'

#6   记住 DEFAULT_REQUEST_HEADERS 默认请求头
DEFAULT_REQUEST_HEADERS = {
   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
   'Accept-Language': 'en',
}

#7 记住 后面学  SPIDER_MIDDLEWARES 爬虫中间件
SPIDER_MIDDLEWARES = {
    'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
}
#8 后面学 DOWNLOADER_MIDDLEWARES  下载中间件
DOWNLOADER_MIDDLEWARES = {
    'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
}

#9 后面学 ITEM_PIPELINES 持久化配置
ITEM_PIPELINES = {
    'cnblogs.pipelines.CnblogsPipeline': 300,
}

4.2 高级配置(提高爬虫效率--scarpy框架)

#1 增加并发:默认16
默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改
CONCURRENT_REQUESTS = 100
值为100,并发设置成了为100。

#2 降低日志级别:
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:
LOG_LEVEL = 'INFO'


# 3 禁止cookie:
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:
COOKIES_ENABLED = False

# 4 禁止重试:
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:
RETRY_ENABLED = False

# 5 减少下载超时:
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:
DOWNLOAD_TIMEOUT = 10 超时时间为10s

补充:后端如何防爬虫

	-1 频率限制(ip,用户)
    -2 尽量登录后才能访问
    -3 爬虫可以拿到cookie,token模拟发送请求
    	-3.1 请求头携带发送请求时间 时间戳--->后端中间件中取出请求头中得时间戳,跟当前时间做比较,如果时间过长,就返回错误
        -3.2 请求头中带sign签名--->签名自己的规则生成
        	-项目名+时间戳+项目名--->md5摘要
            -后端:项目名+时间戳+项目名跟传入的sign比较,如果一样
        -3.3 整个对请求体进行加密
        	-有的只加密了一部分
            -有的直接全部把请求体加密
            	-app端把请求体全加密---->拦截器取出请求体--->同样秘钥解密

五 持久化

# 爬回来,解析完了,想存储,有两种方案
## 方案一:一般不用   parse必须有return值,必须是列表套字典形式--->使用命令,可以保存到json格式中,csv中
-命令
    scrapy crawl cnblogs -o cnbogs.json  #以json形式保存
    scrapy crawl cnblogs -o cnbogs.csv  #以csv形式保存
-cnbogs.json
	    def parse(self, response):
        l = []
        # ...解析数据的代码
        l.append({'title': title, 'desc': desc, 'author_img': author_img, 'author_name': author_name, 'url': url})
        return l
    
#### 方案二: 我们用的,使用pipline存储--->可以存到多个位置

第一步:在item.py中写一个类

import scrapy

# 等同于django的模型类
class MyCnblogItem(scrapy.Item):
    title = scrapy.Field()
    desc = scrapy.Field()
    author_img = scrapy.Field()
    author_name = scrapy.Field()
    url = scrapy.Field()
    # 博客文章内容,但是暂时没有
    content = scrapy.Field()

第二步:在pipline.py中写代码,写一个类:

写几个方法

  • open_spider:开启爬虫会触发

  • close_spider:爬完会触发

  • process_item:每次要保存一个对象会触发

# 数据保存在文件中
class MyCnblogPipeline:
    def open_spider(self, spider):
        print('我开了')
        # 打开文件
        self.f = open('cnblog.txt', 'wt', encoding='utf-8')

    def close_spider(self, spider):
        # 关闭文件
        print('我关了')

    def process_item(self, item, spider):
        print('走这里了')
        # item 当前被处理到的在cnblogs.py 的parser中yield item
        # 保存在文件中
        self.f.write('文章标题:%s,文章摘要:%s,作者名字:%s,作者头像:%s,文章链接:%s\n'
                     % (item['title'], item['desc'], item['author_name'], item['author_img'], item['url']))
        return item  # 如果不返回,后续的pipline就拿不到了


import pymysql


# 存入MySQL中
class MySQLCnblogPipeline:
    def open_spider(self, spider):
        print('我开了')
        # 链接pmysql
        self.conn = pymysql.connect(
            user='root',
            password='1113',
            host='127.0.0.1',
            port=3306,
            database='cnblog',
        )
        self.course = self.conn.cursor()

    def close_spider(self, spider):
        # 关闭文件
        print('我关了')
        self.course.close()
        self.conn.close()

    def process_item(self, item, spider):
        sql = 'insert into article (title,url,`desc`,author_name,author_img) values (%s,%s,%s,%s,%s)'
        self.course.execute(sql,
                            args=[item['title'], item['url'], item['desc'], item['author_name'], item['author_img']])
        self.conn.commit()  # 最好每次操作数据库后就提交一次,这样如果中间断开,之前的数据也不会消失
        return item  # 如果不返回,后续的pipline就拿不到了

第三步:配置文件配置

# 管道,数字代表优先级
ITEM_PIPELINES = {
   "myfirstcrawl.pipelines.MyCnblogPipeline": 300,  # 数字越小,优先级越高
   "myfirstcrawl.pipelines.MySQLCnblogPipeline": 301,
}

第四步:在解析方法parse中yield item对象

import scrapy
from myfirstcrawl.items import MyCnblogItem


class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs' 
    allowed_domains = ['www.cnblogs.com']  
    start_urls = ['http://www.cnblogs.com/']  

    def parse(self, response):
        # 解析文章数据
        article_list = response.css('article.post-item') 
        for article in article_list:
            # 实例化一个item对象,对象是引用类型,最好每篇文章都生成自己的item对象
            item = MyCnblogItem()
            title = article.css('section>div>a.post-item-title::text').extract_first()
            desc = article.css('p.post-item-summary::text').extract()
            real_desc = desc[0].replace('\n', '').replace(' ', '')
            if real_desc:
                desc = real_desc
            else:
                real_desc = desc[1].replace('\n', '').replace(' ', '')
                desc = real_desc
            author_img = article.css('p.post-item-summary img::attr(src)').extract_first()
            author_name = article.css('footer.post-item-foot span::text').extract_first()
            url = article.css('div.post-item-text a::attr(href)').extract_first()

            # 往item对象中放值
            item['title'] = title  # 不能使用点的形式放值,只能使用索引
            item['desc'] = desc
            item['author_img'] = author_img
            item['author_name'] = author_name
            item['url'] = url
            yield item

六 全站爬取cnblgos

  • 继续爬取下一页

  • 爬取文章详情

# Request创建:在parse中,for循环中,创建Request对象时,传入meta
	# item对象一定要在for循环中创建,否则,当前页面都用同一个item导致同一页数据都一样
	yield Request(url=url, callback=self.detail_parse,meta={'item':item})
# 在parser_detail中取出来
	item=response.meta.get('item')
# Response对象:detail_parse中,通过response取出meta取出item,把文章详情写入
	def parser_detail(self,response):
        # content = response.css('#cnblogs_post_body').extract_first()
        item=response.meta.get('item')
        content=str(response.xpath('//div[@id="cnblogs_post_body"]').extract_first())
        item['content']=content
        yield item

cnblogs.py

from myfirstcrawl.items import MyCnblogItem
from scrapy import Request
# from scrapy.http.request import Request


class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['www.cnblogs.com']  # 允许爬取的域
    start_urls = ['http://www.cnblogs.com/']  # 开始爬取的地址

    def parse(self, response):
        article_list = response.css('article.post-item')  # 列表中放对象
        for article in article_list:
            # 实例化一个item对象,对象是引用类型,最好每篇文章都生成自己的item对象
            item = MyCnblogItem()
            title = article.css('section>div>a.post-item-title::text').extract_first()
            desc = article.css('p.post-item-summary::text').extract()
            real_desc = desc[0].replace('\n', '').replace(' ', '')
            if real_desc:
                desc = real_desc
            else:
                real_desc = desc[1].replace('\n', '').replace(' ', '')
                desc = real_desc
            author_img = article.css('p.post-item-summary img::attr(src)').extract_first()
            author_name = article.css('footer.post-item-foot span::text').extract_first()
            url = article.css('div.post-item-text a::attr(href)').extract_first()

            # 往item对象中放值
            item['title'] = title  # 不能使用点的形式放值,只能使用索引
            item['desc'] = desc
            item['author_img'] = author_img
            item['author_name'] = author_name
            item['url'] = url
            # yield item
            # 需要解析文章详情,把本篇文章的item对象传入,这样好解析文章详情
            yield Request(url=url, callback=self.parser_detail, meta={'item': item})  # 详情地址,继续爬取,详情页面解析用parser_detail

        # 解析出下一页地址,然后 yield 一个对象,请求对象(对象中要放入要爬取的地址)
        # 下一页地址
        next = 'https://www.cnblogs.com' + response.css('div.pager> a:last-child::attr(href)').extract_first()
        # next = 'https://www.cnblogs.com' + response.xpath('//div[contains(@class,"pager")]/a[last()]/@href').extract_first()
        # print(next)  # 拿到地址,继续爬取,组装成一个Request对象

        # callback 参数是控制返回response后使用的解析方法
        yield Request(url=next, callback=self.parse)  # 下一页地址,继续爬取,解析还是用parse时,callback可以省略

    def parser_detail(self, response):
        # 文章详情的解析函数
        # 1.获取item对象
        item = response.meta.get('item')
        # print(item)
        # 2.文章详情内容
        content = response.xpath('//div[@id="cnblogs_post_body"]').extract_first()  # 字符串形式
        # content = response.css('#cnblogs_post_body').extract_first()
        # 3.把content内容加到item对象中,后续好保存
        item['content'] = content
        # 4.数据已经增加结束,使用pipline存储,需要yield出item对象
        yield item

items.py

# 等同于django的模型类
class MyCnblogItem(scrapy.Item):
    title = scrapy.Field()
    desc = scrapy.Field()
    author_img = scrapy.Field()
    author_name = scrapy.Field()
    url = scrapy.Field()
    # 博客文章内容,但是暂时没有
    content = scrapy.Field()

piplines.py

import pymysql


# 存入MySQL中
class MySQLCnblogPipeline:
    def open_spider(self, spider):
        print('我开了')
        # 增加个计数器
        self.count = 0
        # 链接pmysql
        self.conn = pymysql.connect(
            user='root',
            password='1113',
            host='127.0.0.1',
            port=3306,
            database='cnblog',
        )
        self.cursor = self.conn.cursor()

    def close_spider(self, spider):
        # 关闭文件
        print('我关了')
        self.cursor.close()
        self.conn.close()

    def process_item(self, item, spider):
        self.count += 1
        # print('进入了------')
        # print(self.count)
        sql = 'insert into article (title,url,`desc`,author_name,author_img,content) values (%s,%s,%s,%s,%s,%s)'
        self.cursor.execute(sql,
                            args=[item['title'], item['url'], item['desc'],
                                  item['author_name'], item['author_img'], item['content']])
        self.conn.commit()  # 最好每次操作数据库后就提交一次,这样如果中间断开,之前的数据也不会消失
        return item  # 如果不返回,后续的pipline就拿不到了

settings.py

# 管道,数字代表优先级
ITEM_PIPELINES = {
   # "myfirstcrawl.pipelines.MyCnblogPipeline": 301,  # 数字越小,优先级越高
   "myfirstcrawl.pipelines.MySQLCnblogPipeline": 300,
}

七 爬虫中间件和下载中间件

# 爬虫中间件:爬虫和引擎之间
	-用的很少,了解即可

# 下载中间件:引擎和下载器之间
	-用的多,能干啥?
    -进来request对象
    	-加代理
        -加cookie
        -加请求头
    -出去response对象
    	-修改响应对象,最后进入到爬虫的parser中就是修改后的response
      

爬虫中间件

# 爬虫中间件 (了解) middlewares.py
class MysfirstscrapySpiderMiddleware:

    @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_spider_input(self, response, spider):
        return None

    def process_spider_output(self, response, result, spider):
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        pass

    def process_start_requests(self, start_requests, spider):
        for r in start_requests:
            yield r

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

下载中间件

class MysfirstscrapyDownloaderMiddleware:

    @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:继续处理本次请求,执行执行下一个中间件的process_request
        # return Response对象:执行当前中间件的process_response回去,进入到引擎,被调度,进入第6步,返回到爬虫的解析方法中
        # return a Request:直接返回,给引擎,等待被调度,进入第2步,进入调度器等待下次被调度爬取
        # raise IgnoreRequest:抛异常,执行 process_exception
        return None

    # 请求走了
    def process_response(self, request, response, spider):
        # 返回值可以是如下:
        # return Response :继续往后走,进入到引擎,被调度到爬虫中解析
        # return Request :进入到引擎,被调度进调度器
        # raise IgnoreRequest:抛异常,会执行process_exception
        return response

    def process_exception(self, request, exception, spider):
        # 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)

        
# 在配置文件中配置
# 下载中间件
DOWNLOADER_MIDDLEWARES = {
   "myfirstcrawl.middlewares.MyfirstcrawlDownloaderMiddleware": 543,
}

八 scrapy加代理,cookie,header

8.1 加代理

# 在下载中间件的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()
            return None
        
# 第二步:代理可能不能用,会触发process_exception,在里面写
    def process_exception(self, request, exception, spider):
        print('-----',request.url)  # 这个地址没有爬
        return request

8.2 加cookie

   # 加cookie
    def process_request(self, request, spider):
        print(request.cookies)
        request.cookies['name']='lqz'
        return None
    
# settings中,打开cookie
COOKIES_ENABLED = True

8.3 修改请求头

    # 修改请求头:referer,Authorization
    def process_request(self, request, spider):
        print(request.headers)
        request.headers['referer'] = 'http://www.lagou.com'
        return None

8.4 随机生成UserAgent

# pip3.8 install fake_useragent

    # 动态生成User-agent使用
    def process_request(self, request, spider):
        # fake_useragent模块:随机生成请求头
        from fake_useragent import UserAgent
        ua = UserAgent()
        request.headers['User-Agent']=str(ua.random)
        print(request.headers)

        return None

九 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

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

原理

# 如果爬取过的地址,就不会再爬了

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



# 要爬取的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

# 想一种方式,极小内存实现去重--->布隆过滤器

使用布隆过滤器

# 总结:scrapy的去重规则
	-根据配置的去重类RFPDupeFilter的request_seen方法,如果返回True,就不爬了,如果返回False就爬
    -后期咱们可以使用自己定义的去重类,实现去重
    
    
 # 更小内存实现去重
	-如果是集合:存的数据库越多,占内存空间越大,如果数据量特别大,可以使用布隆过滤器实现去重

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

	# 原理: BloomFilter 会开辟一个m位的bitArray(位数组),开始所有数据全部置 0 。当一个元素(www.baidu.com)过来时,能过多个哈希函数(h1,h2,h3....)计算不同的在哈希值,并通过哈希值找到对应的bitArray下标处,将里面的值 0 置为 1 。

    

# Python中使用布隆过滤器
# 测试布隆过滤器
# 可以自动扩容指定错误率,底层数组如果大于了错误率会自动扩容
# 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)
# bloom.add(url2)
# print(url in bloom)
# print(url2 in bloom)

from pybloom_live import BloomFilter

bf = BloomFilter(capacity=10)
url = 'www.baidu.com'
bf.add(url)
bf.add('aaaa')
bf.add('ggg')
bf.add('deww')
bf.add('aerqaaa')
bf.add('ae2rqaaa')
bf.add('aerweqaaa')
bf.add('aerwewqaaa')
bf.add('aerereweqaaa')
bf.add('we')


print(url in bf)
print("wa" in bf)



# 如果有去重的情况,就可以使用集合--->但是集合占的内存空间大,如果到了亿级别的数据量,想一种更小内存占用,而去重的方案---->布隆过滤器
# 布隆过滤器:通过不同的hash函数,加底层数组实现的极小内存去重
# python中如何使用:pybloom_live  
	-指定错误率
    -指定大小

   
# 使用redis实现布隆过滤器
	-编译redis--->把第三方扩展布隆过滤器编译进去,才有这个功能
	-https://zhuanlan.zhihu.com/p/94668736

# 重写scrapy的过滤类

十一 分布式爬虫

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


# 所以实现分布式爬取的关键就是,找一台专门的主机上运行一个共享的队列比如Redis,
然后重写Scrapy的Scheduler,让新的Scheduler到共享队列存取Request,并且去除重复的Request请求,所以总结下来,实现分布式的关键就是三点:
    #1、多台机器共享队列
    #2、重写Scheduler,让其无论是去重还是任务都去访问共享队列
    #3、为Scheduler定制去重规则(利用redis的集合类型)
    
    
# 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,
    }
        
    # 再不同多台机器上运行scrapy的爬虫,就实现了分布式爬虫
    
posted @ 2023-08-07 22:00  星空看海  阅读(37)  评论(0编辑  收藏  举报