zoukankan      html  css  js  c++  java
  • Scrapy爬虫笔记

    Scrapy是一个优秀的Python爬虫框架,可以很方便的爬取web站点的信息供我们分析和挖掘,在这记录下最近使用的一些心得。

    1.安装

    通过pip或者easy_install安装:
    1
    
    sudo pip install scrapy

    2.创建爬虫项目

    1
    
    scrapy startproject youProjectName

    3.抓取数据

    首先在items.py里定义要抓取的内容,以豆瓣美女为例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    from scrapy.item import Field,Item
    
    class DoubanmeinvItem(Item):
        feedId = Field()         #feedId
        userId = Field()         #用户id
        createOn = Field()       #创建时间
        title = Field()          #feedTitle
        thumbUrl = Field()       #feed缩略图url
        href = Field()           #feed链接
        description = Field()    #feed简介
        pics = Field()           #feed的图片列表
        userInfo = Field()       #用户信息
    
    class UserItem(Item):
        userId = Field()         #用户id
        name = Field()           #用户name
        avatar = Field()         #用户头像
    创建爬虫文件,cd到工程文件夹下后输入命令:
    1
    
    scrapy crawl XXX(爬虫名字)

    另外可以在该爬虫项目的根目录创建一个main.py,然后在pycharm设置下运行路径

    那么就不用每次都运行上面那行代码,直接运行main.py就能启动爬虫了

    输入代码:

    from scrapy import cmdline
    cmdline.execute('scrapy crawl amazon_products -o items.csv -t csv'.split())
    #-o 代表输出文件 -t 代表文件格式
    

      

    接着编辑爬虫文件,实例如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    
    # -*- coding: utf-8 -*-
    import scrapy
    import re
    from DoubanMeinv.items import DoubanmeinvItem,UserItem
    import json
    import time
    from datetime import datetime
    from scrapy.exceptions import CloseSpider
    
    import sys
    reload(sys)
    sys.setdefaultencoding('utf8')
    
    class DbmeinvSpider(scrapy.Spider):
        name = "dbMeinv"
        allowed_domains = ["www.dbmeinv.com"]
        start_urls = (
            'http://www.dbmeinv.com/dbgroup/rank.htm?pager_offset=1',
        )
        baseUrl = 'http://www.dbmeinv.com'
        close_down = False
    
        def parse(self, response):
            request = scrapy.Request(response.url,callback=self.parsePageContent)
            yield request
    
        #解析每一页的列表
        def parsePageContent(self, response):
            for sel in response.xpath('//div[@id="main"]//li[@class="span3"]'):
                item = DoubanmeinvItem()
                title = sel.xpath('.//div[@class="bottombar"]//a[1]/text()').extract()[0]
                #用strip()方法过滤开头的
    	和空格符
                item['title'] = title.strip()
                item['thumbUrl'] = sel.xpath('.//div[@class="img_single"]//img/@src').extract()[0]
                href = sel.xpath('.//div[@class="img_single"]/a/@href').extract()[0]
                item['href'] = href
                #正则解析id
                pattern = re.compile("dbgroup/(d*)")
                res = pattern.search(href).groups()
                item['feedId'] = res[0]
                #跳转到详情页面
                request = scrapy.Request(href,callback=self.parseMeinvDetailInfo)
                request.meta['item'] = item
                yield request
            #判断是否超过限制应该停止
            if(self.close_down == True):
                print "数据重复,close spider"
                raise CloseSpider(reason = "reach max limit")
            else:
                #获取下一页并加载
                next_link = response.xpath('//div[@class="clearfix"]//li[@class="next next_page"]/a/@href')
                if(next_link):
                    url = next_link.extract()[0]
                    link = self.baseUrl + url
                    yield scrapy.Request(link,callback=self.parsePageContent)
    
        #解析详情页面
        def parseMeinvDetailInfo(self, response):
            item = response.meta['item']
            description = response.xpath('//div[@class="panel-body markdown"]/p[1]/text()')
            if(description):
                item['description'] = description.extract()[0]
            else:
                item['description'] = ''
            #上传时间
            createOn = response.xpath('//div[@class="info"]/abbr/@title').extract()[0]
            format = "%Y-%m-%d %H:%M:%S.%f"
            t = datetime.strptime(createOn,format)
            timestamp = int(time.mktime(t.timetuple()))
            item['createOn'] = timestamp
            #用户信息
            user = UserItem()
            avatar = response.xpath('//div[@class="user-card"]/div[@class="pic"]/img/@src').extract()[0]
            name = response.xpath('//div[@class="user-card"]/div[@class="info"]//li[@class="name"]/text()').extract()[0]
            home = response.xpath('//div[@class="user-card"]/div[@class="opt"]/a[@target="_users"]/@href').extract()[0]
            user['avatar'] = avatar
            user['name'] = name
            #正则解析id
            pattern = re.compile("/users/(d*)")
            res = pattern.search(home).groups()
            user['userId'] = res[0]
            item['userId'] = res[0]
            #将item关联user
            item['userInfo'] = user
            #解析链接
            pics = []
            links = response.xpath('//div[@class="panel-body markdown"]/div[@class="topic-figure cc"]')
            if(links):
                for a in links:
                    img = a.xpath('./img/@src')
                    if(img):
                        picUrl = img.extract()[0]
                        pics.append(picUrl)
            #转成json字符串保存
            item['pics'] = json.dumps(list(pics))
            yield item
    需要说明的几点内容:
    • allowed_domin指定Spider在哪个网站爬取数据
    • start_urls包含了Spider在启动时进行爬取的url列表
    • parse方法继承自父类,每个初始URL完成下载后生成的Response对象将会作为唯一的参数传递给该函数。该方法负责解析返回的数据(response),提取数据(生成item)以及生成需要进一步处理的URL的Request对象
    • xpath解析数据的时候使用(也可以使用css),关于xpath和css的详细用法请自行搜索
    • xpath从某个子元素里解析数据时要使用element.xpath('./***')而不能使用element.xpath('/***'),否则是从最外层解析而不是从element下开始解析
    • web站点爬取的text经常包含了我们不想要的 或者是空格等字符,这个时候就要使用Python的strip()方法来过滤掉这些数据
    • 抓取的web页面时间经常是2015-10-1 12:00:00格式,但是我们存储到数据库时要想转成timeStamp的格式,这里用Python的time相关类库来处理,代码见上面
    • 抓取完某个页面的时候,可能我们还需要抓取跟它相关的详情页面数据,这里用生成Scrapy.Request的方式来继续抓取,并且将当前的item存储到新的request的meta数据中以供后面的代码中读取到已抓取的item
    • 如果我们想要在某些情况下停止Spider的抓取,在这里设置一个flag位,并在适当的地方抛出一个CloseSpider的异常来停止爬虫,后面会接着提到这个技巧

    4.运行爬虫

    1
    
    scrapy crawl youSpiderName

    5.编写Pipeline

    如果我们要将数据存储到MySQL数据库中,需要安装MySQLdb,安装过程很多坑,遇到了再Google解决吧。一切搞定之后开始编写pipelines.py和settings.py文件
    首先在settings.py文件中定义好连接MySQL数据库的所需信息,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    DB_SERVER = 'MySQLdb'
    DB_CONNECT = {
        'host' : 'localhost',
        'user' : 'root',
        'passwd' : '',
        'port' : 3306,
        'db' :'dbMeizi',
        'charset' : 'utf8',
        'use_unicode' : True
    }
    然后编辑pipelines.py文件,添加代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    
    from scrapy.conf import settings
    from scrapy.exceptions import DropItem
    from twisted.enterprise import adbapi
    import json
    
    class DoubanmeinvPipeline(object):
        #插入的sql语句
        feed_key = ['feedId','userId','createOn','title','thumbUrl','href','description','pics']
        user_key = ['userId','name','avatar']
        insertFeed_sql = '''insert into MeiziFeed (%s) values (%s)'''
        insertUser_sql = '''insert into MeiziUser (%s) values (%s)'''
        feed_query_sql = "select * from MeiziFeed where feedId = %s"
        user_query_sql = "select * from MeiziUser where userId = %s"
        feed_seen_sql = "select feedId from MeiziFeed"
        user_seen_sql = "select userId from MeiziUser"
        max_dropcount = 50
        current_dropcount = 0
    
        def __init__(self):
            dbargs = settings.get('DB_CONNECT')
            db_server = settings.get('DB_SERVER')
            dbpool = adbapi.ConnectionPool(db_server,**dbargs)
            self.dbpool = dbpool
            #更新看过的id列表
            d = self.dbpool.runInteraction(self.update_feed_seen_ids)
            d.addErrback(self._database_error)
            u = self.dbpool.runInteraction(self.update_user_seen_ids)
            u.addErrback(self._database_error)
    
        def __del__(self):
            self.dbpool.close()
    
        #更新feed已录入的id列表
        def update_feed_seen_ids(self, tx):
            tx.execute(self.feed_seen_sql)
            result = tx.fetchall()
            if result:
                #id[0]是因为result的子项是tuple类型
                self.feed_ids_seen = set([int(id[0]) for id in result])
            else:
                #设置已查看过的id列表
                self.feed_ids_seen = set()
    
        #更新user已录入的id列表
        def update_user_seen_ids(self, tx):
            tx.execute(self.user_seen_sql)
            result = tx.fetchall()
            if result:
                #id[0]是因为result的子项是tuple类型
                self.user_ids_seen = set([int(id[0]) for id in result])
            else:
                #设置已查看过的id列表
                self.user_ids_seen = set()
    
        #处理每个item并返回
        def process_item(self, item, spider):
            query = self.dbpool.runInteraction(self._conditional_insert, item)
            query.addErrback(self._database_error, item)
    
            feedId = item['feedId']
            if(int(feedId) in self.feed_ids_seen):
                self.current_dropcount += 1
                if(self.current_dropcount >= self.max_dropcount):
                    spider.close_down = True
                raise DropItem("重复的数据:%s" % item['feedId'])
            else:
                return item
    
        #插入数据
        def _conditional_insert(self, tx, item):
            #插入Feed
            tx.execute(self.feed_query_sql, (item['feedId']))
            result = tx.fetchone()
            if result == None:
                self.insert_data(item,self.insertFeed_sql,self.feed_key)
            else:
                print "该feed已存在数据库中:%s" % item['feedId']
            #添加进seen列表中
            feedId = item['feedId']
            if int(feedId) not in self.feed_ids_seen:
                self.feed_ids_seen.add(int(feedId))
            #插入User
            user = item['userInfo']
            tx.execute(self.user_query_sql, (user['userId']))
            user_result = tx.fetchone()
            if user_result == None:
                self.insert_data(user,self.insertUser_sql,self.user_key)
            else:
                print "该用户已存在数据库:%s" % user['userId']
            #添加进seen列表中
            userId = user['userId']
            if int(userId) not in self.user_ids_seen:
                self.user_ids_seen.add(int(userId))
    
        #插入数据到数据库中
        def insert_data(self, item, insert, sql_key):
            fields = u','.join(sql_key)
            qm = u','.join([u'%s'] * len(sql_key))
            sql = insert % (fields,qm)
            data = [item[k] for k in sql_key]
            return self.dbpool.runOperation(sql,data)
    
        #数据库错误
        def _database_error(self, e):
            print "Database error: ", e
    说明几点内容:
    • process_item:每个item通过pipeline组件都需要调用该方法,这个方法必须返回一个Item对象,或者抛出DropItem异常,被丢弃的item将不会被之后的pipeline组件所处理。
    • 已经抓取到的数据不应该再处理,这里创建了两个ids_seen方法来保存已抓取的id数据,如果已存在就Drop掉item
    • 如果重复抓取的数据过多时,这里设置了个上限值(50),如果超过了上限值就改变spider的关闭flag标志位,然后spider判断flag值在适当的时候抛出CloseSpider异常,关闭Spider代码见爬虫文件。这里通过设置flag标志位的方式来关闭爬虫主要是因为我测试的时候发现在pipelines中调用停止爬虫的方法都不起效果,故改成这种方式
    • 因为Scrapy是基于twisted的,所以这里用adbapi来连接并操作MySQL数据库
    最后在settings.py文件中启用pipeline
    1
    2
    3
    4
    
    ITEM_PIPELINES = {
       'DoubanMeinv.pipelines.DoubanmeinvPipeline': 300,
       # 'DoubanMeinv.pipelines.ImageCachePipeline': 500,
    }

    6.变换User-Agent,避免爬虫被ban

    我们抓取的网站可能会检查User-Agent,所以为了爬虫正常运行我们需要设置请求的User-Agent。对于频繁的请求,还要对User-Agent做随机变换以防被ban,这里通过设置Downloader Middleware来修改爬虫的request和respons
    在setting.py文件中添加User-Agent列表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    DOWNLOADER_MIDDLEWARES = {
       'DoubanMeinv.middlewares.RandomUserAgent': 1,
    }
    
    USER_AGENTS = [
        "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
        "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
        "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
        "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
    ]
    修改middlewares.py文件添加如下代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    import random
    
    class RandomUserAgent(object):
        def __init__(self, agents):
            self.agents = agents
    
        @classmethod
        def from_crawler(cls, crawler):
            return cls(crawler.settings.getlist('USER_AGENTS'))
    
        def process_request(self, request, spider):
            request.headers.setdefault('User-Agent', random.choice(self.agents))
    

    7.禁用Cookie+设置请求延迟

    某些网站可能会根据cookie来分析爬取的轨迹,为了被ban,我们最好也禁用掉cookie;同时为了避免请求太频繁而造成爬虫被ban,我们还需要设置请求间隔时间,在settings.py文件中添加以下代码:
    1
    2
    
    DOWNLOAD_DELAY=1
    COOKIES_ENABLED=False

    8.抓取图片并保存到本地

    有时候我们想把抓取到的图片直接下载并保存到本地,可以用Scrapy内置的ImagesPipeline来处理,因为ImagesPipeline用到了PIL这个图片处理模块,所以我们首先需要使用pip来安装Pillow
    安装成功后,在pipelines.py代码中添加以下代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    from scrapy.pipelines.images import ImagesPipeline
    from scrapy import Request
    import json
    
    class ImageCachePipeline(ImagesPipeline):
        def get_media_requests(self, item, info):
            pics = item['pics']
            list = json.loads(pics)
            for image_url in list:
                yield Request(image_url)
    
        def item_completed(self, results, item, info):
            image_paths=[x['path'] for ok,x in results if ok]
            if not image_paths:
                print "图片未下载好:%s" % image_paths
                raise DropItem('图片未下载好 %s'%image_paths)
    ImagesPipeline类有一个get_media_requests方法来进行下载的控制,所以我们在这里解析imgUrl并发起进行一个Request,在下载完成之后,会把结果传递到item_completed方法,包括 下载是否成功( True or False) 以及下载下来保存的路径和下载的路径,这里改写这个方法让他把下载失败的(Flase)的图片的路径输出出来
    接下来在settings.py里设置下载图片的文件目录并启用ImageCachePipeline
    1
    2
    3
    4
    5
    6
    7
    8
    
    #设置图片保存到本地的地址和过期时间
    IMAGES_STORE='/Users/chen/Pictures/Meizi'
    IMAGES_EXPIRES = 90
    
    ITEM_PIPELINES = {
       'DoubanMeinv.pipelines.DoubanmeinvPipeline': 300,
       'DoubanMeinv.pipelines.ImageCachePipeline': 500,
    }
    等待爬虫执行完之后去IMAGES_STORE路径下查看图片就是了

    9.自动运行爬虫

    为了源源不断获取数据,可通过命令让爬虫每天都运行来抓取数据
    1
    2
    3
    4
    
    // 为当前用户新增任务
    crontab -e
    // 增加如下记录 注意替换自己的爬虫目录 由于环境变量的原因,scrapy要给出全路径
    0 10 * * * cd /home/chen/pyWork/DoubanMeinvScrapy && /usr/bin/scrapy crawl dbmeinv
    上面的命令添加了一个任务,这个任务会每天早上10:00启动,这个任务要做得就是进入爬虫目录,并启动爬虫。
    如果你不知道自己的scrapy的全路径,可以用终端下用which scrapy来查看

    最后秀一下抓取到的数据:

  • 相关阅读:
    阿里Java开发规约【摘录】
    JavaWeb【八、JSP指令与动作元素】
    JavaWeb【七、JSP状态管理】
    JavaWeb【六、JavaBean】
    JavaWeb【五、内置对象】
    JavaWeb【四、JSP基础语法】
    JavaWeb【三、Web程序编写】
    JavaWeb【二、Tomcat安装】
    Django 模板层
    Django auth模块
  • 原文地址:https://www.cnblogs.com/alan-babyblog/p/5527934.html
Copyright © 2011-2022 走看看