python网络爬虫——下载缓存(磁盘)

磁盘缓存

为链接爬虫添加缓存支持
将之前的download函数重构为一个类,将限速功能放到下载函数中,只有在真正的下载时在会触发限速,而在加载时不会触发。

#DownLoad.py
import random
import re
import socket
import urllib2
import urlparse
import time
import datetime


#设置代理服务器
DEFAULT_AGENT='wswp'
#时延变量
DEFAULT_DELAY=5
#爬取的深度
DEFAULT_RETRIES=1
#超时时间
DEFAULT_TIMEOUT=60





class Downloader:
    """一个关于下载网页的类"""

    def __init__(self,delay=DEFAULT_DELAY,user_agent=DEFAULT_AGENT,
                 opener=None,proxies=None,num_retries=DEFAULT_RETRIES,
                 timeout=DEFAULT_TIMEOUT,cache=None):
        #如果超时,远程主机强迫关闭了一个现有的连接
        socket.setdefaulttimeout(timeout)
        #实例化一个时延对象
        self.throttle=Throttle(delay)
        #代理服务器
        self.user_agent=user_agent
        self.proxies=proxies
        #设置重试的最大次数
        self.num_retries=num_retries
        #缓存
        self.cache=cache
        self.opener=opener

    # 在对象作为函数被调用的时候会调用这个方法
    def __call__(self,url):
        result=None
        #检查是否有缓存
        if self.cache:
            # 检查是否缓存了该URL
            try:
                # 从cache中加载数据
                result = self.cache[url]
            # 检查报错,如果url没有就什么都不做
            except KeyError:
                pass
            else:
                # 如果以上检测都正确
                # 判断重试的次数
                # 判断服务器报错,即报错码以5开头,
                # 就使result为空并且重新下载
                if self.num_retries > 0 and 500 <= result['code'] < 600:
                    result = None


            # 如果result没有定义,就直接下载
            if result is None:
                # 在下载之前,加上时延,保证安全下载
                self.throttle.wait(url)
                proxy = random.choice(self.proxies) if self.proxies else None
                headers = {'User-agent': self.user_agent}
                result = self.download(url, headers, proxy, self.num_retries)
                if self.cache:
                    # 向cache中保存结果
                    self.cache[url] = result






            return result['html']

    def download(self,url,headers, proxy, num_retries,data=None):
        print 'Downloading:', url
        request = urllib2.Request(url,data,headers or {})

        opener = self.opener or  urllib2.build_opener()
        if opener:
            proxy_params = {urlparse.urlparse(url).scheme: proxy}
            opener.add_handler(urllib2.ProxyHandler(proxy_params))

        try:
            response=urllib2.urlopen(request)
            html=response.read()

            code=response.code
        except Exception as e:
            print 'Download:', str(e)
            html = None
            if hasattr(e, 'code'):
                if num_retries > 0 and 500 <= e.code < 600:
                    return self.download(url,headers,proxy, num_retries - 1,data)
            else:
                code=None
        return {'html':html,'code':code}



# Throttle类记录了每个域名上次访问的时间,如果当前时间距离上次
# 访问的时间小于延迟,则执行睡眠操作。
class Throttle:

    def __init__(self,delay):
        self.delay=delay
        self.domains={}

    def wait(self ,url):
        #urlparse.urlparse将url地址拆成六个部分,netloc为域名服务器,也可能包含用户信息
        domain=urlparse.urlparse(url).netloc
        last_accessed=self.domains.get(domain)

        if self.delay >0 and last_accessed is not None:
            #判断时延时间是否大于现在时间-上一次访问的时间
            sleep_secs=self.delay-(datetime.datetime.now()-last_accessed).seconds
            if sleep_secs>0:
                time.sleep(sleep_secs)

        #更新上一次访问时间
        self.domains[domain]=datetime.datetime.now()

获取中网页链接

import re
import robotparser
import urlparse

from DownLoad import Downloader


#获取网页中的链接
def link_crawler(seed_url,link_regex=None,max_urls=50,max_depth=1,delay=5,user_agent='wswp',proxies=None,num_retries=1,scrape_callback=None,cache=None):

    #存储获取到的url的queue
    craw_queue=[seed_url]
    #记录url以及这个url的深度的字典
    seen={seed_url:0}
    #下载了url的个数
    num_url=0

    #判断该网站是否可以爬取
    rp=get_robots(seed_url)
    D=Downloader(delay=delay,user_agent=user_agent,proxies=proxies,num_retries=num_retries,cache=cache)

    while craw_queue:
        #弹出队列顶的url
        url=craw_queue.pop()
        #获取这个url的深度
        depth=seen[url]

        #检查这个网页是否可以爬取
        if rp.can_fetch(user_agent,url):
            html=D(url)
            links=[]

            if scrape_callback:
                links.extend(scrape_callback(url,html) or [])

            #判断当前爬取的url的深度是否为规定的最大深度
            if depth != max_depth:
                #是否存在要匹配的正则格式
                if link_regex:
                    #将匹配到的链接放到links里
                    links.extend(link for link in get_links(html) if re.search(link_regex,link))
                for link in links:
                    link=normalize(seed_url,link)
                    #判断是否已经爬取过该链接
                    if link not in seen:
                        #将该链接的深度加1,以便后面做判断
                        seen[link]=depth+1
                        #判断该链接与首链接域名是否一致,
                        if same_damain(seed_url,link):
                            craw_queue.append(link)
            #如果爬取链接的数量达到最大值就退出
            num_url+=1
            if num_url==max_urls:
                break
        else:
            print 'Bilocked by robots.txt',url



def get_robots(url):
    #解析robots.txt文件,判断该网站是否可以爬取
    rp=robotparser.RobotFileParser()
    rp.set_url(urlparse.urljoin(url,'/robot.txt'))
    rp.read()
    return rp


#获取网页中所有链接,并返回一个列表
def get_links(html):
    webpage_regx=re.compile('<a[^>]+href=["\'](.*?)["\']',re.IGNORECASE)
    link_list1=webpage_regx.findall(html)
    link_list2=[]
    for link in link_list1:
        if re.search('/view/',link):
            link_list2.append(link)
    return link_list1


#去掉链接的标示符,并且加上绝对路径
def normalize(seed_url,link):

    #如果url包含一个片段标识符,则返回一个没有片段标识符
    # 的修改过的url,并且这个片段标识符作为单独的字符串。
    link,_=urlparse.urldefrag(link)
    return urlparse.urljoin(seed_url,link)

#判断目前爬取的链接和第一个链接域名是否则一致
def same_damain(url1,url2):

    return urlparse.urlparse(url1).netloc==urlparse.urlparse(url2).netloc

磁盘缓存
将下载到的网页存到文件系统中,需要将URL安全地映射为跨平台文件名.

操作系统文件系统非法文件名字符文件名最大长度
LinuxExt3/Ext4/和\0255字节
OS XHFSN Plus:和\0255个UTF-16编码字节
WindowsNTFS,/,?,*,",<,>,和’

为了保证在不同的文件系统文件路径式安全的,就需要限制其只能包含数字,字母和基本符号,并将其他字符替换为下划线.

>>> import re
>>> url='http://example.webscraping.com'
>>> re.sub('[^/0-9a-zA-Z\-.,;_]','_',url)
'http_//example.webscraping.com'

限制文件名以及其符目录的长度在255个字符内

filename='/'.join(segment[:255] for segment in filename.split('/'))

如果URL路径以/结尾,此时斜杠后的空字符串就会成为一个非法文件名,但是如果移除又会造成无法保存其他URL的问题.

>>> import urlparse
]>>> components=urlparse.urlsplit('http://example.webscraping.com/index/')
>>> print components
SplitResult(scheme='http', netloc='example.webscraping.com', path='/index/', query='', fragment='')

使用分隔模块对上述边界情况添加index.htnl示例代码

>>> path=components.path
>>> if not path:
...     path='/index.html'
... elif path.endswith('/'):
...     path+='index.html'
>>> filename=components.netloc+path+components.query
>>> filename
'example.webscraping.com/index/'

实现

import re
import urlparse
from datetime import timedelta
import datetime

# datetime.timedelata两个时间之间的时间差
import os

import zlib

import pickle

import shutil

from Link_crawler import link_crawler


class DiskCache:

    def __init__(self,cache_dir='cache',expires=timedelta(days=30),compress=True):

        self.cache_dir=cache_dir
        #有效日期
        self.expires=expires
        #是否压缩
        self.compress=compress

    def __getitem__(self, url):
        #从磁盘中加载数据

        #获取这个url在文件系统中的绝对路径
        path=self.url_to_path(url)

        #如果这个文件路径存在就读取这个文件的数据
        if os.path.exists(path):
            with open(path,'rb') as fp:
                data=fp.read()
                if self.compress:
                    # zlib.compress用于压缩流数据。参数string指定了要压缩的数据流,
                    # 参数level指定了压缩的级别,它的取值范围是1到9。压缩速度与压缩率成反比,
                    # 1表示压缩速度最快,而压缩率最低,而9则表示压缩速度最慢但压缩率最高。
                    data=zlib.decompress(data)
                # 把变量从内存中变成可存储或传输,必填参数data必须以二进制可读模式打开
                result,timestamp=pickle.loads(data)

                #判断文件是否过期
                if self.has_expired(timestamp):
                    raise KeyError(url+'has expired')

                #返回读取到的数据
                return result

    def __setitem__(self, url,result):
        """将url读取的数据保存到磁盘"""

        #创建这个url的文件在系统中的路径
        path=self.url_to_path(url)
        #获取文件的目录名
        folder=os.path.dirname(path)
        if not os.path.exists(folder):
            #如果目录名不存在则创建
            os.makedirs(folder)
        #将数据和当前时间,返回一个字符串,它包含一个 pickle 格式的对象
        data=pickle.dumps((result,datetime.datetime.utcnow()))
        if self.compress:
            data=zlib.compress(data)

        #将压缩后的数据以二进制形式写入文件
        with open(path,'wb') as fp:
            fp.write(data)

    def __delitem__(self, url):

        path=self.url_to_path(url)
        try:
            #删除该url的文件以及目录
            os.remove(path)
            os.removedirs(os.path.dirname(path))
        except OSError:
            pass



    def url_to_path(self,url):
        #给url创建文件系统的路径

        #分解url为六个部分
        components=urlparse.urlsplit(url)
        #获取url的目录路径名
        path=components.path

        #判断文件路径是否为空
        if not path:
            path='/index.html'
        #判断是否以/结尾,以避免成为非法文件名
        elif path.endswith('/'):
            path+='index.html'

        #合成系统文件名
        #netloc:服务器的位置
        #path:网页文件在服务器中存放的位置
        #query:连接符(&)连接键值对
        filename=components.netloc+path+components.query
        #保证文件名的可移植性
        filename=re.sub('[^/0-9a-zA-Z\-.,;_]','_',filename)
        #文件名及父目录不超过255个字节
        filename='/'.join(segment[:255] for segment in filename.split('/'))

        #添加文件名为绝对路径
        return os.path.join(self.cache_dir,filename)

#
    def has_expired(self,timestamp):
        #判断文件有效期
        #datetime.datetime.utcnow代表目前的时刻(世界时间)
        #文件的创建时间+有效日期是否小于当前时刻
        return datetime.datetime.utcnow() > timestamp +self.expires


    def clear(self):
        #清除缓存

        if os.path.exists(self.cache_dir):
            shutil.rmtree(self.cache_dir)

if __name__=='__main__':
    import time
    start=time.time()
    link_crawler('http://example.webscraping.com/', '/view', cache=DiskCache())
    print "runing:%s" %(time.time()-start)

第一次爬取
这里写图片描述
第二次爬取
这里写图片描述
从运行时间和运行结果来看,第二次没有去下载,而是从磁盘重读取出来,可以看出效率提高了许多.
缺点

  • 受限于本地文件系统的限制。
  • 一些URl会被映射为相同的文件名。
  • 如果文件数量过多的话,会很难实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值