【爬虫】Scrapy实战笔记

Scrapy 介绍


本文介绍的 Scrapy 版本为 Scrapy 2.4.1

资料收集

1、官方教程:https://docs.scrapy.org/en/latest/index.html

2、官方教程中文版:https://www.osgeo.cn/scrapy/intro/tutorial.html

3、崔庆才个人博客:https://cuiqingcai.com/

安装


安装命令

pip install scrapy

安装可能出现的问题

1、Microsoft Visual C++ 14.0 is required

在pip install scrapy时出现错误:

Microsoft Visual C++ 14.0 is required.

此时需要进入网站(注意需要使用 Chrome)

https://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

然后根据pyhon版本和位数下载whl文件,并放到Anaconda3\Scripts下,在该路径下打开cmd,执行:

pip install .\Twisted-19.2.1-cp37-cp37m-win_amd64.whl

然后执行pip install scrapy 就可以成功安装了。

如果是Linux:

yum install python-devel
yum install -y gcc 
yum install -y libffi-devel python-devel openssl-devel

2、from cryptography.hazmat.bindings._openssl import ffi, lib ImportError: DLL load failed: 找不到指定的程序。

安装scrapy成功后,执行spider时,出现错误:

from cryptography.hazmat.bindings._openssl import ffi, 
lib ImportError: DLL load failed: 找不到指定的程序。

只需要执行如下命令即可。

 pip install -I cryptography

建立Scrapy项目的步骤


一般的创建 Scrapy 项目的顺序

1、创建项目

2、定义 Item

3、编写 spider

4、修改配置文件 settings.py

5、编写数据处理文件 pipelines.py

6、编写中间件文件 middlewares.py (可选)

7、启动爬虫

创建项目

在命令行中输入如下命令创建scrapy项目,GetVideoInfo 是项目名称,我这里用了大驼峰命名。

scrapy startproject GetVideoInfo 

创建后,可以看到项目中有如下文件或文件夹:

GetVideoInfo/
   
    scrapy.cfg            # deploy configuration file
   
    GetVideoInfo/         # project's Python module, you'll import your code from here
        
        __init__.py

        items.py          # 爬虫的数据定义文件,用来定义爬取数据的数据模型

        middlewares.py    # 爬虫的中间件程序,用来拓展爬虫功能,比如代理、异常等

        pipelines.py      # 爬虫的管道程序,用来对 item 进一步加工处理

        settings.py       # 项目配置文件

        spiders/          # 放爬虫程序的文件夹
    

定义 Item

定义 Item 其实就是定义一个类,类的属性就是我们要采集的字段;

修改 Items.py :

import scrapy

class GetvideoinfoItem(scrapy.Item):
    redis_key = scrapy.Field()
    item_result = scrapy.Field()

编写一个基础爬虫

1、通过如下 scrapy 命令 或者 在项目的 spiders 文件夹下手动新建文件 来爬虫文件,推荐手动创建文件

scrapy genspider douyin
import scrapy

class Douyin(scrapy.Spider):
    name = 'douyin'
    start_urls = ['https://v.douyin.com/JCVwoC5/']

    def parse(self, response):
        pass

2、编辑爬虫

必需参数:

name: 在项目中必须是唯一的,是这个爬虫的标识,也是启动该爬虫的的必备参数;

start_urls :初始需要采集的url,可以是一个或多个,默认会调用parse方法进行采集后的处理;

start_requests:一般会把 start_urls属性 用 start_requests 方法代替,在方法中处理与定制要爬取的链接,并设定一些参数,比如headers等;

parse:对采集到的数据进行解析与提取;

handle_httpstatus_list:可处理的状态码,列表的形式,可以写多个,

下面是一个基本的spider示例,可以通过 scrapy crawl douyin 去运行;

import scrapy

class Douyin(scrapy.Spider):
    name = 'douyin'

    headers = {
        "user-agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
    }

    def start_requests(self):
        start_url = 'https://v.douyin.com/JCVwoC5/'
        yield scrapy.Request(start_url, callback=self.parse, dont_filter=True, headers=self.headers)

    def parse(self, response):
        print("爬取结果")
        print(response.status,response.url)
        print(response.text)

启动爬虫测试一下

在 spiders 文件的同级目录下新建一个文件,start_by_cmd.py,然后执行这个文件,查看是否可以输出爬取到的信息

from scrapy.cmdline import execute

execute(['scrapy','crawl','douyin'])

更详细的编写爬虫

1、对单个爬虫的配置

custom_settings 是自定义设置的意思;通过类的 custom_settings 属性来配置一些爬虫策略,

比如日志、并发请求数、访问请求间隔、中间件的调用等;

针对该爬虫,m这个设置会覆盖settings.py中的设置。

所有设置项参考官网:https://doc.scrapy.org/en/latest/topics/settings.html#topics-settings-ref

custom_settings = {
    "LOG_FILE" : './logs/{0}.log'.format(name),
    "CONCURRENT_REQUESTS": 4,
    "DOWNLOAD_DELAY":2,
}

2、更多次的请求

对于 douyin 来说,通过分享的链接访问后可以拿到跳转后的链接,但是要获取douyin 该链接更多信息的话,需要从跳转后链接中提取 id,并访问接口;

import scrapy

class Douyin(scrapy.Spider):
    name = 'douyin'
	
	custom_settings = {
    "LOG_FILE" : './logs/{0}.log'.format(name),
    "CONCURRENT_REQUESTS": 4,
    "DOWNLOAD_DELAY":2,
    'LOG_LEVEL':'INFO',
}
    headers = {
        "user-agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
    }
    api_headers = {
        "Host":"www.iesdouyin.com",
        "user-agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
    }
    
    def start_requests(self):
        start_url = 'https://v.douyin.com/JCVwoC5/'
        yield scrapy.Request(start_url, callback=self.parse, dont_filter=True, headers=self.headers)

    def parse(self, response):
        print("爬取结果")
        print(response.status,response.url)
        print(response.text)

        relocal_url = response.url
        keyword = "/?region"
        vid_cnt = relocal_url[:relocal_url.find(keyword)]
        vid = vid_cnt[vid_cnt.rfind("/") + 1:]
        api_url =  "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={vid}".format(vid = vid)
        print(api_url)
        
        yield scrapy.Request(api_url, callback=self.parse_api, dont_filter=True, headers=self.api_headers)

    def parse_api(self, response):
        print(response.text)

3、在 yield 中传递参数

我们经常需要在 start_requests 和 parse 之间,或者 不同的 parse 之间传递数据,比如传递每次爬取的数据,此时可以在yield中通过 meta 字段将参数传递到下一个函数,mata 可以接收一个或多个字典;

在下一个函数中,通过 response.meta[key] 来获取传递过来的字典。

import scrapy
from GetVideoInfo.items import GetvideoinfoItem

class Douyin(scrapy.Spider):
    name = 'douyin'

    headers = {
        "user-agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
    }
    api_headers = {
        "Host":"www.iesdouyin.com",
        "user-agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
    }
    def start_requests(self):
        start_url = 'https://v.douyin.com/JCVwoC5/'
        redis_key = "douyin"
        item_result = {}
        item_result['start_url'] = start_url

        item = GetvideoinfoItem()
        item['redis_key'] = redis_key
        item['item_result'] = item_result

        yield scrapy.Request(start_url,
                             callback = self.parse,
                             dont_filter = True,
                             headers = self.headers,
                             meta = {"item":item}   # 在此处传递参数 item
                             )

    def parse(self, response):
        print(response.status,response.url)
        print(response.text)
		
		# 此处接收传递的参数 item
        item = response.meta['item']
        print(item)
        relocal_url = response.url
        keyword = "/?region"
        vid_cnt = relocal_url[:relocal_url.find(keyword)]
        vid = vid_cnt[vid_cnt.rfind("/") + 1:]
        api_url =  "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={vid}".format(vid = vid)
        print(api_url)
        yield scrapy.Request(api_url, callback=self.parse_api, dont_filter=True, headers=self.api_headers)

    def parse_api(self, response):
        print(response.text)

4、处理 status > 300 的链接(比如重定向、下线链接)

有些跳转链接是不会自动跳转的,请求后只会返回302(重定向)的请求状态;

根据 HTTP标准 ,返回值为200-300之间的值为成功的 response,如果要处理更多的请求返回状态码,则需要通过类的属性:handle_httpstatus_list 来定义,比如想处理 302 或者 404 的 response:

handle_httpstatus_list = [302,404]

5、完善爬虫,下面贴一个完整的爬虫,作为后续处理的爬虫部分

import scrapy
from GetVideoInfo.items import GetvideoinfoItem
import time
import json
import datetime
import redis


class Douyin(scrapy.Spider):
    name = 'douyin'

    custom_settings = {
        #"LOG_FILE": './logs/{0}.log'.format(name),
        "CONCURRENT_REQUESTS": 1,
        "DOWNLOAD_DELAY": 2,
        'LOG_LEVEL':'INFO',
    }

    headers = {
        "user-agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
    }
    api_headers = {
        "Host":"www.iesdouyin.com",
        "user-agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36",
    }


    def get_requests_redis_conn(self):
        requests_redis_host = self.settings.get("REQUESTS_REDIS").get("host")
        requests_redis_passwd = self.settings.get("REQUESTS_REDIS").get("passwd")
        requests_redis_db = self.settings.get("REQUESTS_REDIS").get("db")
        while 1:
            try:
                self.requests_redis_conn = redis.Redis(
                    host=requests_redis_host,
                    port=6379, password=requests_redis_passwd,
                    decode_responses=True,
                    db=requests_redis_db
                )
                print("connect requests_redis_conn success...")
                break
            except Exception as e:
                print("connect requests_redis_conn error...", e)
                time.sleep(5)


    def start_requests(self):
        self.get_requests_redis_conn()

        while 1:
            redis_key = "VideoCompleteList-0.69:shouji_douyin"

            item_json = self.requests_redis_conn.lpop(redis_key)
            item_requests = json.loads(item_json)
            start_url = item_requests.get("infringer_url")

            item_result = item_requests

            item = GetvideoinfoItem()
            item['redis_key'] = redis_key
            item['item_result'] = item_result

            print(self.name,"parse:",start_url)
            yield scrapy.Request(start_url,
                                 callback = self.parse,
                                 dont_filter = True,
                                 headers = self.headers,
                                 meta = {"item":item}
                                 )
            #break
    def parse(self, response):
        print(self.name,"parse:",response.status,response.url)


        item = response.meta['item']
        item_result = item.get("item_result")

        relocal_url = response.url
        if "douyin.com/404" in relocal_url:
            print(self.name,"已下线", item_result.get('url'))
            item_result['id'] = -2
            yield item
        else:
            item_result['infringer_url'] = relocal_url
            keyword = "/?region"
            vid_cnt = relocal_url[:relocal_url.find(keyword)]
            vid = vid_cnt[vid_cnt.rfind("/") + 1:]
            api_url =  "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids={vid}".format(vid = vid)
            print(self.name, "parse_api:", api_url)
            yield scrapy.Request(api_url,
                                 callback=self.parse_api,
                                 dont_filter=True,
                                 headers=self.api_headers,
                                 meta = {"item":item}
                                 )

    def parse_api(self, response):
        print(self.name,"parse_api:",response.status,response.url)
        item = response.meta['item']
        item_result = item.get("item_result")
        result = json.loads(response.text)
        if not result['item_list']:
            print(self.name,"已下线", item_result.get('url'))
            item_result['id'] = -2
            yield item
        else:
            desc = result['item_list'][0]['desc']
            duration = int(int(result['item_list'][0]['duration']) / 1000)
            m, s = divmod(duration, 60)
            duration = str(m).zfill(2) + ":" + str(s).zfill(2)
            author = result['item_list'][0]['author']['nickname']
            author_id = result['item_list'][0]['author']['uid']

            unique_id = result['item_list'][0]['author']['unique_id']
            if not unique_id:
                unique_id = result['item_list'][0]['author']['short_id']
            pubtime = result['item_list'][0]['create_time']
            pubtime = str(datetime.datetime.fromtimestamp(pubtime))
            digg_count = result['item_list'][0]['statistics']['digg_count']

            print(author, pubtime, duration, desc)
            item_result['author'] = author
            item_result['title'] = desc
            item_result['pubDate'] = pubtime
            item_result['duration'] = duration
            item_result['play_count'] = digg_count
            item_result['id'] = -1

            yield item

编写 pipelines 处理爬取数据

参考:https://www.cnblogs.com/dong-/p/10310964.html

1、设置 settings.py ,使 pipelines.py 生效

在 settings.py 中,找到 ITEM_PIPELINES 的配置,取消掉注释,如果需要写多个pipeline,可以在这里声明多个,并用后面的数字进行优先级设定,越小越优先;

还有一个场景:当我们需要对数据进行多次处理时,比如先验证数据格式,再去重,最后入库,就可以写3个pipeline,通过验证 则 return item,否则就 raise DropItem(error_msg);

# settings.py

ITEM_PIPELINES = {
   'GetVideoInfo.pipelines.GetvideoinfoPipeline': 300,
}

2、编辑 pipelines.py

在pipelines.py 中,我们可以编写处理爬到数据的业务,比如 存储、去重、验证数据等;

pipelines.py 中,主要的方法如下:

  • process_item(self, item, spider):必须实现,spider 执行 yield item 的时候,会调用该方法;返回 字典 或 Item 时会调用下一个 pipeline(如果有的话),如果遇到无效数据或想过滤的数据,可以抛出 DropItem 异常,表示不做任何处理;

  • from_crawler(cls, crawler):类方法,可以用来读取 settings 的配置,比如数据库的IP、密码等;

  • open_spider(self, spider):spider 开始之前调用,一般用来打开数据库连接;

  • close_spider(self, spider):spider 结束后调用,一般用来关闭数据库连接;

settings.py

KAFKA_SERVERS = ['120.133.10.001:9096',
                 '120.133.10.002:9096',
                 '120.133.10.003:9096'
                 ]

REQUESTS_REDIS = {}
REQUESTS_REDIS['host'] = "120.133.10.001"
REQUESTS_REDIS['passwd'] = "123"
REQUESTS_REDIS['db'] = 0

PROXY_REDIS = {}
PROXY_REDIS['host'] = "120.133.10.002"
PROXY_REDIS['passwd'] = "123"
PROXY_REDIS['db'] = 1

pipelines.py

from itemadapter import ItemAdapter
from scrapy.utils.project import get_project_settings
from kafka import KafkaProducer
import time
import json

class GetvideoinfoPipeline:

    def get_kafka_conn(self):
        kafka_servers = self.settings.get("KAFKA_SERVERS")
        while 1:
            try:
                self.producer = KafkaProducer(
                    bootstrap_servers=kafka_servers,
                    acks="all",
                    linger_ms=50,
                    retries=30,
                    reconnect_backoff_ms=20000,
                    retry_backoff_ms=20000,
                )
                print("connect kafka success...")
                break
            except Exception as e:
                print("connect kafka error...", e)
                time.sleep(5)

    def kafka_send(self,topic,value):
        while 1:
            try:
                value_json = json.dumps(value, ensure_ascii=False)
                value_str = str(value_json)
                value_bytes = bytes(value_str, encoding="utf8")
                future = self.producer.send(topic, value=value_bytes)
                break
            except Exception as e:
                print(value, e, "重试...")
                time.sleep(1)
                self.get_kafka_conn()
        return future

    def item_map(self, item_result):
        item_result_new = {}
		...
        return item_result_new

    def open_spider(self, spider):
        print("open_spider:",spider.name)
        self.settings = get_project_settings()
        print(self.settings.get("KAFKA_SERVERS"))
        self.get_kafka_conn()


    def process_item(self, item, spider):
        print(spider.name,"pipelines:",item)
        item_result = item.get("item_result")
        item_result_new = self.item_map(item_result)
        tid = item_result_new.get("task")
        topic = "tbl_stream_{tid}".format(tid=tid)
        self.kafka_send(topic,item_result_new)
        #print(spider.name,"pipelines_result:",item_result_new)
        return item

修改项目配置文件 settings.py

所有配置项参考官网:https://doc.scrapy.org/en/latest/topics/settings.html#topics-settings-ref

在这个示例中,我主要做了四处改动:

  • 加入数据库、redis、kafka 配置信息
  • ROBOTSTXT_OBEY = False
  • 去掉 ITEM_PIPELINES
  • LOG_LEVEL= “INFO”

到此位置,一个完整的爬虫项目就完成了,使用之前写的 start_by_cmd.py 测试一下吧!

启动爬虫的几种方式

1、命令行输入

scrapy crawl yangshipin

2、脚本启动命令行

from scrapy import cmdline

if __name__=='__main__':
    cmdline.execute("scrapy crawl baidu".split())

3、 通过 CrawlerProcess

from scrapy.utils.project import get_project_settings
from scrapy.crawler import CrawlerProcess

settings = get_project_settings()
spidername = "yangshipin"

if __name__ == '__main__':
	p = CrawlerProcess(settings)
	p.crawl(spidername)
	p.start()
	p.join()
	print("进程:", spidername, "终止")

4、通过 CrawlerRunner(适合运行多个爬虫)

# -*- coding: utf-8 -*-
import time
from twisted.internet import reactor
from scrapy.crawler import CrawlerRunner
from scrapy.settings import Settings
from spiders.yangshipin import Yangshipin

settings = Settings()

if __name__ == '__main__':
	runner = CrawlerRunner(settings)
	runner.crawl(Yangshipin)
	d = runner.join()
	d.addBoth(lambda _: reactor.stop())
	reactor.run()

扩展爬虫的功能

1、爬虫策略的配置

custom_settings 是自定义设置的意思;通过类的 custom_settings 属性来配置一些爬虫策略、比如日志、并发请求数、访问请求间隔、中间件的调用等;针对该爬虫,这个设置会覆盖settings.py中的设置。

所有设置项参考官网:https://doc.scrapy.org/en/latest/topics/settings.html#topics-settings-ref

custom_settings = {
    "LOG_FILE" : './logs/{0}.log'.format(name),
    "CONCURRENT_REQUESTS": 4,
    "DOWNLOAD_DELAY":2,
    'DOWNLOADER_MIDDLEWARES': {'AntSample.middlewares.ProxyMiddleware': 543, },
}

2、处理更多的response

根据 HTTP标准 ,返回值为200-300之间的值为成功的 response,如果要处理更多的response,则需要通过类的属性:handle_httpstatus_list 来定义,比如想处理 302 或者 404 的 response:

handle_httpstatus_list = [302,404]

3、在 yield 中传递参数

可以在yield中通过 meta 参数将参数传递到下一个函数,mata 接收的参数只能是字典类型(scrapy的Item也理解为字典),可以直接传递字典,也可以主动构建字典:

# 直接传递字典
item = AntsampleItem()
item['title'] = "新闻联播"
yield scrapy.Request(api_url,dont_filter = True,callback = self.parse, meta = item)

# 主动构建字典
yield scrapy.Request(api_url,dont_filter = True,callback = self.parse, meta = {"my_item",item})

在parse 函数中接收参数:

def parse(self, response):
	# 接收直接传递的字典
    item = response.meta
    # 接收主动构建的字典
    item = response.meta['my_item']

处理爬到的数据

参考:https://www.cnblogs.com/dong-/p/10310964.html

在 settings.py 中,找到 ITEM_PIPELINES 的配置,取消掉注释,如果需要写多个pipeline,可以在这里声明多个,并用后面的数字进行优先级设定,越小越优先;

当我们需要对数据进行多次处理时,比如先验证数据格式,再去重,最后入库,就可以写3个pipeline,通过 则 return item,否则就 raise DropItem(error_msg);

ITEM_PIPELINES = {
   'AntSample.pipelines.AntsamplePipeline': 300,
}

在pipelines.py 中,我们可以编写处理爬到数据的业务,比如存到数据库、消息队列,或是生成到excel、txt。

pipelines.py 中,主要的方法如下。

process_item(self, item, spider):必须实现,spider 执行 yield item 的时候,会调用该方法;返回 字典 或 Item 时会调用下一个 pipeline(如果有的话),如果遇到无效数据或想过滤的数据,可以抛出 DropItem 异常,表示不做任何处理;

from_crawler(cls, crawler):类方法,可以用来读取 settings 的配置,比如数据库的IP、密码等;

open_spider(self, spider):spider 开始之前调用,一般用来打开数据库连接;

close_spider(self, spider):spider 结束后调用,一般用来关闭数据库连接;

一个例子:

settings.py

MYSQL_DB = {}
MYSQL_DB['db_ip'] = "120.xxx.xx.xx"
MYSQL_DB['db_name'] = "ant_receive"
MYSQL_DB['db_password'] = "123456"

pipelines.py

import pymysql
import time

class AntsamplePipeline:
    def __init__(self, db_ip, db_name,db_password):
        self.db_ip = db_ip
        self.db_name = db_name
        self.db_password = db_password
        print(db_password)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            db_ip = crawler.settings.get('MYSQL_DB').get("db_ip"),
            db_name = crawler.settings.get('MYSQL_DB').get("db_name"),
            db_password = crawler.settings.get('MYSQL_DB').get("db_password"),
        )

    def connect_mysql(self):
        while 1:
            try:
                self.conn =  pymysql.connect(host=self.db_ip, port=3306, user='root', passwd=self.db_password,db=
                                             self.db_name, charset='utf8')
                self.cur = self.conn.cursor()
                print("启动爬虫,数据库连接成功...\n",self.db_ip,self.db_name)
                break
            except Exception as e:
                print(e,"数据库连接失败,10秒后重试")
                time.sleep(10)

    def open_spider(self, spider):
        self.connect_mysql()

    def close_spider(self, spider):
        print("爬虫结束,关闭数据库连接...")
        self.cur.close()
        self.conn.close()


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

scrapy定时爬虫的实现方案


通过 CrawlerRunner 类启动
from twisted.internet import reactor, defer
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
import time
import logging
from scrapy.utils.project import get_project_settings

# 在控制台打印日志
configure_logging()
# CrawlerRunner获取settings.py里的设置信息
runner = CrawlerRunner(get_project_settings())


@defer.inlineCallbacks
def crawl():
    while True:
        logging.info("new cycle starting")
        yield runner.crawl("yangshipin")
        # 1s跑一次
        time.sleep(1)
    reactor.stop()


crawl()
reactor.run()
在 spider中的start_requests 方法中设置
import scrapy
import json

class YangshipinSpider(scrapy.Spider):
    name = 'yangshipin'
    headers = {
        "Host": "m.yangshipin.cn",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0"
    }

    def start_requests(self):
    	while 1:
	        start_url = "https://m.yangshipin.cn/video?type=0&vid=v000047rtb6"
	        yield scrapy.Request(start_url, headers = self.headers,callback=self.parse, dont_filter=True)
	        time.sleep(300)
在 spider中通过sinal实现

先说一下 修饰符 @classmethod ,这个修饰符修饰的函数是类函数,不需要实例化,不需要 self 参数即可调用,但是第一参数必须是 cls 参数,代表自身的类。

from_crawler 是一个类方法,该方法中的 cls 参数代表的是这个方法所属的类;

下面是 from_crawler 类方法的源码,在方法里创建了一个 cls 类的实例 spider ,配合 from_crawler 可以实现 sinal 和 spider 的连接。

from scrapy import signals
from scrapy.exceptions import DontCloseSpider

class Yangshipin(scrapy.Spider):
    name = 'yangshipin'

    custom_settings = {
        'DOWNLOAD_DELAY': 2,
        'DOWNLOADER_MIDDLEWARES': {'news.middlewares.ProxyMiddleware': 543, },
    }

    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = cls(*args, **kwargs)
        spider.set_crawler(crawler)
        crawler.signals.connect(spider.spider_idle, signal=scrapy.signals.spider_idle)
        return spider

    def spider_idle(self):
        print('spider_idle...')
        self.schedule_next_requests()
        time.sleep(60)
        raise DontCloseSpider
    
    def schedule_next_requests(self):
        print('schedule_next_requests')
        start_url = "https://m.yangshipin.cn/video?type=0&vid=v000047rtb6"
        print(start_url)
        req = scrapy.Request(url, dont_filter=True, headers=self.headers)
        self.crawler.engine.crawl(req, spider=self)
CrawlerProcess 的多线程方法
from scrapy.conf import settings
from multiprocessing import Process
import time
from scrapy.crawler import CrawlerProcess

def start_spider(spidername):
    p = CrawlerProcess(settings)
    p.crawl(spidername)
    p.start()
    p.join()
    print("进程:",spidername,"终止")

if __name__ == '__main__': 
    crawl_nodes = ['qiehao_spider']
    while 1:
        for spidername in crawl_nodes:
            p = Process(target=start_spider,args=(spidername,),name = spidername)
        p.start()
        p.join()
        print("一轮已爬完,休息5分钟")
        time.sleep(300)

Scrapy中的防爬策略


随机UA

1、安装scrapy-fake-useragent模块

pip install scrapy-fake-useragent

2、修改scrapy项目的settings.py文件

DOWNLOADER_MIDDLEWARES = {

# 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware':None , # 注释掉默认方法

'scrapy_fake_useragent.middleware.RandomUserAgentMiddleware':400,# 开启
请求间隔

1、修改scrapy项目的settings.py文件

DOWNLOAD_DELAY = 4 

RANDOMIZE_DOWNLOAD_DELAY = True

DOWNLOAD_DELAY 设置两次请求间隔是4秒,RANDOMIZE_DOWNLOAD_DELAY 设置请求间隔随机开启,也就是实际间隔是0.5 * 4 秒 ~ 1.5 * 4 秒之间的随机数。

并发请求数

1、修改scrapy项目的settings.py文件

CONCURRENT_REQUESTS = 4

CONCURRENT_REQUESTS:并发请求最大值
CONCURRENT_REQUESTS_PER_DOMAIN:单个网站的并发请求最大值
CONCURRENT_REQUESTS_PER_IP:单个IP并发请求最大值,会覆盖上个设置

代理

1、在spider中加入代理

proxy = "http:100.100.100.10:1000"
yield scrapy.Request(start_url,callback=self.parse,dont_filter=True,headers=self.headers,meta={"proxy": proxy}

2、在中间件中加入代理

编辑中间件

class RandomProxyMiddleware(object):
    def __init__(self):
        settings = get_project_settings()
        self.PROXY_REDIS_HOST = settings.get('PROXY_REDIS_HOST')
        self.PROXY_REDIS_PORT = settings.get('PROXY_REDIS_PORT')
        self.PROXY_REDIS_PARAMS = settings.get('PROXY_REDIS_PARAMS')
        self.PROXY_REDIS_KEY = settings.get('PROXY_REDIS_KEY')
        self.pool = redis.ConnectionPool(host=self.PROXY_REDIS_HOST,
                                         port=self.PROXY_REDIS_PORT,
                                         db=self.PROXY_REDIS_PARAMS['db'],
                                         password=self.PROXY_REDIS_PARAMS['password'])
        self.conn = redis.StrictRedis(connection_pool=self.pool)

    def process_request(self, request, spider):
        proxy = self.conn.srandmember(self.PROXY_REDIS_KEY)
        proxy = proxy.decode('utf-8')
        proxy = json.loads(proxy)
        ip = proxy['proxy']
        request.meta['proxy'] = "https://%s" % ip

编辑settings.py

DOWNLOADER_MIDDLEWARES = {
   'crawl_spider.middlewares.RandomProxyMiddleware': 400,
}

Scrapy 中间件的使用


青南的链接:https://www.cnblogs.com/xieqiankun/default.html?page=2

两类 Scrapy 中间件

从 Scrapy 项目的 setting.py 中搜索 MIDDLEWARE 可以看到被注释掉的配置项有两个,分别是:

1、下载器中间件(DOWNLOADER_MIDDLEWARES)

下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。

2、Spider中间件(SPIDER_MIDDLEWARES)

Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items及requests)。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。

DOWNLOADER_MIDDLEWARES 的方法

1、process_response(request, response, spider)

返回Response对象,它会被下个中间件中的process_response()处理

返回Request对象,中间链停止,然后返回的Request会被重新调度下载

抛出IgnoreRequest,回调函数 Request.errback将会被调用处理,若没处理,将会忽略

2、process_exception(request, exception, spider)

当下载处理模块或process_request()抛出一个异常(包括IgnoreRequest异常)时,该方法被调用

3、from_crawler

类方法,通常是访问settings和signals的入口函数

SPIDER_MIDDLEWARES 的方法

1、process_spider_input(response, spider)

当response通过spider中间件时,这个方法被调用,返回None

2、process_spider_output(response, result, spider)

当spider处理response后返回result时,这个方法被调用,必须返回Request或Item对象的可迭代对象,一般返回result

3、process_spider_exception(response, exception, spider)

当 spider(parse)或spider的中间件抛出异常时,这个方法被调用,返回None或可迭代对象的Request、dict、Item

中间件的调用顺序

Scrapy中的数据流由执行引擎控制,其过程如下:

1、引擎打开一个网站(open a domain),找到处理该网站的Spider并向该spider请求第一个要爬取的URL(s)。

2、引擎从Spider中获取到第一个要爬取的URL并在调度器(Scheduler)以Request调度。

3、引擎向调度器请求下一个要爬取的URL。

4、调度器返回下一个要爬取的URL给引擎,引擎将URL通过下载中间件(请求(request)方向)转发给下载器(Downloader)。

5、一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件(返回(response)方向)发送给引擎。

6、引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理。

7、Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎。

8、引擎将(Spider返回的)爬取到的Item给Item Pipeline,将(Spider返回的)Request给调度器。

9、(从第二步)重复直到调度器中没有更多地request,引擎关闭该网站。

开启中间件

要开启中间件,只需要在 settings.py 中 去掉对应中间件的注释,并把编写的中间件类名和优先级序号写入即可,序号越小越先执行,设置为 None 即可关闭该中间件;中间件类可以用已经提供好的 项目名SpiderMiddleware 或 项目名DownloaderMiddleware,也可以自己根据用途命名新的类,比如下面的 ProxyMiddleware。

SPIDER_MIDDLEWARES = {
   'GetVideoInfo.middlewares.GetvideoinfoSpiderMiddleware': 543,
}

DOWNLOADER_MIDDLEWARES = {
   'GetVideoInfo.middlewares.ProxyMiddleware': 543,
}

常用场景

1、下载中间件(DOWNLOADER_MIDDLEWARES)

下载中间件在 yield Request 和 进入 parse 函数 之间调用,也就是在请求连接和返回结果间调用,在这其中可以做如下使用:

  • 配置IP代理(process_request)
  • 如果 response 的 状态码异常,比如404,则更换代理重启请求(process_response)
  • 如果请求报错,比如请求太频繁报 max requests 错误,则更换代理重新请求(process_exception)
  • 修改 headers 或 更换 UA(process_request)
  • 更换 Cookie
  • 遇到验证码处理
  • 对有必要的爬虫,嵌入Selenium(process_request)

2、爬虫中间件(SPIDER_MIDDLEWARES)

用于处理response及spider(parse函数)生成的 item 和 Request,可以用作如下使用场景:

  • spider (也就是parse函数)出错时,可能需要更换代理,也可能需要修改解析代码,可以在 process_spider_exception 做日志记录或更换代理重新请求

  • 有些爬虫,我想用 requests 模块去请求,可以在 process_start_requests 函数中修改,并自己生成一个 Response 对象,返回

完善的容错机智

通过中间件来保证各个地方的出错不会导致数据丢失的方案。

1、请求可能出现的错误:

  • 防止被封IP:在 GetvideoinfoDownloaderMiddleware(Downloader 中间件)类的 process_request 中设定 request.meta[‘proxy’] 。

  • 请求超时、请求被封:在 GetvideoinfoDownloaderMiddleware(Downloader 中间件)类的 process_exception 中设定新的 request.meta[‘proxy’],注意增加试错次数上限,达到上限则返回一个 Response对象,标识出请求出错。

  • 请求返回结果的状态码不是200(比如302,404,504 等):判断状态码,非200则设定新的代理,重新请求,注意增加试错次数上限,达到上限则返回一个 Response对象,标识出请求出错。

2、解析页面(parse 方法)可能出现的错误

  • 由于页面结构改变,解析报错 或 由于被封,返回html不符合解析情况:在 GetvideoinfoSpiderMiddleware(Spider 中间件)类的 process_exception 重新请求,注意增加试错次数上限,达到上限则返回一个 Item 对象(只能 yield),直接通过 Pipelines 去处理,同时把错误记录在日志文件(如果是爬虫问题便于修改爬虫)

Scrapy 的 选择器


Xpath选择器
response.xpath('//div[@id="images"]/a/text()').get()
CSS选择器

Scrapy 中间件使用案例

Scrapy 集成 selenium

http://www.qfrost.com/Scrapy4Selenium/

Scrapy-Redis


参考:https://cuiqingcai.com/6058.html

思路

Scrapy-Redis,是Scrapy的一种分布式方案,目的是部署多个Scrapy,对任务队列同时采集,提高效率;

一个Scrapy采集时,SCHEDULER 是基于本地的磁盘和内存进行 Request 和 Response 的调 和 去重的;

现在,由于多个机器的同时采集,就要考虑 把多个机器中 SCHEDULER 的两大功能提取出来,使其同时服务多个 Scrapy;

Scrapy-Redis 就是将 Scrapy 中 SCHEDULER 的两大功能 在 Redis 中实现,在 Redis 中 同时记录了 多台机器的 去重队列,以及 Request 队列;

组成模块

1、Scheduler

scrapy-redis 模块 将 scrapy 原有的 collection.deque 队列 用 redis的队列代替。

2、Duplication Filter

scrapy-redis 模块 通过 redis 的 set 类型队列 实现去重功能。

3、Item Pipeline

可以将返回给 pipeline 的 Item 存入 redis 的 items queue。

4、Base Spider

不再使用scrapy原有的Spider类,重写的RedisSpider继承了Spider和RedisMixin这两个类,RedisMixin是用来从redis读取url的类。

当我们生成一个Spider继承RedisSpider时,调用setup_redis函数,这个函数会去连接redis数据库,然后会设置signals(信号):

spider_idle:当spider空闲时候的signal,会调用函数,这个函数调用schedule_next_request函数,保证spider是一直活着的状态,并且抛出DontCloseSpider异常。

item_scraped:当抓到一个item时的signal,会调用函数,这个函数会调用schedule_next_request函数,获取下一个request。

安装
pip install scrapy-redis
实现方案

1、必需配置项

需要在 settings.py 添加如下三个配置项,这三个是必需配置项:

SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 如果不需要去重可以写空,但不能没有该项 
#DUPEFILTER_CLASS = ""
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

REDIS_URL = 'redis://root:密码@主机IP:端口'

在spider 中,将 继承类 由scrapy.Spider 修改为 RedisSpider;

在spider 中,需要自定义 要取任务的 redis_key,不定义的话就会按默认的 “%(name)s:start_urls” 设置;

在spider 中,需要去掉 start_urls 参数 和 start_requests 方法;

from scrapy_redis.spiders import RedisSpider

class AntSample(RedisSpider):
	redis_key = "Ant:yangshipin"

2、可选配置项

在 settings.py 中,还可以设置如下可选的配置项:

#使用优先级调度请求队列 (默认使用)
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'

#可选用的其它队列
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'
#不清除Redis队列、这样可以暂停/恢复 爬取
SCHEDULER_PERSIST = True

#序列化项目管道作为redis Key存储
REDIS_ITEMS_KEY = '%(spider)s:items'
 
#默认使用ScrapyJSONEncoder进行项目序列化
#You can use any importable path to a callable object.
REDIS_ITEMS_SERIALIZER = 'json.dumps'

#自定义redis客户端类
REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient'
 
#如果为True,则使用redis的'spop'进行操作。
#如果需要避免起始网址列表出现重复,这个选项非常有用。开启此选项urls必须通过sadd添加,否则会出现类型错误。
REDIS_START_URLS_AS_SET = False
 
#RedisSpider和RedisCrawlSpider默认 start_usls 键,可以在Spider中自定义
REDIS_START_URLS_KEY = '%(name)s:start_urls'
 
#设置redis使用utf-8之外的编码
REDIS_ENCODING = 'latin1'

3、处理redis中带有其他参数的任务

如果 spider 指定的 redis 队列中 只有待爬的url,则 按上述修改后直接启动爬虫即可,如果任务队列还带有其他参数需要解析或者传递,则需要覆盖 RedisMixin 类的 某些方法来实现;

通过阅读 RedisMixin 类的源码,可以看到 scrapy-redis 取任务的步骤如下;

start_requests方法: 返回 next_request

next_request 方法:

  • 从redis取任务(spop 或 lpop)
  • 调用 make_request_from_data 解析出 url 并返回 make_requests_from_url
  • 在 make_requests_from_url(Scrapy的方法) 返回 Request 对象
  • 在 next_request 中 yield 这个 Request 对象

因此,如果想解析出更多参数,可以修改 make_request_from_data 方法,并在方法中手动 yield 一个 Request 对象,从而方便传递解析出来的参数;

当然,如果想修改取任务的方式,则可以修改 next_request 方法;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值