Python学习笔记之Scrapy爬虫

Scrapy架构图(绿线是数据流向)

Python学习笔记之Scrapy爬虫

  • Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯,信号、数据传递等。

  • Scheduler(调度器): 它负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。

  • Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,

  • Spider(爬虫):它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器).

  • Item Pipeline(管道):它负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤、存储等)的地方。

  • Downloader Middlewares(下载中间件):你可以当作是一个可以自定义扩展下载功能的组件。

  • Spider Middlewares(Spider中间件):你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)

Scrapy的运作流程

代码写好,程序开始运行...

  • 1 引擎:Hi!Spider, 你要处理哪一个网站?
  • 2 Spider:老大要我处理xxxx.com。
  • 3 引擎:你把第一个需要处理的URL给我吧。
  • 4 Spider:给你,第一个URL是xxxxxxx.com。
  • 5 引擎:Hi!调度器,我这有request请求你帮我排序入队一下。
  • 6 调度器:好的,正在处理你等一下。
  • 7 引擎:Hi!调度器,把你处理好的request请求给我。
  • 8 调度器:给你,这是我处理好的request
  • 9 引擎:Hi!下载器,你按照老大的下载中间件的设置帮我下载一下这个request请求
  • 10 下载器:好的!给你,这是下载好的东西。(如果失败:sorry,这个request下载失败了。然后引擎告诉调度器,这个request下载失败了,你记录一下,我们待会儿再下载)
  • 11 引擎:Hi!Spider,这是下载好的东西,并且已经按照老大的下载中间件处理过了,你自己处理一***意!这儿responses默认是交给def parse()这个函数处理的)
  • 12 Spider:(处理完毕数据之后对于需要跟进的URL),Hi!引擎,我这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的Item数据。
  • 13 引擎:Hi !管道 我这儿有个item你帮我处理一下!调度器!这是需要跟进URL你帮我处理下。然后从第四步开始循环,直到获取完老大需要全部信息。
  • 14 管道调度器:好的,现在就做!

安装

在pip安装会有问题,直接在Anaconda里安装。

创建Scrapy项目

PyCharm里没有直接的创建入口,在命令行创建(从Anaconda安装后似乎自动就在环境变量里了,可以直接用Scrapy命令):
Python学习笔记之Scrapy爬虫

Scrapy项目结构

Python学习笔记之Scrapy爬虫

设置settings.py
该文件里的ROBOTSTXT_OBEY = True会让Scrapy启动后先访问网站的robots.txt,然后根据其内容决定爬取范围,即遵循了Robot协议,这里将它设置为False。

ROBOTSTXT_OBEY = False
设置爬取间隔。由于选项RANDOMIZE_DOWNLOAD_DELAY默认是启用的,那么Scrapy就会在0.5倍的DOWNLOAD_DELAY和1.5倍之间取随机值,作为相邻两次发送请求之间的间隔。

DOWNLOAD_DELAY = 0.25
设置自定义的Exporter类,用于导出支持中文的JSON文件。

"""
在该文件中定义可显示中文的JSON Lines Exporter,并设置爬取的时间间隔为0.25
"""
from scrapy.exporters import JsonLinesItemExporter
#默认显示的中文是阅读性较差的Unicode字符
#需要定义子类显示出原来的字符集(将父类的ensure_ascii属性设置为False即可)
class CustomJsonLinesItemExporter(JsonLinesItemExporter):
    def __init__(self,file,**kwargs):
        #直接调用父类构造,只是将其ensure_ascii设置为False,这样就可以支持中文
        super(CustomJsonLinesItemExporter,  self).__init__(file,  ensure_ascii=False,**kwargs)


#启动新定义的Exporter类
FEED_EXPORTER={
    #当导出到json文件时使用该类
     'json':'stockstar.setting.CustomJsonLinesItemExporter'
        }

items.py设置Item容器和ItemLoader填充器

Item容器的字段一定要设置,填充器这里是自定义的,为了把输出处理器改成TakeFirst(),它从接收到的值中返回第一个非null非empty的值(空字符串就算empty值)。

import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst


# Itemloader提供的是填充Item容器的机制
# (自定义Itemloader只需要继承scrapy.loader.Itemloader类,并选择性地声明其[默认]输入/输出处理器等)
class ScrapylzhItemLoader(ItemLoader):
    default_output_processor = TakeFirst()  # 声明默认输出处理器


# Item提供保存抓取到数据的容器
# (定义Item只需要继承scrapy.Item类,并将所有字段都定义为scrapy.Field类型)
class ScrapylzhItem(scrapy.Item):
    code = scrapy.Field()  # 股票代码
    abbr = scrapy.Field()  # 股票简称
    last_trade = scrapy.Field()  # 最新价
    chg_ratio = scrapy.Field()  # 涨跌幅
    chg_amt = scrapy.Field()  # 涨跌额
    chg_ratio_5min = scrapy.Field()  # 5分钟涨幅
    volumn = scrapy.Field()  # 成交量
    turn_over = scrapy.Field()  # 成交额

生成和完善spiders/下的爬虫文件
在同名子目录(也即标了源文件根的目录,即spiders/的上级目录)下执行:

scrapy genspider stock quote.stockstar.com
1
这表示:调用Scrapy的genspider命令生成爬虫文件,文件名是stock,该爬虫的活动域名和初始页面都设置为quote.stockstar.com。

要爬取的页面上信息在HTML里,可以观察到它可以由某个简易的CSS选择器在页面中进行唯一表示,那么就可以用Scrapy的CSS方式来对Response进行解析。

生成的class里会有一个名为parse的generator,在那里写爬虫逻辑。所有的结果放到Item容器后都yield起来,最终由Scrapy调用前面定义的Exporter来处理。
 

# -*- coding: utf-8 -*-
import scrapy
from items import ScrapylzhItem, ScrapylzhItemLoader

'''
证券之星 quote.stockstar.com
证券之星-沪深市场-A股市场(第一页) http://quote.stockstar.com/stock/ranklist_a_3_1_1.html
'''


# 用命令生成的完成爬虫功能的类
class StockSpider(scrapy.Spider):
    # 爬虫名称
    name = 'stock'
    # 爬虫活动的域.这里就是指定的那个域名
    allowed_domains = ['quote.stockstar.com']
    # 爬虫的活动起点,是活动域下的一个页面.这里就是要爬取的若干页面的第一页
    start_urls = ['http://quote.stockstar.com/stock/ranklist_a_3_1_1.html']

    # 在这里完成爬虫逻辑,这是一个generator(在里面可见yield)
    def parse(self, response):
        # 从url中获取页码
        # 观察url可以看到第一页是'_a_3_1_1.html'结尾,而第二页是'_a_3_1_2.html'结尾
        # 先按'_'分割成list,取最后一个也就是'页码.html',然后再按'.'分割再取第一个即可
        page = int(response.url.split('_')[-1].split('.')[0])
        # 用CSS选择器选取页面中股票的所有项
        # 观察可以看到它们都在id为datalist的tbody里,以tr元素的形式存在,两者间用后代选择器(空格)
        item_nodes = response.css('#datalist tr')
        # 对其中的每一项(一支股票)
        for item in item_nodes:
            # 根据item.py中所定义的字段内容,进行字段内容的抓取
            # 声明一个Item填充器,接收Item实例和转化前的Item原型
            item_loader = ScrapylzhItemLoader(item=ScrapylzhItem(), selector=item)
            # 向Item中的各个字段写数据,用CSS方式解析值
            item_loader.add_css('code', 'td:nth-child(1) a::text')  # 这表示第一个td标签下a标签内的文本
            item_loader.add_css('abbr', 'td:nth-child(2) a::text')
            item_loader.add_css('last_trade', 'td:nth-child(3) span::text')
            item_loader.add_css('chg_ratio', 'td:nth-child(4) span::text')
            item_loader.add_css('chg_amt', 'td:nth-child(5) span::text')
            item_loader.add_css('chg_ratio_5min', 'td:nth-child(6) span::text')
            item_loader.add_css('volumn', 'td:nth-child(7)::text')
            item_loader.add_css('turn_over', 'td:nth-child(8)::text')
            stock_item = item_loader.load_item()
            # 用yield使当执行到这里时返回一个迭代值
            yield stock_item
        # 如果当前item_nodes非空,说明这一页是有东西的,那么可能存在下一页
        if item_nodes:
            # 下一页页码
            next_page = page + 1
            # 替换url中的页码部分
            next_url = response.url.replace("{0}.html".format(page), "{0}.html".format(next_page))
            # 向该url发出请求,在获取响应后回调到这个函数,并将结果继续投入迭代
            yield scrapy.Request(url=next_url, callback=self.parse)
            # 所以这个generator将返回一个generator对象,其中每次获取的都是下一支股票的ScrapylzhItem形式

运行爬虫

from scrapy.cmdline import execute

# 相当于在本目录下执行'scrapy crawl stock -o items.json'
# 即让scrapy调用spiders下的stock.py爬虫,将运行结果保存到items.json中
execute(["scrapy", "crawl", "stock", "-o", "items.json"])

运行结果(部分)

{'abbr': '郴电国际',
 'chg_amt': '0.70',
 'chg_ratio': '10.04%',
 'chg_ratio_5min': '0.00%',
 'code': '600969',
 'last_trade': '7.67',
 'turn_over': '3175.12',
 'volumn': '41841.87'}
2019-03-18 15:13:27 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quote.stockstar.com/stock/ranklist_a_3_1_1.html>
{'abbr': '和胜股份',
 'chg_amt': '1.09',
 'chg_ratio': '10.03%',
 'chg_ratio_5min': '0.00%',
 'code': '002824',
 'last_trade': '11.96',
 'turn_over': '6285.65',
 'volumn': '53186.47'}
2019-03-18 15:13:27 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quote.stockstar.com/stock/ranklist_a_3_1_1.html>
{'abbr': '道道全',
 'chg_amt': '1.45',
 'chg_ratio': '10.03%',
 'chg_ratio_5min': '0.00%',
 'code': '002852',
 'last_trade': '15.90',
 'turn_over': '12198.13',
 'volumn': '78997.36'}
2019-03-18 15:13:27 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quote.stockstar.com/stock/ranklist_a_3_1_1.html>
{'abbr': '宁波韵升',
 'chg_amt': '0.66',
 'chg_ratio': '10.03%',
 'chg_ratio_5min': '0.00%',
 'code': '600366',
 'last_trade': '7.24',
 'turn_over': '42341.90',
 'volumn': '602369.14'}