[记录]scrapy爬虫框架

碎碎念

​ 其实学scrapy已经有一段时间了,前些日子写论文的时候,要用到这一块知识点,所以就顺便捡起来再记下来。写的只是一部分,更加全面的要移步去官方那里噢。

官方文档传送门

scrapy文档

简介

​ Scrapy 是一个快速的高级网页抓取和网页抓取框架,用于抓取网站并从其页面中提取结构化数据。 它可用于广泛的用途,从数据挖掘到监控和自动化测试。

基本架构

  • Scrapy Engine(引擎):负责Spider、Downloader、Scheduler、Item Pipeline的通讯、数据传递。
  • Spider(爬虫):负责处理Response并从中提取数据,然后将提取的URL交给引擎。
  • Scheduler(调度器):接收引擎发送过来的request请求,并将其进行排列入队。
  • Downloader(下载器):下载引擎发送的request请求,并将获得的response交给引擎
  • Item Pipeline(管道):处理从spider中获取的item(过滤、存储)
  • Downloader Middlewares(下载中间件):可扩展的下载组件
  • Spider Middlewares(爬虫中间件):可扩展的下载组件

工作流程

1.爬虫(起始的url构造成request对象)→爬虫中间件→引擎→调度器

2.调度器(把request排序入队,将处理的request)→引擎→下载中间件→下载器

3.下载器(发送请求并将获取的response响应)→下载中间件→引擎→爬虫中间件→爬虫

4.爬虫(提取url,组成新的request对象)→爬虫中间件→引擎→调度器(这里将重复步骤2)

5.爬虫(提取数据)→引擎→管道(处理和保存数据)

结构使用

Item Pipeline

​ 当爬虫的数据存储到item之后就会被送到Item Pipeline,通过一些组件依次处理Item。

​ 每个Item Pipeline组件是一个实现简单方法的python类,它们接收到Item并通过它执行一些行为,同时决定它是否继续通过pipeline或者被丢弃不再执行处理。

Item Pipeline的一些典型应用:

  • 清理HTML数据
  • 验证爬取的数据(检查items容器字段)
  • 查重(并丢弃)
  • 存储爬取的数据并保存到数据库中

编写Item Pipeline

每个Item Pipeline组件时一个独立的python类,同时必须实现以下方法:

  • process_item(item,spider)

    每个Item Pipeline都要调用这个方法 ,这个方法会返回一个item对象或者抛出异常。

    • 参数解释:
      • item:对象,指被爬取的item
      • spider: 对象,指爬取item的spider
  • open_spider(spider)

​ 这个方法当spider被执行时调用

  • close_spider(spider)

​ 当spider被关闭时调用

Logging

日志功能,它分为五个层次的logging级别:

  • CRITICAL - 严重错误
  • ERROR - 一般错误
  • WARNING - 警告信息
  • INFO - 一般信息
  • DEBUG - 调试信息

Items

首先先定义我想要抓取的内容

1
2
3
4
5
6
7
import scrapy

class Product(scrapy.Item):
    # 商品名称
    name = scrapy.Field()
    #商品价格
    price = scrapy.Field()

Field()类

​ Field对象指明每个字段的元数据。**注意:用来声明item的Field对象并没有被赋值class属性,不能通过item.attr去访问,但可以用item.fields属性进行访问。**即,Field类只不过是内置字典类(dict)的一个别名,并没有提供额外的方法和属性。

settings

Scrapy设置允许自定义所有Scrapy组件,包括核心,插件,pipeline和spider。

settings的参数分为五个级别(从高到低排序):

  • 命令行选项(最高级)
  • 单独爬虫设定模块(即每个spider的Settings)
  • 项目设定模块(项目的Settings)
  • 命令默认设定
  • 全局默认设定(默认的全局Settings)

1.命令行选项

​ 使用命令行听过的参数拥有最高的优先级,会覆盖所有其他方式设置的Settings选项。你可以用**-s或者–set**来明确指定覆盖一个或多个Settings

1
scrapy crawl myspider -s LOG_FILE=scrapy.log

2.每个spider的Settings

​ 在每个spider中,可以定义这个spider所特有的settings。在spider类中定义suctom_settings属性

​ 当一个项目有多个spider,每个spider需要设置的参数是不一样的,举个例子,spider1要用pipeline1将数据放到MySQLl中,spider2用pipeline2将数据放到MongoDB中,这时我们要对两个spider分开进行pipeline配置,此时在spider中添加custom_settings属性

1
2
3
4
5
class MySpider(scrapy.Spider):
    name = 'spider1'
    custom_settings = {
        'ITEM_PIPELINES': {'myproject.pipelines.pipeline1': 301},
    }
1
2
3
4
5
class MySpider(scrapy.Spider):
    name = 'spider2'
    custom_settings = {
        'ITEM_PIPELINES': {'myproject.pipelines.pipeline2': 301},
    }

3.项目的Settings

​ 项目的Settings是Scrapy项目的标准配置文件,在settings.py这个文件中添加或者修改配置的字段。

4.命令默认设定

​ 每个 Scrapy tool 命令拥有其默认设定,并覆盖了全局默认的设定。 这些设定在命令的类的 default_settings 属性中指定。

5.全局默认设定

默认的全局变量设定在scrapy.settings.default_settings模块里。

常用的settings参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
CONCURRENT_REQUESTS = 16                 # 全局最大并发数
CONCURRENT_REQUESTS_PER_DOMAIN = 8       # 单个域名最大并发数,如果下一个参数设置非0,此参数无效
CONCURRENT_REQUESTS_PER_IP = 0           # 单个ip最大并发数
COOKIES_ENABLED = True                   # 默认启用cookie
 
DEFAULT_REQUEST_HEADERS = {              # 设置默认请求头
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
}
 
DOWNLOAD_DELAY = 0                       # 下载延时,高并发采集时设为0
DOWNLOAD_TIMEOUT = 180                   # 超时时间设置,一般设置在10-30之间

LOG_ENABLED = True                       # 启用日志
LOG_STDOUT = False                       # 将进程所有的标准输出(及错误)重定向到log中,默认False。如果开启,在项目中的print方法也会以log的形式输出
LOG_LEVEL = 'DEBUG'                      # 日志输出级别,上线后至少使用info级别
LOG_FILE = None                          # 将日志输出到文件中
LOGSTATS_INTERVAL = 60.0                 # 吞吐量输出间隔,就是输出每分钟下载多少个页面、捕获多少个item的那个,默认每分钟输出一次,自主配置

REDIRECT_ENABLED = True                  # 默认开启页面跳转,一般选择关闭
RETRY_ENABLED = True                     # 默认开启失败重试,一般关闭
RETRY_TIMES = 2                          # 失败后重试次数,默认两次
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408]    # 碰到这些验证码,才开启重试
ROBOTSTXT_OBEY = False                   # 遵守网站robot协议,一般不遵守

DOWNLOADER_MIDDLEWARES = {               # 下载中间件
   'myproject.middlewares.MyDownloaderMiddleware': 543,
}

ITEM_PIPELINES = {                       # 数据处理、存储pipeline
   'myproject.pipelines.MyPipeline': 300,
}

实战

简单的实战一下,这里拿来练习的网站是苏宁图书

创建项目

win+R进入cmd后输入以下命令:

1
scrapy startproject suningBook

将会看到这样子的结果(表示项目创建成功):

1
2
3
4
5
6
7
D:\Scrapy Project>scrapy startproject suningBook
New Scrapy project 'suningBook', .....(此处省略) created in:
    D:\Scrapy Project\suningBook

You can start your first spider with:
    cd suningBook
    scrapy genspider example example.com

创建虫子

项目创建之后再创建一只虫子(spider):

命令:

1
scrapy genspider [spider文件命名] [爬取的网站域名]

过程如下:

1
2
3
4
5
D:\Scrapy Project>cd suningBook

D:\Scrapy Project\suningBook>scrapy genspider books book.suning.com
Created spider 'books' using template 'basic' in module:
  suningBook.spiders.books

Settings设置

1
2
3
4
5
6
7
8
9
#在settings.py编辑
# 不遵循Robot协议
ROBOTSTXT_OBEY = False
# 设置请求头
DEFAULT_REQUEST_HEADERS = {
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36'
}
# 输出的结果只显示要print的内容
LOG_LEVEL = 'WARNING'

明确需求

​ 首先要明确想要爬取的数据是哪些然后再去分析网页,不然需求都不清楚就跑去分析网页,就是瞎搞。

​ 那么我想爬取的数据是一个分类页面下的所有书本信息

分析网页

​ 这个网站存二级分类,一级如下图的”文学艺术“,二级为”小说“、”散文随笔“等

在网站的首页右键→检查

一级二级分类这些元素都在一个div标签中class="ment-item"里面,一级分类的内容在标签里面,二级分类内容在
标签里面,只要明确了最外层装着这些我们想要的内容,顺藤摸瓜就能找到目标数据的定位。

定位一级分类

1
2
3
4
5
6
7
8
9
#在books.py这个爬虫文件里面
    def parse(self, response):
        # 一级分类(L1表示Lecel 1)
        L1_list = response.xpath('//div[@class="menu-list"]/div/dl')
        # 获取大分类
        for L1 in L1_list:
            item = {}
            item['L1_cate'] = dl.xpath('./dt/h3/a/text()').extract_first()
            print(item)

这时候执行启动爬虫

1
2
# 执行命令
scrapy crawl [爬虫文件名称]

结果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
D:\Scrapy Project\suningBook>scrapy crawl books
{'L1_cate': '文学艺术'}
{'L1_cate': '少儿'}
{'L1_cate': '人文社科'}
{'L1_cate': '经管励志'}
{'L1_cate': '健康生活'}
{'L1_cate': '考试教育'}
{'L1_cate': '科技'}
{'L1_cate': '进口原版书'}
{'L1_cate': '期刊杂志'}

定位二级分类

既然一级分类已经获取到了,那么自然二级分类也是用同样的方法去解决

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#在books.py这个爬虫文件里面
    def parse(self, response):
        # 一级分类(L1表示Lecel 1)
        L1_list = response.xpath('//div[@class="menu-list"]/div/dl')
        # 获取大分类
        for L1 in L1_list:
            item = {}
            item['L1_cate'] = L1.xpath('./dt/h3/a/text()').extract_first()
#从这里接着写
            # 获取二级分类
            L2_list = L1.xpath('./dd/a')
            for L2 in L2_list:
                # 二级分类url
                item['L2_href'] = L2.xpath('./@href').extract_first()
                # 二级分类名称
                item['L2_name'] = L2.xpath('./text()').extract_first()
                yield scrapy.Request(
                    url=item['L2_href'],
                    callback=self.parse_book_list,#这行代码是进入二级URL的
                    meta={'item': deepcopy(item)}
                )
    # 进入二级URL            
	def parse_book_list(self, response):
        item = response.meta.get('item')
        print(item)

运行结果

1
2
3
4
5
{'L1_cate': '文学艺术', 'L2_href': 'https://list.suning.com/1-502320-0.html', 'L2_name':'小说'}
{'L1_cate': '文学艺术', 'L2_href': 'https://search.suning.com/%E6%95%A3%E6%96%87%E9%9A%8F%E7%AC%94/', 'L2_name': '散文随笔'}
{'L1_cate': '文学艺术', 'L2_href': 'https://list.suning.com/1-502348-0.html', 'L2_name':'青春文学'}
.....
#以下省略

​ 拿到了二级分类的url和名称,这样就让虫子爬取二级分类的url,恰好书籍信息在二级url的页面里。类似无限的取出套娃。

书籍列表页

​ 进入二级URL之后的工作就是分析书籍列表页面,并获取每一本书的信息。先分析页面,发现:

​ 每本书的信息在

  • 标签里,而这些
  • 标签又全都在
      这个标签里,那么获取每本书的连接,详情,图片连接的代码可以这样写

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      
          def parse_book_list(self, response):
              item = response.meta.get('item')
              #获取每一本书的信息
              books_list = response.xpath('//ul[@class="clearfix"]/li')
              for book in books_list:
                  #书籍详情URL
                  item['book_href'] = book.xpath('//div[@class="res-img"]/div/a/@href').extract_first()
                  # 书籍名字
                  item['book_name'] = book.xpath('//div[@class="res-img"]/div/a/img/@alt').extract_first()
                  # 书籍图片
                  item['src'] = book.xpath('//div[@class="res-img"]/div/a/img/@src2').extract_first()
                  print(item)
                  yield scrapy.Request(
                      url=item['book_href '],
                      callback=self.parse_book_detail,
                      meta={'item': deepcopy(item)}
                  )
      

      结果如下:

      1
      2
      3
      
      {'L1_cate': '文学艺术', 'L2_href': 'https://search.suning.com/%E7%A7%91%E5%B9%BB/', 'L2_name': '科幻', 'book_href':'//product.suning.com/0070129646/12181637004.html', 'book_name': '绿野仙踪正版书小学生正版 阅读励志7-9-12岁青少年版儿童文学书籍名著三年级四年级课外书书读物','src':'//imgservice3.suning.cn/uimg1/b2c/image/ZbF_iVDMfL1hCj6diWLkOg.jpg_220w_220h_4e'}
       #省略
       ......
      

      书本详情页面

      ​ 每一本书的详情URL拿到了,那就从URL入手,拿到书籍的详情信息(包括作者,出版社)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
          def parse_book_detail(self, response):
              item = response.meta.get('item')
              # 作者
              item['author'] = response.xpath('//ul[@class="bk-publish clearfix"]/li[1]/text()').extract_first()
              # 出版社
              item['publish'] = response.xpath('//ul[@class="bk-publish clearfix"]/li[2]/text()').extract_first()
              # 数据处理,采集到的数据有些多余的空格,需要去除
              item['author'] = item['author'].replace('\n', '').replace('\t', '').replace('</span>', '')
              item['publish'] = item['publish'].replace('\n', '').replace('\t', '').replace('</span>', '')
      

      ​ ok,至此暂时算是能够把书籍信息采集到,但是问题是,数据量有点少,还有我们只采集当前页面,并没有对书籍列表页面进行翻页。那接下来要做的就是解决翻页问题

      翻页

      翻页的时候,观察url变化规律:

      1
      2
      3
      4
      5
      6
      7
      
      # 第一页
      https://list.suning.com/1-502687-0.html
      # 第二页
      https://list.suning.com/1-502687-0.html#second-filter 
      # 第三页
      https://list.suning.com/1-502687-0.html#second-filter
      ....
      

      发现并没有什么规律,那只能去看看network了

      ​ 每翻一页,showProductList文件中的Request URl值都不一样,如下:

      1
      2
      3
      4
      5
      
      https://list.suning.com/emall/showProductList.do?ci=502320&pg=03&cp=0&il=0&iy=0&adNumber=0&n=1&ch=4&prune=0&sesab=ACBAABC&id=IDENTIFYING&paging=1&sub=0
      
      https://list.suning.com/emall/showProductList.do?ci=502320&pg=03&cp=1&il=0&iy=0&adNumber=0&n=1&ch=4&prune=0&sesab=ACBAABC&id=IDENTIFYING&cc=020
      
      https://list.suning.com/emall/showProductList.do?ci=502320&pg=03&cp=2&il=0&iy=0&adNumber=0&n=1&ch=4&prune=0&sesab=ACBAABC&id=IDENTIFYING&cc=020
      

      好像很乱还看不出什么规律,试试将一些参数删除掉会不会影响页面访问

      整理如下:

      1
      2
      3
      4
      5
      6
      
      # 第一页
      https://list.suning.com/emall/showProductList.do?ci=502320&pg=03&cp=0  
      # 第二页
      https://list.suning.com/emall/showProductList.do?ci=502320&pg=03&cp=1  
      # 第三页
      https://list.suning.com/emall/showProductList.do?ci=502320&pg=03&cp=2  
      

      ​ 规律出来了! 观察出ci代表分类代号,cp后面跟着的就是页码。ci后面的参数可以在parse方法中获取

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
       # 一级分类(L1表示Lecel 1)
              L1_list = response.xpath('//div[@class="menu-list"]/div/dl')
              # 获取大分类
              for L1 in L1_list:
                  item = {}
                  item['L1_cate'] = L1.xpath('./dt/h3/a/text()').extract_first()
      
                  # 获取二级分类
                  L2_list = L1.xpath('./dd/a')
                  for L2 in L2_list:
                      # 二级分类url
                      item['L2_href'] = L2.xpath('./@href').extract_first()
                      # 二级分类名称
                      item['L2_name'] = L2.xpath('./text()').extract_first()
                      # 获取ci
                      item['ci'] = item['L2_href'][26:32]
                      # 翻页url
                      item['next_url'] = 'https://list.suning.com/emall/showProductList.do?ci=' + item['ci'] + '&pg=03&cp={}'
                      yield scrapy.Request(
                          url=item['L2_href'],
                          callback=self.parse_book_list,
                          meta={'item': deepcopy(item)}
                      )
      

      在parse_book_list里面执行翻页代码

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      
       def parse_book_list(self, response):
              item = response.meta.get('item')
              #获取每一本书的信息
              books_list = response.xpath('//ul[@class="clearfix"]/li')
              for book in books_list:
                  #书籍详情URL
                  item['book_href'] = 'https:' + book.xpath('//div[@class="res-img"]/div/a/@href').extract_first()
                  # 书籍名字
                  item['book_name'] = book.xpath('//div[@class="res-img"]/div/a/img/@alt').extract_first()
                  # 书籍图片
                  item['src'] = 'https:' + book.xpath('//div[@class="res-img"]/div/a/img/@src2').extract_first()
      
                  yield scrapy.Request(
                      url=item['book_href'],
                      callback=self.parse_book_detail,
                      meta={'item': deepcopy(item)}
                  )
              # 翻页
              # 这里的页数是写死的,也可以抓取网页的pageNumber(总页数)
              for i in range(5):
                  next_url = item['next_url'].format(i+1)
                  yield scrapy.Request(
                      url=next_url,
                      callback=self.parse_book_list,
                      meta={'item': deepcopy(item)}
                  )
      

      这里差不多就写完了,我这里没有增加数据额外的数据清洗和数据处理(懒)

      存储数据

      把数据存到MySQl里面,代码就不必多讲了,看注释就能看得懂

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      
      #编辑pipelines.py文件
      from itemadapter import ItemAdapter
      import pymysql
      import sshtunnel
      
      class SuningbookPipeline:
      
          server = sshtunnel.SSHTunnelForwarder(
              ('xxx.xxx.xxx.xxx', 22), # 填写你服务器的IP
              ssh_username='xxx',		# 连接的用户名
              ssh_password='xxxx',	# 密码
              remote_bind_address=('xx.xx.xx.xx', 3306), #也是服务器IP
              local_bind_address=('127.0.0.1', 3306)
          )
          server.start()
      
          print('SSH连接成功')
      
          def __init__(self):
                  self.connect = pymysql.connect(
                      host='127.0.0.1',
                      port=3306,		#MySQL端口
                      user='xxx',		#数据库用户名
                      database='xxx',	#数据库名
                      password='xxx',	#数据库密码
                      charset='utf8'
                  )
                  print('mysql数据库连接成功')
                  self.cursor = self.connect.cursor()
                  print('游标获取成功')
      
          def process_item(self, item, spider):
      
      
              info = """INSERT INTO Book(BookcName,Author,Publish) VALUES ('%s','%s','%s')""" \
                     % (
                         item['book_name'],
                         item['author'],
                         item['publish'],
                     )
              self.cursor.execute(info)
              self.connect.commit()
              print('insert succeed')
              return item
      
          def close_spider(self,spider):
              self.cursor.close()
              self.connect.close()
      

      实战就到这了,多动手写多观察,总是能学会的。

      具体代码在这里👉传送门

      结尾

      不知不觉写了好多字。。。。