Day 23 23.2 Scrapy框架之详解

发布时间 2023-04-07 16:01:53作者: Chimengmeng

Scrapy框架详解

【1】 Spider类

  • Spiders是定义如何抓取某个站点(或一组站点)的类,包括如何执行爬行(即跟随链接)以及如何从其页面中提取结构化数据(即抓取项目)。
  • 换句话说,Spiders是您为特定站点(或者在某些情况下,一组站点)爬网和解析页面定义自定义行为的地方。 
1、 生成初始的Requests来爬取第一个URLS,并且标识一个回调函数
     第一个请求定义在start_requests()方法内默认从start_urls列表中获得url地址来生成Request请求,
     默认的回调函数是parse方法。回调函数在下载完成返回response时自动触发

2、 在回调函数中,解析response并且返回值
     返回值可以4种:
          包含解析数据的字典
          Item对象
          新的Request对象(新的Requests也需要指定一个回调函数)
          或者是可迭代对象(包含Items或Request)

3、在回调函数中解析页面内容
   通常使用Scrapy自带的Selectors,但很明显你也可以使用Beutifulsoup,lxml或其他你爱用啥用啥。

4、最后,针对返回的Items对象将会被持久化到数据库
   通过Item Pipeline组件存到数据库:https://docs.scrapy.org/en/latest/topics/item-pipeline.html)
   或者导出到不同的文件(通过Feed exports:https://docs.scrapy.org/en/latest/topics/feed-exports.html)
   

应用(网易新闻)

import scrapy

class WangyiSpider(scrapy.Spider):
    name = 'wangyi'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://news.163.com/']
    cate_list = []  # 每一个板块对应的url

    cate_num_map = {
        "{{__i == 0}}": "新闻",
        "{{__i == 1}}": "北京",
        "{{__i == 2}}": "国内",
        "{{__i == 3}}": "国际",
        "{{__i == 4}}": "独家",
    }

    # 数据解析:每一个板块对应的url
    def parse(self, response):
        with open("news163.html", "w") as f:
            f.write(response.text)

        cate_num_list = response.xpath('//*[contains(@ne-if,"{{")]/@ne-if').extract()
        print("cate_num_list", cate_num_list)

        for cate_num in cate_num_list:
            print("cate_num", cate_num)
            if cate_num in self.cate_num_map:
                cate_title = self.cate_num_map.get(cate_num)
                news_list = response.xpath(f'//*[contains(@ne-if,"{cate_num}")]/div/a')
                print(news_list)

                for news_selector in news_list:
                    news_title = news_selector.xpath('text()').extract_first()
                    news_detail_url = news_selector.xpath('@href').extract_first()

                    print("new_title", news_title)
                    print("new_detail_url", news_detail_url)
                    print("cate_title", cate_title)

                    # 对新闻详情页的url发起请求
                    print("发起新的请求,", news_detail_url)
                    yield scrapy.Request(url=news_detail_url, callback=self.parse_news_detail)

    def parse_news_detail(self, response):
        # 解析新闻内容
        # content = response.xpath('//*[@id="content"]/div[@class="post_body"]/p[contains(@id,"1M")]/text()').extract()
        content = response.xpath('//*[@id="content"]/div[@class="post_body"]/p/text()').extract()
        content = ''.join([i.strip() for i in content])

        if content:
            print("content", content)

    # 爬虫类父类的方法,该方法是在爬虫结束最后一刻执行
    def closed(self, spider):
        # self.bro.quit()
        print("此次爬虫全部结束!")

【2】 Item

  • 抓取的主要目标是从非结构化源(通常是网页)中提取结构化数据。
    • Scrapy爬虫可以像Python一样返回提取的数据。
    • 虽然方便和熟悉,但很容易在字段名称中输入拼写错误或返回不一致的数据,尤其是在具有许多爬虫的较大项目中。
  • 为了定义通用输出数据格式,Scrapy提供了Item类。
    • Item对象是用于收集抓取数据的简单容器。
      • 它们提供类似字典的 API,并具有用于声明其可用字段的方便语法。

(1)、声明项目

  • 使用简单的类定义语法和Field 对象声明项。
  • 这是一个例子:
import scrapy
 
class Product(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    stock = scrapy.Field()
    last_updated = scrapy.Field(serializer=str)
  • 注意那些熟悉Django的人会注意到Scrapy Items被宣告类似于Django Models,
    • 除了Scrapy Items更简单,因为没有不同字段类型的概念。
  • Field对象用于指定每个字段的元数据。
    • 例如,last_updated上面示例中说明的字段的序列化函数。
  • 您可以为每个字段指定任何类型的元数据。
    • Field对象接受的值没有限制。
    • 出于同样的原因,没有所有可用元数据键的参考列表。
  • Field对象中定义的每个键可以由不同的组件使用,只有那些组件知道它。
    • 您也可以根据Field自己的需要定义和使用项目中的任何其他键。
  • Field对象的主要目标是提供一种在一个地方定义所有字段元数据的方法。
    • 通常,行为取决于每个字段的那些组件使用某些字段键来配置该行为。

(2)、应用(网易新闻)

import scrapy
from items import NewsProItem


class WangyiSpider(scrapy.Spider):
    name = 'wangyi'
    # allowed_domains = ['www.xxx.com']
    start_urls = ['https://news.163.com/']
    cate_list = []  # 每一个板块对应的url
    cate_num_map = {
        "{{__i == 0}}": "新闻",
        "{{__i == 1}}": "北京",
        "{{__i == 2}}": "国内",
        "{{__i == 3}}": "国际",
        "{{__i == 4}}": "独家",
    }

    # 数据解析:每一个板块对应的url
    def parse(self, response):
        with open("news163.html", "w") as f:
            f.write(response.text)

        cate_num_list = response.xpath('//*[contains(@ne-if,"{{")]/@ne-if').extract()
        print("cate_num_list", cate_num_list)

        for cate_num in cate_num_list:
            print("cate_num", cate_num)
            if cate_num in self.cate_num_map:
                cate_title = self.cate_num_map.get(cate_num)
                news_list = response.xpath(f'//*[contains(@ne-if,"{cate_num}")]/div/a')
                print(cate_title)
                print(news_list)

                for news_selector in news_list:
                    news_title = news_selector.xpath('text()').extract_first()
                    news_detail_url = news_selector.xpath('@href').extract_first()
                    cate_title = self.cate_num_map.get(cate_num)
                    print("new_title", news_title)
                    print("new_detail_url", news_detail_url)
                    print("cate_title", cate_title)
                    # 存储数据到item对象
                    item = NewsProItem()
                    item['cate'] = cate_title
                    item['title'] = news_title
                    # 对新闻详情页的url发起请求
                    print("发起新的请求,", news_detail_url)
                    yield scrapy.Request(url=news_detail_url, callback=self.parse_news_detail, meta={'item': item})

    def parse_news_detail(self, response):
        # 解析新闻内容
        # content = response.xpath('//*[@id="content"]/div[@class="post_body"]/p[contains(@id,"1M")]/text()').extract()
        content = response.xpath('//*[@id="content"]/div[@class="post_body"]/p/text()').extract()
        content = ''.join([i.strip() for i in content])
        item = response.meta['item']
        item['content'] = content
        if content:
            print("item", item)
            yield item

    # 爬虫类父类的方法,该方法是在爬虫结束最后一刻执行
    def closed(self, spider):
        # self.bro.quit()
        print("此次爬虫全部结束!")

【3】 Item PipeLine

  • 在一个项目被蜘蛛抓取之后,
    • 它被发送到项目管道,该项目管道通过顺序执行的几个组件处理它。
  • 每个项目管道组件(有时简称为“项目管道”)是一个实现简单方法的Python类。
    • 他们收到一个项目并对其执行操作,
    • 同时决定该项目是否应该继续通过管道或被丢弃并且不再处理。
  • 项目管道的典型用途是:
  • cleansing HTML data
  • validating scraped data (checking that the items contain certain fields)
  • checking for duplicates (and dropping them)
  • storing the scraped item in a database

(1) 编写自己的项目管道

'''
每个项管道组件都是一个必须实现以下方法的Python类:

process_item(self,项目,蜘蛛)
为每个项目管道组件调用此方法。process_item() 

必须要么:返回带数据的dict,返回一个Item (或任何后代类)对象,返回Twisted Deferred或引发 DropItem异常。丢弃的项目不再由其他管道组件处理。

此外,他们还可以实现以下方法:

open_spider(self,蜘蛛)
打开蜘蛛时会调用此方法。

close_spider(self,蜘蛛)
当蜘蛛关闭时调用此方法。

from_crawler(cls,crawler )
如果存在,则调用此类方法以从a创建管道实例Crawler。它必须返回管道的新实例。Crawler对象提供对所有Scrapy核心组件的访问,
如设置和信号; 它是管道访问它们并将其功能挂钩到Scrapy的一种方式。
'''

(2) 项目管道示例

[2.1)]价格验证和丢弃物品没有价格

  • 让我们看看下面的假设管道,
    • 它调整 price那些不包含增值税(price_excludes_vat属性)的项目的属性,并删除那些不包含价格的项目:
from scrapy.exceptions import DropItem
 
class PricePipeline(object):
 
    vat_factor = 1.15
 
    def process_item(self, item, spider):
        if item['price']:
            if item['price_excludes_vat']:
                item['price'] = item['price'] * self.vat_factor
            return item
        else:
            raise DropItem("Missing price in %s" % item)

[2.2] 将项目写入JSON文件

  • 以下管道将所有已删除的项目(来自所有蜘蛛)存储到一个items.jl文件中,
    • 每行包含一个以JSON格式序列化的项目:
  • 注意JsonWriterPipeline的目的只是介绍如何编写项目管道。
    • 如果您确实要将所有已删除的项目存储到JSON文件中,则应使用Feed导出。
import json
 
class JsonWriterPipeline(object):
 
    def open_spider(self, spider):
        self.file = open('items.jl', 'w')
 
    def close_spider(self, spider):
        self.file.close()
 
    def process_item(self, item, spider):
        line = json.dumps(dict(item)) + "\n"
        self.file.write(line)
        return item

[2.3] 将项目写入数据库(网易新闻应用)

  • 在这个例子中,我们将使用pymongo将项目写入MongoDB
    • MongoDB地址和数据库名称在Scrapy设置中指定;
    • MongoDB集合以item类命名。
from scrapy.exceptions import DropItem
import pymongo


class NewsproPipeline:
    def process_item(self, item, spider):
        print("item:::", item)
        if not item["content"]:
            raise DropItem(f"{item['title']}content为空")

        return item


class MongoPipeline(object):
    mongo_db = "news"
    collection_name = 'wangyiNews'

    def __init__(self, mongo):
        self.mongo = mongo

    @classmethod
    def from_crawler(cls, crawler):
        # Scrapy会先通过getattr判断我们是否自定义了from_crawler,有则调它来完成实例化

        return cls(crawler.settings.get('MONGO'))

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(host=self.mongo[0], port=self.mongo[1])
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        self.db[self.collection_name].insert_one(dict(item))
        print(f"news数据库的{self.collection_name}表中插入{item['title']}记录")
        return item

  • 这个例子的要点是展示如何使用from_crawler() 方法以及如何正确地清理资源:

[2.4] 重复过滤

  • 一个过滤器,用于查找重复项目,并删除已处理的项目。
    • 假设我们的项目具有唯一ID,但我们的蜘蛛会返回具有相同ID的多个项目:
from scrapy.exceptions import DropItem
 
class DuplicatesPipeline(object):
 
    def __init__(self):
        self.ids_seen = set()
 
    def process_item(self, item, spider):
        if item['id'] in self.ids_seen:
            raise DropItem("Duplicate item found: %s" % item)
        else:
            self.ids_seen.add(item['id'])
            return item

(3) 激活项目管道组件

  • 要激活Item Pipeline组件,必须将其类添加到settings.pyITEM_PIPELINES设置中,
    • 如下例所示:
ITEM_PIPELINES = {
    'myproject.pipelines.PricePipeline': 300,
    'myproject.pipelines.JsonWriterPipeline': 800,
    "NewsPro.pipelines.MongoPipeline": 500,
}
  • 您在此设置中为类分配的整数值决定了它们运行的顺序:
    • 项目从较低值到较高值类进行。
    • 习惯上在0-1000范围内定义这些数字。

【4】下载中间件

class MyDownMiddleware(object):
    def process_request(self, request, spider):
        """
        请求需要被下载时,经过所有下载器中间件的process_request调用
        :param request: 
        :param spider: 
        :return:  
            None,继续后续中间件去下载;
            Response对象,停止process_request的执行,开始执行process_response
            Request对象,停止中间件的执行,将Request重新调度器
            raise IgnoreRequest异常,停止process_request的执行,开始执行process_exception
        """
        pass


    def process_response(self, request, response, spider):
        """
        spider处理完成,返回时调用
        :param response:
        :param result:
        :param spider:
        :return: 
            Response 对象:转交给其他中间件process_response
            Request 对象:停止中间件,request会被重新调度下载
            raise IgnoreRequest 异常:调用Request.errback
        """
        print('response1')
        return response

    def process_exception(self, request, exception, spider):
        """
        当下载处理器(download handler)或 process_request() (下载中间件)抛出异常
        :param response:
        :param exception:
        :param spider:
        :return: 
            None:继续交给后续中间件处理异常;
            Response对象:停止后续process_exception方法
            Request对象:停止中间件,request将会被重新调用下载
        """
        return None