MySQL
在pipelines.py中引入数据库连接模块:
__init__是对数据进行初始化,定义连接信息如host,数据库用户名、密码、数据库名称、数据库编码, 在process_item中进行插入数据操作,格式都是固定的
class MysqlPipeline(object):
def __init__(self):
self.conn = MySQLdb.connect('127.0.0.1', 'root', 'root', 'jobbole', charset='utf8', use_unicode=True)
self.cursor = self.conn.cursor()
def process_item(self, item, spider):
insert_sql = 'INSERT INTO jobbole_article (`title`, `create_date`, `url`, `url_object_id`, `content`, `front_image_path`, `comment_nums`, `fav_nums`, `praise_nums`, `tags`) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)'
self.cursor.execute(insert_sql, (item['title'], item['create_date'], item['url'], item['url_object_id'], item['content'], item["front_image_path"], item['comment_nums'], item['fav_nums'], item['praise_nums'], item['tags']))
self.conn.commit()
最后在settings.py中把MysqlPipelint()加入到系统中,需要注意的是优先级要小于之前加入处理图片路径的优先级
(先进行ArticleimagePipeline的处理,再进行MysqlPipeline处理)
ITEM_PIPELINES = {
'articlespider.pipelines.ArticlespiderPipeline': 300, #系统自动生成pipeline,未用
'articlespider.pipelines.ArticleimagePipeline': 1,
'articlespider.pipelines.MysqlPipeline': 4,
}
from twisted.enterprise import adbapi
import MySQLdb
import MySQLdb.cursors
class MysqlPipeline(object):
def __init__(self, dbpool):
self.dbpool = dbpool
@classmethod
def from_settings(cls, settings):
dbparms = dict(
host = settings["MYSQL_HOST"],
db = settings["MYSQL_DBNAME"],
user = settings["MYSQL_USER"],
passwd = settings["MYSQL_PASSWORD"],
charset='utf8',
cursorclass=MySQLdb.cursors.DictCursor,
use_unicode=True,
)
dbpool = adbapi.ConnectionPool("MySQLdb", **dbparms)
return cls(dbpool)
def process_item(self, item, spider):
# 使用twisted将mysql插入变成异步执行
query = self.dbpool.runInteraction(self.do_insert, item)
query.addErrback(self.handle_error)
def handle_error(self, failure):
print(failure)
def do_insert(self, cursor, item):
# 具体执行插入
insert_sql = 'INSERT INTO jobbole_article (`title`, `create_date`, `url`, `url_object_id`, `content`, `front_image_path`, `comment_nums`, `fav_nums`, `praise_nums`, `tags`) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)'
cursor.execute(insert_sql, (item['title'], item['create_date'], item['url'], item['url_object_id'], item['content'], item["front_image_path"], item['comment_nums'], item['fav_nums'], item['praise_nums'], item['tags']))
1.Scrapy
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。。用这个框架可以轻松爬下来如亚马逊商品信息之类的数据。
2.Beautiful Soup
Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时甚至数天的工作时间。
IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。在实际的数据采集过程中,既考虑网速和响应的问题,也需要考虑自身机器的硬件情况,来设置多线程或多进程
从功能上来讲,爬虫一般分为数据采集,处理,储存三个部分。这里我们只讨论数据采集部分。
一般网站从三个方面反爬虫:用户请求的Headers,用户行为,网站目录和数据加载方式。前两种比较容易遇到,大多数网站都从这些角度来反爬虫。第三种一些应用ajax的网站会采用,这样增大了爬取的难度。
0x02 通过Headers反爬虫
从用户请求的Headers反爬虫是最常见的反爬虫策略。很多网站都会对Headers的User-Agent进行检测,还有一部分网站会对Referer进行检测(一些资源网站的防盗链就是检测Referer)。如果遇到了这类反爬虫机制,可以直接在爬虫中添加Headers,将浏览器的User-Agent复制到爬虫的Headers中;或者将Referer值修改为目标网站域名。对于检测Headers的反爬虫,在爬虫中修改或者添加Headers就能很好的绕过。
0x03 基于用户行为反爬虫
还有一部分网站是通过检测用户行为,例如同一IP短时间内多次访问同一页面,或者同一账户短时间内多次进行相同操作。
大多数网站都是前一种情况,对于这种情况,使用IP代理就可以解决。可以专门写一个爬虫,爬取网上公开的代理ip,检测后全部保存起来。这样的代理ip爬虫经常会用到,最好自己准备一个。有了大量代理ip后可以每请求几次更换一个ip,这在requests或者urllib2中很容易做到,这样就能很容易的绕过第一种反爬虫。
对于第二种情况,可以在每次请求后随机间隔几秒再进行下一次请求。有些有逻辑漏洞的网站,可以通过请求几次,退出登录,重新登录,继续请求来绕过同一账号短时间内不能多次进行相同请求的限制。
0x04 动态页面的反爬虫
上述的几种情况大多都是出现在静态页面,还有一部分网站,我们需要爬取的数据是通过ajax请求得到,或者通过JavaScript生成的。首先用Firebug或者HttpFox对网络请求进行分析。如果能够找到ajax请求,也能分析出具体的参数和响应的具体含义,我们就能采用上面的方法,直接利用requests或者urllib2模拟ajax请求,对响应的json进行分析得到需要的数据。
能够直接模拟ajax请求获取数据固然是极好的,但是有些网站把ajax请求的所有参数全部加密了。我们根本没办法构造自己所需要的数据的请求。我这几天爬的那个网站就是这样,除了加密ajax参数,它还把一些基本的功能都封装了,全部都是在调用自己的接口,而接口参数都是加密的。遇到这样的网站,我们就不能用上面的方法了,我用的是selenium+phantomJS框架,调用浏览器内核,并利用phantomJS执行js来模拟人为操作以及触发页面中的js脚本。从填写表单到点击按钮再到滚动页面,全部都可以模拟,不考虑具体的请求和响应过程,只是完完整整的把人浏览页面获取数据的过程模拟一遍。
用这套框架几乎能绕过大多数的反爬虫,因为它不是在伪装成浏览器来获取数据(上述的通过添加 Headers一定程度上就是为了伪装成浏览器),它本身就是浏览器,phantomJS就是一个没有界面的浏览器,只是操控这个浏览器的不是人。利用 selenium+phantomJS能干很多事情,例如识别点触式(12306)或者滑动式的验证码,对页面表单进行暴力破解等等。它在自动化渗透中还 会大展身手,以后还会提到这个。
解决限制IP可以使用代理IP地址池、服务器;不适用动态爬取的情况下可以使用反编译JS文件获取相应的文件,或者换用其它平台(比如手机端)看看是否可以获取相应的json文件。
1.输入式验证码
解决思路:这种是最简单的一种,只要识别出里面的内容,然后填入到输入框中即可。这种识别技术叫OCR,这里我们推荐使用Python的第三方库,tesserocr。对于没有什么背影影响的验证码如图2,直接通过这个库来识别就可以。但是对于有嘈杂的背景的验证码这种,直接识别识别率会很低,遇到这种我们就得需要先处理一下图片,先对图片进行灰度化,然后再进行二值化,再去识别,这样识别率会大大提高。
验证码识别大概步骤
2.滑动式验证码
解决思路:对于这种验证码就比较复杂一点,但也是有相应的办法。我们直接想到的就是模拟人去拖动验证码的行为,点击按钮,然后看到了缺口 的位置,最后把拼图拖到缺口位置处完成验证。
第一步:点击按钮。然后我们发现,在你没有点击按钮的时候那个缺口和拼图是没有出现的,点击后才出现,这为我们找到缺口的位置提供了灵感。
第二步:拖到缺口位置。我们知道拼图应该拖到缺口处,但是这个距离如果用数值来表示?通过我们第一步观察到的现象,我们可以找到缺口的位置。这里我们可以比较两张图的像素,设置一个基准值,如果某个位置的差值超过了基准值,那我们就找到了这两张图片不一样的位置,当然我们是从那块拼图的右侧开始并且从左到右,找到第一个不一样的位置时就结束,这是的位置应该是缺口的left,所以我们使用selenium拖到这个位置即可。这里还有个疑问就是如何能自动的保存这两张图?这里我们可以先找到这个标签,然后获取它的location和size,然后 top,bottom,left,right = location['y'] ,location['y']+size['height']+ location['x'] + size['width'] ,然后截图,最后抠图填入这四个位置就行。具体的使用可以查看selenium文档,点击按钮前抠张图,点击后再抠张图。最后拖动的时候要需要模拟人的行为,先加速然后减速。因为这种验证码有行为特征检测,人是不可能做到一直匀速的,否则它就判定为是机器在拖动,这样就无法通过验证了。
3.点击式的图文验证 和 图标选择
图文验证:通过文字提醒用户点击图中相同字的位置进行验证。
图标选择: 给出一组图片,按要求点击其中一张或者多张。借用万物识别的难度阻挡机器。
这两种原理相似,只不过是一个是给出文字,点击图片中的文字,一个是给出图片,点出内容相同的图片。
这两种没有特别好的方法,只能借助第三方识别接口来识别出相同的内容,推荐一个超级鹰,把验证码发过去,会返回相应的点击坐标。
然后再使用selenium模拟点击即可。具体怎么获取图片和上面方法一样。
破解核心思路:
1、如何确定滑块滑动的距离?
滑块滑动的距离,需要检测验证码图片的缺口位置
滑动距离 = 终点坐标 - 起点坐标
然后问题转化为我们需要屏幕截图,根据selenium中的position方法并进行一些坐标计算,获取我们需要的位置
2、坐标我们如何获取?
起点坐标:
每次运行程序,位置固定不变,滑块左边界离验证码图片左边界有6px的距离
终点坐标:
每次运行程序,位置会变,我们需要计算每次缺口的位置
怎么计算终点也就是缺口的位置?
先举个例子,比如我下面两个图片都是120x60的图片,一个是纯色的图片,一个是有一个蓝色线条的图片(蓝色线条位置我事先设定的是60px位置),我现在让你通过程序确定蓝色线条的位置,你怎么确定?
答案:
遍历所有像素点色值,找出色值不一样的点的位置来确定蓝色线条的位置
这句话该怎么理解?大家点开我下面的图片,是不是发现图片都是由一个一个像素点组成的,120×60的图片,对应的像素就是横轴有120个像素点,纵轴有60个像素点,我们需要遍历两个图片的坐标并对比色值,从(0,0)(0,1)......一直到(120,60),开始对比两个图片的色值,遇到色值不一样的,我们return返回该位置即可
这时候就需要cookie自动的更新了。通常怎样自动更新cookie呢?这里会用到selenium。
步骤1、 采用selenium自动登录获取cookie,保存到文件;
步骤2、 读取cookie,比较cookie的有效期,若过期则再次执行步骤1;
步骤3、 在请求其他网页时,填入cookie,实现登录状态的保持。
import os
import pickle
import time
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium import webdriver
def get_cookie_from_network():
# 这个url比较关键,网络上比较老的办法是
# url_login = 'https://login.taobao.com/member/login.jhtml'
# 测试之后发现,会报错“可能不是一个可交互的element”
# 在后面添加?style=mini后就可以了
url_login = 'https://login.taobao.com/member/login.jhtml?style=mini'
# 这一段是为了给selenium添加user-agent。模拟浏览器
dcap = dict(DesiredCapabilities.PHANTOMJS)
dcap["phantomjs.page.settings.userAgent"] = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:25.0) Gecko/20100101 Firefox/25.0 ")
driver = webdriver.PhantomJS(desired_capabilities=dcap)
driver.implicitly_wait(1)
driver.get(url_login)
# driver.find_element_by_id("J_Static2Quick").click()
driver.find_element_by_id("TPL_username_1").clear()
driver.find_element_by_id("TPL_password_1").clear()
driver.find_element_by_id('TPL_username_1').send_keys(u'张小呆920318')
driver.find_element_by_id('TPL_password_1').send_keys('plus820828')
driver.find_element_by_id('J_SubmitStatic').click()
# 获得 cookie信息
cookie_list = driver.get_cookies()
print cookie_list
cookie_dict = {}
for cookie in cookie_list:
#写入文件
f = open('cookies/' + cookie['name']+'.taobao','w')
pickle.dump(cookie, f)
f.close()
if cookie.has_key('name') and cookie.has_key('value'):
cookie_dict[cookie['name']] = cookie['value']
return cookie_dict
将获取的cookies存储在文件中。后期,你可以把它封装成一个函数放在爬虫的__init__中,也可以利用linux的crontab来定时获取最新的cookies值。
Selenium是一个用于Web应用程序测试的工具。Selenium测试直接运行在浏览器中,就像真正的用户在操作一样。支持的浏览器包括IE(7, 8, 9, 10, 11),Mozilla Firefox,Safari,Google Chrome,Opera等主流浏览器。这个工具的主要功能包括:测试与浏览器的兼容性——测试你的应用程序看是否能够很好得工作在不同浏览器和操作系统之上。
它的功能有:
框架底层使用JavaScript模拟真实用户对浏览器进行操作。测试脚本执行时,浏览器自动按照脚本代码做出点击,输入,打开,验证等操作,就像真实用户所做的一样,从终端用户的角度测试应用程序。
使浏览器兼容性测试自动化成为可能,尽管在不同的浏览器上依然有细微的差别。
使用简单,可使用Java,Python等多种语言编写用例脚本
也就是说,它可以根据指令,做出像真实的人在访问浏览器一样的动作,比如打开网页,截图等功能。
(新版本的selenium已经开始弃用phantomjs, 不过有时候我们可以单独用它做一些事情)
是一个基于Webkit的无界面浏览器,可以把网站内容加载到内存中并执行页面上的各种脚本(比如js)。
proxies = {'http':'http://10.10.10.10:8765','https':'https://10.10.10.10:8765'}
resp = requests.get(url,proxies = proxies)
从start_urls里获取第一批url并发送请求,请求由引擎交给调度器入请求队列,获取完毕后,调度器将请求队列里的请求交给下载器去获取请求对应的响应资源,并将响应交给自己编写的解析方法做提取处理:1. 如果提取出需要的数据,则交给管道文件处理;2. 如果提取出url,则继续执行之前的步骤(发送url请求,并由引擎将请求交给调度器入队列…),直到请求队列里没有请求,程序结束。
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,我们只需要实现少量代码,就能够快速的抓取的数据内容。Scrapy使用了Twisted一部网络框架来处理网络通讯,可以加快我们的下载速度,不用自己去实现异步框架,并且包含了各种中间件接口,可以灵活的完成各种需求。
scrapy框架的工作流程:
1、304页面http状态码
当第二次请求页面访问的时候,该页面如果未更新,则会反馈一个304代码,而搜索引擎也会利用这个304http状态码来进行判断页面是否更新。
首先第一次肯定是要爬取网页的,假设是A.html,这个网页存储在磁盘上,相应地有个修改时间(也即是更新这个文件的时间)。
那么第二次爬取的时候,如果发现这个网页本地已经有了,例如A.html,这个时候,你只需要向服务器发送一个If-Modified-Since的请求,把A.html的修改时间带上去。
如果这段时间内,A.html更新了,也就是A.html过期了,服务器就会HTTP状态码200,并且把新的文件发送过来,这时候只要更新A.html即可。
如果这段时间内,A.html的内容没有变,服务器就会返返回HTTP状态码304(不返回文件内容),这个时候就不需要更新文件。
2、Last-Modified文件最后修改时间
这是http头部信息中的一个属性,主要是记录页面最后一次的修改时间,往往我们会发现,一些权重很高的网站,及时页面内容不更新,但是快照却还是能够每日更新,这其中就有Last-Modified的作用。通产情况下,下载网页我们使用HTTP协议,向服务器发送HEAD请求,可以得到页面的最后修改时间LastModifed,或者标签ETag。将这两个变量和上次下载记录的值的比较就可以知道一个网页是否跟新。这个策略对于静态网页是有效的。是对于绝大多数动态网页如ASP,JSP来说,LastModifed就是服务器发送Response的时间,并非网页的最后跟新时间,而Etag通常为空值。所以对于动态网页使用LastModifed和Etag来判断是不合适的,因此Last-Modified只是蜘蛛判断页面是否更新的一个参考值,而不是条件。
URL url = new URL("");
// 获得连接
URLConnection connection = url.openConnection();
connection.setRequestProperty("Referer", "http://www.xxx.com");
因为一些网站在解决盗链问题时是根据Referer的值来判断的,所以在请求头上添加Referer属性就好(可以填爬取网站的地址)。
另外Referer携带的数据 是用来告诉服务器当前请求是从哪个页面请求过来的。
分布式爬虫的部署
1.下载scrapy_redis模块包
2.打开自己的爬虫项目,找到settings文件,配置scrapy项目使用的调度器及过滤器
3.修改自己的爬虫文件
4.如果连接的有远程服务,例如MySQL,Redis等,需要将远程服务连接开启,保证在其他主机上能够成功连接
5.配置远程连接的MySQL及redis地址
6.上面的工作做完以后,开启我们的redis服务器
进入到redis文件下打开我们的cmd窗口:输入:redis-server redis.windows.conf
如果出现错误:# Creating Server TCP listening socket 127.0.0.1:6379: bind: No error
解决方法:在命令行中运行
redis-cli
127.0.0.1:6379>shutdown
not connected>exit
然后重新运行redis-server redis.windows.conf,启动成功!
7.修改redis.windows.conf配置文件,修改内容如下:
# 配置远程IP地址,供其他的电脑进行连接redis
bind: (当前电脑IP) (192.168.40.217)
# 关闭redis保护模式
protected-mode: no
8.所有爬虫都启动之后,部署redis-server服务的电脑再打开一个命令窗口,输入redis-cli.exe -h 127.0.0.1(如果是自己的ip改成自己的IP地址) -p 6379连上服务端
9.连上之后会有127.0.0.1:6379>这样的字样提示,然后输入如下命令
10.lpush 爬虫文件里面自己定义的爬虫名字:start_urls 爬虫的网址
12.数据写不进去数据库里面:
修改MySQL的my.ini文件,以MySQL8为例
路径在C:\ProgramData\MySQL\MySQL Server 8.0
找到sql-mode="STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION"这一行
把里面的STRICT_TRANS_TABLES,删除,逗号也删除,保存文件
修改过之后需要重启mysql服务
在windows命令窗口中使用net stop mysql80先停止服务,再使用net start mysql80启动服务
如果my.ini文件不修改,爬虫的数据写入不了数据库
①缓存
通过开启缓存,将每个请求缓存至本地,下次爬取时,scrapy会优先从本地缓存中获得response,这种模式下,再次请求已爬取的网页不用从网络中获得响应,所以不受带宽影响,对服务器也不会造成额外的压力,但是无法获取网页变化的内容,速度也没有第二种方式快,而且缓存的文件会占用比较大的内存,在setting.py的以下注释用于设置缓存。这种方式比较适合内存比较大的主机使用。
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
②对item实现去重
#处理书信息
def process_BookItem(self,item):
bookItemDick = dict(item)
try:
self.bookColl.insert(bookItemDick)
print("插入小说《%s》的所有信息"%item["novel_Name"])
except Exception:
print("小说《%s》已经存在"%item["novel_Name"])
#处理每个章节
def process_ChapterItem(self,item):
try:
self.contentColl.insert(dict(item))
print('插入小说《%s》的章节"%s"'%(item['novel_Name'],item['chapter_Name']))
except Exception:
print("%s存在了,跳过"%item["chapter_Name"])
def process_item(self, item, spider):
'''
if isinstance(item,ChaptersItem):
self.process_ChaptersItem(item)
'''
if isinstance(item,BookItem):
self.process_BookItem(item)
if isinstance(item,ChapterItem):
self.process_ChapterItem(item)
return item
两种方法判断mongodb中是否存在已有的数据,一是先查询后插入,二是先设置唯一索引或者主键再直接插入,由于mongodb的特点是插入块,查询慢,所以这里直接插入,需要将唯一信息设置为”_id”列,或者设置为唯一索引,在mongodb中设置方法如下
db.集合名.ensureIndex({"要设置索引的列名":1},{"unique":1})
需要用什么信息实现去重,就将什么信息设置为唯一索引即可(小说章节信息由于数据量比较大,用于查询的列最好设置索引,要不然会非常慢),这种方法对于服务器的压力太大,而且速度比较慢,我用的是第二种方法,即对已爬取的url进行去重
③对url实现去重
class UrlFilter(object):
#初始化过滤器(使用mongodb过滤)
def __init__(self):
self.settings = get_project_settings()
self.client = pymongo.MongoClient(
host = self.settings['MONGO_HOST'],
port = self.settings['MONGO_PORT'])
self.db = self.client[self.settings['MONGO_DB']]
self.bookColl = self.db[self.settings['MONGO_BOOK_COLL']]
#self.chapterColl = self.db[self.settings['MONGO_CHAPTER_COLL']]
self.contentColl = self.db[self.settings['MONGO_CONTENT_COLL']]
def process_request(self,request,spider):
if (self.bookColl.count({"novel_Url":request.url}) > 0) or (self.contentColl.count({"chapter_Url":request.url}) > 0):
return http.Response(url=request.url,body=None)
但是又会有一个问题,就是有可能下次开启时,种子url已经被爬取过了,爬虫会直接关闭,后来想到一个笨方法解决了这个问题,即在pipeline.py里的open_spider方法中再爬虫开启时删除对种子url的缓存
def open_spider(self,spider):
self.bookColl.remove({"novel_Url":"http://www.23us.so/xiaoshuo/414.html"})
通过 MD5 生成电子指纹来判断页面是否改变
nutch 去重。nutch 中 digest 是对采集的每一个网页内容的 32 位哈希值,如果两个网页内容完 全一样,它们的 digest 值肯定会一样。
数据量不大时,可以直接放在内存里面进行去重,python 可以使用 set()进行去重。当去重数据 需要持久化时可以使用 redis 的 set 数据结构。
当数据量再大一点时,可以用不同的加密算法先将长字符串压缩成 16/32/40 个字符,再使用 上面两种方法去重。
当数据量达到亿(甚至十亿、百亿)数量级时,内存有限,必须用“位”来去重,才能够满足需 求。Bloomfilter 就是将去重对象映射到几个内存“位”,通过几个位的 0/1 值来判断一个对象是 否已经存在。
然而 Bloomfilter 运行在一台机器的内存上,不方便持久化(机器 down 掉就什么都没啦),也不 方便分布式爬虫的统一去重。如果可以在 Redis 上申请内存进行
Bloomfilter,以上两个问题就都能解 决了。
simhash 最牛逼的一点就是将一个文档,最后转换成一个 64 位的字节,暂且称之为特征字,然后 判断重复只需要判断他们的特征字的距离是不是小于n(根据经验这个 n 一般取值为 3),就可以判断两个 文档是否相似。
可见 scrapy_redis 是利用 set 数据结构来去重的,去重的对象是 request 的 fingerprint(其实 就是用 hashlib.sha1()对 request 对象的某些字段信息进行压缩)。其实 fp 就是 request 对象加密 压缩后的一个字符串(40 个字符,0~f)。
# 去重源码分析
# from scrapy.core.scheduler import Scheduler
# Scheduler下:def enqueue_request(self, request)方法判断是否去重
if not request.dont_filter and self.df.request_seen(request):
Requests对象,RFPDupeFilter对象
# 如果要自己写一个去重类
-写一个类,继承BaseDupeFilter类
-重写def request_seen(self, request):
-在setting中配置:DUPEFILTER_CLASS = '项目名.dup.UrlFilter'
-增量爬取(100链接,150个链接)
-已经爬过的,放到某个位置(mysql,redis中:集合)
-如果用默认的,爬过的地址,放在内存中,只要项目一重启,就没了,它也不知道我爬过那个了,所以要自己重写去重方案
-你写的去重方案,占得内存空间更小
-bitmap方案
-BloomFilter布隆过滤器
from scrapy.http import Request
from scrapy.utils.request import request_fingerprint
# 这种网址是一个
requests1=Request(url='https://www.baidu.com?name=lqz&age=19')
requests2=Request(url='https://www.baidu.com?age=18&name=lqz')
ret1=request_fingerprint(requests1)
ret2=request_fingerprint(requests2)
print(ret1)
print(ret2)
# bitmap去重 一个小格表示一个连接地址 32个连接,一个比特位来存一个地址
# https://www.baidu.com?age=18&name=lqz ---》44
# https://www.baidu.com?age=19&name=lqz ---》89
# c2c73dfccf73bf175b903c82b06a31bc7831b545假设它占4个bytes,4*8=32个比特位
# 存一个地址,占32个比特位
# 10个地址,占320个比特位
#计算机计量单位
# 比特位:只能存0和1
def request_seen(self, request):
# 把request对象传入request_fingerprint得到一个值:aefasdfeasd
# 把request对象,唯一生成一个字符串
fp = self.request_fingerprint(request)
#判断fp,是否在集合中,在集合中,表示已经爬过,return True,他就不会再爬了
if fp in self.fingerprints:
return True
# 如果不在集合中,放到集合中
self.fingerprints.add(fp)
if self.file:
self.file.write(fp + os.linesep)
传统定义:分布式存储系统是大量 PC 服务器通过 Internet 互联,对外提供一个整体的服务。
分布式存储系统具有以下的几个特性:
可扩展 :分布式存储系统可以扩展到几百台甚至几千台这样的一个集群规模,系统的 整体性能线性增长。
低成本 :分布式存储系统的自动容错、自动负载均衡的特性,允许分布式存储系统可 以构建在低成本的服务器上。另外,线性的扩展能力也使得增加、减少服务器的成本低, 实现分布式存储系统的自动运维。
高性能 :无论是针对单台服务器,还是针对整个分布式的存储集群,都要求分布式存 储系统具备高性能。
易用 :分布式存储系统需要对外提供方便易用的接口,另外,也需要具备完善的监 控、运维工具,并且可以方便的与其他的系统进行集成。
布式存储系统的挑战主要在于数据和状态信息的持久化,要求在自动迁移、自动容 错和并发读写的过程中,保证数据的一致性。
容错:如何可以快速检测到服务器故障,并自动的将在故障服务器上的数据进行迁移
负载均衡:新增的服务器如何在集群中保障负载均衡?数据迁移过程中如何保障不影 响现有的服务。
事务与并发控制:如何实现分布式事务。
易用性:如何设计对外接口,使得设计的系统易于使用。