Scrapy爬虫笔记

栏目: 编程工具 · 发布时间: 6年前

内容简介:Scrapy是一个优秀的Python爬虫框架,可以很方便的爬取web站点的信息供我们分析和挖掘,在这记录下最近使用的一些心得。通过pip或者easy_install安装:首先在items.py里定义要抓取的内容,以豆瓣美女为例:

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

1.安装

通过pip或者easy_install安装:

sudo pip install scrapy

2.创建爬虫项目

scrapy startproject youProjectName

3.抓取数据

首先在items.py里定义要抓取的内容,以豆瓣美女为例:

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到工程文件夹下后输入命令:

scrapy genspider dbMeinv dbmeinv.com

接着编辑爬虫文件,实例如下:

# -*- 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()方法过滤开头的\r\n\t和空格符
            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经常包含了我们不想要的\r\n\t或者是空格等字符,这个时候就要使用Python的 strip() 方法来过滤掉这些数据
  • 抓取的web页面时间经常是2015-10-1 12:00:00格式,但是我们存储到数据库时要想转成timeStamp的格式,这里用Python的time相关类库来处理,代码见上面
  • 抓取完某个页面的时候,可能我们还需要抓取跟它相关的详情页面数据,这里用生成 Scrapy.Request 的方式来继续抓取,并且将当前的item存储到新的request的meta数据中以供后面的代码中读取到已抓取的item
  • 如果我们想要在某些情况下停止Spider的抓取,在这里设置一个flag位,并在适当的地方抛出一个 CloseSpider 的异常来停止爬虫,后面会接着提到这个技巧

4.运行爬虫

scrapy crawl youSpiderName

5.编写Pipeline

如果我们要将数据存储到 MySQL 数据库中,需要安装MySQLdb,安装过程很多坑,遇到了再Google解决吧。一切搞定之后开始编写pipelines.py和settings.py文件

首先在settings.py文件中定义好连接MySQL数据库的所需信息,如下所示:

DB_SERVER = 'MySQLdb'
DB_CONNECT = {
    'host' : 'localhost',
    'user' : 'root',
    'passwd' : '',
    'port' : 3306,
    'db' :'dbMeizi',
    'charset' : 'utf8',
    'use_unicode' : True
}

然后编辑pipelines.py文件,添加代码如下:

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
CloseSpider

最后在settings.py文件中启用pipeline

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列表

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文件添加如下代码:

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文件中添加以下代码:

DOWNLOAD_DELAY=1
COOKIES_ENABLED=False

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

有时候我们想把抓取到的图片直接下载并保存到本地,可以用Scrapy内置的 ImagesPipeline 来处理,因为 ImagesPipeline 用到了PIL这个图片处理模块,所以我们首先需要使用pip来安装 Pillow
安装成功后,在pipelines.py代码中添加以下代码:

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

#设置图片保存到本地的地址和过期时间
IMAGES_STORE='/Users/chen/Pictures/Meizi'
IMAGES_EXPIRES = 90

ITEM_PIPELINES = {
   'DoubanMeinv.pipelines.DoubanmeinvPipeline': 300,
   'DoubanMeinv.pipelines.ImageCachePipeline': 500,
}

等待爬虫执行完之后去IMAGES_STORE路径下查看图片就是了

9.自动运行爬虫

为了源源不断获取数据,可通过命令让爬虫每天都运行来抓取数据

// 为当前用户新增任务
crontab -e
// 增加如下记录 注意替换自己的爬虫目录 由于环境变量的原因,scrapy要给出全路径
0 10 * * * cd /home/chen/pyWork/DoubanMeinvScrapy && /usr/bin/scrapy crawl dbmeinv

上面的命令添加了一个任务,这个任务会每天早上10:00启动,这个任务要做得就是进入爬虫目录,并启动爬虫。

如果你不知道自己的scrapy的全路径,可以用终端下用 which scrapy 来查看

总结:

以上的内容只是自己在使用Scrapy过程中的一些心得体会,算是简单入门Scrapy了,接下来还得了解分布式爬取以及一些更高级的技巧,以后有新的经验了再更新。Enjoy it!


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python Cookbook 中文版,第 3 版

Python Cookbook 中文版,第 3 版

David M. Beazley、Brian K. Jones / 陈舸 / 人民邮电出版社 / 2015-5-1 / 108.00元

《Python Cookbook(第3版)中文版》介绍了Python应用在各个领域中的一些使用技巧和方法,其主题涵盖了数据结构和算法,字符串和文本,数字、日期和时间,迭代器和生成器,文件和I/O,数据编码与处理,函数,类与对象,元编程,模块和包,网络和Web编程,并发,实用脚本和系统管理,测试、调试以及异常,C语言扩展等。 本书覆盖了Python应用中的很多常见问题,并提出了通用的解决方案。......一起来看看 《Python Cookbook 中文版,第 3 版》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具