爬虫笔记(11)性能问题

作者: 无事扯淡 | 来源:发表于2017-02-03 08:23 被阅读530次

    1.

    过年也没啥事干,继续捣鼓爬虫。开始是准备爬豆瓣电影的,豆瓣存在一些反爬机制,爬一会就爬不动了。当然后面是突破了这个限制,使用随机bid,设置cookie。据说会出现验证码,我爬了几万部电影也没有出现这个问题。初期的想法是使用代理ip,网络上的免费代理ip大都不靠谱,懒得捣鼓。
    在豆瓣电影这个爬虫中,我其实是使用两个步骤来执行。第一部分是按照年标签查找电影,从1900到2017年,将每个电影链接存入到mongodb中,然后进行去重(手动)。

    class DoubanMovies(scrapy.Spider):
        name = "douban"
        prefix = 'https://movie.douban.com/tag/'
        start_urls = []
        for i in range(1900,2018):
            start_urls.append(prefix+str(i))
    
        def parse(self,response):
            names = response.xpath('//div[@class="pl2"]/a/text()').extract()
            names = [name.strip('\n/ ') for name in names]
            names = [name for name in names if len(name)>0] #去掉空名字
            movie_urls = response.xpath('//div[@class="pl2"]/a/@href').extract()
            hrefs = response.xpath('//div[@class="paginator"]/a/@href').extract()#获取分页链接
            for href in hrefs:
               yield scrapy.Request(href, callback=self.parse)
            for i in range(len(names)):
                yield {'name':names[i],'url':movie_urls[i]}
    
    

    关于mongodb去重的问题,我使用的临时表。主要是我对mongodb确实不熟悉,而且我对JavaScript这样的语法着实不感冒。下面的代码很简单,每个电影链接就是https://movie.douban.com/subject/26685451/ ,我这里特地把这中间的数字提取出来,然后进行对比,这个肯定是唯一的。distinct会把获取的数据重复的进行合并,这在sql中也有这功能。

    nums = movies.distinct("number")#我把链接中的数字提取了出来
    m = db.movies1
    for num in mums:
      movie = movies.find_one({"number":num})
      m.insert_one(movie)
    moives.drop()#删除原来的数据表
    m.rename('movie')#把新表命名
    

    还有个问题就是针对douban的时间限制,需要使用DOWNLOAD_DELAY设置时间间隔,当然我使用bid突破了这个限制。
    下面是第二个爬虫的代码,这个代码就是访问每个电影页面提取相关数据,然后存到mongodb中。数据提取没什么难度,为了快速判断xpath有效,最好开一个scrapy shell进行测试。

    class DoubanMovies(scrapy.Spider):
        name = "doubansubject"
        def start_requests(self):
            MONGO_URI = 'mongodb://localhost/'
            client = MongoClient(MONGO_URI)
            db = client.douban
            movie = db.movie
            cursor = movie.find({})
            urls = [c['url'] for c in cursor]
            for url in urls:
                bid = "".join(random.sample(string.ascii_letters + string.digits, 11))
                yield scrapy.Request(url,callback=self.parse,cookies={"bid":bid})
    
    def parse(self,response):
    
        title = response.xpath('//span[@property="v:itemreviewed"]/text()').extract_first()
        year = response.xpath('//span[@class="year"]/text()').extract_first()#(2016)
        pattern_y = re.compile(r'[0-9]+')
        year = pattern_y.findall(year)
        if len(year)>0:
            year = year[0]
        else:
            year = ""
    
        directors = response.xpath('//a[@rel="v:directedBy"]/text()').extract()#导演?有没有可能有多个导演呢
        '''
        评分人数
        '''
        votes= response.xpath('//span[@property="v:votes"]/text()').extract_first()#评分人数
        '''
        分数
        '''
        score = response.xpath('//strong[@property="v:average"]/text()').extract_first()#抓取分数
        #编剧不好找等会弄
        '''
        演员
        '''
        actors = response.xpath('//a[@rel="v:starring"]/text()').extract()#演员
        genres = response.xpath('//span[@property="v:genre"]/text()').extract()#电影类型
        html = response.body.decode('utf-8')
        pattern_zp = re.compile(r'<span class="pl">制片国家/地区:</span>(.*)<br/>')
        nations = pattern_zp.findall(html)
        if len(nations)>0 :
            nations = nations[0]
            nations = nations.split('/')
            nations = [n.strip() for n in nations]
        '''
        多个国家之间以/分开,前后可能出现空格也要删除
        '''
        pattern_bj = re.compile(r"<span ><span class='pl'>编剧</span>: <span class='attrs'>(.*)</span></span><br/>")
        bj_as = pattern_bj.findall(html)
        '''
        bj_as 内容是
        [<a>编剧</a>,<a></a>,<a></a>,<a></a>,]
        需要进一步提取
        '''
        p = re.compile(r'>(.*)<')
        bj = [p.findall(bj) for bj in bj_as]
        '''
        p.findall也会产生数组,需要去掉括号,只有有数据才能去掉
        '''
        bj = [b[0].strip() for b in bj if len(b)>0]#编剧的最终结果
    
        '''
        语言
        <span class="pl">语言:</span> 英语 / 捷克语 / 乌克兰语 / 法语<br/>
        '''
        pattern_lang = re.compile(r'<span class="pl">语言:</span>(.*)<br/>')
        langs = pattern_lang.findall(html)
        if len(langs)>0:
            langs = langs[0]
            langs = langs.split('/')
            langs = [l.strip() for l in langs]
        runtime = response.xpath('//span[@property="v:runtime"]/@content').extract_first()
        '''
        上映日期也有多个
        '''
        releasedates = response.xpath('//span[@property="v:initialReleaseDate"]/text()').extract()
        '''
        标签
        '''
        tags = response.xpath('//div[@class="tags-body"]/a/text()').extract()
    
        ##这里不能用return
        yield {"title":title,"year":year,"directors":directors,"score":score,"votes":votes,
        "actors":actors,"genres":genres,"nations":nations,"bj":bj,"langs":langs,"runtime":runtime,
        "releasedates":releasedates,"url":response.url
        }
    

    上面的代码确实能正常工作,但是有个缺点就是太慢,不到五万个页面就要几个小时,显然瓶颈在分析这一块。性能问题会在下面一个例子中讨论。

    2

    性能问题确实是个大问题,在满足能爬取的情况下,速度要优化。这几天抓取一个AV网站,没错AV网站的种子文件。先抓取文章列表,再抓取每个详细页面,访问种子下载页面,最后下载里面的种子文件。

    • 方法一
      这个代码很简单使用的是requests来下载文件,里面的下载功能代码就是从requests教程中拷贝出来的。
    def process_item(self, item, spider):
            try:
                bt_urls = item['bt_urls']
                if not os.path.exists(self.dir_path):
                    os.makedirs(self.dir_path)
                '''
                检查文件夹是否存在这段代码应该放到open_spider中去才是合适的,启动检查一下后面就不管了
                '''
                for url in bt_urls:
                    response = requests.get(url,stream=True)
                    attachment = response.headers['Content-Disposition']
                    pattern = re.compile(r'filename="(.+)"')
                    filename = pattern.findall(attachment)[0]
                    filepath = '%s/%s' % (self.dir_path,filename)
                    with open(filepath, 'wb') as handle:
                        #response = requests.get(image_url, stream=True)
                        for block in response.iter_content(1024):
                            if not block:
                                break
                            handle.write(block)
                    '''
                    整个代码肯定会严重影响爬虫的运行速度,是否考虑多进程方法
                    '''
            except Exception as e:
                print(e)
            return item
    

    bt_url种子的链接就放在bt_urls,由于是从下载页面返回的item,实际中最多只有一个链接。这个代码运行起来没什么问题,但是速度相当慢。scrapy使用的是异步网络框架,但是requests是实实在在的同步方法,单线程的情况下必然影响到整个系统的执行。必须要突破这个瓶颈,实际中要先考虑代码能正确运行再考虑其它方面。

    • 方法二
      既然在本线程中直接下载会造成线程阻塞,那开启一个新的进程如何。
    class DownloadBittorrent2(object):
    
        def __init__(self, dir_path):
            self.dir_path = dir_path
            # self.mongo_db = mongo_db
    
        @classmethod
        def from_crawler(cls, crawler):
            return cls(
                dir_path =crawler.settings.get('DIR_PATH'),
            )
    
        def open_spider(self, spider):
            if not os.path.exists(self.dir_path):
                os.makedirs(self.dir_path)
    
        def close_spider(self, spider):
            pass
    
        def downloadprocess(self,url):
            try:
                response = requests.get(url,stream=True)
                attachment = response.headers['Content-Disposition']
                pattern = re.compile(r'filename="(.+)"')
                filename = pattern.findall(str(attachment))[0]#这里attachment是bytes必须要转化
                filepath = '%s/%s' % (self.dir_path,filename)
                with open(filepath, 'wb') as handle:
                    #response = requests.get(image_url, stream=True)
                    for block in response.iter_content(1024):
                        if not block:
                            break
                        handle.write(block)
            except Exception as e:
                print(e)
        def process_item(self, item, spider):
    
            bt_urls = item['bt_urls']
            if len(bt_urls)>0:#最多只有一个url
                p = Process(target=self.downloadprocess,args=(bt_urls[0],))
                p.start()
    
            return item
    

    这个代码也能正常工作,但是报错,直接导致服务器挂了。
    HTTPConnectionPool(host='taohuabbs.info', port=80): Max retries exceeded with url: /forum.php?mod=attachment&aid=MjAxNzk 0fDA0OTkxZjM0fDE0ODU4NjY0OTZ8MHwxMzMzNDA= (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConne ction object at 0x00000000041F9048>: Failed to establish a new connection: [WinError 10060]由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。',))
    这个可能跟设置的延迟有关系(我就没有设置延迟),反正就是把服务器弄死了。还有就是requests在这种异常情况下容错能力有问题。

    • 方法三
      既然scrapy自带了一个Filespipeline,那么是不是可以考虑用这个来下载呢!可以试试!
    class DownloadBittorrent3(FilesPipeline):
        def get_media_requests(self, item, info):
            for file_url in item['bt_urls']:
                yield scrapy.Request(file_url)
    

    代码报错了,原因是文件名打不开。这个就涉及到如何命名下载文件名的问题。如果链接中带*.jpg这样类似的名字,程序不会有问题,如果不是会怎么样,链接中可能出现操作系统不允许在文件名中出现的字符,这就会报错。我对系统自带的这个pipeline了解甚少,就没有继续研究。
    还有一点我希望文件名来自于服务器的反馈,对于下载文件服务器可能会把文件名发过来,这个就在headers的Content-Disposition字段中。也就是是说我必须要先访问网络之后才能确定文件名。

    • 方法四
      前面我们都使用了pipeline来处理,实际上我们完全可以不用pipeline而直接在spider中处理。
        def download(self,response):
            '''
            在爬取过程中发现有可能返回不是torrent文件,这时候要考虑容错性问题,虽然爬虫并不会挂掉
            '''
            attachment = response.headers['Content-Disposition']
            pattern = re.compile(r'filename="(.+)"')
            filename = pattern.findall(attachment.decode('utf-8'))[0]
            filepath = '%s/%s' % (self.settings['DIR_PATH'],filename)
            with open(filepath, 'wb') as handle:
                handle.write(response.body)
    

    这种方法性能不错,对比前面50/min速度,这个可以达到100/min。其实我们可以更快。

    3

    在实际的下载中,我们要充分利用scrapy的网络下载框架,这个性能好容错性高,而且也好排错。上面的10060错误,我估计放在http中可能就是503(服务器无法到达)。
    前面的方法都在单线程中运作,虽然后面有多进程版的下载代码,由于没有scrapy稳定所以我考虑用多个爬虫来实现。如果启动两个scrapy爬虫,一个负责爬页面,一个负责下载,这样的效率应该会高不少。虽然前面的笔记中有提到相关代码,使用redis来实现分布式。当然在单机上称不上分布式,但是使用redis作为进程间通讯手段确实极好的,不管是多进程还是分布式都能非常高效的工作。github上有基于redis版本的scrapy,这里我的想法是第一个爬虫负责爬页面的属于一般爬虫(使用原版的scrapy),而第二个爬虫使用基于redis的爬虫。

    • 1 scrapy-redis安装
      pip install scrapy-redis
      安装方法倒是很简单,但是这个代码比较旧了,版本是0.6.3,这个版本在python3.5上工作不正常(出错跟转码有关str,具体情况不懂),处理的办法就是把0.6.7的代码下载下来直接覆盖就可以了(反正我也看不懂代码,覆盖了能工作)。
    • 2 配置
      scrapy-redis的配置还是在settings中,参考文档
      文档中有几个必须配置的参数:
      SCHEDULER = "scrapy_redis.scheduler.Scheduler"
      DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
      后面还可以配置redis服务器端口号,还有redis服务器地址。
      REDIS_START_URLS_BATCH_SIZE
      上面的参数对代表每次redis从redis服务器中获取的链接数量,这个调高可能会增加性能。
    • 3 页面爬虫
    class BtSpiderEx(scrapy.Spider):
        name = 'btspiderex'
        start_urls = ['http://taohuabbs.info/forum-181-1.html']
        def parse(self,response):
            urls = response.xpath('//a[@onclick="atarget(this)"]/@href').extract()
            for url in urls:
                yield scrapy.Request(response.urljoin(url),callback=self.parsedetail)
    
            page_urls = response.xpath('//div[@class="pg"]/a/@href').extract()
            for url in page_urls:
                yield scrapy.Request(response.urljoin(url),callback=self.parse)
    
        def parsedetail(self,response):
            hrefs = response.xpath('//p[@class="attnm"]/a/@href').extract()
            for h in hrefs:
                yield scrapy.Request(response.urljoin(h),callback=self.parsedown)
    
        def parsedown(self,response):
            '''
            其实每次只能分析出一个bt链接
            '''
            bt_urls = response.xpath('//div[@style="padding-left:10px;"]/a/@href').extract()
            yield {'bt_urls':bt_urls}
    

    页面爬虫代码其实相对于前面的实现,变得更加简单,这里把将下载链接推送到redis服务器的任务交给pipeline。

    class DownloadBittorrent(object):
        def __init__(self, dir_path):
            self.dir_path = dir_path
        @classmethod
        def from_crawler(cls, crawler):
            return cls(
                dir_path =crawler.settings.get('DIR_PATH'),
            )
        def open_spider(self, spider):
            if not os.path.exists(self.dir_path):
                os.makedirs(self.dir_path)
            self.conn = redis.Redis(port=6666)
        def close_spider(self,spdier):
            pass
        def process_item(self, item, spider):
    
            bt_urls = item['bt_urls']
            for url in bt_urls:
                self.conn.lpush('redisspider:start_urls',url)
            return item
    

    open_spider在爬虫启动的时候启动,这里就可以打开redis和建立下载文件夹。redisspider:start_urls这个是redis队列名,缺省情况下scrapy-redis爬虫的队列就是爬虫名+start_urls。

    • 4 下载爬虫
      下载爬虫只负责从redis获取链接然后下载。
    from scrapy_redis.spiders import RedisSpider
    import re
    class DistributeSpider(RedisSpider):
        name = 'redisspider'
        def parse(self,response):
            DIR_PATH = "D:/bt"
            if 'Content-Disposition' in response.headers:
                attachment = response.headers['Content-Disposition']
                pattern = re.compile(r'filename="(.+)"')
                filename = pattern.findall(attachment.decode('utf-8'))[0]
                filepath = '%s/%s' % (DIR_PATH,filename)#DIR_PATH = "D:/bt"
                with open(filepath, 'wb') as handle:
                    handle.write(response.body)
    

    settings.py配置,只列出了主要参数,这里修改了默认端口号:

    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    REDIS_PORT = 6666
    

    相关文章

      网友评论

        本文标题:爬虫笔记(11)性能问题

        本文链接:https://www.haomeiwen.com/subject/bfxrittx.html