几大主流文件系统的限制
操作系统 | 文件系统 | 非法文件名字符 | 文件名最大长度 |
Linux | Ext3/Ext4 | / and \0 | 255字节 |
OS X | HFS Plus | : and \0 | 255个UTF-16编码单元 |
Windows | NTFS | \、/、?、:、*、"、>、< and | | 255个字符 |
为了保证在不同的文件系统中,我们的文件路径都是安全的,就需要限制其只能包含数字、字母和基本符号,并将其他字符替换为下划线。代码如下:
>>>import re
>>>url='http://...'
>>>re.sub('[^/0-9a-zA-Z\-.,;_]','_'.url)
'http_//...'
此外,文件名及其父目录的长度需要限制在255个字符内,代码如下:
>>>filename='/'.join(segment[:255] for segment in filename.split('/'))
还有一种边界情况需要考虑,那就是URL路径可能会以斜杠 (/) 结尾,此时斜杠后面的空字符串就会成为一个非法的文件名。但是,如果移除这个 斜杠,使用其父字符串作为文件名,又会造成无法保存其他URL 的问题。考虑下面这两个URL:
http://example.webscraping.com/index/
http://example.webscraping. com/index/1
如果我们希望这两个URL都能保存下来,就需要以 index 作为目录名,以1作为子路径。 对于像第一个URL 路径这样以斜杠结尾的情况, 这里使用 的解决方案是添加index.html 作为其文件名。同样地,当URL路径为空时也进行相同的操作。为了解析URL, 我们需要使用 urlparse.urlsplit () 函数,将URL分割成几个部分。
>>>import urlparse
>>>components=urlparse.urlsplit('http://example.webscraping.com/index/')
>>>print components
SplitResult(scheme='http',netloc='example.webscraping.com',path='/index/',query='',fragment='')
>>>print components.path
'/index'
该函数提供了解析和处理URL 的便捷接口。下面是使用该模块对上述边界情况添加 index.html 的示例代码。
>>>path=components.path
>>>if not path:
>>>path='/index.html'
>>>elif path.endwith('/'):
>>>path+='index.html'
>>>filename=components.netloc+path+components.query
>>>filename
'example.webscraping.com/index/index.html')
实现
把URL到文件名的这些映射逻辑结合起来,就形成了磁盘缓存的主要部分。下面是DiskCache类的初始实现代码。
import os
import re
import urlparse
class DiskCache:
def __init__(self,cache_dir='cache'):
self.cache_dir=cache_dir
self.max_length=max_length
def url_to_path(self,url):
#给这个URL创造文件路径
components=urlparse.urlsplit(url)
#增加index.html给这个空路径
path=components.path
if not path:
path='/index.html'
elif path.endwith('/')
path+='index.html'
filename=components.netloc+path+components.query
#替换非法字符
filename=re.sub('[^/0-9a-zA-Z\-.,;_]','_',filename)
#限制最大字符数
filename='/'.join(segment[:255] for segment in filename.split('/'))
return os.path.join(self.cache_dir,filename)
def __getitem__(self,url):
#从磁盘中下载数据在这个链接中
path=self.url_to_path(url)
if os.path.exists(path)
with open(path,'rb') as fp:
return pickle.load(fp)
else:
#链接没有被存储
raise KeyError(url+'does not exist')
def __setitem__(self,url,result):
#在这个链接中保存数据到磁盘中
path=self.url_to_path(url)
folder=os.path.dirname(path)
if not os.path.exists(folder):
os.makedirs(folder)
with open(path,'wb') as fp:
fp.write(pickle.dumps(result))
在上面的代码中,构造方法传入了一个用于设定缓存位置的参数,然后在url_to_path方法中应用了文件名的限制。
在__setitem__()中,我们使用url_to_path()方法将URL映射为安全文件名,在必要的情况下还需要创建父目录。这里使用的 pickle 模块会把输入转化为字符串,然后保存到磁盘中。 而在__getitem__()方法中,首先将URL 映射为安全文件名。然后,如果文件存在,则加载其内容,并执行反序列化, 恢复其原始数据类型: 如果文件不存在,则说明缓存中还没有该 URL 的数据,此时会抛出 KeyError异常。
缓存测试
通过爬虫传递cache回调,来检验DiskCache类。
完整代码需要上外网,所以暂时贴不上来。
节省磁盘空间
为了最小化缓存所需的磁盘空间,我们可以对下载得到的HTML文件进行压缩处理。处理方法,只需保存到磁盘之前使用zlib压缩序列化字符串即可:
fp.write(zlib.compress(pickle.dumps(read)))
压缩磁盘空间,会略微增加项目速度。
清理过期数据
当前版本的磁盘缓存使用键值对的形式在磁盘上保存缓存,未来无论何时请求都会返回结果。对于缓存网页而言, 该功能可能不太理想, 因为网页内容随时都有可能发生变化, 存储在缓存中的数据存在过期风险。 接下来,我们将为缓存数据添加过期时间,以便爬虫知道何时需要重新下载网页。
import os
import re
import urlparse
from datetime import datetime,timedelta
class DiskCache:
def __init__(self,cache_dir='cache',expires=timedelta(days=30)):
self.cache_dir=cache_dir
self.max_length=max_length
self.expires=expires
def url_to_path(self,url):
#给这个URL创造文件路径
components=urlparse.urlsplit(url)
#增加index.html给这个空路径
path=components.path
if not path:
path='/index.html'
elif path.endwith('/')
path+='index.html'
filename=components.netloc+path+components.query
#替换非法字符
filename=re.sub('[^/0-9a-zA-Z\-.,;_]','_',filename)
#限制最大字符数
filename='/'.join(segment[:255] for segment in filename.split('/'))
return os.path.join(self.cache_dir,filename)
def __getitem__(self,url):
#从磁盘中下载数据在这个链接中
path=self.url_to_path(url)
if os.path.exists(path)
with open(path,'rb') as fp:
result,timestamp=pickle.loads(zlib.decompress(fp.read()))
if self.has_expired(timestamp):
raise KeyError(url+'has expired')
return result
else:
#链接没有被存储
raise KeyError(url+'does not exist')
def __setitem__(self,url,result):
#在这个链接中保存数据到磁盘中
path=self.url_to_path(url)
folder=os.path.dirname(path)
if not os.path.exists(folder):
os.makedirs(folder)
timestamp=datetime.utcnow()
data=pickle.dumps((result,timestamp))
with open(path,'wb') as fp:
fp.write(zlib.compress(data))
def has_expired(self,timestamp):
#返回这个时间戳是否过期
return datetime.utcnow()>timemap+self.expires
在构造方法中,我们使用 timedelta对象将默认过期时间设置为30天。 然后,在__set__一方法中,把当前时间戳保存到序列化数据中:而在__get__方法中,对比当前时间和缓存时间,检查是否过期。 为了测试过期时间功能,我们可以将其缩短为5秒
缓存结果最初是可用的,经过5s的睡眠之后,再次调用同一URL。则会抛出KeyError异常,也就是缓存下载失效了。
缺点
磁盘的缓存系统比较容易实现,无需安装其他模块,并且在文件管理器中就能查看结果。
但是它受制于本地文件系统的限制。比如,为了将URL映射为安全文件名,应用了多种限制,但是一些URL会被映射成为相同的文件名。在对下面几个URL进行字符替换之后,就会得到相同的文件名。
http://example.com/?a+b
http://example.com/?a*b
http://example.com/?a=b
http://example.com/?a!b
这就意味着,如果其中一个 URL 生成了缓存, 其他3个URL也会被认为已经生成缓存, 因为它们映射到了同一个文件名。 另外, 如果一些长URL 只 在255 个字符之后存在区别,截断后的版本也会被映射为相同的文件名。 这个问题非常重要, 因为URL的最大长度并没有明确限制。 尽管在实践中URL很少会超过2000个字符, 并且早期版本的 IE浏览器也不支持超过2083 个字符的URL。
避免这些限制的一种解决方案是使用 URL 的哈希值作为文件名。尽管该方法可以带来一定改善,但是最终还是会面临许多文件系统具有的一个关键问题, 那就是每个卷和每个目录下的文件数量是有限制的。 如果缓存存储在 FAT32文件系统中,每个目录的最大文件数是65535。 该限制可以通过将缓存分割到不同目录来避免, 但是文件系统可存储的文件总数也是有限制的。 我使用的 ext4 分区目前支持略多于1500万个文件, 而一个大型网站往往拥有超过1 亿个网页。 很遗憾, DiskCache 方法想要通用的话存在太多限制。要想避免这些问题, 我们需要把多个缓存网页合并到一个文件中, 并使用类似 B+树的算法进行索引。我们并不会自己实现这种算法的数据库。
为了避免磁盘缓存方案的己知限制,下面我们会在现有数据库系统之上创建缓存。 爬取时, 我们可能需要缓存大量数据,但又无须任何复杂的连接操作, 因此我们将选用 NoSQL数据库, 这种数据库比传统的关系型数据库更易于扩展。在本节中, 我们将会选用目前非常流行的 MongoDB 作为缓存数据库。(真的是对这个数据库一点儿都不了解,咨询了同事,他说这个数据很简单!所以打算简单的了解一下,不过,数据库缓存可以用到其他的数据库吗?菜鸟教程里关于python有关于MySQL的数据库,所以我觉得这个数据库也是可以使用的吧)。
NoSQL
NoSQL全称Not Only SQL,是一种相对较新的数据库设计方式。 传统的关系模型使用的是固定模式, 并将数据分割到各个表中 。然而, 对于大数据集的情况, 数据量太大使其难以存放在单一服务器中, 此时就需要扩展到多台服务器。不过,关系模型对于这种扩展的支持并不够好,因为在查询多个表时,数据可能在不同的服务器中。相反,NoSQL 数据库通常是无模式的,从设计之初就考虑了跨服务器无缝分片的问题。 在 NoSQL 中,有多种方式可 以实现该目标,分别是列数据存储 (如 HBase)、键值对存储 (如Redis)、面向文档的数据库 (如 MongoDB)以及图形数据库(如Neo4j)。
安装MongoDB
这一部分暂时先跳过,我会把MongoDB这个数据库搞清楚后来写。