python爬虫框架scrapy使用

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架,用于抓取web站点并从页面中提取结构化的数据。可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。

其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services) 或者通用的网络爬虫。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。

Scrapy是一个基于Twisted,纯Python实现的爬虫框架,使用Twisted异步网络库来处理网络通讯,架构清晰,并且包含了各种中间件接口,可以灵活的完成各种需求。整体架构大致如下:

scrapy架构图

绿线是数据流向,首先从初始URL开始,Scheduler会将其交给Downloader进行下载,下载之后会交给Spider进行分析Spider分析出来的结果有两种:一种是需要进一步抓取的链接,例如之前分析的“下一页”的链接,这些东西会被传回 Scheduler;另一种是需要保存的数据,它们则被送到Item Pipeline那里,那是对数据进行后期处理(详细分析、过滤、存储等)的地方。另外,在数据流动的通道里还可以安装各种中间件,进行必要的处理。

创建一个爬取工程

在开始爬取之前,您必须创建一个新的Scrapy项目。 进入您打算存储代码的目录中,运行下列命令:

scrapy startproject fund

该命令将会创建包含下列内容的 fund 目录:

fund/
    scrapy.cfg              # 项目的配置文件
    fund/                   # 该项目的python模块
        __init__.py
        items.py            # 相当于实体类
        pipelines.py        # 对Spider返回的item列表进行后续操作:过滤,保存等
        settings.py            # 配置文件
        spiders/             # 爬取类文件夹
            __init__.py
            ...                # 这儿主要是爬取类

数据结构定义Item类

Item 是保存爬取到的数据的容器;其使用方法和python字典类似,并且提供了额外保护机制来避免拼写错误导致的未定义字段错误。

# -*- coding: utf-8 -*-    
import scrapy

class FundItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    product_name = scrapy.Field()        # 基金名称
    product_code = scrapy.Field()        # 基金代码

    product_type = scrapy.Field()        # 基金类型
    venture_grade = scrapy.Field()        # 风险等级
    setup_day = scrapy.Field()            # 成立日期
    product_scale = scrapy.Field()        # 基金规模

    product_company = scrapy.Field()    # 基金公司
    fund_manager = scrapy.Field()        # 基金经理

    year_1 = scrapy.Field()                # 一年 本基金/同类基金
    year_2 = scrapy.Field()                # 两年 本基金/同类基金

    increase_last_year = scrapy.Field() # 去年的涨幅
    increase_year_before_last = scrapy.Field()    # 前年的涨幅

爬虫爬取Spider类

Spider 是用户编写的类, 用于从一个域(或域组)中抓取信息, 定义了用于下载的URL的初步列表, 如何跟踪链接,以及如何来解析这些网页的内容用于提取items。

要建立一个 Spider,继承 scrapy.Spider 基类,并确定三个主要的、强制的属性:

  • name:爬虫的识别名,它必须是唯一的,在不同的爬虫中你必须定义不同的名字.
  • start_urls:包含了Spider在启动时进行爬取的url列表。因此,第一个被获取到的页面将是其中之一。后续的URL则从初始的URL获取到的数据中提取。我们可以利用正则表达式定义和过滤需要进行跟进的链接。
  • parse():是spider的一个方法。被调用时,每个初始URL完成下载后生成的 Response 对象将会作为唯一的参数传递给该函数。该方法负责解析返回的数据(response data),提取数据(生成item)以及生成需要进一步处理的URL的 Request 对象。

parse()是scrapy默认的解析html的回调方法,当然可以指定这儿的方法名并自己实现,负责解析返回的数据、匹配抓取的数据(解析为item)并跟踪更多的URL。

使用Item返回抓取的值

# -*- coding: utf-8 -*-

import scrapy
from scrapy.selector import Selector
from scrapy.contrib.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from fund.items import FundItem

class FundSpider(CrawlSpider):
    start_urls = []
    for i in range(1,19):
        start_urls.append("https://list.lu.com/list/fund?subType=&haitongGrade=4&fundGroupId=&currentPage="+str(i)+"&orderType=this_year_increase_desc&searchWord=#sortTab")

    # 定义spider名字的字符串(string),一般取值为站点的域名,如xiaoyu.com -> xiaoyu
    name = "fund"

    #設置延時
    download_delay = 1

    # 包含了spider允许爬取的域名(domain)列表(list)
    # 当 OffsiteMiddleware 启用时, 域名不在列表中的URL不会被跟进。
    allowed_domains = ["list.lu.com"]

    # 当没有制定特定的URL时,spider将从该列表中开始进行爬取
    # start_urls = ["https://list.lu.com/list/fund?subType=&haitongGrade=&fundGroupId=&currentPage=1&orderType=one_month_increase_desc&searchWord=#sortTab"]

    rules = (
        #将所有符合正则表达式的url加入到抓取列表中
        Rule(LinkExtractor(allow=(r"https://list.lu.com/list/fund?subType=&haitongGrade=&fundGroupId=&currentPage=\d+&orderType=one_month_increase_desc&searchWord=#sortTab",))),
        #将所有符合正则表达式的url请求后下载网页代码, 形成response后调用自定义回调函数
        Rule(LinkExtractor(allow=(r"\S+productDetail\S+",)),callback="parse_item"),
    )

    def parse_item(self,response):
        #self.log("======>>>>>>:" + response.url)
        sel = Selector(response)

        item = FundItem()

        # 通过css来获取值
        item['product_name'] = sel.css('div[class*=product-name]::text').extract()[0].strip().encode('utf8')
        item['product_code'] = sel.css('div[class*=product-code]::text').extract()[0].strip().encode('gbk').split(":")[1].decode("gbk").encode("utf8")

        # 通过xpath来获取值,也可以使用正则表达式来获取值
        item['product_type'] = sel.xpath('//ul[@class="fund-info clearfix"]/li[4]/b/text()').extract()[0].strip().encode('utf8')
        item['venture_grade'] = sel.xpath('//div[@class="venture-grade"][1]/span/text()').extract()[0].strip().encode("utf8")
        item['setup_day'] = sel.xpath('//ul[@class="fund-info clearfix"]/li[8]/b/text()').extract()[0].strip().encode('utf8')
        item['product_scale'] = sel.xpath('//ul[@class="fund-info clearfix"]/li[10]/b/text()').extract()[0].strip().encode('gbk')[:-4].decode("gbk").encode("utf8")

        item['product_company'] = sel.xpath('//ul[@class="fund-info clearfix"]/li[3]/b/text()').extract()[0].strip().encode('utf8')
        item['fund_manager'] = sel.xpath('//p[@class="manager-icon"]/span/text()').extract()[0].strip().encode('utf8')

        # 计算每年的成长与同类比较值
        self1 = sel.xpath('//table[@class="product-table phase-increase-table"]/tbody/tr[1]/td[5]/span/text()').extract()[0].strip().encode('gbk')[:-1].decode("gbk").encode("utf8")
        self2 = sel.xpath('//table[@class="product-table phase-increase-table"]/tbody/tr[1]/td[6]/span/text()').extract()[0].strip().encode('gbk')[:-1].decode("gbk").encode("utf8")

        other1 = sel.xpath('//table[@class="product-table phase-increase-table"]/tbody/tr[2]/td[5]/span/text()').extract()[0].strip().encode('gbk')[:-1].decode("gbk").encode("utf8")
        other2 = sel.xpath('//table[@class="product-table phase-increase-table"]/tbody/tr[2]/td[6]/span/text()').extract()[0].strip().encode('gbk')[:-1].decode("gbk").encode("utf8")

        item["year_1"] = float(self1) / float(other1)
        item["year_2"] = float(self2) / float(other2)

        item["increase_last_year"] = float(self1)
        item["increase_year_before_last"] = float(self2) - float(self1)

        return item

使用ItemLoader返回抓取的值

# -*- coding: utf-8 -*-

import scrapy
from scrapy.spider import Spider
from scrapy.selector import Selector
from scrapy.contrib.loader import ItemLoader
from lu.items import LuItem

class LuSpider(Spider):

    # 定义spider名字的字符串(string),一般取值为站点的域名,如xiaoyu.com -> xiaoyu
    name = "lu"

    #設置延時
    download_delay = 2

    # 包含了spider允许爬取的域名(domain)列表(list)
    # 当 OffsiteMiddleware 启用时, 域名不在列表中的URL不会被跟进。
    allowed_domains = ["lu.com"]

    # 当没有制定特定的URL时,spider将从该列表中开始进行爬取
    start_urls = ["https://list.lu.com/list/fund"]
    #start_urls = ["https://list.lu.com/list/productDetail?productId=2278857"]

    # 当response没有指定回调函数时,parse方法是Scrapy处理下载的response的默认方法
    # 负责处理response并返回处理的数据以及(/或)跟进的URL
    def parse(self,response):
        self.log(">>>>>>:" + response.url) # 将抓取的URL保存到log文件中

        ''' 将抓取到的页面保存到文件中
        filename = response.url.split("=")[1] + ".html" #基金的代码作为名称
        with open(filename, 'wb') as f:
            f.write(response.body)
        '''

        sel = Selector(response)

        for url in sel.css('a[class*=project-name]::attr("href")').extract():
            self.log("======>>>>>>:" + url)
            newUrl = "https://list.lu.com" + url
            yield scrapy.Request(newUrl, callback=self.parseItem)

    def parseItem(self,response):
        #self.log(".........:" + response.url) # 将抓取的URL保存到log文件中    

        l = ItemLoader(item=LuItem(),response=response)
        # 通过css来获取值
        l.add_css("product_name",'div[class*=product-name]::text')
        l.add_css("product_code",'div[class*=product-code]::text')

        # 通过xpath来获取值,也可以使用正则表达式来获取值
        l.add_xpath("product_type",'//ul[@class="fund-info clearfix"]/li[4]/b/text()')
        l.add_xpath("venture_grade",'//div[@class="venture-grade"][1]/span/text()')
        l.add_xpath("setup_day",'//ul[@class="fund-info clearfix"]/li[8]/b/text()')
        l.add_xpath("product_scale",'//ul[@class="fund-info clearfix"]/li[10]/b/text()')

        l.add_xpath("haitong_grade",'//table[@class="grade-table"]/tbody/tr[1]/td[2]/i/@class')
        l.add_xpath("shangzheng_grade",'//table[@class="grade-table"]/tbody/tr[2]/td[2]/i/@class')
        l.add_xpath("yinghe_grade",'//table[@class="grade-table"]/tbody/tr[3]/td[2]/i/@class')

        # 直接赋值
        sel = Selector(response)
        procuct_company = sel.xpath('//ul[@class="fund-info clearfix"]/li[3]/b/text()').extract()
        fund_manager = sel.xpath('//p[@class="manager-icon"]/span/text()').extract()

        l.add_value("procuct_company",procuct_company)
        l.add_value("fund_manager",fund_manager)

        return l.load_item()

        #sel = Selector(response)
        # css选择器 获取div中的class为product-name的值,序列化之后取出list的第一个值,
        # 并进行去空格操作,然后进gbk解码,从而获取到基金名称
        #print sel.css('div[class*=product-name]::text').extract()[0].strip().encode('gbk')
        #print sel.css('div[class*=product-code]::text').extract()[0].strip().encode('gbk')

pipelines类

当Item在Spider中被收集之后,它将会被传递到Item Pipeline,一些组件会按照一定的顺序执行对Item的处理。
这儿可以做如下处理:清理HTML数据验证爬取的数据(检查item包含某些字段),查重(并丢弃),保存数据到数据库等操作
这儿主要实行process_item()方法,主要是这儿进行数据的处理!

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html

import MySQLdb

# 数据库链接方法
def ConnectDB():
    conn = MySQLdb.connect(host='localhost',user='root',passwd='xiaode',db='spider',port=3306,charset='utf8')
    return conn

class FundPipeline(object):
    def process_item(self, item, spider):
        #for key in item:
            #print key,":",item[key]

        # 链接数据库 并存入数据库中
        conn = ConnectDB()
        cur = conn.cursor()

        cur.execute("select product_code from fund where product_code = %s",(item['product_code'],))
        result = cur.fetchone()

        if result is None:
            cur.execute("insert into fund (product_code,product_name,product_type,venture_grade,setup_day,product_scale,product_company,fund_manager,year_1,year_2,increase_last_year,increase_year_before_last) \
                values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",(item['product_code'],item['product_name'],item['product_type'],item['venture_grade'],item['setup_day'],str(item['product_scale']),item['product_company'],item['fund_manager'],str(item['year_1']),str(item['year_2']),str(item["increase_last_year"]),str(item["increase_year_before_last"])))
            print str(item['product_code'])+"记录插入"

        else:
            cur.execute("update fund set product_name=%s,product_type=%s,venture_grade=%s,setup_day=%s,product_scale=%s,product_company=%s,fund_manager=%s,year_1=%s,year_2=%s,increase_last_year=%s,increase_year_before_last=%s where \
                product_code = %s",(item['product_name'],item['product_type'],item['venture_grade'],item['setup_day'],str(item['product_scale']),item['product_company'],item['fund_manager'],str(item['year_1']),str(item['year_2']),str(item['increase_last_year']),str(item['increase_year_before_last']),item['product_code']))
            print str(item['product_code'])+"记录更新"

        conn.commit()
        conn.close()    

        return item

项目配置文件

Scrapy设定(settings)提供了定制Scrapy组件的方法。您可以控制包括核心(core),插件(extension),pipeline及spider组件。

# -*- coding: utf-8 -*-

# Scrapy settings for fund project
#
# For simplicity, this file contains only the most important settings by
# default. All the other settings are documented here:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#

BOT_NAME = 'fund'

SPIDER_MODULES = ['fund.spiders']
NEWSPIDER_MODULE = 'fund.spiders'

# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'fund (+http://www.yourdomain.com)'

# 编写完pipeline后,为了能够启动它,必须将其加入到ITEM_PIPLINES配置
ITEM_PIPELINES = {
    'fund.pipelines.FundPipeline': 1, #后面数值越小,执行的优先级越大
}

运行项目

使用如下的命令运行项目进行爬取:

scrapy crawl fund

这样就可以爬取网站并保存数据到数据库中,保存后的数据截图如下:
爬取的基金截图

参考

Scrapy 0.25 文档