Scrapy选择器和持久化

介绍

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 其可以应用在数据挖掘,信息处理或存储历史数据等一系列的程序中。理解scrapy可以参考django,django框架是用帮助我们快速开发web程序的,而scrapy框架就是用来帮助我们快速抓取网页信息的。

安装

#Windows平台
    1、pip3 install wheel #  pip默认只是去网络去找包,要想pip支持本地wheel文件安装,需要安装wheel包。安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
    3、pip3 install lxml
    4、pip3 install pyopenssl
    5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
    6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
    7、执行pip3 install 下载目录\Twisted-17.9.0-cp36-cp36m-win_amd64.whl
    8、pip3 install scrapy
  
#Linux平台
    1、pip3 install scrapy

整体架构

在Scrapy的数据流是由执行引擎控制,具体流程如下:

  1. spiders产生request请求,将请求交给引擎
  2. 引擎把请求交给调度器,调度器具有url去重功能(因为request对象里有url),调度器会对需要执行的请求按照优先级排列放到一个地方
  3. 调取器把请求给引擎,引擎把调度好的请求发送给download,通过中间件发送(这个中间件至少有 两个方法,一个请求的,一个返回的)
  4. 一旦完成下载就返回一个response,通过下载器中间件,返回给引擎,引擎把response 对象传给下载器中间件,最后到达引擎
  5. 引擎从下载器中收到response对象,经过爬虫中间件传给了spiders(spiders里面做两件事,1、产生request请求,2、为request请求绑定一个回调函数),spiders只负责解析爬取的任务。不做存储(可以做,但是不建议做,一来是为了分工明确,二来是为了解决重复打开文件的尴尬)
  6. 解析完成之后返回一个解析之后的结果items对象及(跟进的)新的Request给引擎
  7. 引擎将(Spider返回的)爬取到的Item给Item Pipeline,存入数据库,持久化。如果yield的是一个request对象,就传给调度器再次去下载
  8. 重复上述过程

Scrapy主要包括了以下组件:

  • 引擎(Scrapy)
    用来处理整个系统的数据流处理, 触发事务(框架核心)
  • 调度器(Scheduler)
    用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
  • 下载器中间件(Downloader Middlewares)
    位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
  • 爬虫中间件(Spider Middlewares)
    介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
  • 调度中间件(Scheduler Middewares)
    介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

创建项目

scrapy创建项目也是和django创建项目类似

1. scrapy startproject 项目名称
   - 在当前目录中创建中创建一个项目文件(类似于Django)
 
2. scrapy genspider [-t template] <name> <domain>
   - 创建爬虫应用,需要cd到项目目录
   如:
      scrapy gensipider -t basic oldboy oldboy.com
      scrapy gensipider -t xmlfeed autohome autohome.com.cn
   PS:
      查看所有命令:scrapy gensipider -l
      查看模板命令:scrapy gensipider -d 模板名称
 
3. scrapy list
   - 展示爬虫应用列表
 
4. scrapy crawl 爬虫应用名称  
   - 运行单独爬虫应用,加上--nolog参数就不会打印提示信息

目录结构

 myproject/
   scrapy.cfg    # 项目部署的配置文件
    myproject/
       __init__.py
       items.py          # 用于结构化数据,类似于django中的model
       pipelines.py    # 数据处理,一般做数据持久化
       middlewares.py   # 中间件
       settings.py    # 爬虫程序使用的配置文件
       spiders/
           __init__.py
           jd.py       # 文件名一般和爬虫名一样,但是不一样也没关系,因为scrapy crawl jd,使用的是爬虫的name属性

如果在windows中国之行爬虫出现编码问题,在爬虫程序的最上面加上如下代码

import sys,os
sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')

起步

# -*- coding: utf-8 -*-
import scrapy
from scrapy.http import Request

class ChoutiSpider(scrapy.Spider):
    # 爬虫名称,不能更改
    name = "chouti"
    # 爬去过程可能遇到一些a标签,这个选项用来控制爬虫只爬取chouti.com这个网站的连接,不往外爬
    allowed_domains = ["chouti.com"]

    # 初始url
    start_urls = ['http://chouti.com/']

    def start_requests(self):
        # 爬虫刚开启会执行这个,这个函数要么是一个生成器,要么返回一个可迭代对象,因为源码是把生成器或者可迭代对象通过
        # iter() 方法转化为迭代器,然后next取值
        # for url in self.start_urls:
        #     # 默认callback调用的是parse
        #     yield Request(url)
        url_list = []
        for url in self.start_urls:
            url_list.append(Request(url))
        return url_list

    def parse(self, response):
        # response 是爬取得到的对象HtmlResponse,里面不仅封装了响应头,也封装了响应体response.body
        items = response.xpath('//*[@id="content-list"]/div[@class="item"]')
        # 每次
        f = open('chouti.txt', 'a+')
        for i in items:
            """
            extract 得到列表, extract_first 得到单个值,extract 或 extract_first 得到的里面的内容都是字符串
            而不是可以使用xpath方法的selector对象
            """
            link = i.xpath('.//a/@href').extract_first()
            f.write(link + '\n')
        f.close()

        page_links = response.xpath('//*[@id="dig_lcpage"]//a/@href').extract()
        for i in page_links:
            # 发送下一个请求
            yield Request(url='https://dig.chouti.com' + i, callback=self.parse)

选择器

在scrapy中可以使用beautifulsoup,但是如果你在scrapy里面使用beautifulsoup就有点非主流了,因为scrapy可以使用xpath语法,关键是在浏览器有一个copy xpath的功能以及xpath组件(crtl + shift + x)可以快速帮我们寻找的我们想要寻找的内容

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from scrapy.http import HtmlResponse, Response
from scrapy.selector import Selector
html = """<!DOCTYPE html>
<html>
    <head lang="en">
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <ul>
            <li class="item-"><a id='i1' href="link.html">first item</a></li>
            <li class="item-0"><a id='i2' href="llink.html">first item</a></li>
            <li class="item-1"><a href="llink2.html">second item<span>vv</span></a></li>
        </ul>
        <div><a href="llink2.html">second item</a></div>
    </body>
</html>
"""
response = HtmlResponse(url='http://xxx.com', body=html, encoding='utf8')
# // 表示子子孙孙, / 表示儿子
hxs = response.xpath('//a')
print(hxs)
hxs = response.xpath('//a[2]')
print(hxs)
hxs = response.xpath('//a[@id]')
print(hxs)
hxs = response.xpath('//a[@id="i1"]')
print(hxs)
# 并且的条件
hxs = response.xpath('//a[@href="link.html"][@id="i1"]')
print(hxs)
# 包含,比较常用
hxs = response.xpath('//a[contains(@href, "link")]')
print(hxs)
# 没有end-with
hxs = response.xpath('//a[starts-with(@href, "link")]')
print(hxs)
# 正则表达式,其中re:test 是固定写法
hxs = response.xpath('//a[re:test(@id, "i\d+")]')
print(hxs)
hxs = response.xpath('//a[re:test(@id, "i\d+")]/text()').extract()
print(hxs)
hxs = response.xpath('//a[re:test(@id, "i\d+")]/@href').extract()
print(hxs)
# 抽取li标签下的所有a的href属性组成一个列表,列表里放着字符串
hxs = response.xpath('/html/body/ul/li/a/@href').extract()
print(hxs)
# 抽取li标签下的第一个a的href属性组成一个列表,列表里放着字符串
hxs = response.xpath('//body/ul/li/a/@href').extract_first()
print(hxs)

ul_list = response.xpath('//body/ul/li')
for item in ul_list:
    # 如果这里用item.xpath('/a/span'),那么寻找范围是response,而不是item
    v = item.xpath('./a/span')
    # 或
    # v = item.xpath('a/span')
    # 或
    # v = item.xpath('*/a/span')
    print(v)

cookie的处理

cookie的处理有两种方式,第一种是自己在程序中获取cookie并每次显示地带上cookie

from scrapy.http.cookies import CookieJar
        cookie_jar = CookieJar()
        cookie_jar.extract_cookies(response, response.request)
        # 去对象中将cookie解析到字典
        for k, v in cookie_jar._cookies.items():
            for i, j in v.items():
                for m, n in j.items():
                    self.cookie_dict[m] = n.value
发送请求时携带cookies,需要注意的一点是body传入的值是‘’k1=1&k2=2‘’这样的格式
yield Request(
            url='https://xxx',
            method='post',
            body='xxoo',
            cookies=self.cookie_dict,
            headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
            callback = self.check_login
        )

from urllib.parse import urlencode

dic = {
    'k1':1,
    'k2':2
}
print(urlencode(dic))

第二种方式是在每个请求加上这样一句meta={'cookiejar': True},这样发请求的时候会自动解析cookie并携带cookie

pipeline

我们在parse爬取的内容,可以直接在里面打开文件写入文件并关闭文件。但是这样会存在两个问题:

  1. 程序的耦合性较高,因为爬虫代码和存储数据的代码放到一起,这样一旦程序大了就会很乱,我们希望的是爬虫代码就仅仅是爬虫代码,数据持久化代码就只写数据保存相关的逻辑
  2. 一旦在程序设计到翻页,想要再次发请求并且回调函数还是parse的时候,就会重复打开文件,这肯定会浪费资源
    那么我们能否做到在爬虫启动到结束只打开一次文件呢?答案是肯定的,借助item和pipeline就能完成。

cnblogs.py

from nj.items import NjItem

class CnblogSpider(scrapy.Spider):
    # 爬虫名称,不能更改
    name = "cnblog"
    allowed_domains = ["cnblogs.com"]

    # 初始url
    start_urls = ['https://www.cnblogs.com/longyunfeigu/']
    def parse(self, response):
        links = response.xpath("//a[@class='postTitle2']/@href").extract()
        for link in links:
            yield NjItem(href=link)

items.py 相当于django的model

import scrapy

class NjItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    href = scrapy.Field()

pipelines.py

# -*- 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
from scrapy.exceptions import DropItem

"""
源码内容:
    1. 判断当前FilePipeline类中是否有from_crawler
        有:
            obj = FilePipeline.from_crawler(....)
        否:
            obj = FilePipeline()
    2. obj.open_spider()
    
    3. obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item()
    
    4. obj.close_spider()
"""

"""
这里的两个Pipeline不管parse函数是否yield,都会实例化相应的对象并调用open_spider方法,
然后这两个对象就坐在这抽烟等着了,等待着parse返回item对象,然后一层层调用process_item处理,最终爬虫结束的时候接关闭close_spider

raise 一个DropItem() 对象的结果

File.from_crawler
DB.from_crawler
File.open_spider
Db.open_spider
File
Db.close_spider
File.close_spider
"""

class FilePipeline(object):
    def __init__(self, path):
        # 把这个类的对象后续方法可能会用到的属性定义在__init__里面,这样看代码的人一眼就能看到这个对象具有的属性
        # 否则调用open方法self才有f属性,别人可能有疑惑,明明__init__没有f,现在怎么就有了呢,还得一个个去方法找哪一个给self绑定f属性,这也是编程规范
        self.f = None
        self.path = path

    @classmethod
    def from_crawler(cls, crawler):
        """
        初始化时候,用于创建pipeline对象,我们在这里取settings的配置,注意我们写的settings不是爬虫所有的配置
        类似django的settings也不是项目的完整配置
        :param crawler:
        :return:
        """
        print('File.from_crawler')
        path = crawler.settings.get('FILE_PATH')
        return cls(path)

    def open_spider(self, spider):
        """
        爬虫开始执行时,调用
        :param spider:
        :return:
        """
        # if spider.name == 'chouti':
        print('File.open_spider')
        self.f = open(self.path, 'a+')

    def process_item(self, item, spider):
        # f = open('xx.log','a+')
        # f.write(item['href']+'\n')
        # f.close()
        print('File')
        self.f.write(item.get('href','') + '\n')
        # 返回item或者返回None, 写一个pipeline都的process_item都会被调用,调用传入的参数就是在这返回的值
        # 如果返回None,那么下一个pipeline都的process_item的item参数就是None
        # 要想pipeline都的process_item不再执行,可以使用raise DropItem() 异常实例
        return item
        # raise DropItem()

    def close_spider(self, spider):
        """
        爬虫关闭时,被调用
        :param spider:
        :return:
        """
        print('File.close_spider')
        self.f.close()

class DbPipeline(object):
    def __init__(self,path):
        self.f = None
        self.path = path

    @classmethod
    def from_crawler(cls, crawler):
        """
        初始化时候,用于创建pipeline对象
        :param crawler:
        :return:
        """
        print('DB.from_crawler')
        path = crawler.settings.get('DB_PATH')
        return cls(path)

    def open_spider(self,spider):
        """
        爬虫开始执行时,调用
        :param spider:
        :return:
        """
        print('Db.open_spider')
        self.f = open(self.path,'a+')

    def process_item(self, item, spider):
        # f = open('xx.log','a+')
        # f.write(item['href']+'\n')
        # f.close()
        print('Db',item)
        # self.f.write(item['href']+'\n')
        return item

    def close_spider(self,spider):
        """
        爬虫关闭时,被调用
        :param spider:
        :return:
        """
        print('Db.close_spider')
        self.f.close()

settings.py

ITEM_PIPELINES = {
   'nj.pipelines.FilePipeline': 300,
   'nj.pipelines.DbPipeline': 400,
}
posted @ 2018-08-16 14:01  龙云飞谷  阅读(153)  评论(0编辑  收藏  举报