爬虫之Scrapy架构
Scrapy架构介绍
Scrapy是适用于Python的一个快速、高层次的屏幕抓取和web抓取框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫
引擎(EGINE)
引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。
调度器(SCHEDULER)
用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想像成一个URL的优先级队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址。
下载器(DOWLOADER)
用于下载网页内容,并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的。
爬虫(SPIDERS)
SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求。
项目管道(ITEM PIPLINES)
在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作。
下载器中间件(Downloader Middlewares)
位于Scrapy引擎和下载器之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response。
爬虫中间件(Spider Middlewares)
位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)。
Scrapy下载
安装:
pip install scrapy
如果安装失败,就需要走下面的流程:
1.安装wheel,之后通过wheel文件安装软件,wheel文件官网
pip install wheel
pip install lxml
pip install pyopenssl
3.下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
4.下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
5.twisted的wheel文件通过pip安装
pip install 下载目录/Twisted-17.9.0-cp36-cp36m-win_amd64.whl
6.安装scrapy
pip install scrapy
Scrapy基本使用
创建项目
通过cmd窗口:在当前目录下创建项目
scrapy startproject 项目名
爬虫创建
在项目目录下,打开cmd窗口:
scrapy genspider 爬虫名 爬虫地址
比如创建爬博客园的爬虫:
scrapy genspider cnblogs cnblogs.com
此时会在项目spiders文件夹下创建cnblogs.py
import scrapy
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] # 允许爬取的地址
start_urls = ['http://cnblogs.com/'] # 起始爬取的地址
def parse(self, response): # response就是爬取结果
# 爬取完之后的代码,解析代码放在这里
print(response.text)
爬虫启动方式一
--nolog代表不要日志输出
scrapy crawl 爬虫名
scrapy crawl 爬虫名 --nolog
比如启动刚刚创建的博客园爬虫:
scrapy crawl cnblogs --nolog
爬虫启动方式二
项目路径下新建py文件:
from scrapy.cmdline import execute
execute(['scrapy','crawl','爬虫名','--nolog'])
启动爬虫运行这个py文件即可。
Scrapy目录结构
项目名
├── 项目同名文件夹
├ ├── spiders -- 文件夹,专门存放爬虫文件
├ ├ ├── 爬虫1.py -- 其中一个爬虫,重点写代码的地方
├ ├ └── 爬虫2.py -- 其中一个爬虫,重点写代码的地方
├ ├── items.py -- 类比djagno的models,表模型--->类
├ ├── middlewares.py -- 爬虫中间件和下载中间件都在里面
├ ├── pipelines.py -- 管道,做持久化需要在这写代码
├ └── settings.py -- 配置文件
└── scrapy.cfg -- 上线配置,开发阶段不用
Scrapy解析数据
创建爬虫时会自动在spiders下创建对应的爬虫名py文件,里面一个类,类中有一个解析数据用的方法parse(self, response):
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] # 允许爬取的地址
start_urls = ['http://cnblogs.com/'] # 起始爬取的地址
def parse(self, response): # response就是爬取结果
# 爬取完之后的代码,解析代码放在这里
print(type(response))
response对象的方法:
方法 | 作用 |
---|---|
response.css('css选择器') | 根据css选择器选择标签 |
response.css('css选择器::text') | 获取标签文本内容 |
response.css('css选择器::attr(class)') | 获取标签class属性 |
response.xpath('xpath') | 根据xpath选择标签 |
response.xpath('xpath/text()') | 获取标签文本内容 |
response.xpath('xpath/@class') | 获取标签class属性 |
取个数 | |
response.css().extract_first() | 根据css选择器选择标签,取一个 |
response.css().extract() | 根据css选择器选择标签,取全部 |
举例:获取博客园首页博客标题
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] # 允许爬取的地址
start_urls = ['http://cnblogs.com/'] # 起始爬取的地址
def parse(self, response):
print(response.css('.post-item-title::text').extract())
settings相关配置
基础配置
# 是否遵循爬虫协议
ROBOTSTXT_OBEY = False
# 输出日志级别
LOG_LEVEL='ERROR'
# 客户端类型
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
# 默认请求头
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}
# 爬虫中间件
SPIDER_MIDDLEWARES = {
'项目名.middlewares.项目名SpiderMiddleware': 543,
}
# 下载中间件
DOWNLOADER_MIDDLEWARES = {
'项目名.middlewares.项目名DownloaderMiddleware': 543,
}
# 持久化配置
ITEM_PIPELINES = {
'项目名.pipelines.项目名Pipeline': 300,
}
增加爬虫的爬取效率
1.增加并发:
默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改:并发设置成了为100
CONCURRENT_REQUESTS = 100
2.降低日志级别:
在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:
LOG_LEVEL = 'ERROR'
3.禁止cookie:
如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:
COOKIES_ENABLED = False
4 禁止重试:
对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:
RETRY_ENABLED = False
5.减少下载超时:
如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:
DOWNLOAD_TIMEOUT = 10 # 超时时间为10s
去重规则(布隆过滤器)
scrapy中 实现了去重,爬过的网址不会再爬。
原理:每次根据爬取的地址对象request生成一个指纹(唯一),判断是否在集合中,如果在集合中,就不爬取了,如果不在,就爬取并且把生成的指纹(唯一)放到集合中。
源码剖析
在scrapy自己的配置文件中配置了一个过滤器:
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
RFPDupeFilter类中有一个方法request_seen:集合去重
def request_seen(self, request: Request) -> bool:
# 生成指纹
fp = self.request_fingerprint(request)
# self.fingerprints是一个集合
# 判断指纹是否存在
if fp in self.fingerprints:
# 返回True就说明重复了,不爬取这个地址了
return True
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + '\n')
return False
布隆过滤器
极小内存校验是否重复
https://zhuanlan.zhihu.com/p/94668361
可以自己写一个类,替换掉内置的去重:
class MyRFPDupeFilter(RFPDupeFilter):
fingerprints=布隆过滤器
持久化方案(数据保存)
方式一:保存到文件
解析函数parse,需要返回 [{}, {}] 的格式:
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] # 允许爬取的地址
start_urls = ['http://cnblogs.com/'] # 起始爬取的地址
def parse(self, response): # response就是爬取结果
# 爬取完之后的代码,解析代码放在这里
return [{'title': v} for v in response.css('.post-item-title::text').extract()]
然后终端命令:
scrapy crawl 爬虫名 -o 文件名(json,pkl,csv等结尾)
方式二:pipline模式
1.在items.py中写一个类,继承scrapy.Item
2.在类中写属性:
import scrapy
class CnblogsItem(scrapy.Item):
title = scrapy.Field()
3.在爬虫中导入类,实例化得到对象,把要保存的数据放到对象中
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] # 允许爬取的地址
start_urls = ['http://cnblogs.com/'] # 起始爬取的地址
def parse(self, response): # response就是爬取结果
from ..items import CnblogsItem
item = CnblogsItem()
for title in response.css('.post-item-title::text').extract():
item['title'] = title
# 变成生成器
yield item
4.修改配置文件,指定pipline,数字表示优先级,越小越大
ITEM_PIPELINES = {
'项目名.pipelines.CrawlCnblogsPipeline': 300,
}
5.在pipelines.py中写一个pipline,和配置文件中的名字对应
class CrawlCnblogsPipeline:
# 数据初始化,打开文件,打开数据库链接
def open_spider(self, spider):
# spider: 即爬虫对象
# 打开文件
self.f = open('cnblogs.txt', 'w', encoding='utf-8')
# 真正存储的地方,一定不要忘了return item,交给后续的pipline继续使用
def process_item(self, item, spider):
# 存储
self.f.write('标题:' + item['title'] + '\n')
return item
# 销毁资源,关闭文件,关闭数据库链接
def close_spider(self, spider):
# spider: 即爬虫对象
# 关闭文件
self.f.close()
request和response传递参数
传递参数需要使用Request方法。
from scrapy.http import Request
Request(url=爬虫, callback=调用哪个爬虫函数, meta=参数)
举例:博客园爬取首页标题和文章详情
爬虫代码:
import scrapy
from scrapy.http import Request
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] # 允许爬取的地址
start_urls = ['http://cnblogs.com/'] # 起始爬取的地址
def parse(self, response): # response就是爬取结果
from ..items import CnblogsItem
for title in response.css('.post-item-title'):
# 要写在里面
item = CnblogsItem()
# 标题
item['title'] = title.css('::text').extract_first()
# 文章详情地址
url = title.css('::attr(href)').extract_first()
# 爬取文章详情地址,把item对象传过去修改
yield Request(url=url, callback=self.parser_detail, meta={'item': item})
def parser_detail(self, response):
item = response.meta.get('item')
# 文章内容
content = response.css('#post_detail').extract_first()
item['content'] = content
print('一篇文章完成')
yield item
pipelines.py:(记得添加到配置文件中)
class CrawlCnblogsPipeline:
# 数据初始化,打开文件,打开数据库链接
def open_spider(self, spider):
# spider: 即爬虫对象
# 打开文件
self.f = open('cnblogs.txt', 'w', encoding='utf-8')
# 真正存储的地方,一定不要忘了return item,交给后续的pipline继续使用
def process_item(self, item, spider):
# 存储
self.f.write('标题:' + item['title'] + '\n')
self.f.write('内容:' + item['content'] + '\n')
return item
# 销毁资源,关闭文件,关闭数据库链接
def close_spider(self, spider):
# spider: 即爬虫对象
# 关闭文件
self.f.close()
items.py:
import scrapy
class CnblogsItem(scrapy.Item):
title = scrapy.Field() # 标题
content = scrapy.Field() # 文章内容
网页解析下一页继续爬取
import scrapy
from scrapy.http import Request
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] # 允许爬取的地址
start_urls = ['http://cnblogs.com/'] # 起始爬取的地址
def parse(self, response): # response就是爬取结果
from ..items import CnblogsItem
item = CnblogsItem()
for title in response.css('.post-item-title::text').extract():
item['title'] = title
yield item
# 下一页的标签的href属性获取
next_url = response.css('div.pager>a:last-child::attr(href)').extract_first()
next_url = 'https://www.cnblogs.com' + next_url
print('准备爬取 ' + next_url)
# 把下一页的爬取继续交给当前self.parse方法
yield Request(url=next_url, callback=self.parse)
爬虫和下载中间件
配置文件中:
# 爬虫中间件
SPIDER_MIDDLEWARES = {
'项目名.middlewares.项目名SpiderMiddleware': 543,
}
# 下载中间件
DOWNLOADER_MIDDLEWARES = {
'项目名.middlewares.项目名DownloaderMiddleware': 543,
}
用的比较多的是下载中间件,里面的两个方法:
class DownloaderMiddleware:
# 请求来的时候
def process_request(self, request, spider):
# - return None: 继续执行下一个中间件的process_request
# - return a Response object :直接返回给engin,去解析
# - return a Request object :给engin,再次被放到调度器中
# - raise IgnoreRequest: 执行 process_exception()方法
return None
# 响应走的时候
def process_response(self, request, response, spider):
# - return a Response :继续走下一个中间件的process_response,给engin,进爬虫解析
# - return a Request :给engin,进入调度器,等待下一次爬取
# - raise IgnoreRequest:抛异常
return response
加代理,cookie,header,集成selenium
代理、cookie、header都是在下载中间件中添加
代理:
def process_request(self, request, spider):
print('下载中间件:',request)
# 代理
request.meta['proxy'] = 'http://221.6.215.202:9091'
return None
cookie:
def process_request(self, request, spider):
print('下载中间件:',request)
# cookie
request.cookies= {}
# 或者
request.cookies['name']='value'
return None
请求头:
def process_request(self, request, spider):
print('下载中间件:',request)
# 请求头
request.headers['Auth']='asdfasdfasdfasdf'
request.headers['USER-AGENT']='ssss'
return None
随机生成user-agent:
pip install fake_useragent
from fake_useragent import UserAgent
ua = UserAgent()
print(ua.ie) # 随机打印ie浏览器任意版本
print(ua.firefox) # 随机打印firefox浏览器任意版本
print(ua.chrome) # 随机打印chrome浏览器任意版本
print(ua.random) # 随机打印任意厂家的浏览器
集成selenium
在爬虫类中集成:
import scrapy
from selenium import webdriver
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['http://cnblogs.com/']
# 添加属性
driver = webdriver.Chrome()
def parse(self, response):
print(response)
# 关闭触发
def close(self, reason):
self.driver.close()
下载中间件中:
def process_request(self, request, spider):
"""spider就是爬虫类对象"""
from scrapy.http import HtmlResponse
spider.driver.get(url=request.url)
response = HtmlResponse(url=request.url, body=spider.driver.page_source.encode('utf-8'), request=request)
return response
scrapy-redis实现分布式爬虫
第一步:安装scrapy-redis
pip install scrapy-redis
第二步:改造爬虫类
from scrapy_redis.spiders import RedisSpider
class CnblogSpider(RedisSpider):
name = 'cnblog_redis'
allowed_domains = ['cnblogs.com']
# 写一个key:redis列表的key,起始爬取的地址
redis_key = 'myspider:start_urls'
第三步:配置文件配置
# 分布式爬虫配置
# 去重规则使用redis
REDIS_HOST = 'localhost' # 主机名
REDIS_PORT = 6379 # 端口
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 持久化:文件,mysql,redis
ITEM_PIPELINES = {
'scrapy_redis.pipelines.RedisPipeline': 400,
}
第四步:在多台机器上启动scrapy项目
第五步:把起始爬取的地址放到redis的列表中
lpush myspider:start_urls http://www.cnblogs.com/