05 Scrapy框架

一. 介绍

img

'''
介绍: 通用的网络爬虫框架, 可以说它是爬界的django
作用: 网络页面抓取
起源: 由twisted框架开发而来, 开发的Scrapy是非阻塞

图分析: 五大组件 Components
	1. spiders    /ˈspaɪdə(r)z/  网页爬虫 
		作用: 开发人员自定义的类, 解析response, 提取items, 或者发送新的请求request
	2. ENGINE     /ˈendʒɪn/      引擎   
		作用: 负责控制数据的流向(总管)   
	3. SCHEDULER  /ˈʃedjuːlə(r)/ 调度器 
		作用: 去重, 决定下一个通过配置实现深度优先爬取还是广度优先爬取 (原理: 队列 -> 广度, 堆栈 -> 深度), 由它来决定下一个要抓取的网址是什么.
	4. DOWNLOADER  下载器   
		作用: 用于下载网页内容. 并将网页内容返回给EGINE, 下载器是建立在twisted这个搞笑的异步模型之上.
		注意: 进过下载中间件过来. 中间件中可以使用user-agent, proxies
	5. ITEM PIPELINES 项目管道
		作用: 在items被提取后负责处理它们,主要包括清理、验证、持久化(比如: 存到数据库)等操作
		
两大中间件: 
	1. spiders和egine之间: 处理输入, 输出
	2. downloader和egine之间: 加proxies, headers, 集成selenium
'''

二. Scrapy安装(windows, mac, linux)

'''
一. pip3 install scrapy 
二. Windows平台(成功率80%)
    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平台
    1、pip3 install scrapy	
'''

三. Scrapy创建项目, 创建爬虫, 运行爬虫

'''
# 1. scrapy startproject 项目名
	scrapy startproject first_scrapy
    
# 2. 创建爬虫
	cd first_scrapy
    scrapy genspider 爬虫名 爬虫地址
    scrapy genspider chouti dig.chouti.com
    执行就会在spider文件夹下创建出一个py文件,名字叫chouti
    
# 3. 运行爬虫
	scrapy crawl chouti   # 带运行日志
	scrapy crawl chouti --nolog  # 不带日志
    
# 4. 支持右键执行爬虫
	在项目路径下新建一个.py文件 推荐: main.py 
	from scrapy.cmdline import execute
	execute(['scrapy','crawl','chouti','--nolog'])
'''

四. 目录介绍

first_scrapy/               # 项目名
├── first_scrapy/           # 包
    ├── spiders/            # 所有的爬虫文件存放路径
        ├── __init__.py
        ├── baidu.py        # 新建的爬虫文件1
        ├── chouti.py       # 新建的爬虫文件2
    ├── __init__.py
    ├── items.py            # items类
    ├── main.py             # 自己创建, 作为启动脚本
    ├── middlewares.py      # 中间件(爬虫,下载中间件都写在这)
    ├── pipelines.py        # 持久化相关(items.py中类的对象)
    └── settings.py         # 配置文件
└── scrapy.cfg              # 上线相关

五. settings 介绍

ROBOTSTXT_OBEY = False   # 关闭默认遵守的爬虫协议,  默认情况,scrapy会去遵循爬虫协议

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36'

LOG_LEVEL = 'ERROR'

六. 解析器的使用方式

1. 在爬取页面以后爬取其他网址

def parse(self, response):
    print(response.status)

    from scrapy.http.request import Request
    return Request('https://www.baidu.com', dont_filter=True)

2. 使用第三方解析

def parse(self, response):
    from bs4 import BeautifulSoup
    soup = BeautifulSoup(response.text, 'lxml')
    div_list = soup(class_='link-con')
    print(div_list)
    for div in div_list:
        image_url = div.a.get('data-pic')
        desc = div.find(class_='link-title').text
        print(f"""
        图片链接: {image_url}
        内容详情: {desc}
        """)  

3. 使用自带解析

# 自带解析: xpath, css
resposne.xpath('xpath语法')
resposne.css('css语法')

# css
    取文本: css('::text')
    取属性: css('::attr(href)')

# xpath
    取文本: xpath("/text()")
     取属性: xpath("/@href")

# 取值:  注意: 如果指定取出来的结果是一个 Selector 对象
    取出列表: .extract()
    取出单个: .extract_first()

1) 使用css解析

def parse(self, response):
    # 使用css解析
    div_list = response.css('.link-item')
    print(len(div_list))
    for div in div_list:
        desc = div.css('.link-title::text').extract_first()
        image_url = div.css('.common-matching-con::attr(data-pic)').extract_first()
        if not desc:
            continue
        print(f"""
        文本描述: {desc}
        图片链接: {image_url}
        """)

2) 使用xpath解析

def parse(self, response):
    # 使用xpath解析
    div = response.xpath('//div[contains(@class, "link-item")]')[0]
    desc_list = div.xpath('//a[contains(@class, "link-title")]/text()').extract()
    image_url_list = div.xpath('//a[contains(@class, "common-matching-con")]/@data-pic').extract()
    for desc, image_url in zip(desc_list, image_url_list):
        if not desc:
            continue
        print(f"""
        文本描述: {desc}
        图片链接: {image_url}
        """)

七. 数据持久化的两种方式

'''
持久化方案一: 
	提示: parse函数中必须返回列表套自己的格式 [{}, {}, ...]
	scrapy crawl chouti -o chouti.csv
	scrapy crawl chouti -o chouti.json
	持久化支持格式: 'json', 'jsonl
ines', 'jl', 'csv', 'xml', 'marshal', 'pickle'

持久化方案二: 通过pipline item存储
	1. items.py中定义items类
	2. 在spinder中导入, 实例化, 把数据放入(注意: 使用yield)
		item[key] = value
		yield item
	3. 配置文件中找到 ITEM_PIPELINES 配置pipelines.py中定义的pipelines类
        ITEM_PIPELINES = {
           'first_scrapy.pipelines.pipelines.py中定义的pipelines类': 300,  # 数子越小级别越高
        }		
	4. 在piplines.py中写配置的类
		open_spider(self, spider)  开
		process_item(self, item, spider)  
			注意: 一定要return item
		close_spider(self, spider) 关
'''

1. 持久化方案一

def parse(self, response):
    li = []
    div = response.xpath('//div[contains(@class, "link-item")]')[0]
    desc_list = div.xpath('//a[contains(@class, "link-title")]/text()').extract()
    image_url_list = div.xpath('//a[contains(@class, "common-matching-con")]/@data-pic').extract()
    li = []
    for desc, image_url in zip(desc_list, image_url_list):
        li.append({'desc': desc, 'image_url': image_url})
    return li 

# 命令行终端中执行持久化生成对应格式的文件: 
scrapy crawl chouti -o chouti.csv

2. 持久化方案二

1) items.py

class ChouTiScrapyItem(scrapy.Item):
    desc = scrapy.Field()
    image_url = scrapy.Field()

2) spiders/chouti.py

def parse(self, response):
    from first_scrapy.items import ChouTiScrapyItem
    item = ChouTiScrapyItem()

    div_list = response.css('.link-item')
    print(len(div_list))
    for div in div_list:
        desc = div.css('.link-title::text').extract_first()
        image_url = div.css('.common-matching-con::attr(data-pic)').extract_first()
        if not desc or not image_url:
            continue
        item['desc'] = desc
        item['image_url'] = image_url
        yield item

3) pipelines.py: 书写存储到文件, mysql, redis中的持久化类

import json
import pymysql
from redis import Redis
from itemadapter import ItemAdapter


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


class ChouTiFilePipline:
    """存储到文件中的持久化类"""
    def open_spider(self, spider):
        print('ChouTiFilePipline 开始!')
        self.f = open('chouti.json', 'w', encoding='utf-8')

    def process_item(self, item, spider):
        data = {'desc': item.get('desc'), 'image_url': item['image_url']}
        json.dump(data, self.f, ensure_ascii=False)
        self.f.write('\n\n')
        # 注意: 这里一定要返回item, 不然后续的持久化类获取的值就是None
        return item

    def close_spider(self, spider):
        print("ChouTiFilePipline 结束!")
        self.f.close()


class ChouTiMySQLPipline:
    """存储到MySQL中的持久化类"""
    def open_spider(self, spider):
        print('ChouTiMySQLPipline 开始!')
        self.conn = pymysql.connect(
            host='127.0.0.1', user="root", password="123",
            database='crawler', port=3306, charset='utf8'
        )
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        sql = 'insert into chouti(desc_content, image_url) values(%s, %s);'
        try:
            self.cursor.execute(sql, [item.get("desc"), item.get('image_url')])
            self.conn.commit()
        except Exception as e:
            print(e)
            self.conn.rollback()
        return item

    def close_spider(self, spider):
        print('ChouTiMySQLPipline 结束!')
        self.cursor.close()
        self.conn.close()


class ChouTiReidsPipline:
    """存储到redis中的持久化类"""
    def open_spider(self, spider):
        print('ChouTiReidsPipline 开始!')
        self.conn = Redis.from_url('redis://127.0.0.1/0')

    def process_item(self, item, spider):
        data = {'desc': item.get('desc'), 'image_url': item['image_url']}
        pipe = self.conn.pipeline(transaction=True)
        pipe.multi()    # 启动事务
        self.conn.rpush('chouti', json.dumps(data, ensure_ascii=False))
        pipe.execute()  # 提交
        return item

    def close_spider(self, spider):
        print('ChouTiReidsPipline 结束!')
        self.conn.close()

八. scrapy的请求传参

# 传递: 把要传递的数据放到meta中
	yield Request(url, meta={'item': item})
	
# 取值: 在response对象中取出来
	item = response.meta.get('item')

# 实现思路:
	downloader中将出去的request中包含的item, 保存. 在response返回的时候赋值给response. 之后通过engine调度返回给spider. 

实例: 全站爬取cnblogs

'''
爬取原则: scrapy默认是先进先出
	深度优先:详情页先爬   队列,先进先出
	广度优先:每一页先爬   堆栈,后进先出
'''

# spiders/cnblogs.py
import scrapy
from scrapy.http.request import Request
from first_scrapy.items import CnblogsScrapyItem


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('#post_list .post-item')
        for article in article_list:
            desc = article.css('.post-item-summary::text').extract_first().strip()
            title_url = article.css('.post-item-title::attr(href)').extract_first()
            title = article.css('.post-item-title::text').extract_first()
            item = CnblogsScrapyItem()
            item['title_url'] = title_url
            item['title'] = title
            item['desc'] = desc.strip()
            # 继续爬取详情. 提示: callback如果不写,默认回调到parse方法, 如果写了,响应回来的对象就会调到自己写的解析方法中.
            yield Request(title_url, callback=self.parse_article, meta={'item': item})

        next_page_url = 'https://www.cnblogs.com' + response.css(
            '#paging_block a:last-child::attr(href)').extract_first()
        yield Request(next_page_url)

    def parse_article(self, response):
        # content = response.css('//[@id="cnblogs_post_body"]').extract()
        content = response.css('#cnblogs_post_body').extract_first()
        item = response.meta.get('item')
        item['content'] = content
        yield item

        
# items.py
class CnblogsScrapyItem(scrapy.Item):
    """
    item['title_url'] = title_url
    item['title'] = title
    item['desc'] = desc
    item['content'] = content
    """
    title_url = scrapy.Field()
    content = scrapy.Field()
    desc = scrapy.Field()
    title = scrapy.Field()
    
# settings.py
ITEM_PIPELINES = {
   # cnblogs
   'first_scrapy.pipelines.CnblogsMySQLPipline': 301,
}

# pipelines.py
import json
import pymysql

class CnblogsMySQLPipline:
    """将爬取的Cnblogs存储到MySQL中进行持久化"""

    def open_spider(self, spider):
        print('CnblogsMySQLPipline 开始!')
        self.conn = pymysql.connect(
            host='127.0.0.1', user="root", password="123",
            database='crawler', port=3306, charset='utf8'
        )
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        sql = 'insert into cnblogs(title, title_url, `desc`, content) values(%s, %s, %s, %s);'
        try:
            self.cursor.execute(sql, [item['title'], item['title_url'], item['desc'], item['content']])
            self.conn.commit()
        except Exception as e:
            print(e)
            self.conn.rollback()
        return item

    def close_spider(self, spider):
        print('CnblogsMySQLPipline 结束!')
        self.cursor.close()
        self.conn.close()

九. 提升scrapy爬取数据的效率

提示: 以下的实现在配置文件中进行相关的配置即可(注意: 默认还有一套setting)

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

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

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

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

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

十. scrapy的中间件: 下载中间件

1. process_request

def process_request(self, request, spider):
    # 返回值:
    # 1. return None: 请求正常继续往后走
    # 2. return Response: 请求不出去, 而是直接原路返回交给engine进行处理
    # 3. return Request:  请求不出去, 而是直接原路返回交给engine进行处理
    # 4. raise: 一旦抛出异常就会交给process_exception方法处理. 应用场景: 筛选排除不爬取的request.

1) 加随机请求头: fake-useragent 或者 Faker

方式一: 使用fake-useragent

# 1. 下载: 
pip3 install fake-useragent

# 2. 导入使用:
# 声明成中间件类属性
from fake_useragent import UserAgent
ua = UserAgent()

# 在process_request中使用: 返回值None
request.headers['User-Agent'] = ua.random

方式二: 使用Faker

# 1. 下载: 
pip3 install faker    	

# 2. 导入使用:
# 声明成中间件类属性
from faker import Faker
f = Faker(locale='zh_CN')

# 在process_request中使用: 返回值None
request.headers['User-Agent'] = f.user_agent()

注意: headers类最终继承了dict因此可以使用字段赋值的方式给请求头加user-agent

from scrapy.http.headers import Headers

2) 加cookie

加cookie

# 在process_request中使用: 返回值None
request.cookies = {'cookie1': 'value1'}

加cookie池

# 声明成spiders类属性
cookie_pool = [{'cookie1': 'value1'}, {'cookie2': 'value2'}, ...]

# 在process_request中使用: 返回值None
imoport random
request.cookies = random.choice(spider.cookie_pool)

3) 加代理

加代理

# 在process_request中使用: 返回值None
request.meta['download_timeout'] = 20
request.meta['proxy'] = 'http://111.72.149.117:30057'

加代理池

# spiders中
import requests

class DemoSpider(scrapy.Spider):
    def __init__(self, name=None, **kwargs):
        super().__init__(name=None, **kwargs)
        self.proxy = None

    @staticmethod
    def get_proxy():
        return requests.get("http://127.0.0.1:5010/get/").json().get("proxy")

    @staticmethod
    def delete_proxy(proxy):
        requests.get("http://127.0.0.1:5010/delete/?proxy={}".format(proxy))

    def close(self, reason):
        """self就是spider对象"""
        self.delete_proxy(self.proxy)
    ...
    
# 在process_request中使用: 返回值None
request.meta['download_timeout'] = 20
spider.proxy = spider.get_proxy()
request.meta['proxy'] = f'http://{spider.proxy}'

4) 加selenium

# spiders中
from selenium import webdriver

class DemoSpider(scrapy.Spider):
    browser = webdriver.Chrome()
    
    def close(self, reason):
        """self就是spider对象"""
        self.browser.close()
 
# 在process_request中使用: 返回response对象
from scrapy.http import HtmlResponse, Response

spider.browser.get(request.url)
response = HtmlResponse(url=request.url, body=spider.browser.page_source.encode('utf-8'), request=request)
return response

2. process_response

def process_response(self, request, response, spider):
    # 返回值
    # 1. return Response: 正常继续执行.
    # 2. return Request: 交给engine, 让engine进行处理以后继续往下走.
    # 3. raise: 一旦抛出异常就不因该走ITEMS进行持久化, 因为这样的数无法解析一解析就会抛出异常! 应用场景: 排除爬取网页频繁从而出现的验证校验.

3. process_exception

def process_exception(self, request, exception, spider):
    # 返回值:
    # 1. return None: 捕获process_request和process_response抛出的异常, 将本次数据及请求丢弃.
    # 2. return Response: 捕获process_request和process_response抛出的异常, 停止当连接将返回的response的结果丢给engine, 让engine继续进行处理
    # 3. return Request:  捕获process_request和process_response抛出的异常, 停止当连接将返回的request对象的结果丢给engine, 让engine继续进行处理

    # 应用场景: 捕获process_request和process_response抛出的异常, 停止当连接指定一个新的url使用Request进行切换
    '''
    from scrapy import Request

    # 注意: url不能直接修改
    request.url = 'https://www.cnblogs.com'
    # request = Request(url='https://www.cnblogs.com', callback=spider.spider, )
    return request
    '''

    pass

十一. 去重

1. 内置去重

'''
# 去重关键: request_seen
# 默认内置: 用的是集合去重
'''
# 去重类: RFPDupeFilter
from scrapy.dupefilters import RFPDupeFilter

# 去重关键方法: RFPDupeFilter下的request_seen方法下的request_fingerprint方法
from scrapy.utils.request import request_fingerprint

# fingerprint指纹本质就是使用md5加密, 但是是通过对?号后面参数进行排序实现
from scrapy.utils.request import request_fingerprint
from scrapy import Request

url1 = Request(url='https://www.baidu.com/?age=18&name=yang')
url2 = Request(url='https://www.baidu.com/?name=yang&age=18')
res1 = request_fingerprint(url1)
res2 = request_fingerprint(url2)
print(res1)  # 5cee3ea26b02e17f343ebb143dbbb3cc69f406bc
print(res2)  # 5cee3ea26b02e17f343ebb143dbbb3cc69f406bc
print(res1 is res2)  # False

2. 布隆过滤器去重

1) 下载

  1. 先去这个网站下载bitarray这个依赖 https://www.lfd.uci.edu/~gohlke/pythonlibs/#bitarray
  2. 直接安装会报错error: Microsoft Visual C++ 14.0 is required. Get it with "Build Tools for Visual Studio": https://visualstudio.microsoft.com/downloads/
  3. 安装wheel文件, 防止我们主动安装报这样的错误pip3 install bitarray-1.1.0-cp36-cp36m-win_amd64.whl
  4. pip3 install pybloom_live

wheel文件如何安装: https://www.cnblogs.com/justblue/p/13198202.html

2) 使用

'''
# 更高效方式实现: 布隆过滤器
	作用:去重, 解决缓存穿透
	缺点: 错误率存在, 不过取绝与数组的位数
	
# 自定义使用布隆过滤器去重流程
	1. 新建.py文件重写过滤类, 并继承BaseDupeFilter
	2. 重写request_seen方法
	3. 配置文件中配置. 使用它默认加载
		项目结构: first_scrapy/first_scrapy/bloom_filter.py/BloomFilter
		DUPEFILTER_CLASS = 'first_scrapy.bloom_filter.BloomFilter'  # 默认的去重规则帮我们去重,去重规则在内存中, settings.py配置文件中需要自己配置
'''
from scrapy.dupefilters import BaseDupeFilter
from pybloom_live import ScalableBloomFilter


class BloomFilter(BaseDupeFilter):
    """Request Fingerprint duplicates filter"""

    def __init__(self, path=None, debug=False):
        # ScalableBloomFilter可自动扩容的布隆过滤器
        self.bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH)
        super().__init__(path=None, debug=False)

    def request_seen(self, request):
        if request.url in self.bloom:
            return True
        self.bloom.add(request.url)

参考: https://www.cnblogs.com/xiaoyuanqujing/protected/articles/11969224.html

posted @ 2020-08-16 16:10  给你加马桶唱疏通  阅读(113)  评论(0编辑  收藏  举报