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'制片国家/地区:(.*)
') 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"编剧: (.*)
") bj_as = pattern_bj.findall(html) ''' bj_as 内容是 [编剧,,,,] 需要进一步提取 ''' p = re.compile(r'>(.*)0]#编剧的最终结果 ''' 语言 语言: 英语 / 捷克语 / 乌克兰语 / 法语
''' pattern_lang = re.compile(r'语言:(.*)
') 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(': 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)性能问题

发表评论