文章目录
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 方法;