Scrapy框架之全站爬取,Scrapy框架之中间件,Scrapy框架之集成Selenium框架,Scrapy框架之请求头操作,Scrapy框架之去重过滤器源码,Scrapy框架之布隆过滤器

Ⅰ Scrapy框架之全站爬取

【一】全站爬取

【1】介绍

  • CrawlSpider:全站数据爬虫的方式,它是一个类,属于Spider的子类
  • 如果不使用CrawlSpider,那么就相当于基于spider,手动发送请求,太不方便
  • 基于CrawlSpider可以很方便地进行全站数据爬取

【2】CrawlSpider

(1)创建一个工程

scrapy startproject SpiderBlog    

(2)创建爬虫文件

  • 切换到爬虫工程中后,创建爬虫文件:scrapy genspider -t crawl xxx www.xxx.com

(3)规则解析器

  • 使用CrawlSpider和spider产生的爬虫文件除了继承类不一样外还有一个rules的规则解析器
rules = (
    Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
  • 在rules规则解析器内有一个链接提取器LinkExtractor(allow=r'Items/'),callback是规则解析器指定的解析方法,follow是指爬取页面内可见部分页面还是全部
  • 页面内可见部分页面如下:

  • 链接提取器作用:根据指定的规则allow=r'Items/'进行指定的链接的提取
  • 规则解析器作用:把链接提取器提取到的链接进行指定规则callback='parse_item'的解析操作
  • follow作用:True可以把 链接提取器 继续作用到 链接提取器提取到的链接所对应的 页面 中,False爬取页面内可见部分页面

【二】需求介绍

  • 继续爬取下一页
  • 爬取文章详情

【三】思路分析

【1】Request创建

  • Request创建:在parse中,for循环中,创建Request对象时,传入meta
  • item对象一定要在for循环中创建,否则,当前页面都用同一个item导致同一页数据都一样
yield Request(url=url, callback=self.detail_parse,meta={'item':item})

【2】Response对象

  • Response对象:detail_parse中,通过response取出meta取出item,把文章详情写入
def parser_detail(self,response):
    # 获取到传入的 item 对象
    item=response.meta.get('item') 	
    # 继续解析需要解析的内容
    content=str(response.xpath('//div[@id="cnblogs_post_body"]').extract_first())
    # 向  item 对象中添加信息
    item['content']=content
    # 将完整的信息 返回
    yield item

【四】功能实现实现–爬取博客园文章信息

【1】先保存数据到本地

# 有时候xpath爬取数据的时候   数据是由js里面的创建节点渲染的
# 所以可能会拿不到数据   所以将数据先保存到本地
  • settings.py
from fake_useragent import UserAgent

BOT_NAME = "SpiderBlog"

SPIDER_MODULES = ["SpiderBlog.spiders"]
NEWSPIDER_MODULE = "SpiderBlog.spiders"


# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = UserAgent().random

# Obey robots.txt rules
ROBOTSTXT_OBEY = False



# Set settings whose default value is deprecated to a future-proof value
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

  • start.py
from scrapy.cmdline import execute

#  等价于 execute(['scrapy', 'crawl', 'baidu', "--nolog"])  切分成一块一块
# cmd = "scrapy crawl blog".split()
cmd = "scrapy crawl blog --nolog".split()


execute(cmd)
  • blog.py
import scrapy


class BlogSpider(scrapy.Spider):
    name = "blog"
    allowed_domains = ["www.cnblogs.com"]
    start_urls = ["https://www.cnblogs.com"]

    # 有时候xpath爬取数据的时候   数据是由js里面的创建节点渲染的
    # 所以可能会拿不到数据   所以将数据先保存到本地
    def __save(self, data):
        with open("blog.html", "w", encoding="utf-8") as fp:
            fp.write(data)


    def parse(self, response):
        self.__save(response.text)
  • blog.html

  • 然后在blog.html前端网页中 再拿取数据

【2】将数据保存到数据库

  • 利用PDManer和Navicat创建数据库和表

  • 再增加一个字段

【五】代码实现

  • settings.py
from fake_useragent import UserAgent

BOT_NAME = "SpiderBlog"

SPIDER_MODULES = ["SpiderBlog.spiders"]
NEWSPIDER_MODULE = "SpiderBlog.spiders"


# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = UserAgent().random

# Obey robots.txt rules
ROBOTSTXT_OBEY = False


ITEM_PIPELINES = {
   "SpiderBlog.pipelines.SpiderblogPipeline": 300,
   "SpiderBlog.pipelines.ArticlePipeline": 300,
}


# Set settings whose default value is deprecated to a future-proof value
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

  • pipelines.py
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


# useful for handling different item types with a single interface
from itemadapter import ItemAdapter


class SpiderblogPipeline:
    def process_item(self, item, spider):
        return item



'''
DROP TABLE IF EXISTS blog;
CREATE TABLE blog(
    `id` INT NOT NULL AUTO_INCREMENT  COMMENT '主键ID' ,
    `article_title` VARCHAR(255)    COMMENT '文章标题' ,
    `article_desc` VARCHAR(2555)    COMMENT '文章简介' ,
    `article_detail_url` VARCHAR(2555)    COMMENT '文章详情链接' ,
    `article_author_name` VARCHAR(255)    COMMENT '文章作者名字' ,
    `article_author_url` VARCHAR(2555)    COMMENT '文章作者主页地址' ,
    `article_comment_num` INT    COMMENT '文章评论数' ,
    `article_recommend_num` INT    COMMENT '文章推荐数' ,
    `article_read_num` INT    COMMENT '文章阅读数' ,
    PRIMARY KEY (id)
)  COMMENT = '博客信息';

'''

import pymysql
from pymysql.cursors import DictCursor


class ArticlePipeline:

    def open_spider(self, spider):
        self.conn = pymysql.connect(
            host='localhost',
            user='root',
            password='123456',
            db='cnblog',
            port=3306,
            charset='utf8mb4',
            cursorclass=DictCursor
        )
        self.cursor = self.conn.cursor()

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

    def process_item(self, item, spider):
        # key_list = ['article_title',
        #             'article_desc',
        #             'article_detail_url',
        #             'article_author_name',
        #             'article_author_url',
        #             'article_comment_num',
        #             'article_recommend_num',
        #             'article_read_num']
        # for key in key_list:
        #     value = item.get(key)
        article_title = item.get("article_title")
        sql = "select * from blog where article_title=%s;"
        self.cursor.execute(sql, article_title)
        if self.cursor.fetchone():
            print(f"{article_title}已存在!")
        else:
            sql = "insert into blog(article_title,article_desc,article_detail_url,article_author_name,article_author_url,article_comment_num,article_recommend_num,article_read_num,article_content) values(%s,%s,%s,%s,%s,%s,%s,%s,%s)"
            self.cursor.execute(sql, list(item.values()))
            self.conn.commit()
            print(f"{article_title}插入成功!")
        return item

  • start.py
from scrapy.cmdline import execute

#  等价于 execute(['scrapy', 'crawl', 'baidu', "--nolog"])  切分成一块一块
# cmd = "scrapy crawl blog".split()
cmd = "scrapy crawl blog --nolog".split()


execute(cmd)
  • items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class SpiderblogItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass



class BlogArticleItem(scrapy.Item):
    article_title = scrapy.Field()
    article_desc = scrapy.Field()
    article_detail_url = scrapy.Field()
    article_author_name = scrapy.Field()
    article_author_url = scrapy.Field()
    article_comment_num = scrapy.Field()
    article_recommend_num = scrapy.Field()
    article_read_num = scrapy.Field()
    article_content = scrapy.Field()
  • blog.py
import scrapy
from scrapy import Request

from SpiderBlog.items import BlogArticleItem

class BlogSpider(scrapy.Spider):
    name = "blog"
    allowed_domains = ["www.cnblogs.com"]
    start_urls = ["https://www.cnblogs.com"]

    # 有时候xpath爬取数据的时候   数据是由js里面的创建节点渲染的
    # 所以可能会拿不到数据   所以将数据先保存到本地
    def __save(self, data):
        with open("blog.html", "w", encoding="utf-8") as fp:
            fp.write(data)





    def parse(self, response):
        # self.__save(response.text)
        # 获取所有文章对象的每一个标签
        article_list = response.xpath('//*[@id="post_list"]/article/section')
        # 遍历每一个标签提取数据
        for article_obj in article_list:
            # 创建 item 对象
            item = BlogArticleItem()

            # //*[@id="post_list"]/article[1]/section/div/a
            # ./section/div/a
            # 文章标题
            article_title = article_obj.xpath('./div/a/text()').extract_first()
            # 文章详情链接
            article_detail_url = article_obj.xpath('./div/a/@href').extract_first()
            # 文章简介
            # /html/body/div/div[3]/div/div[2]/div[1]/div[4]/article[1]/section/div/p/text()
            try:
                article_desc = article_obj.xpath('./div/p/text()').extract()[1].strip()
            except:
                article_desc = ""

            # 文章作者名字
            # //*[@id="post_list"]/article[1]/section/footer/a[1]/span
            article_author_name = article_obj.xpath("./footer/a[1]/span/text()").extract_first()
            # 文章作者主页地址
            # //*[@id="post_list"]/article[1]/section/footer/a[1]
            article_author_url = article_obj.xpath("./footer/a[1]/@href").extract_first()
            # 文章评论数
            # //*[@id="post_list"]/article[1]/section/footer/a[2]/span
            article_comment_num = article_obj.xpath("./footer/a[2]/span/text()").extract_first()
            # 文章推荐数
            # /html/body/div/div[3]/div/div[2]/div[1]/div[4]/article[1]/section/footer/a[3]/span
            article_recommend_num = article_obj.xpath("./footer/a[3]/span/text()").extract_first()
            # 文章阅读数
            # /html/body/div/div[3]/div/div[2]/div[1]/div[4]/article[1]/section/footer/a[4]/span
            article_read_num = article_obj.xpath("./footer/a[4]/span/text()").extract_first()

            article_info = {
                "article_title": article_title,
                "article_desc": article_desc,
                "article_detail_url": article_detail_url,
                "article_author_name": article_author_name,
                "article_author_url": article_author_url,
                "article_comment_num": article_comment_num,
                "article_recommend_num": article_recommend_num,
                "article_read_num": article_read_num
            }

            # print(article_info)

            item.update(article_info)
            # yield item
            # 在当前页面可以获取到当前页面的下一页链接 ---> 拿到下一页的链接 对下一页发起请求 ---> 获取响应数据 解析响应数据就行了
            # 在scrapy 项目中也有一个 request 对象

            # 已经获取到了当前文章的详情地址 ---> 对详情地址发起请求解析 文章详情内容
            yield Request(
                url=article_detail_url,
                callback=self.parse_article_detail,
                meta={"item": item}
            )
        # 获取下一页的地址
        next_url = 'https://www.cnblogs.com' + response.xpath(
            '//*[@id="paging_block"]/div/a[last()]/@href').extract_first()

        # 已经获取到了下一页的地址 ---> 对下一页发起请求解析 文章内容
        yield Request(
            url=next_url,
            callback=self.parse,
        )

    def parse_article_detail(self, response):
        article_content = response.xpath('//*[@id="topics"]/div/div[2]').extract_first()

        item = response.meta.get("item")
        item.update({
            "article_content": str(article_content)
        })

        # 提交管道
        yield item

Ⅱ Scrapy框架之中间件

【一】爬虫中间件

  • 爬虫和引擎之间的叫:爬虫中间件
# 爬虫中间件
class SpiderblogSpiderMiddleware:
    # 如果有某一个函数没有被重写 那就使用默认的函数对象

    @classmethod
    def from_crawler(cls, crawler):
        # 创建爬虫实例
        s = cls()
        # 把爬虫实例对象 绑定到爬虫信号上
        # spider_opened 爬虫启动打开
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        # 范湖爬虫实例对象
        return s

    def process_spider_input(self, response, spider):
        # 当响应从爬虫中间件进入爬虫时,调用该方法进行处理。
        # 应返回None或引发异常。
        return None

    def process_spider_output(self, response, result, spider):
        # 当爬虫处理完响应后,调用该方法对处理结果进行处理。
        # 必须返回一个可迭代的Request对象或item对象。
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        # 当爬虫中抛出异常时,调用该方法进行处理。
        # 应返回None或者一个可迭代的Request对象或item对象。
        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)

【1】from_crawler

  • from_crawler 方法是一个类方法,它会在创建爬虫实例时被Scrapy调用,用于初始化爬虫中间件的实例。
class Day06StartSpiderMiddleware:
  
@classmethod
def from_crawler(cls, crawler):
  # 该方法是Scrapy用于创建爬虫实例的方法。
	
  # 首先创建了一个中间件实例 `s`
  s = cls()
  # 然后通过 `crawler.signals.connect` 方法连接了 `spider_opened` 信号和对应的处理方法。
  crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
  return s

【2】process_spider_input

  • process_spider_input 方法会在响应从爬虫中间件传递到爬虫之前调用。
  • 可以用来对响应进行预处理或检查
def process_spider_input(self, response, spider):
  # 当响应从爬虫中间件进入爬虫时,调用该方法进行处理。
  
  # response: 是响应对象
  # spider: 是当前爬虫实例

  # 应该返回 `None` 或引发异常
  return None

【3】process_spider_output

  • process_spider_output 方法在爬虫处理完响应后会被调用。
  • 主要用于对爬虫处理结果进行进一步处理或过滤,并将处理结果返回
  • 必须返回一个可迭代的Request对象或item对象。
def process_spider_output(self, response, result, spider):
  # 当爬虫处理完响应后,调用该方法对处理结果进行处理。
  
  # response 是爬虫处理后的响应对象
  # result 是爬虫的处理结果
  # spider 是当前爬虫实例
	
  # 必须返回一个可迭代的Request对象或item对象。
  for i in result:
      yield i

【4】process_spider_exception

  • process_spider_exception 方法在爬虫或 process_spider_input() 方法中抛出异常时会被调用。
  • 这个方法可以用来对爬虫处理过程中的异常进行处理
  • 可以返回 None 或一个可迭代的Request对象或item对象。
def process_spider_exception(self, response, exception, spider):
  # 当爬虫中抛出异常时,调用该方法进行处理。
  
  # response 是发生异常的响应对象
  # exception 是抛出的异常对象
  # spider 是当前爬虫实例。

  # 应返回None或者一个可迭代的Request对象或item对象。
  pass

【5】process_start_requests

  • process_start_requests 方法在爬虫启动时被调用
  • 用于对初始请求进行处理。
  • 这个方法必须返回一个可迭代的Request对象,而不能返回item对象。
def process_start_requests(self, start_requests, spider):
  # 在爬虫启动时,对初始请求进行处理。
  
  # start_requests 是初始请求的列表
  # spider 是当前爬虫实例

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

【6】spider_opened

  • spider_opened 方法在爬虫打开时被调用。
  • 在这个方法中,通过日志记录器(logger)输出 "Spider opened: 爬虫名称" 的信息。
def spider_opened(self, spider):
  #  spider: 表示当前爬虫实例
  spider.logger.info("Spider opened: %s" % spider.name)

【二】下载中间件

  • 引擎和下载器之间的叫:下载中间件

# 下载中间件
class SpiderblogDownloaderMiddleware:
    # 不是所有的方法都需要定义。如果某个方法没有被定义,
    # Scrapy会认为这个下载中间件不会修改传递的对象。

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy使用该方法创建您的爬虫。
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 拦截处理所有的请求对象
    # 参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
    # spider 参数的作用可以实现爬虫类和中间类的数据交互
    def process_request(self, request, spider):
        
        # 返回None:继续处理本次请求,执行下一个中间件的process_request方法
        # 返回一个Response对象:执行当前中间件的process_response方法,重新回到引擎,被调度
        # 返回一个Request对象:直接返回给引擎,被调度。进入调度器等待下次被调用
        # 抛出IgnoreRequest异常:调用已安装的下载中间件的process_exception方法                
    
        return None

    # 拦截处理所有的响应对象
    # 参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
    def process_response(self, request, response, spider):
        # - 返回一个Response对象:继续执行,进入引擎,被调度到爬虫进行解析
        # - 返回一个Request对象:进入引擎,返回到调度器被重新调用
        # - 或者抛出IgnoreRequest异常:抛出异常
        return response

    # 拦截和处理发生异常的请求对象
    # 参数:reqeust就是拦截到的发生异常的请求对象
    # 方法存在的意义:将发生异常的请求拦截到,然后对其进行修正
    def process_exception(self, request, exception, spider):
        # 当下载处理程序或process_request()方法(来自其他下载中间件)引发异常时调用。

        # 必须返回以下之一:
        # - 返回None:继续处理该异常
        # - 返回一个Response对象:停止process_exception()链
        # - 返回一个Request对象:停止process_exception()链
        pass

    # 控制日志数据的(忽略)
    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)
  • from_crawler(cls, crawler)方法:该方法是Scrapy用来创建爬虫的入口点。它返回一个中间件对象,并通过crawler.signals连接到spider_opened信号,以便在爬虫开启时执行相应的操作。
  • process_request(self, request, spider)方法:该方法在发送请求之前被调用。您可以在此方法中对请求进行处理和修改。返回值决定了后续处理的行为,可以是None继续处理当前请求,返回一个Response对象以便执行process_response方法,返回一个Request对象以便重新调度,或者抛出IgnoreRequest异常以调用其他下载中间件的process_exception方法。
  • process_response(self, request, response, spider)方法:该方法在收到响应后被调用。您可以在此方法中对响应进行处理和修改。返回值决定了后续处理的行为,可以是返回Response对象以便进一步处理和解析,返回Request对象以便重新调度,或者抛出IgnoreRequest异常。
  • process_exception(self, request, exception, spider)方法:当下载处理程序或其他下载中间件的process_request方法引发异常时调用该方法。您可以在此处处理异常,并根据需要返回值。
  • spider_opened(self, spider)方法:在爬虫开启时被调用。在这个示例中,它会输出一个日志信息。

【三】配置文件

# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
SPIDER_MIDDLEWARES = {
    "SpiderBlog.middlewares.SpiderblogSpiderMiddleware": 543,
 }

# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    # "SpiderBlog.middlewares.SpiderblogDownloaderMiddleware": 543,
    "SpiderBlog.middlewares.SpiderBlogDownloaderMiddlewareOne": 543,
}

Ⅲ Scrapy框架之集成Selenium框架

【一】引言

  • 使用scrapy默认下载器
    • 类似于requests模块发送请求,不能执行js,有的页面拿回来数据不完整
  • 想在scrapy中集成selenium
    • 获取数据更完整,获取完后,自己组装成 Response对象
    • 就会进爬虫解析,现在解析的是使用selenium拿回来的页面,数据更完整

【二】使用步骤

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

【1】第一步:定义浏览器对象

  • 在爬虫类中写
from selenium import webdriver
class CnblogsSpider(scrapy.Spider):
    # 实例化WebDriver,这里以Chrome为例
    bro = webdriver.Chrome()
    bro.implicitly_wait(10)
    def close(spider, reason):
        spider.bro.close() #浏览器关掉

【2】第二步:定义中间件处理

  • 在中间件中
from scrapy.http.response.html import HtmlResponse

def process_request(self, request, spider):
    # 爬取下一页这种地址---》用selenium,但是文章详情,就用原来的
    if 'sitehome/p' in request.url:
        print(request.url)
        # 调用父类的 browser 对象 对目标地址发起请求
        spider.browser.get(request.url)
        # 导入 Scrapy 内置的响应数据解析器解析页面数据
        from scrapy.http.response.html import HtmlResponse
        # 创建一个 HtmlResponse 对象,并设置其 body 属性为浏览器对象获取到的页面数据
        response = HtmlResponse(url=request.url, body=bytes(spider.browser.page_source, encoding='utf-8'))
        # 返回解析后的响应对象
        return response
    else:
        return None

【3】第三步:开启中间件

DOWNLOADER_MIDDLEWARES = {
   "SpiderFirst.middlewares.SpiderfirstDownloaderMiddleware": 543,
}

【三】总结

爬虫程序

  • settings.py
from fake_useragent import UserAgent

BOT_NAME = "SpiderBlog"

SPIDER_MODULES = ["SpiderBlog.spiders"]
NEWSPIDER_MODULE = "SpiderBlog.spiders"


# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = UserAgent().random

# Obey robots.txt rules
ROBOTSTXT_OBEY = False


DOWNLOADER_MIDDLEWARES = {
   "SpiderBlog.middlewares.SpiderblogDownloaderMiddleware": 543,
}


ITEM_PIPELINES = {
   "SpiderBlog.pipelines.SpiderblogPipeline": 300,
   "SpiderBlog.pipelines.ArticlePipeline": 300,
}



# Set settings whose default value is deprecated to a future-proof value
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"

  • pipelines.py
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html


# useful for handling different item types with a single interface
from itemadapter import ItemAdapter


class SpiderblogPipeline:
    def process_item(self, item, spider):
        return item



'''
DROP TABLE IF EXISTS blog;
CREATE TABLE blog(
    `id` INT NOT NULL AUTO_INCREMENT  COMMENT '主键ID' ,
    `article_title` VARCHAR(255)    COMMENT '文章标题' ,
    `article_desc` VARCHAR(2555)    COMMENT '文章简介' ,
    `article_detail_url` VARCHAR(2555)    COMMENT '文章详情链接' ,
    `article_author_name` VARCHAR(255)    COMMENT '文章作者名字' ,
    `article_author_url` VARCHAR(2555)    COMMENT '文章作者主页地址' ,
    `article_comment_num` INT    COMMENT '文章评论数' ,
    `article_recommend_num` INT    COMMENT '文章推荐数' ,
    `article_read_num` INT    COMMENT '文章阅读数' ,
    PRIMARY KEY (id)
)  COMMENT = '博客信息';

'''

import pymysql
from pymysql.cursors import DictCursor


class ArticlePipeline:

    def open_spider(self, spider):
        self.conn = pymysql.connect(
            host='localhost',
            user='root',
            password='123456',
            db='cnblog',
            port=3306,
            charset='utf8mb4',
            cursorclass=DictCursor
        )
        self.cursor = self.conn.cursor()

    def close_spider(self, spider):
        self.cursor.close()
        self.conn.close()

    def process_item(self, item, spider):
        # key_list = ['article_title',
        #             'article_desc',
        #             'article_detail_url',
        #             'article_author_name',
        #             'article_author_url',
        #             'article_comment_num',
        #             'article_recommend_num',
        #             'article_read_num']
        # for key in key_list:
        #     value = item.get(key)
        article_title = item.get("article_title")
        sql = "select * from blog where article_title=%s;"
        self.cursor.execute(sql, article_title)
        if self.cursor.fetchone():
            print(f"{article_title}已存在!")
        else:
            sql = "insert into blog(article_title,article_desc,article_detail_url,article_author_name,article_author_url,article_comment_num,article_recommend_num,article_read_num,article_content) values(%s,%s,%s,%s,%s,%s,%s,%s,%s)"
            self.cursor.execute(sql, list(item.values()))
            self.conn.commit()
            print(f"{article_title}插入成功!")
        return item

  • middlewares.py
# Define here the models for your spider middleware
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
from fake_useragent import UserAgent
from scrapy import signals

# useful for handling different item types with a single interface
from itemadapter import is_item, ItemAdapter


# 爬虫中间件
class SpiderblogSpiderMiddleware:
    # 如果有某一个函数没有被重写 那就使用默认的函数对象

    @classmethod
    def from_crawler(cls, crawler):
        # 创建爬虫实例
        s = cls()
        # 把爬虫实例对象 绑定到爬虫信号上
        # spider_opened 爬虫启动打开
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        # 范湖爬虫实例对象
        return s

    def process_spider_input(self, response, spider):
        # 当响应从爬虫中间件进入爬虫时,调用该方法进行处理。
        # 应返回None或引发异常。
        return None

    def process_spider_output(self, response, result, spider):
        # 当爬虫处理完响应后,调用该方法对处理结果进行处理。
        # 必须返回一个可迭代的Request对象或item对象。
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        # 当爬虫中抛出异常时,调用该方法进行处理。
        # 应返回None或者一个可迭代的Request对象或item对象。
        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 SpiderblogDownloaderMiddleware:
    # 不是所有的方法都需要定义。如果某个方法没有被定义,
    # Scrapy会认为这个下载中间件不会修改传递的对象。

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy使用该方法创建您的爬虫。
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 拦截处理所有的请求对象
    # 参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
    # spider 参数的作用可以实现爬虫类和中间类的数据交互
    def process_request(self, request, spider):
        # 提取当前的 url ---> 找到 含有 /sitehome/p 的地址 利用自己的 selenium 访问并拿到源码交给Response
        if "/sitehome/p" in request.url:
            # 调用自己的 selenium对象进行页面摘取
            spider.browser.get(request.url)
            # 获取到 页面你的响应源码
            body = spider.browser.page_source
            # 问题是 我们用的 response 都是 scrapy 处理好的对象
            # 将页面源码转换为 scrapy 的response对象
            from scrapy.http.response.html import HtmlResponse
            response = HtmlResponse(
                url=request.url,
                body=bytes(body, encoding='utf-8')
            )
            print(f"当前请求地址是 {request.url} 由 selenium 发起")
            return response
        else:
            return None

    # 拦截处理所有的响应对象
    # 参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
    def process_response(self, request, response, spider):
        # - 返回一个Response对象:继续执行,进入引擎,被调度到爬虫进行解析
        # - 返回一个Request对象:进入引擎,返回到调度器被重新调用
        # - 或者抛出IgnoreRequest异常:抛出异常
        return response

    # 拦截和处理发生异常的请求对象
    # 参数:reqeust就是拦截到的发生异常的请求对象
    # 方法存在的意义:将发生异常的请求拦截到,然后对其进行修正
    def process_exception(self, request, exception, spider):
        # 当下载处理程序或process_request()方法(来自其他下载中间件)引发异常时调用。

        # 必须返回以下之一:
        # - 返回None:继续处理该异常
        # - 返回一个Response对象:停止process_exception()链
        # - 返回一个Request对象:停止process_exception()链
        pass

    # 控制日志数据的(忽略)
    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)


class SpiderBlogDownloaderMiddlewareOne:
    # 不是所有的方法都需要定义。如果某个方法没有被定义,
    # Scrapy会认为这个下载中间件不会修改传递的对象。

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy使用该方法创建您的爬虫。
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 拦截处理所有的请求对象
    # 参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
    # spider 参数的作用可以实现爬虫类和中间类的数据交互
    def process_request(self, request, spider):
        # 【一】携带代理
        '''
        ip = '114.231.82.116'
        port = '8089'
        proxies = {
            "http": f"http://{ip}:{port}"
        }
        # 通过 request 的 meta 属性添加额外的代理IP
        request.meta["proxy"] = proxies
        '''

        #  【二】携带cookie
        '''
        ['__annotations__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_body', '_cb_kwargs', '_encoding', '_get_body', '_get_url', '_meta', '_set_body', '_set_url', '_url', 'attributes', 'body', 'callback', 'cb_kwargs', 'cookies', 'copy', 'dont_filter', 'encoding', 'errback', 'flags', 'from_curl', 'headers', 'meta', 'method', 'priority', 'replace', 'to_dict', 'url']
        '''
        # cookies 从页面上的请求头里面复制出来的 Cookie 字符串
        # request.cookies = "cookies"

        # 【三】向请求头中设置参数
        # referer : 声明来源地址
        # request.headers["referer"] = "https://www.baidu.com"

        # 【四】设置随机的请求头
        # request.headers["User-Agent"] = UserAgent().random
        return None

    # 拦截处理所有的响应对象
    # 参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
    def process_response(self, request, response, spider):
        # - 返回一个Response对象:继续执行,进入引擎,被调度到爬虫进行解析
        # - 返回一个Request对象:进入引擎,返回到调度器被重新调用
        # - 或者抛出IgnoreRequest异常:抛出异常
        return response

    # 拦截和处理发生异常的请求对象
    # 参数:reqeust就是拦截到的发生异常的请求对象
    # 方法存在的意义:将发生异常的请求拦截到,然后对其进行修正
    def process_exception(self, request, exception, spider):
        # 当下载处理程序或process_request()方法(来自其他下载中间件)引发异常时调用。

        # 必须返回以下之一:
        # - 返回None:继续处理该异常
        # - 返回一个Response对象:停止process_exception()链
        # - 返回一个Request对象:停止process_exception()链
        pass

    # 控制日志数据的(忽略)
    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

  • items.py
# Define here the models for your scraped items
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/items.html

import scrapy


class SpiderblogItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass



class BlogArticleItem(scrapy.Item):
    article_title = scrapy.Field()
    article_desc = scrapy.Field()
    article_detail_url = scrapy.Field()
    article_author_name = scrapy.Field()
    article_author_url = scrapy.Field()
    article_comment_num = scrapy.Field()
    article_recommend_num = scrapy.Field()
    article_read_num = scrapy.Field()
    article_content = scrapy.Field()
  • blog_selenium.py
import scrapy
from scrapy import Request
from selenium.webdriver import Chrome
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
import os

from SpiderBlog.items import BlogArticleItem

base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

driver_path = os.path.join(base_dir, 'chromedriver.exe')






class BlogSeleniumSpider(scrapy.Spider):
    name = "blog_selenium"
    allowed_domains = ["www.cnblogs.com"]
    start_urls = ["https://www.cnblogs.com"]

    # 实例化得到浏览器对象
    browser = Chrome(
        service=Service(
            executable_path=driver_path
        )
    )

    # 给浏览器对象加一个隐式等待
    browser.implicitly_wait(10)

    def parse(self, response):
        # 获取所有文章对象的每一个标签
        article_list = response.xpath('//*[@id="post_list"]/article/section')
        # 遍历每一个标签提取数据
        for article_obj in article_list:
            # 创建 item 对象
            item = BlogArticleItem()

            # //*[@id="post_list"]/article[1]/section/div/a
            # ./section/div/a
            # 文章标题
            article_title = article_obj.xpath('./div/a/text()').extract_first()
            # 文章详情链接
            article_detail_url = article_obj.xpath('./div/a/@href').extract_first()
            # 文章简介
            # /html/body/div/div[3]/div/div[2]/div[1]/div[4]/article[1]/section/div/p/text()
            try:
                article_desc = article_obj.xpath('./div/p/text()').extract()[1].strip()
            except:
                article_desc = ""

            # 文章作者名字
            # //*[@id="post_list"]/article[1]/section/footer/a[1]/span
            article_author_name = article_obj.xpath("./footer/a[1]/span/text()").extract_first()
            # 文章作者主页地址
            # //*[@id="post_list"]/article[1]/section/footer/a[1]
            article_author_url = article_obj.xpath("./footer/a[1]/@href").extract_first()
            # 文章评论数
            # //*[@id="post_list"]/article[1]/section/footer/a[2]/span
            article_comment_num = article_obj.xpath("./footer/a[2]/span/text()").extract_first()
            # 文章推荐数
            # /html/body/div/div[3]/div/div[2]/div[1]/div[4]/article[1]/section/footer/a[3]/span
            article_recommend_num = article_obj.xpath("./footer/a[3]/span/text()").extract_first()
            # 文章阅读数
            # /html/body/div/div[3]/div/div[2]/div[1]/div[4]/article[1]/section/footer/a[4]/span
            article_read_num = article_obj.xpath("./footer/a[4]/span/text()").extract_first()

            article_info = {
                "article_title": article_title,
                "article_desc": article_desc,
                "article_detail_url": article_detail_url,
                "article_author_name": article_author_name,
                "article_author_url": article_author_url,
                "article_comment_num": article_comment_num,
                "article_recommend_num": article_recommend_num,
                "article_read_num": article_read_num
            }
            item.update(article_info)
            # yield item
            # 在当前页面可以获取到当前页面的下一页链接 ---> 拿到下一页的链接 对下一页发起请求 ---> 获取响应数据 解析响应数据就行了
            # 在scrapy 项目中也有一个 request 对象

            # 已经获取到了当前文章的详情地址 ---> 对详情地址发起请求解析 文章详情内容
            yield Request(
                url=article_detail_url,
                callback=self.parse_article_detail,
                meta={"item": item}
            )
        # 获取下一页的地址
        next_url = 'https://www.cnblogs.com' + response.xpath(
            '//*[@id="paging_block"]/div/a[last()]/@href').extract_first()

        # 已经获取到了下一页的地址 ---> 对下一页发起请求解析 文章内容
        yield Request(
            url=next_url,
            callback=self.parse,
        )

    def parse_article_detail(self, response):
        article_content = response.xpath('//*[@id="topics"]/div/div[2]').extract_first()

        item = response.meta.get("item")
        item.update({
            "article_content": str(article_content)
        })

        # 提交管道
        yield item


  • chromedriver.exe

  • start.py

from scrapy.cmdline import execute

#  等价于 execute(['scrapy', 'crawl', 'baidu', "--nolog"])  切分成一块一块
# cmd = "scrapy crawl blog_selenium".split()
cmd = "scrapy crawl blog_selenium --nolog".split()


execute(cmd)

Ⅳ Scrapy框架之请求头操作

【一】使用代理

  • 在下载中间件写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):
    # 第二步:代理可能不能用,会触发process_exception,在里面写

    def process_exception(self, request, exception, spider):
        print('-----', request.url)  # 这个地址没有爬
        return request

【二】携带Cookie

def process_request(self, request, spider):
    # 添加cookie
    request.cookies['cookies'] = 'cookies'
    print(request.url+':请求对象拦截成功!')
    return None

【三】携带请求头

def process_request(self, request, spider):
    request.headers['referer'] = 'http://www.lagou.com'
    return None

【四】随机UA

# fake_useragent模块
from fake_useragent import UserAgent

# 动态生成User-agent使用
def process_request(self, request, spider):

    request.headers['User-Agent']=str(UserAgent().random)
    print(request.url+':请求对象拦截成功!')
    
    return None

【五】小结

def process_request(self, request, spider):
    # 返回None:继续处理本次请求,执行下一个中间件的process_request方法
    # 返回一个Response对象:执行当前中间件的process_response方法,重新回到引擎,被调度
    # 返回一个Request对象:直接返回给引擎,被调度。进入调度器等待下次被调用
    # 抛出IgnoreRequest异常:调用已安装的下载中间件的process_exception方法

    # 构建代理池
    # 第一步:构建代理池
    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):
        # 在 meta 中加入代理
        request.meta['proxy'] = self.get_proxy()
        # 代理可能不能用,会触发process_exception,在里面写
        return None

    # 添加cookie
    request.cookies['cookies'] = 'cookies'

    # 修改请求头
    request.headers['referer'] = 'http://www.lagou.com'

    # 动态生成User-agent使用
    request.headers['User-Agent'] = str(UserAgent().random)

    return None

def process_exception(self, request, exception, spider):
    # 第二步:代理可能不能用,会触发process_exception,在里面写

    def process_exception(self, request, exception, spider):
        print('-----', request.url)  # 这个地址没有爬
        return request

【六】总结

# Define here the models for your spider middleware
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.html
from fake_useragent import UserAgent
from scrapy import signals

# useful for handling different item types with a single interface
from itemadapter import is_item, ItemAdapter


# 爬虫中间件
class SpiderblogSpiderMiddleware:
    # 如果有某一个函数没有被重写 那就使用默认的函数对象

    @classmethod
    def from_crawler(cls, crawler):
        # 创建爬虫实例
        s = cls()
        # 把爬虫实例对象 绑定到爬虫信号上
        # spider_opened 爬虫启动打开
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        # 范湖爬虫实例对象
        return s

    def process_spider_input(self, response, spider):
        # 当响应从爬虫中间件进入爬虫时,调用该方法进行处理。
        # 应返回None或引发异常。
        return None

    def process_spider_output(self, response, result, spider):
        # 当爬虫处理完响应后,调用该方法对处理结果进行处理。
        # 必须返回一个可迭代的Request对象或item对象。
        for i in result:
            yield i

    def process_spider_exception(self, response, exception, spider):
        # 当爬虫中抛出异常时,调用该方法进行处理。
        # 应返回None或者一个可迭代的Request对象或item对象。
        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 SpiderblogDownloaderMiddleware:
    # 不是所有的方法都需要定义。如果某个方法没有被定义,
    # Scrapy会认为这个下载中间件不会修改传递的对象。

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy使用该方法创建您的爬虫。
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 拦截处理所有的请求对象
    # 参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
    # spider 参数的作用可以实现爬虫类和中间类的数据交互
    def process_request(self, request, spider):
        # 提取当前的 url ---> 找到 含有 /sitehome/p 的地址 利用自己的 selenium 访问并拿到源码交给Response
        if "/sitehome/p" in request.url:
            # 调用自己的 selenium对象进行页面摘取
            spider.browser.get(request.url)
            # 获取到 页面你的响应源码
            body = spider.browser.page_source
            # 问题是 我们用的 response 都是 scrapy 处理好的对象
            # 将页面源码转换为 scrapy 的response对象
            from scrapy.http.response.html import HtmlResponse
            response = HtmlResponse(
                url=request.url,
                body=bytes(body, encoding='utf-8')
            )
            print(f"当前请求地址是 {request.url} 由 selenium 发起")
            return response
        else:
            return None

    # 拦截处理所有的响应对象
    # 参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
    def process_response(self, request, response, spider):
        # - 返回一个Response对象:继续执行,进入引擎,被调度到爬虫进行解析
        # - 返回一个Request对象:进入引擎,返回到调度器被重新调用
        # - 或者抛出IgnoreRequest异常:抛出异常
        return response

    # 拦截和处理发生异常的请求对象
    # 参数:reqeust就是拦截到的发生异常的请求对象
    # 方法存在的意义:将发生异常的请求拦截到,然后对其进行修正
    def process_exception(self, request, exception, spider):
        # 当下载处理程序或process_request()方法(来自其他下载中间件)引发异常时调用。

        # 必须返回以下之一:
        # - 返回None:继续处理该异常
        # - 返回一个Response对象:停止process_exception()链
        # - 返回一个Request对象:停止process_exception()链
        pass

    # 控制日志数据的(忽略)
    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)


class SpiderBlogDownloaderMiddlewareOne:
    # 不是所有的方法都需要定义。如果某个方法没有被定义,
    # Scrapy会认为这个下载中间件不会修改传递的对象。

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy使用该方法创建您的爬虫。
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 拦截处理所有的请求对象
    # 参数:request就是拦截到的请求对象,spider爬虫文件中爬虫类实例化的对象
    # spider 参数的作用可以实现爬虫类和中间类的数据交互
    def process_request(self, request, spider):
        # 【一】携带代理
        '''
        ip = '114.231.82.116'
        port = '8089'
        proxies = {
            "http": f"http://{ip}:{port}"
        }
        # 通过 request 的 meta 属性添加额外的代理IP
        request.meta["proxy"] = proxies
        '''

        #  【二】携带cookie
        '''
        ['__annotations__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_body', '_cb_kwargs', '_encoding', '_get_body', '_get_url', '_meta', '_set_body', '_set_url', '_url', 'attributes', 'body', 'callback', 'cb_kwargs', 'cookies', 'copy', 'dont_filter', 'encoding', 'errback', 'flags', 'from_curl', 'headers', 'meta', 'method', 'priority', 'replace', 'to_dict', 'url']
        '''
        # cookies 从页面上的请求头里面复制出来的 Cookie 字符串
        # request.cookies = "cookies"

        # 【三】向请求头中设置参数
        # referer : 声明来源地址
        # request.headers["referer"] = "https://www.baidu.com"

        # 【四】设置随机的请求头
        # request.headers["User-Agent"] = UserAgent().random
        return None

    # 拦截处理所有的响应对象
    # 参数:response就是拦截到的响应对象,request就是被拦截到响应对象对应的唯一的一个请求对象
    def process_response(self, request, response, spider):
        # - 返回一个Response对象:继续执行,进入引擎,被调度到爬虫进行解析
        # - 返回一个Request对象:进入引擎,返回到调度器被重新调用
        # - 或者抛出IgnoreRequest异常:抛出异常
        return response

    # 拦截和处理发生异常的请求对象
    # 参数:reqeust就是拦截到的发生异常的请求对象
    # 方法存在的意义:将发生异常的请求拦截到,然后对其进行修正
    def process_exception(self, request, exception, spider):
        # 当下载处理程序或process_request()方法(来自其他下载中间件)引发异常时调用。

        # 必须返回以下之一:
        # - 返回None:继续处理该异常
        # - 返回一个Response对象:停止process_exception()链
        # - 返回一个Request对象:停止process_exception()链
        pass

    # 控制日志数据的(忽略)
    def spider_opened(self, spider):
        spider.logger.info("Spider opened: %s" % spider.name)

Ⅴ Scrapy框架之去重过滤器源码

【一】源码去重规则

【1】内置去重规则

  • 在Scrapy框架中,为了防止重复爬取相同的URL地址,采用了去重机制。
  • 其中,一种常用的去重方法是使用布隆过滤器(Bloom Filter)。
  • 如果爬取过的地址,就不会再爬了
    • 调度器可以去重,研究一下,如何去重的
    • 使用了集合

【2】内置去重原理

from scrapy.core.scheduler import BaseScheduler
class BaseScheduler(metaclass=BaseSchedulerMeta):

    @classmethod
    def from_crawler(cls, crawler: Crawler):
        """
        接收当前:class:`~scrapy.crawler的工厂方法。爬网程序的对象作为参数。
        """
        return cls()

    def open(self, spider: Spider) -> Optional[Deferred]:
        """
        当引擎打开spider时调用。它接收spider实例作为参数,执行初始化代码非常有用。

        :param spider:当前爬网的spider对象
        :type spider::class:`~scrapy.spiders.Spider`
        """
        pass

    def close(self, reason: str) -> Optional[Deferred]:
        """
        当spider被引擎关闭时调用。它接收爬取的原因作为参数完成,执行清理代码非常有用。

        :param reason:描述spider关闭原因的字符串
        :type原因::class:`str`
        """
        pass

    @abstractmethod
    def has_pending_requests(self) -> bool:
        """
        ``如果调度程序已将请求排入队列,则为True“”,否则为False“”
        """
        raise NotImplementedError()

    @abstractmethod
    def enqueue_request(self, request: Request) -> bool:
        """
        处理引擎收到的请求。

        如果请求存储正确,则返回“True”,否则返回“False”。

        如果为“False”,则引擎将发出“request_dropped”信号,并且以后不会再尝试安排请求。
        作为参考,当请求被dupfilter拒绝。
        """
        raise NotImplementedError()

    @abstractmethod
    def next_request(self) -> Optional[Request]:
        """
        返回下一个:class:`~scrapy.http。请求`待处理,或``无``以表明目前没有被视为准备就绪的请求。

        返回“None”意味着不会发送来自调度程序的请求到当前反应堆循环中的下载器。
        发动机将继续运转调用“next_request”直到“has_pending_requests”为“False”。
        """
        raise NotImplementedError()
from scrapy.core.scheduler import Scheduler
def enqueue_request(self, request: Request) -> bool:
    """
    除非接收到的请求被Dupefilter过滤掉,否则尝试推送它进入磁盘队列,然后回落到内存队列中。

    增加适当的统计信息,例如:`scheduler/enqueued``,``调度器/排队/磁盘``,`scheduler/enqueued/memory``。

    如果请求存储成功,则返回“True”,否则返回“False”。
    """
    # 如何判断这个Request要不要丢弃掉,执行了self.df.request_seen(request)
    # RFPDupeFilter类中的方法
    # 如果这个request在集合中,说明爬过了,就return True
    # 如果不在集合中,就加入到集合中,然后返回False
    if not request.dont_filter and self.df.request_seen(request):
        # 增加日志信息
        self.df.log(request, self.spider)
        return False
    dqok = self._dqpush(request)
    assert self.stats is not None
    if dqok:
        self.stats.inc_value("scheduler/enqueued/disk", spider=self.spider)
    else:
        self._mqpush(request)
        self.stats.inc_value("scheduler/enqueued/memory", spider=self.spider)
    self.stats.inc_value("scheduler/enqueued", spider=self.spider)
    return True
  • self.df: BaseDupeFilter = dupefilter
# 创建请求对象指纹数据
def request_fingerprint(self, request: Request) -> str:
  	# 获取到当前指纹对象并利用指纹对象添加指纹信息
    return self.fingerprinter.fingerprint(request).hex()

# 校验当前请求对象是否存在过
def request_seen(self, request: Request) -> bool:
  	# 获取当前请求对象的指纹数据
    fp = self.request_fingerprint(request)
    # 判断当前指纹数据是否存在过
    if fp in self.fingerprints:
        # 指纹数据存在返回 True
        return True
    # 指纹数据不存在则添加到指纹数据列表中
    self.fingerprints.add(fp)
    if self.file:
        self.file.write(fp + "\n")
    return False
# 在Scrapy框架中,为了防止重复爬取相同的URL地址,采用了去重机制。
# 其中,一种常用的去重方法是使用布隆过滤器(Bloom Filter)。
# 如果爬取过的地址,就不会再爬了
# 调度器可以去重,研究一下,如何去重的
# 使用了集合

# from scrapy.core.scheduler import BaseScheduler
from scrapy.core.scheduler import BaseScheduler
'''
def enqueue_request(self, request: Request) -> bool:
    """
    除非接收到的请求被Dupefilter过滤掉,否则尝试推送它进入磁盘队列,然后回落到内存队列中。

    增加适当的统计信息,例如:`scheduler/enqueued``,``调度器/排队/磁盘``,`scheduler/enqueued/memory``。

    如果请求存储成功,则返回“True”,否则返回“False”。

    If ``False``, the engine will fire a ``request_dropped`` signal, and
    will not make further attempts to schedule the request at a later time.
    For reference, the default Scrapy scheduler returns ``False`` when the
    request is rejected by the dupefilter.
    """
    raise NotImplementedError()
'''

# 简单来说原理就是 用你当前的request对象生成一个指纹(基于request对象生成md5哈希值)
# 扔到一个列表中
# 下次请求进来的时候就判断当前request请求的指纹在不在列表中
# 如果在就过滤掉当前请求
# 如果不在就继续请求
  • 要爬取的Request对象
  • 在进入到scheduler调度器排队之前,先执行enqueue_request
    • 它如果return False,这个Request就丢弃掉,不爬了
    • 如何判断这个Request要不要丢弃掉,执行了self.df.request_seen(request),它来决定的
  • RFPDupeFilter类中的方法
    • request_seen
    • 会返回True或False
    • 如果这个request在集合中,说明爬过了,就return True,如果不在集合中,就加入到集合中,然后返回False

【3】代码举例

from scrapy.utils.request import RequestFingerprinter
from scrapy import Request

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

res1 = finger_printer.fingerprint(request1).hex()
res2 = finger_printer.fingerprint(request2).hex()
print(res1)
print(res2)
# a6af0a0ffa18a9b2432550e1914361b6bffcff1a
# a6af0a0ffa18a9b2432550e1914361b6bffcff1a

【二】调度器源码

  • 在Scrapy框架的调度器中
  • 通过enqueue_request方法来判断是否需要对某个URL进行爬取
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
  • 如果调度器中的去重类(RFPDupeFilter)的request_seen方法返回True,表示该URL已经被爬取过,不需要再次爬取;
  • 如果返回False,表示该URL是新的,需要进行爬取。

【三】self.df 去重类

  • 去重类的对象 RFPDupeFilter
# 表示,使用它作为去重类,按照它的规则做去重
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
  • 该类的request_seen方法负责判断URL是否已经被爬取过,并返回相应的结果。
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

【四】指纹生成原理

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)
  • 假设爬了1亿条url,放在内存中,占空间非常大
    • a6af0a0ffa18a9b2432550e1914361b6bffcff1a
    • a6af0a0ffa18a9b2432550e191361b6bffc34f1a
  • 想一种方式,极小内存实现去重--> 布隆过滤器

【五】Scrapy的去重规则

【1】源码分析入口

scrapy.Spider

【2】起始位置

# 起始爬取的地址
# 1 类属性中写了起始位置: start_urls = ["https://www.cnblogs.com"]
# 2 爬虫运行时,会执行类中得start_requests
def start_requests(self) -> Iterable[Request]:
   for url in self.start_urls:
       yield Request(url, dont_filter=True)

【3】去重位置

  • 去重位置入口
from scrapy.core.scheduler import Scheduler
  • 默认配置文件
DUPEFILTER_CLASS = "scrapy.dupefilters.RFPDupeFilter"
class Scheduler:
    # 在排队之前---》先执行enqueue_request--》完成去重
    def enqueue_request(self, request: Request) -> bool:
        # self 是调度器
        # self.df 是去重类 RFPDupeFilter 的对象
        # 调用RFPDupeFilter中得方法request_seen,会返回True或False
        if self.df.request_seen(request): #返回True说明集合中有了
            return False # 说明不再爬取了
        dqok = self._dqpush(request)

        return True

class RFPDupeFilter:
    def request_seen(self, request: Request) -> bool:
        # 16 进制字符串
        fp = self.request_fingerprint(request)
        #self.fingerprints 是 set() 集合
        if fp in self.fingerprints:
            return True # 如果在集合中,就返回True
        # 加入到集合中,返回False
        self.fingerprints.add(fp)
        return False
    def request_fingerprint(self, request: Request) -> str:
      return self.fingerprinter.fingerprint(request).hex() # 16进制 字符串

【4】指纹生成原理

request_fingerprint 生成指纹
# 请求回来的数据是完全一致的---> 如果仅仅使用地址区分-->他们不是一个
# 但是被request_fingerprint执行完后,它生成的16进制是一样的
-www.cnblogs.com/?name=dream&age=19
-www.cnblogs.com/?age=19&name=dream

【5】总结

  • 去重是使用集合去重
  • 高级在(生成的指纹):
    • get请求,参数如果一样,就是一样的
    • post请求,请求体不一样,就不一样

Ⅵ Scrapy框架之布隆过滤器

【一】布隆过滤器

【1】介绍

  • 布隆过滤器是一种基于哈希函数映射的数据结构,常用于快速判断一个元素是否在一个集合内,具有较好的时间和空间效率。
  • 它主要解决了传统集合数据结构在大规模数据存储和查询时所遇到的空间占用和时间复杂度的问题。

【2】特点

  • 它可以使用相对较小的内存空间来实现去重功能。
  • 普通集合在存储大量数据时,占用的内存空间会随着数据量的增加而线性增长
  • 而布隆过滤器则可以通过多个哈希函数将数据映射到一个位数组中,并将该位置的值置为1来表示存在,从而大大减少了空间占用。

【3】原理

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

【4】使用场景

  • 在一些需要进行去重操作的场景中,如果数据量特别大,选择使用布隆过滤器可以提供较好的性能。
    • 例如,在爬虫中进行URL去重时,由于爬取的网页数量巨大,使用传统集合进行去重会占用大量的内存空间
    • 而布隆过滤器可以有效地减少内存消耗,提高去重的效率。

【二】Python中使用布隆过滤器

  • 在Python中,可以使用第三方库来实现布隆过滤器的功能。

【1】安装模块

pip install pybloom_live

【2】测试布隆过滤器

  • 可以自动扩容指定错误率,底层数组如果大于了错误率会自动扩容
from pybloom_live import ScalableBloomFilter, BloomFilter

# 【一】创建一个可扩容的布隆过滤器对象
# initial_capacity - 初始的过滤器容量
# error_rate - 期望的错误率
# mode - 过滤器模式,可选的值有:
# ScalableBloomFilter.SMALL_SET_GROWTH:slower, but takes up less memory 很慢但是占用更少的内存
# ScalableBloomFilter.LARGE_SET_GROWTH:faster, but takes up more memory faster 更快,但是需要更多的内存
bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)

# 【二】添加元素到布隆过滤器中
url = "www.cnblogs.com"
url2 = "www.baidu.com"
bloom.add(url)
bloom.add(url2)

# 【三】判断元素是否在布隆过滤器中
print(url in bloom)
print(url2 in bloom)
  • 上述示例中,使用ScalableBloomFilter类创建了一个可扩容的布隆过滤器对象,并通过add方法将元素添加到布隆过滤器中。
  • 最后,使用in关键字判断元素是否存在于布隆过滤器中。

【三】小结

  • 如果有去重的情况,就可以使用集合
    • 但是集合占的内存空间大,如果到了亿级别的数据量,想一种更小内存占用,而去重的方案
    • 布隆过滤器
  • 布隆过滤器:
    • 通过不同的hash函数,加底层数组实现的极小内存去重
  • python中如何使用:pybloom_live
    • 指定错误率
    • 指定大小
  • 使用redis实现布隆过滤器
    • 编译redis(把第三方扩展布隆过滤器编译进去,才有这个功能)

【四】代码实现

# 布隆过滤器

# 【一】先安装
# pip install pybloom_live

# 【二】使用
from pybloom_live import BloomFilter, ScalableBloomFilter

# BloomFilter
# 创建一个布隆过滤器实例
# initial_capacity : 样本容量
# error_rate : 期望的错误率
bloom = ScalableBloomFilter(
    initial_capacity=1000000000, error_rate=0.001
)

# 【2】向布隆过滤器中仍数据
url_list = [f"https://www.baidu.com/?page={i}" for i in range(1000000)]
for url in url_list:
    bloom.add(url)

# 【3】判断当前的值是否在当前过滤器当中
data = {
    True: 0,
    False: 0
}
for url in url_list:
    if url in bloom:
        data[True] += 1
    else:
        data[False] += 1

print(data)
# {True: 1000000, False: 0}

posted on   silence^  阅读(28)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示