13. 爬虫之Scrapy框架的使用总结

介绍

scrapy 是一个开源和协作的框架,用于爬虫。
以快速、简单、可扩展的方式。

项目地址:https://github.com/scrapy/scrapy
官方网站:https://scrapy.org/
官方文档:https://docs.scrapy.org/en/latest/
官方教程:https://docs.scrapy.org/en/latest/intro/tutorial.html

架构:

13. 爬虫之Scrapy框架的使用总结

scrapy-redis架构:
13. 爬虫之Scrapy框架的使用总结

安装

基于 1.5.0
使用 pip install scrapy

各个模块

(1)settings,配置文件


所有的key 都是全部大写!

BOT_NAME = 'firstscrapy' # 项目名
ROBOTSTXT_OBEY = Ture,是否遵守 robots.txt, 修改为False
DEFAULT_REQUEST_HEADERS : 默认的请求headers
PROXIES = [
    {'ip_port': '111.8.60.9:8123', 'user_pass': ''},
    {'ip_port': '101.71.27.120:80', 'user_pass': ''},
]                       #  代理  ,在downloder middleware 的 代理中间件中 proxy = random.choice(PROXIES)   request.meta['proxy'] = "http://%s" % proxy['ip_port']
SPIDER_MIDDLEWARES:爬虫中间层
DOWNLOADER_MIDDLEWARES:下载中间层
ITEM_PIPELINES = {'项目名.pipelines.PipeLine类名': 300,}

# 开发模式时,把下面的注释取消掉,启用缓存,可以提高调试效率
# 同样的请求,如果 缓存 当中有保存内容的话,不会去进行网络请求,直接从缓存中返回
# 记住:开发环境下启用!!!!部署时一定要注释掉!!!
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

# 日志管理
LOG_ENABLED 默认: True,启用logging
LOG_ENCODING 默认: 'utf-8',logging使用的编码
LOG_FILE 默认: None,在当前目录里创建logging输出文件的文件名,例如:LOG_FILE = 'log.txt'
    配置了这个文件,就不会在控制台输出日志了
LOG_LEVEL 默认: 'DEBUG',log的最低级别,共五级:
    CRITICAL - 严重错误
    ERROR - 一般错误
    WARNING - 警告信息
    INFO - 一般信息  print 属于这个 info
    DEBUG - 调试信息
LOG_STDOUT 默认: False 如果为 True,进程所有的标准输出(及错误)将会被重定向到log中。
        例如,执行 print("hello") ,其将会在Scrapy log中显示

# 并发 ,现在 = 号右边的 value 就是默认值
CONCURRENT_ITEMS = 100 #  并发处理 items 的最大数量
CONCURRENT_REQUESTS = 16  #  并发下载request页面的最大数量
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # 并发下载任何单域的最大数量, baidu.com , sina.cn 各 8 个
CONCURRENT_REQUESTS_PER_IP = 0 # 并发 每个IP 请求的最大数量,
DOWNLOAD_DELAY = 0.25 # 单位秒,支持小数,一般都是随机范围:
                        0.5*DOWNLOAD_DELAY 到 1.5*DOWNLOAD_DELAY 之间
                        CONCURRENT_REQUESTS_PER_IP 不为0时,这个延时是针对每个IP,而不是每个域

(2)爬虫类(spider怎么写)


继承自scrapy.Spider,用于构造Request对象给Scheduler
属性
name:爬虫的名字,必须唯一  ,必须写!
start_urls:爬虫初始爬取的链接列表

custom_settings = {} # 自定义的setting配置

方法
start_requests:启动爬虫的时候调用,爬取urls的链接,可以省略
parse:response到达spider的时候默认调用,如果在Request对象配置了callback函数,则不会调用,
    parse方法可以迭代返回Item或Request对象,如果返回Request对象,则会进行增量爬取

items


定义:
class Product(scrapy.Item):
    name = scrapy.Field()
    title = scrapy.Field()

调用:
和dict一样的调用
product = Product(	, title='pc title')

# 像字典一样的使用:
print(product['name'])
print(product.get('name'))
product['title'] = 'new title'

可以这样转换为字典:dict(product)

pipelines


必须在settings中,添加
ITEM_PIPELINES = {
    'first_scrapy.pipelines.FirstScrapyPipeline': 300, # 优先级,数字越小,
                                                    优先级越高,越早调用范围 0-1000
}

对象如下:
class FirstScrapyPipeline(object):

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

方法:
process_item(self, item, spider): 处理item的方法, 必须有的!!!
参数:
item (Item object or a dict) : 获取到的item
spider (Spider object) : 获取到item的spider
返回    一个dict或者item

open_spider(self, spider) : 当spider启动时,调用这个方法
参数:
spider (Spider object) – 启动的spider

close_spider(self, spider): 当spider关闭时,调用这个方法
参数:
spider (Spider object) – 关闭的spider


@classmethod
from_crawler(cls, crawler)
参数:
crawler (Crawler object) – 使用这个pipe的爬虫crawler


4.1  返回 item 的例子:
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)

4.2 写入文件的例子:
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

4.3 写入 mongodb 的例子:
import pymongo

class MongoPipeline(object):

    collection_name = 'scrapy_items'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
         #  必须在settings中 配置 MONGO_URI 和 MONGO_DATABASE
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            # items 是默认值,如果settings当中没有配置 MONGO_DATABASE ,那么 mongo_db = 'items'
            mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        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))
        return item

4.4  网页快照的例子
import scrapy
import hashlib
from urllib.parse import quote


class ScreenshotPipeline(object):
    """Pipeline that uses Splash to render screenshot of
    every Scrapy item."""

    SPLASH_URL = "http://localhost:8050/render.png?url={}"

    def process_item(self, item, spider):
        encoded_item_url = quote(item["url"])
        screenshot_url = self.SPLASH_URL.format(encoded_item_url)
        request = scrapy.Request(screenshot_url)
        dfd = spider.crawler.engine.download(request, spider)
        dfd.addBoth(self.return_item, item)
        return dfd

    def return_item(self, response, item):
        if response.status != 200:
            # Error happened, return item.
            return item

        # Save screenshot to file, filename will be hash of url.
        url = item["url"]
        url_hash = hashlib.md5(url.encode("utf8")).hexdigest()
        filename = "{}.png".format(url_hash)
        with open(filename, "wb") as f:
            f.write(response.body)

        # Store filename in item.
        item["screenshot_filename"] = filename
        return item

4.5 去重复值的pipe
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

第一个项目

1、在命令行中,切换到 项目目录:譬如,f:\py_study>
    执行命令:scrapy startproject first_scrapy
    将在 f:\py_study 路径下建立一个 first_scrapy 项目文件夹
    文件夹结构如下:
scrapy.cfg            # 部署的配置文件,不需要修改
    first_scrapy/
        __init__.py
        items.py          # 类一定要继承scrapy.Item,定义我们需要的结构化数据,和ORM有点类似,使用相当于dict
      first_item = FirstscrapyItem()
      name = first_item['name']
      name1 = first_item.get('name')
      first_item['name']  = 'lucy'

      # 可以转换为字典
      dict_item = dict(first_item)
      # 格式是: dict_item = {name:'terry', age:10, sex:'1'}

        middlewares.py    # 中间件,相当于钩子,可以对爬取前后做预处理,如修改请求header,url过滤等
        分 downloadermiddleware、spidermiddleware

        pipelines.py      # 数据处理,将items中结构化的数据进行处理

        settings.py       # 项目配置文件,key=value 的方式,key必须全部大写!
                            包括所有的配置,一些公用的常量也可以写在里面

        spiders/          # 爬虫模块的目录,负责配置需要爬取的数据和爬取规则,以及解析数据,
                            并且把结构化数据,return 到 pipelines 模块处理
            __init__.py

2、在 spiders 中新建:
quotes.py:.

import scrapy

# 必须继承 scrapy.Spider
class QuotesSpider(scrapy.Spider):
    # 用于区别Spider。 该名字必须是唯一的,您不可以为不同的Spider设定相同的名字。
    name = "quotes"

    def start_requests(self):
        # 包含了Spider在启动时进行爬取的url列表。 因此,第一个被获取到的页面将是其中之一。
        # 后续的URL则从初始的URL获取到的数据中提取。
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            # 必须使用 yield
            # Request 是 scrapy 自定义的类
            # callback, 获取到response 之后的 回调函数
            yield scrapy.Request(url=url, callback=self.parse)

    # 是spider的一个方法。 被调用时,每个初始URL完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数。
    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)class BlogSpider(scrapy.Spider):
    name = 'blogspider'
    # 允许访问的域名,可以不写
    allowed_domains = ['scrapinghub.com']
    start_urls = ['https://blog.scrapinghub.com']

    def parse(self, response):
        for title in response.css('h2.entry-title'):
            yield {'title': title.css('a ::text').extract_first()}

        for next_page in response.css('div.prev-post > a'):
            yield response.follow(next_page, self.parse)


补充:
数据流,所有中间件都启动:
1、启动 spiders
2、spiders 包装一个request, 发送到 scheduler
    2.1 request进入 scheduler 之前,会先到 scheduler middleware 进行处理,
        处理后,再发送给 scheduler
3、scheduler 接收到 request 后,将之放入到一个 队列
4、engine 向 scheduler  申请 request,得到后,将request
    发送给 downloader
    4.1 request 从 scheduler 出来后,先到 scheduler middeware 进行处理,
        再传给 scheduler middeware
    4.2 request 从 scheduler middeware 出来后,需要到 downloader middleware
        进行处理,再传给 downloader
5、downloader 接收到 request 之后,访问对应的http资源,接收 response
6、downloader 接收到 response 之后,将 response 发送给 spider
    6.1 response 从 downloader 出来后,先经过 downloader middleware 处理
    6.2 response 从 ownloader middleware 处理后,经过 spider middleware 处理,
        再 传给 spider
7、spider 接收到reponse之后,解析内容,
    7.1  url 资源,需要再次请求的,继续包装成 request ,继续第二个步骤
    7.2  非 url 资源,就是 数据,结构化成 item ,向后传
8、item 从 spider 出来后,经过 spider middleware ,处理后 ,发送给
   item pipeline
9、pipeline 接收到 item 后,读取其中的数据,进行数据持久化

运行


1、命令行中运行:
命令行 中 进入到 first_scrapy 目录中,执行:
scrapy crawl quotes
quotes是spider类中的 name 属性

2、pycharm 运行
在 项目 根目录 添加 run.py 文件:
from first_scrapy.spiders.quotes import QuotesSpider
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings

# 获取settings.py模块的设置
settings = get_project_settings()
process = CrawlerProcess(settings=settings)

# 可以添加多个spider
# process.crawl(Spider1)
# process.crawl(Spider2)
process.crawl(QuotesSpider)

# 启动爬虫,会阻塞,直到爬取完成
process.start()

或者:

from scrapy.cmdline import execute

#设置工程命令
import sys
import os
#设置工程路径,在cmd 命令更改路径而执行scrapy命令调试
#获取run文件的父目录,os.path.abspath(__file__) 为__file__文件目录
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
execute(["scrapy","crawl","quotes" ])

spider的 parse

1、ItemLoader
    def parse(self, response):
        # text = response.text
        # tree = etree.HTML(text)
        # product = Product()
        # product['name'] = tree.xpath('//div[@class="product_name"]')[0].text + tree.xpath('//div[@class="product_title"]')[0].text
        # product['price'] = tree.xpath('//p[@id="price"]')[0].text
        # product['stock'] = tree.xpath('//p#stock')[0].text
        # product['last_updated'] = 'today'
        # return product

        l = ItemLoader(item=Product(), response=response)
        l.add_xpath('name', '//div[@class="product_name"]')
        l.add_xpath('title', '//div[@class="product_title"]')
        l.add_xpath('price', '//p[@id="price"]')
        l.add_css('stock', 'p#stock')
        l.add_value('last_updated', 'today')  # you can also use literal values
        return l.load_item()

2、selector
response.selector.xpath('//span/text()').extract()
# 结果是 list,但是一般情况我们都是只取到一个值,如 ['想要的值'], extract_first 取第一个

response.css('title::text')

response.css('img').xpath('@src').extract()

response.xpath('//div[@id="not-exists"]/text()').extract_first()

response.xpath('//div[@id="not-exists"]/text()').extract_first(default='not-found')

response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')

3、meta
yield Request(novel_url, self.parse_next, meta={'name':name, 'age':age})
在 parse_next 中 通过  response.meta['name'] 获取参数
传递参数到下一个 parse_next 函数

middleware

1、settings中配置
DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.CustomDownloaderMiddleware': 543,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,  # 不启用
}

2、process_request(request, spider)
2.1 如果其返回 None ,Scrapy将继续处理该request,执行其他的中间件的相应方法,
    直到合适的下载器处理函数(download handler)被调用, 该request被执行(其response被下载)2.2 如果其返回 Response 对象,Scrapy将不会调用 任何 其他的 process_request() 或 process_exception() 方法,
    或相应地下载函数; 其将返回该response。 已安装的中间件的 process_response() 方法则会在每个response返回时被调用。

2.3 如果其返回 Request 对象,Scrapy则停止调用 process_request方法并重新调度返回的request。
    当新返回的request被执行后, 相应地中间件链将会根据下载的response被调用。

2.4 如果其raise一个 IgnoreRequest 异常,则安装的下载中间件的 process_exception() 方法会被调用。
    如果没有任何一个方法处理该异常, 则request的errback(Request.errback)方法会被调用。
    如果没有代码处理抛出的异常, 则该异常被忽略且不记录(不同于其他异常那样)3、process_response(request, response, spider)
3.1 如果其返回一个 Response (可以与传入的response相同,也可以是全新的对象),
    该response会被在链中的其他中间件的 process_response() 方法处理。

3.2 如果其返回一个 Request 对象,则中间件链停止, 返回的request会被重新调度下载。
    处理类似于 process_request() 返回request所做的那样。

3.3 如果其抛出一个 IgnoreRequest 异常,则调用request的errback(Request.errback)。
    如果没有代码处理抛出的异常,则该异常被忽略且不记录(不同于其他异常那样)4、process_exception(request, exception, spider)

5、from_crawler(cls, crawler)

笔记1:

1. 幂等函数&纯函数

幂等函数: 只要输入参数不变,返回值永远一样,不管执行多少次
纯函数 : 首先必须是幂等函数,其次不会和环境有任何交互, 不能执行的操作包括:外部变量、print、数据库操作

2、request请求中的 callback 参数

绝大部分情况下,是可以不用提交的!!!

3、在 python 下运行

1、import scrapy   出错!
2、提示错误是  from .. import etree 出错, 查看错误可以找到是  lxml 出问题了
3、查看 lxml 是否安装: pip install lxml , 提示我们已经安装
4、继续运行 python 命令,  执行 :   from lxml import etree  ,继续提示错误
5、基本可以确定是 lxml 库安装有问题!!!
6、使用卸载命令:  pip uninstall lxml
7、重新安装lxml:  pip install lxml
8、问题解决

4、常见的配置文件

ini、xml、config、conf、cfg等
python项目中,一般使用 .py  作为配置文件,譬如: settings.py

项目:顶点小说

需求:

1、访问 https://www.23us.so/
2、访问 从玄幻魔法 到 其他小说 的 9个类别的 小说
3、获取第二步中 9 个类别 的所有小说信息:
小说名
类别
作者
状态
全文长度
最后更新时间

4、保存到 mongodb
5、使用 scrapy 框架实现

实现:

1、建立一个 scrapy 项目
2、修改 settings
3、分析目标网站
类似这样的小说网站,一般都全部是 get 请求, 不需要抓包即可实现功能
如果是其他类型的网站,需要抓包进行分析===》对比拉勾网
4、建立 spider
5、建立 item
6、修改 settings中的 pipeline 的配置
7、建立 pipeline

笔记2

1、scrapy框架的 response

去属性和文本,都只能通过xpath表达式,不像 lxml中 有 .text 和 get('属性名')
获取一个值(多个值时取第一个值):
response.xpath('').extract_first()

获取列表:
response.xpath('').extract()

2、scrapy框架 Request 对象的参数

常用:
url  :  网页地址
callback  :  回调处理response的函数
meta: 字典,用于 上下多个函数之间传递参数
dont_filter: 默认是 False, 是过滤重复的!!如果不顾虑设置为 True

偶尔用:
headers:  一般是哟用 settings 中的默认headers, 有些请求需要使用 Referer
errback:  错误处理函数,类似 callback,  一般不用

不用:
method:   http协议的 请求 method,默认是GET,  post请求一般直接使用 FormRequest
body:  请求体, 等同 上面的 method,  就是charles中的raw 视图下看到的 body 内容, 传的就是 字符串
cookies:  cookies,一般使用不上
encoding: 默认 UTF-8
priority:  默认 0, 调度器调度时随机的, 其实根据这个优先级选择的
flags: 几乎用不上

3、downloader middleware

process_request
4种返回值:
None:
    如果当前中间层,后续还有其他中间层,那么将 request对象发送给下一个中间层的 process_request 进行处理
    如果当前中间层是最后一个中间层了,那么 将 request对象 发送给 downloader 进行处理

Request:
    将 返回的 request 冲入放入到 请求队列 中,重新接受 scheduler 的调度
    相当于在 spider 中, yield request  一样的作用

Response:
    一般自己构建一个 response
    中间层就不会继续往后执行,而是反过来,往前执行
    相当于已经从 downloader 中 获得了 response
    假设 6个 中间层, 顺序是 1到 6 ,如果你在 中间层3 就 return response
    那么,接下操作的就是 中间层2 的 process_response 接受 这个 response

raise 异常:
    会调用 本中间层 的  process_exception

4、scrapy框架提交 请求

GET:  scrapy.Request
POST:  scrapy.FormRequest  , 比 Request 多了一个 formdata, 传的参数是字典
        data = {}
        scrapy.FormRequest(url, callback=self.parse, formdata=data)

如果使用 response.follow 来构造请求对象的话,提交post请求
    yield response.follow(url, callback=self.parse_list, method='POST', body='请求体的字符串')

5、密码学相关的知识

拖库:不管使用什么手段,得到某一个应用的数据库,把这个数据库下载了
洗库:将拖库得到的数据库进行清洗,整理,分类
撞库:将拖库得到的 账号、密码, 进行多个其他应用的尝试

彩虹表: 将 某一个组合(6到16位,数字+大小写字母 组成的字符串)的 可能性, 穷举, 使用某一个 加密手段进行加密
    保存到一个数据库的表中, 2个字段, 原始密码、加密后的密码

盐:特殊的字符串,   在进行 密码的加密之前, 将 原始的密码和 这个盐,进行组合  盐+原始密码 ,
    将 组合后 的 字符串 进行加密, 保存 加密后的密码串

爬虫中,对js加密手段的处理
1、找到 加密方式
2、找到 加密的盐
3、找到上述2个信息后,在python代码实现一个函数, 达到 js 加密函数一样的加密效果,那么在我们的爬虫程序中,
    就可以提交,同样的加密后的 密码串

6、如何查找js代码中的加密函数

譬如:百度的
    password	r9Xu1+WwLLgZZ0MG5L2PKPnXyuoJqxoZh3RNgwb0yVzwoMqEid6IqxrzDQE+roUtgXmzINwWFII/0kqc4yk1wbtD355ydG2ADLxJ087ChcKzt7jocUD3FaylxtQ+a6WKfrSwOI7SAyfXqlCuA8zWL6nN5OwzYN1Y40NJUfFpCII=

    搜索 value ,肯定是找不到的!因为是加密算出来的
    不能搜索 value,那么就搜索 key
    因为这种加密算法, 终究在 js 代码中,会有 类似这样的代码存在:
    password=加密函数()

    搜索范围必然是: response body
    1、搜索 password  ,会得到很多值
    2、 搜索 password=  , 因为所有的js基本都会进行压缩, 所以一般都直接搜索  password=  可以得到
        但是少部分, 得搜索  'password ='
        还有部分网站, 搜索 password 搜索不到, password 这个key 是拼接得来的 'pass' + 'word'

项目:拉勾网

1、拉勾网的岗位搜索
2、登录拉钩:
url: https://passport.lagou.com/login/login.html
3、登录成功后,搜索
python爬虫
python工程师
4、获取如下岗位信息
岗位名
薪资
公司
发布时间
工作经验
5、用 scrapy 项目实现