Scrapy一个开源和协作的框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫
引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。
用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL的优先级队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
用于下载网页内容, 并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的
SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求
在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作
位于Scrapy引擎和下载器之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response,
位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)
针对于mac、linux基本不会出现问题
pip3 install scrapy
针对windows有概率失败
失败解决方案:
1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
2、pip3 install lxml
3、pip3 install pyopenssl
4、下载并安装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
scrapy与django类似
scrapy startproject 项目名
scrapy genspider 爬虫名 爬取网址
方法一指令启动:
scrapy crawl 爬虫名字 --nolog(该参数为不打印日志,可忽略)
方法二使用脚本来启动:
项目路径下新建main.py
from scrapy.cmdline import execute
execute(['scrapy','crawl','爬虫名','--nolog'])# --nolog不打印日志,可不带
firstscrapy # 项目名
firstscrapy # 文件夹
spiders # 文件夹,一个个的爬虫
cnblogs.py # 其中一个爬虫,重点写代码的地方(解析数据,发起请求)
items.py # 类比djagno的models,表模型
middlewares.py # 中间件:爬虫中间件和下载中间件都在里面
pipelines.py # 管道,做持久化需要在这写代码
settings.py # 配置文件
scrapy.cfg # 上线配置,开发阶段不用
分析数据写在爬虫文件中的parse方法里(spiders下的爬虫文件,本文为cnblogs.py)
response对象有css方法和xpath方法
俩种方法可以独立使用也可以混合使用,俩种方法找到的是标签对象,会转化为类对象,所以可以根据类对象再次进行标签查找
示例:
-xpath取文本内容
'.//a[contains(@class,"link-title")]/text()'
-xpath取属性
'.//a[contains(@class,"link-title")]/@href'
-css取文本
'a.link-title::text'
-css取属性
'img.image-scale::attr(src)'
取到的标签对象可以是多个或者一个再选择对应的方法即可(使用以下俩个方法获取的标签不再是类对象,无法再次对其下内容进行查找)
.extract_first() 取一个,使得提取内容转换为unicodez字符串
.extract() 取所有,使得提取内容转换为unicodez字符串,返回一个list
示例代码:
import scrapy
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs' # 爬虫名字
allowed_domains = ['cnblogs.com'] #爬取网址名
start_urls = ['http://cnblogs.com/'] # 爬取的url
def parse(self, response):
articles = response.xpath('.//article[@class="post-item"]') # 找到article标签
for article in articles: # 对每一个article标签中再次查找标签
title = article.xpath('.//a[@class="post-item-title"]/text()').extract_first()
url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first()
desc = article.xpath('.//p[@class="post-item-summary"]/text()').extract()
author = article.xpath('.//footer[@class="post-item-foot"]/a/span/text()').extract_first()
修改settings.py
#1 是否遵循爬虫协议,默认为True
ROBOTSTXT_OBEY = False
#2 LOG_LEVEL 日志级别
LOG_LEVEL='ERROR' # 报错如果不打印日志,在控制台看不到错误
# 3 USER_AGENT
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'
# 4 DEFAULT_REQUEST_HEADERS 默认请求头
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
}
# 5 SPIDER_MIDDLEWARES 爬虫中间件
SPIDER_MIDDLEWARES = {
'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
}
# 6 DOWNLOADER_MIDDLEWARES 下载中间件
DOWNLOADER_MIDDLEWARES = {
'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
}
# 7 ITEM_PIPELINES 持久化配置,配置数据持久化方式(如文件、数据库等)
ITEM_PIPELINES = {
'cnblogs.pipelines.CnblogsPipeline': 300,
}
#1 增加并发:
#默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改
CONCURRENT_REQUESTS = 100
#值为100,并发设置成了为100。
#2 降低日志级别:
#在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:
LOG_LEVEL = 'INFO'
# 3 禁止cookie:
#如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:
COOKIES_ENABLED = False
# 4 禁止重试:
#对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:
RETRY_ENABLED = False
# 5 减少下载超时:
#如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:
DOWNLOAD_TIMEOUT = 10 #超时时间为10s
解析函数中parse,要return [{},{},{}],返回一个列表套字典的数据
def parse(self, response):
articles = response.xpath('.//article[@class="post-item"]')
article_list = []
for article in articles:
item = CnblogsItem()
title = article.xpath('.//a[@class="post-item-title"]/text()').extract_first()
url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first()
desc = article.xpath('.//p[@class="post-item-summary"]/text()').extract()
author = article.xpath('.//footer[@class="post-item-foot"]/a/span/text()').extract_first()
article_list.append({'title':title, 'url':url, 'desc':desc, 'desc':desc, 'author':author, })
return article_list
使用指令 scrapy crawl cnblogs -o 文件名(json,csv等后缀)
scrapy crawl cnblogs -o data.json
1.在items.py中写一个类,继承scrapy.Item
在类中编写属性
import scrapy
class CnblogsItem(scrapy.Item):
title = scrapy.Field()
desc = scrapy.Field()
pub_time = scrapy.Field()
author = scrapy.Field()
url = scrapy.Field()
content = scrapy.Field()
2.在爬虫中导入类,实例化得到对象,把要保存的数据放到对象中
import scrapy
from scrapy import Request
from testscrapy.items import CnblogsItem
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['http://cnblogs.com/']
def parse(self, response):
articles = response.xpath('.//article[@class="post-item"]')
for article in articles:
item = CnblogsItem()
title = article.xpath('.//a[@class="post-item-title"]/text()').extract_first()
url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first()
desc = article.xpath('.//p[@class="post-item-summary"]/text()').extract()
author = article.xpath('.//footer[@class="post-item-foot"]/a/span/text()').extract_first()
res_desc = desc[0].replace('\n', '').replace(' ', '')
if not res_desc:
res_desc = desc[1].replace('\n', '').replace(' ', '')
item['title'] = title
item['url'] = url
item['desc'] = desc
item['author'] = author
item['desc'] = res_desc
# 由于需要拿到文章正文,再次发送request,url为request的爬取地址,callback为解析数据的函数,meta为需要传递过去的参数
yield item
3.修改配置文件,指定pipline,数字表示优先级,越小越大
ITEM_PIPELINES = {
'crawl_cnblogs.pipelines.CrawlCnblogsPipeline': 300,
}
4.写一个pipline:CrawlCnblogsPipeline
-open_spider:数据初始化,打开文件,打开数据库链接
-process_item:真正存储的地方
-一定不要忘了return item,交给后续的pipline继续使用
-close_spider:销毁资源,关闭文件,关闭数据库链接
pipelines.py
# 数据持久化文件形式
class CnblogsFilePipeline:
def open_spider(self, spider):
# print(spider) # 就是爬虫类的对象
# print(type(spider)) # 就是爬虫类的对象
print('open')
# 打开文件,链接数据库
self.f = open('cnblogs.txt', 'w', encoding='utf-8')
def close_spider(self, spider):
# print(spider) # 就是爬虫类的对象
# print(type(spider)) # 就是爬虫类的对象
print('close')
# 关闭文件,关闭数据库
self.f.close()
def process_item(self, item, spider):
# 具体存数据,存文件
# with open('cnblog.txt', 'a', encoding='utf-8') as f:
# f.write('标题:%s,作者:%s' % (item['title'], item['author']))
# print(item)
self.f.write('标题:%s,作者:%s\n' % (item['title'], item['author']))
return item # 一定不要忘了返回
import pymysql
class CnblogsMysqlPipeline:
def open_spider(self, spider):
self.conn = pymysql.connect(user='root',
password="密码",
host='127.0.0.1',
port=3306,
database='cnblogs')
self.cursor = self.conn.cursor()
def close_spider(self, spider):
# self.conn.commit()
self.cursor.close()
self.conn.close()
def process_item(self, item, spider):
sql = 'insert into article (title,`desc`,url,pub_time,author,content) values (%s,%s,%s,%s,%s,%s)'
self.cursor.execute(sql, args=[item['title'], item['desc'], item['url'], item['pub_time'], item['author'],
item['content']])
self.conn.commit()
return item # 一定不要忘了返回
在settings.py中可看到以下配置
SPIDER_MIDDLEWARES 爬虫中间件 (了解即可,用的少)
SPIDER_MIDDLEWARES = {
'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
}
DOWNLOADER_MIDDLEWARES 下载中间件(用的多)
DOWNLOADER_MIDDLEWARES = {
'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
}
最重要的是下载中间件,里面的两个方法
middlewares.py
class CnblogsDownloaderMiddleware:
# 请求来的时候
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
在下载中间件中重写process_request
def process_request(self, request, spider):
request.meta['proxy'] = 'http://221.6.215.202:9091' #设置proxy即可
return None
在中间件中重写process_request
def process_request(self, request, spider):
request.cookies['name']='bbc‘
return None
在中间件中重写process_request
def process_request(self, request, spider):
request.headers['Auth']='asdfasdfasdfasdf'
request.headers['USER-AGENT']='ssss'
scrapy框架的下载器,是无法触发js的,当我们需要的内容时通过js获取到的,可以在scrapy框架中对下载器进行selenium的集成,实质就是将一些需要触发js的请求交由selenium来处理,其他的请求照常走下载器。
第一步:
在爬虫类中为需要使用selenium的爬虫添加一个类属性以及一个类方法
import scrapy
from selenium import webdriver
class CnblogsSpider(scrapy.Spider):
name = 'cnblogs'
allowed_domains = ['cnblogs.com']
start_urls = ['http://cnblogs.com/']
driver = webdriver.Edge() # 定义一个selenium对象
def close(spider, reason):
spider.driver.close() # 使用完毕后关闭selenium对象
第二步:
创建一个下载中间件,重写process_request
class CnblogsDownloaderMiddlewareProxy:
def process_request(self, request, spider): # 通过分辨url来判断是否使用selenium来获取html源码
url=request.url
if 'sitehome' in url:
spider.driver.get(url)
response=HtmlResponse(url=url,body=spider.driver.page_source.encode('utf-8'))
return response
else:
return None
scrapy 实现了去重,爬过的网址不会再爬了
scrapy使用了集合进行去重
第一步:
找到scrapy的配置文件
scrapy.settings下的default_settings.py中
找到DUPEFILTER_CLASS = ‘scrapy.dupefilters.RFPDupeFilter’
第二步:
找到RFPDupeFilter,该类负责处理指纹判断是否爬取过
class RFPDupeFilter(BaseDupeFilter):
def request_seen(self, request: Request) -> bool:
fp = self.request_fingerprint(request) # 生成指纹
if fp in self.fingerprints: # 对比指纹是否存在
return True # 返回True就是已爬取
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + '\n')
return False # 返回false就是未爬取过
第三步:
找到爬虫对象执行的入口
在scrapy.spiders下的_ init _.py中
class Spider(object_ref):
def start_requests(self):
if not self.start_urls and hasattr(self, 'start_url'):
raise AttributeError(
"Crawling could not start: 'start_urls' not found "
"or empty (but found 'start_url' attribute instead, "
"did you miss an 's'?)")
for url in self.start_urls:
yield Request(url, dont_filter=True)
第四步:
爬虫执行后在调度器中去重
在scrapy.core下的scheduler中
class Scheduler(BaseScheduler):
def enqueue_request(self, request: Request) -> bool:
if not request.dont_filter and self.df.request_seen(request): #此处调用了之前找到的去重类
self.df.log(request, self.spider)
return False
dqok = self._dqpush(request)
if dqok:
self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)
else:
self._mqpush(request)
self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)
self.stats.inc_value('scheduler/enqueued', spider=self.spider)
return True
以上就是scrapy框架的去重解析
中小型的去重使用集合、字典等去重比较适合,并发量大了之后再使用这些方式进行就会极大的影响效率和内存。
这个时候可以使用布隆过滤器。
布隆过滤器是以bit为单位的数组,数组初始状态每一位都是0,当需要去重的数据经过哈希函数处理后,将数组对应位置的0变为1,一但哈希函数计算的位置都是1就表明该数据已存在,反之只要有一位不为1就表示该数据此前没有存在过。布隆过滤器,占用空间很小,但只能进行过滤对比,无法取出值,所以应用时需要考虑是否需要取出已存在的值。
pip3 install pybloom_live
from pybloom_live import BloomFilter
bf = BloomFilter(capacity=1000) # capacity为数组长度,持续使用后,定长布隆过滤器准确性会越来越低
url='www.baidu.com'
bf.add(url)
print(url in bf)
print("www.liuqingzheng.top" in bf)
from pybloom_live import ScalableBloomFilter
bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH) # initial_capacity为数组初始长度 error_rate为误判概率
url = "www.cnblogs.com"
url2 = "www.liuqingzheng.top"
bloom.add(url)
print(url in bloom)
print(url2 in bloom)
add、madd
布隆过滤器对象.add {key} {item}
布隆过滤器对象.madd {key} {item} [item…]
往过滤器中添加元素。如果key不存在,过滤器会自动创建。
exists、 mexists
布隆过滤器对象.exists {key} {item}
布隆过滤器对象.mexists {key} {item} [item…]
判断过滤器中是否存在该元素,不存在返回0,存在返回1。
第一步:
安装scrapy-redis
pip3 install scrapy-redis
第二步:
改造爬虫类
from scrapy_redis.spiders import RedisSpider
class CnblogSpider(RedisSpider):
name = 'cnblog_redis'
allowed_domains = ['cnblogs.com']
# start_urls = ['http://cnblogs.com/'] 该属性去掉
# 写一个key:redis列表的key,起始爬取的地址
redis_key = 'myspider:start_urls'
# 正常编写解析函数
def parse(self, response):
...
第三步:
配置文件配置
# 分布式爬虫配置
# 去重规则使用redis
REDIS_HOST = 'localhost' # 主机名
REDIS_PORT = 6379 # 端口
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 持久化:文件,mysql,redis
ITEM_PIPELINES = {
# 'cnblogs.pipelines.CnblogsFilePipeline': 300, # 写入文件 数字越小的先进行操作
# 'cnblogs.pipelines.CnblogsMysqlPipeline': 100, # 写入mysql
'scrapy_redis.pipelines.RedisPipeline': 400, # 写入redis
}
第四步:
在多台机器上启动scrapy项目
第五步:
向redis放入一个起始url,之后分布式项目才会开始爬取
可以在redis中添加、或者使用脚本添加
lpush myspider:start_urls value http://www.cnblogs.com/