scrapy

10 篇文章 1 订阅
10 篇文章 1 订阅

简介

Scrapy,Python开发的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试.

在这里插入图片描述

Scrapy主要包括了以下组件:

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

Scrapy运行流程大概如下:

  1. 引擎从调度器中取出一个链接(URL)用于接下来的抓取
  2. 引擎把URL封装成一个请求(Request)传给下载器
  3. 下载器把资源下载下来,并封装成应答包(Response)
  4. 爬虫解析Response
  5. 解析出实体(Item),则交给实体管道进行进一步的处理
  6. 解析出的是链接(URL),则把URL交给调度器等待抓取

安装配置

Linux下的安装(包括mac)

pip install scrapy

Windows下的安装

a. 下载twisted
    http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
  b. 安装wheel
    pip3 install wheel
  c. 安装twisted
    进入下载目录,执行 pip3 install Twisted‑18.7.0‑cp36‑cp36m‑win_amd64.whl
  d. 安装pywin32
    pip3 install pywin32
  e. 安装scrapy
    pip3 install scrapy

基本命令

一,全局命令

在这里插入图片描述

1,startproject—创建项目命令

语法:scrapy startproject <project_name>
前置条件:项目不需要存在
示例:scrapy startproject TestDemo001
进入到相应的文件目录下,打开cmd,输入以上命令,则会在相应的文件目录下建立一个项目

2,shell—创建一个shell环境调试response的命令

语法:scrapy shell
前置条件:项目不需要存在
示例:scrapy shell “http://www.yetianlian.com/”
打开cmd命令窗口,运行该命令,(安装IPython的情况下默认启动IPython编辑器)

如果不想输出log可以使用这个命令:
scrapy shell --nolog “http://www.yetianlian.com/”

左侧标记部分是scrapy shell 可使用的命令,其中view (查看页面的响应部分,处理含有动态js网址时较实用)和response.xpath(查看xpath路径选取的正确性的)命令经常使用(需注意!)

3,view—查看页面内容

语法:scrapy view
前置条件:项目不需要存在
示例:scrapy view “http://www.yetianlian.com/”
打开cmd命令窗口,运行该命令,在浏览器中打开指定的URL

4,version—查看版本信息

语法:scrapy version [-v]
前置条件:项目不需要存在
示例:scrapy version [-v]
打开cmd命令窗口,运行该命令,显示相应scrapy的版本号

此命令可以用来查看scrapy是否安装成功,该命令同时输出Python、Twisted以及平台的信息,方便bug的提交。

5,settings—查看配置文件参数命令

语法:scrapy settings [options]
前置条件:项目不需要存在
示例:scrapy settings --get BOT_NAME
输出:scrapybot
显示settings中的具体设置的设定值

6,runspider 运行爬虫命令

语法:scrapy runspider <spider_file.py>
前置条件:项目不需要存在
示例:scrapy runspider ytl.py
未创建项目的情况下,运行一个编写好的spider模块

7,fetch 显示爬取过程

语法:scrapy fetch [url]
前置条件:项目存在/不存在均可
示例:scrapy fetch “http://www.yetianlian.com/”
命令其实是在标准输出中显示调用一个爬虫来爬取指定的url的全过程。

8,bench 硬件测试命令

语法:scrapy bench
前置条件:项目不需要存在

测试scrapy在硬件上运行的效率

注:错误解析

在这里插入图片描述

这是因为您enum不是标准的库enum模块。您可能已enum34安装该软件包。
检查是否属于这种情况的一种方法是检查财产

解决错误方法:参考
从python 3.6开始,enum34库不再与标准库兼容。该库也是不必要的,因此您只需卸载它即可。我安装的python版本是3.6.5版本的,所有直接卸载enum库即可

卸载enum34库

pip uninstall enum34

二,项目命令

1,genspider—通过模板生成scrpay爬虫

查看scrapy genspider 的模板
命令:scrapy genspider -l

在这里插入图片描述

语法:scrapy genspider [-t template]
前置条件:项目需要存在
展示basic模板的内容:scrapy genspider -d basic

在这里插入图片描述

示例:创建spider.py,命令:scrapy genspider -t basic ytl yetianlian.com

在这里插入图片描述

文件目录图示:

在这里插入图片描述

自动生成的代码图示:

在这里插入图片描述

由此可见,genspider生成的默认代码与刚doc窗口展示的代码相同,其中命令中代表爬虫的唯一名称代表的是爬虫所允许爬取的域名范围。

2,crawl—启动爬虫命令

语法:scrapy crawl
前置条件:项目需要存在
示例:scrapy crawl ytl
启动命名为ytl的爬虫(需要cd进入相应的文件目录下,运行该命令)

3,check—检查爬虫完整性

语法:scrapy check [-l]
前置条件:项目需要存在
示例:scrapy check ytl
检查一些语法、import和warning等错误

4,list—查看爬虫列表命令

语法:scrapy list
前置条件:项目需要存在
示例:scrapy list
查看一下这个项目里面目前有哪些爬虫

5,edit—编辑爬虫命令

语法:scrapy edit
前置条件:项目需要存在
示例:scrapy edit ytl
用设定的编辑器编辑给定的spider,该命令是提供一个快捷方式,开发者可以自由选择其他工具或者IDE来编写和调试spider。(Linux系统下)

6,parse—获取给定的URL使用相应的spider分析处理

语法:scrapy parse [options]
前置条件:项目需要存在
示例:scrapy parse “http://www.yetianlian.com/” -c parse
如果提供–callbac选项,可以自动检测spider中的解析方法进行处理。支持以下的选项:

–spider=SPIDER: 没有指定spider的情况下其程序搜索,用这个选项可以强行指定某个spider

–a NAME=VALUE: 用来设定spider需要的参数,可以多个

–callback或-c: 指定spider里面用于处理response的函数,没有强行制定的话,默认使用parse函数

–pipelines:用来指定后续的pipelines,可以灵活定制

–rules或-r: 通过CrawlSpider设定的规则来选取对应的函数当作解析response的回调函数

–noitems: 不显示抓取的items

–nolinks: 不显示提取的链接

–nocolour: 输出的结果不要高亮显示(不建议用)

–depth或-d: 设置爬取深度,默认为1

–verbose或-v: 显示被爬取的每层的相关信息

7,deploy命令

语法:scrapy deploy [target:project|-l|-L]

目的:将项目部署到scrapyd服务。

项目结构以及爬虫应用简介

project_name/
   scrapy.cfg
   project_name/
       __init__.py
       items.py
       middlewares.py
 
       pipelines.py
       settings.py
       spiders/
           __init__.py
           爬虫1.py
           爬虫2.py
           爬虫3.py

文件说明:

  • scrapy.cfg 项目的主配置信息。(真正爬虫相关的配置信息在settings.py文件中)

  • items.py 设置数据存储模板,用于结构化数据,如:Django的Model

  • pipelines 数据处理行为,如:一般结构化的数据持久化

  • settings.py 配置文件,如:递归的层数、并发数,延迟下载等

    #项目名称
    BOT_NAME = 'DemoScrapy'
    #爬虫的位置
    SPIDER_MODULES = ['DemoScrapy.spiders']
    #新建爬虫的位置
    NEWSPIDER_MODULE = 'DemoScrapy.spiders'
    #浏览器信息设置
    USER_AGENT = 'DemoScrapy (+http://www.yourdomain.com)'
    #是否遵守robots.txt规则
    ROBOTSTXT_OBEY = True
    #并发数,默认16
    CONCURRENT_REQUESTS = 32
    #下载延迟,每个请求等待3秒
    DOWNLOAD_DELAY = 3
    #启用COOKIES
    COOKIES_ENABLED = False
    #默认的请求报头
    DEFAULT_REQUEST_HEADERS = {}
    ...
    
  • spiders 爬虫目录,如:创建文件,编写爬虫规则

注意:一般创建爬虫文件时,以网站域名命名

windows系统编码错误时:

解决方法:

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

import scrapy

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/']

    def parse(self, response):
        pass

创建爬虫

scrapy genspider dfcf “http://data.eastmoney.com/zjlx/detail.html”

执行爬虫

scrapy crawl dfcf

备注:可以在settings.py中配置user-agent

在爬取数据时,可以选择是否往…/robots.txt/发送验证,是否允许爬取,一般设置为False

使用scrapy解析文本内容时,可以使用每个应用中的response.xpath(xxx) 进行数据的解析。

print(response.xpath(…)) 得到的是一个`Selector对象。selector对象可以继续xpath进行数据的解析。

备注:xpath使用方法:
  1.//+标签 表示从全局的子子孙孙中查找标签

2./+标签 表示从子代中查找标签

3.查找带有xxx属性的标签: 标签+[@标签属性=“值”]

4.查找标签的某个属性: /标签/@属性

5.从当前标签中查找时:.//+标签

response = HtmlResponse(url='http://example.com', body=html,encoding='utf-8')
hxs = HtmlXPathSelector(response)
print(hxs)   # selector对象
hxs = Selector(response=response).xpath('//a')
print(hxs)    #查找所有的a标签
hxs = Selector(response=response).xpath('//a[2]')
print(hxs)    #查找某一个具体的a标签    取第三个a标签
hxs = Selector(response=response).xpath('//a[@id]')
print(hxs)    #查找所有含有id属性的a标签
hxs = Selector(response=response).xpath('//a[@id="i1"]')
print(hxs)    # 查找含有id=“i1”的a标签
# hxs = Selector(response=response).xpath('//a[@href="link.html"][@id="i1"]')
# print(hxs)   # 查找含有href=‘xxx’并且id=‘xxx’的a标签
# hxs = Selector(response=response).xpath('//a[contains(@href, "link")]')
# print(hxs)   # 查找 href属性值中包含有‘link’的a标签
# hxs = Selector(response=response).xpath('//a[starts-with(@href, "link")]')
# print(hxs)   # 查找 href属性值以‘link’开始的a标签
# hxs = Selector(response=response).xpath('//a[re:test(@id, "i\d+")]')
# print(hxs)   # 正则匹配的用法   匹配id属性的值为数字的a标签
# hxs = Selector(response=response).xpath('//a[re:test(@id, "i\d+")]/text()').extract()
# print(hxs)    # 匹配id属性的值为数字的a标签的文本内容
# hxs = Selector(response=response).xpath('//a[re:test(@id, "i\d+")]/@href').extract()
# print(hxs)    #匹配id属性的值为数字的a标签的href属性值
# hxs = Selector(response=response).xpath('/html/body/ul/li/a/@href').extract()
# print(hxs)
# hxs = Selector(response=response).xpath('//body/ul/li/a/@href').extract_first()
# print(hxs)
 
# ul_list = Selector(response=response).xpath('//body/ul/li')
# for item in ul_list:
#     v = item.xpath('./a/span')
#     # 或
#     # v = item.xpath('a/span')
#     # 或
#     # v = item.xpath('*/a/span')
#     print(v)

备注:xpath中支持正则的使用: 用法 标签+[re:test(@属性值,“正则表达式”)]

​ 获取标签的文本内容: /text()

获取第一个值需要 selector_obj.extract_first() 获取所有的值 selector_obj.extract() 值在一个list中

小爬虫及管道文件

class DemoscrapyItem(scrapy.Item):
    # define the fields for your item here like:
    name = scrapy.Field()
    balance = scrapy.Field()
    pass
  #生成器
  tables = response.xpath("//table[@class='newdatatable kcb_tables']")
  for table in tables:
        di = DemoscrapyItem()
        #extract()返回unicode字符串
        name = table.xpath("./tr[2]/td[1]/a/text()").extract()
        balance = table.xpath("./tr[2]/td[2]/span/text()").extract()
        di["name"] = name[0]
        di["balance"] = balance[0]
        yield di
        
   #return 和 yield区别?
   #yield scrapy.Request(url)返回给引擎,引擎寻找调度器
#启用管道
ITEM_PIPELINES = {
    'DemoScrapy.pipelines.DemoscrapyPipeline': 300,
}
class DemoscrapyPipeline(object):
    def __init__(self):
        self.f = open("aaa.txt","w+")
        #可选,做参数初始化
    def process_item(self, item, spider):
        #ensure_ascii=False中文不按照ascii处理
        content = json.dumps(dict(item),ensure_ascii=False)
        print(content)
        self.f.write(content)
        #返回给引擎
        return item
    #def open_spider(self,spider):
    #    self.f.close()
    #    和__init__一样
    def close_spider(self,spider):
        self.f.close()

当然也可以直接输出文件

scrapy crawl xxx -o xxx.json
scrapy crawl xxx -o xxx.jsonl
scrapy crawl xxx -o xxx.csv
scrapy crawl xxx -o xxx.xml

模拟scrapy发送请求,得到response

scrapy shell "http://data.eastmoney.com/zjlx/detail.html"

spiders

Spider类定义了如何爬取某个网站,包括了爬取的动作以及如何从网页的内容中提取结构化数据。还句话说,Spider就是你定义爬取的动作及分析某个网页的地方。

  • 以初始的URL初始化Request,并设置回调函数。当该request下载完毕并返回时,将生成response,并作为参数传给该回调函数。start_requests()读取start_urls中的URL,并以parse为回调函数生成Request.

  • 在回调函数内分析返回的网页内容,返回Item对象或者Request或者一个包含二者的可迭代容器。返回的Request对象之后回经过Scrapy处理,下载相应的内容,并调用设置的callback函数。

  • 在回调函数内,你可以使用选择器来分析网页内容,并根据分析的数据生成item。

  • 最后,由spider返回的item将被存到数据库或使用Feed exports存到文件中.

    Spider参数
    Spider可以通过接收参数来修改其功能。spider参数一般用来定义初始URL或者指定先值爬取网站的部分。
    在运crawl时添加-a可以传递Spider参数:

scrapy crawl myspider -a category=electronics

Spider在构造器中获取参数。

import scrapy

class MySpider(Spider):
    name = 'myspider'
def __init__(self, category=None, *args, **kwargs):
    super(MySpider, self).__init__(*args, **kwargs)
    self.start_urls = ['http://www.example.com/categories/%s' % category]
    # ...

class scrapy.spider.Spider

Spider是最简单的spider。每个器它的spider必须继承自该类。Spider仅仅请求给定的
start_urls/start_requests,并根据返回的结果(resulting/responses)调用spider的parse方法。

**name:**定义spider名字的字符串。定义了Scrapy如何定位spider,必须是唯一的。不过可以生成多个相同的spider实例。
如果该spider爬取单个网站,一个常见的做法是以该网站来命名spider。
**allowed_domains:**可选,包含了spider允许爬取的域名列表。当offsiteMiddleware启用时,域名不在列表中的URL不会被跟进。
**start_urls:**URL列表,当没有制定特定的URL时,spider将从该列表中来时进行爬取。因此,第一个被获取到的页面的URL将是该列表之一。后续的URL将会从获取到的数据中提取。
start_requests() 该方法必须返回一个可迭代对象。该对象包含了spider用于爬取的第一个Request。当spider启动爬取并且未指定URL时,该方法被调用。当指定了URL时,make_requests_from_url()将被调用来创建Request对象。仅近乎uibei调用一次,可视为生成器。该方法默认使用start_urls的url生成Reqest。如果你想修改最初爬取某个网站的Request对象,可以重写该方法。
**make_requests_from_url(url)**该方法接收一个URL并返回用于爬取的Request对象。在初始化时被start_requests()调用,也用于转化url为request。默认未被复写的情况下该方法返回的Request对象中,parse()作为回调函数,dont_filter参数也被设置为开启。
**parse(respone)**当response没有指定回调函数时,该方法时Scapy处理下载的response的默认方法。
parse负责处理respones并返回处理的数据以及跟进的URL。
Spider对其他的Request及Item的可迭代的对象。
**log(message[,level,component])?*使用scrapy.log.msg()方法记录message。log中自动带上该spider的name属性。
**closed(reason)**当spider关闭时,该函数被调用。该方法提供了一个提到调用signals.connect()来监听spider_closed信号的快捷方式。
样例:

import scrapy

class MySpider(scrapy.Spider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = [
        'http://www.example.com/1.html',
        'http://www.example.com/2.html',
        'http://www.example.com/3.html',
    ]
def parse(self, response):
    self.log('A response from %s just arrived!' % response.url)

class scrapy.contrib.spiders.CrawlSpider
爬取一般网站常用的spider。其定义了一些规则来提供跟link的方便机制。除了继承过来的属性外,其提供了一个新的属性。

**rules:**一个包含一个或多个Rule对象的集合。每个Rule对爬取网站的动作定义了特定的表现。如果rule匹配了相同的链接,则根据它们在本属性中被定义的顺序,第一个会被使用。
**parse_start_url(response)**当start_url的请求返回时,该方法被调用。该方法分析最初的返回值并必须返回一个 Item 对象或者 一个 Request 对象或者 一个可迭代的包含二者对象。
爬取规则(Crawling rules)

class scrapy.contrib.spiders.Rule(link_extractor, callback=None, 
cb_kwargs=None, follow=None, process_links=None, process_request=None)

link_extractor是一个Link Extractor对象,其定义了如何从爬取到的页面提取链接。
callback是一个callable或string。从link_extractor中每获取到链接时将调用该函数。该回调函数接受一个response作为其第一个参数,并返回一个包含Itm以及Request对象的列表。
**cb_kwargs:**包含传递给回调函数的参数的字典。
follow是一个布尔值,指定了根据该规则从response提取的链接是否需要跟进。如果callback为None,follow默认设置为True,否则默认为False。
process_link是一个callable或string。主要用来过滤。
process_reqeust是一个callable或string。该规则提取到每个request时都会调用该函数。该函数必须返回一个request或None.

coding:utf-8

import scrapy
from scrapy.contrib.spiders import CrawlSpider,Rule
from scrapy.contrib.linkextractors import LinkExtractor
class MySpider(CrawlSpider):
    name='example.com'
    # 允许爬取链接的域名
    allowed_domains=['example.com']
    start_urls=['http://www.example.com']
rules=(
    # # 提取匹配 'category.php' (但不匹配 'subsection.php') 的链接并跟进链接(没有callback意味着follow默认为True)
    Rule(LinkExtractor(allow=('category\.php'),deny=('subsection\.php'))),
    # # 提取匹配 'item.php' 的链接并使用spider的parse_item方法进行分析
    Rule(LinkExtractor(allow=('item\.php'),callback='parse_item')),
)

def parse_item(self,response):
    self.log('Hi, this is an item page! %s' % response.url)
    item=scrapy.Item()
    item['id'] = response.xpath('//td[@id="item_id"]/text()').re(r'ID: (\d+)')
    item['name'] = response.xpath('//td[@id="item_name"]/text()').extract()
    item['description'] = response.xpath('//td[@id="item_description"]/text()').extract()
    return item

该spider将从example.com的首页开始爬取,获取category以及item的链接并对后者使用parse_item方法。当item获得返回时,将使用XPath处理HTML并生成一些数据填入Item中。

class scrapy.contrib.spiders.XMLFeedSpider

XMLFeedSpider被设计用于通过迭代各个节点来分析XML源。迭代其可以从iternodes,xml,html选择。鉴于xml以及html迭代器需要先读取所有DOM再分析而引起的性能问题,一般还是推荐使用iternodes。

你必须定义下列类属性来设置迭代器以及标签名:

**iterator:**用于确定使用哪个迭代器,

  • iternodes-一个高性能的基于正则表达式的迭代器;

  • html’-使用selector的迭代器。使用DOM进行分析,其需要将所有的DOM载入内存,当数据量大的时候会产生问题。

  • xml-和html一样。
    默认值为iternodes。

    itertag一个包含开始迭代的节点名的sring。
    namespaces一个由(prefix,url)元组所组成的list。其定义了在该文档中会被spider处理的可用的namespace。prefix及url会被自动调用redister_namespace()生成namespace。可以通过itertag属性中制定节点的namespace。

    class YourSpider(XMLFeedSpider):
    namespaces = [(‘n’, ‘http://www.sitemaps.org/schemas/sitemap/0.9’)]
    itertag = ‘n:url’
    # …

**adapt_response(response)**该方法在spider分析response前被调用。可以在response被分析前使用该函数来修改内容。
**parse_node(response, selector)**当节点符合提供的标签名时itertag该方法被调用。接收到的response以及相应的Selector作为参数传递给该方法。该方法返回一个Item对象或者Request对象或者一个包含二者的可迭代对象。
**process_results(response, results)**当spider返回结果时该方法被调用。设定该方法的目的是在结果返回给框架核心之前作最后的处理。

from scrapy import log
from scrapy.contrib.spiders import XMLFeedSpider
from myproject.items import TestItem

class MySpider(XMLFeedSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/feed.xml']
    iterator = 'iternodes' # This is actually unnecessary, since it's the default value
    itertag = 'item'
def parse_node(self, response, node):
    log.msg('Hi, this is a <%s> node!: %s' % (self.itertag, ''.join(node.extract())))

    item = TestItem()
    item['id'] = node.xpath('@id').extract()
    item['name'] = node.xpath('name').extract()
    item['description'] = node.xpath('description').extract()
    return item

class scrapy.contrib.spiders.CSVFeedSpider

用来爬取CSV文件网页,该spider除了其按行遍历而不是节点之外其他和XMLFeedSpider十分类似,而每次迭代时调用的是parse_row().

delimiter在CSV文件中用于区分字段的分隔符。类型为string。默认为’,’;

headers在CSV文件中包含的用来提取字段的行的列表。
**parse_row(response,row)**该方法接收一个response对象及一个以提供或检测出来的header为键的字典。该spider中,你可以覆盖adapt_response及process_results方法来进行预处理及后处理。

from scrapy import log
from scrapy.contrib.spiders import CSVFeedSpider
from myproject.items import TestItem

class MySpider(CSVFeedSpider):
    name = 'example.com'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/feed.csv']
    delimiter = ';'
    headers = ['id', 'name', 'description']
def parse_row(self, response, row):
    log.msg('Hi, this is a row!: %r' % row)

    item = TestItem()
    item['id'] = row['id']
    item['name'] = row['name']
    item['description'] = row['description']

class scrapy.contrib.spiders.SitemapSpider

SitemapSpider使你爬取网站时可以通过Sitemaps来发现爬取的URL。其支持嵌套的sitemap,并能从robots.txt会获取sitemap的url。

sitemap_urls包含你要爬取的url的sitemap的url列表。也可以指定为一个robots.txt,spider会从中分析并提取url。
sitemap_rules一个包含(regex,callback)元组的列表。

regex是一个用于匹配从sitemap提供的url的正则表达式;可以是一个字符串或者编译的正则对象。
callback指定了匹配正则表达式的url的处理函数。可以是一个字符串或者callale。规则按顺序进行匹配,之后第一个匹配才会被应用。如果忽略该属性,sitemap中发现的所有url将会被parse函数处理。
sitemap_follow一个用于匹配要跟进的sitemap的正则表达式的列表。其仅仅被应用在使用Sitemap index files来指向其他sitemap文件的站点。默认情况下所有的sitemap都回被跟进。
sitemap_alternate_links指定当一个url又可选的链接时,是否跟进。有些非英文网站会在一个url块内提供其他语言的网站链接。

<url>
    <loc>http://example.com/</loc>
    <xhtml:link rel="alternate" hreflang="de" href="http://example.com/de"/>
</url>

当 sitemap_alternate_links 设置时,两个URL都会被获取。 当 sitemap_alternate_links 关闭时,只有 http://example.com/ 会被获取。默认 sitemap_alternate_links 关闭。
样例
使用parse处理通过sitemap发现的所有url:

from scrapy.contrib.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']
def parse(self, response):
    pass # ... scrape item here ...

用特定的函数处理某些url,其他的使用另外的callback:

from scrapy.contrib.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/sitemap.xml']
    sitemap_rules = [
        ('/product/', 'parse_product'),
        ('/category/', 'parse_category'),
    ]
def parse_product(self, response):
    pass # ... scrape product ...

def parse_category(self, response):
    pass # ... scrape category ...

跟进robots.txt文件定义的sitemap并只跟进包含有…sitemap_shop的url:

from scrapy.contrib.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
        ('/shop/', 'parse_shop'),
    ]
    sitemap_follow = ['/sitemap_shops']
def parse_shop(self, response):
    pass # ... scrape shop here ...

在SitemapSpider中使用其他url:

from scrapy.contrib.spiders import SitemapSpider

class MySpider(SitemapSpider):
    sitemap_urls = ['http://www.example.com/robots.txt']
    sitemap_rules = [
        ('/shop/', 'parse_shop'),
    ]
other_urls = ['http://www.example.com/about']

def start_requests(self):
    requests = list(super(MySpider, self).start_requests())
    requests += [scrapy.Request(x, self.parse_other) for x in self.other_urls]
    return requests

def parse_shop(self, response):
    pass # ... scrape shop here ...

def parse_other(self, response):
    pass # ... scrape other here ...

获取响应数据中的cookie

返回的response中,无法通过 .cookies 获取cookie,只能通过从响应头中获取,但是获取的结果还得需要解析。

{b'Server': [b'Tengine'], b'Content-Type': [b'text/html; charset=UTF-8'], b'Date': [
b'Fri, 20 Jul 2018 13:43:42 GMT'], b'Cache-Control': [b'private'], b'Content-Language': [b'en'], 
b'Set-Cookie': [b'gpsd=5b05bcae8c6f4a273a53addfc8bbff22; domain=chouti.com; path=/; expires=Sun, 
19-Aug-2018 13:43:42 GMT', b'JSESSIONID=aaadbrXmU-Jh2_kvbaysw; path=/'], b'Vary': [b'Accept-Encoding'],
 b'Via': [b'cache15.l2nu29-1[69,0], kunlun9.cn125[73,0]'], b'Timing-Allow-Origin': [b'*'],
 b'Eagleid': [b'6a78b50915320942226762320e']}

所以,要通过scrapy封装的方法,将cookie解析出来

import scrapy
from scrapy.http.cookies import CookieJar

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/']
    cookie_dict = {}
    def parse(self, response):

        cookie_jar = CookieJar()
        cookie_jar.extract_cookies(response,response.request)   
        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
        print(self.cookie_dict)

备注:CookieJar中封装的内容特别丰富,print(cookie_jar._cookies) 包含很多

{'.chouti.com': {'/': {'gpsd': Cookie(version=0, name='gpsd', value='fcb9b9da7aaede0176d2a88cde8b6adb',
 port=None, port_specified=False, domain='.chouti.com', domain_specified=True, domain_initial_dot=False, 
path='/', path_specified=True, secure=False, expires=1534688487, discard=False, comment=None, 
comment_url=None, rest={}, rfc2109=False)}}, 'dig.chouti.com': {'/': {'JSESSIONID': 
Cookie(version=0, name='JSESSIONID', value='aaa4GWMivXwJf6ygMaysw', port=None, port_specified=False, 
domain='dig.chouti.com', domain_specified=False, domain_initial_dot=False, path='/',
 path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, 
rfc2109=False)}}}

自动登录抽屉并点赞和取消赞代码示例

import scrapy
from scrapy.http.response.html import HtmlResponse
# import sys,os,io
# sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
from ..items import XzxItem
from scrapy.http import Request
from scrapy.http.cookies import CookieJar

class ChoutiSpider(scrapy.Spider):
    name = 'chouti'
    allowed_domains = ['chouti.com']
    start_urls = ['https://dig.chouti.com/r/ask/hot/1']
    cookie_dict = {}
    def start_requests(self):
        for url in self.start_urls:
            yield Request(url=url,callback=self.parse)

    def parse(self, response):
        # 1. 去第一次访问页面中获取cookie
        # print(response.headers['Set-Cookie'],type(response.headers['Set-Cookie']))
        cookie_jar = CookieJar() # 空
        cookie_jar.extract_cookies(response, response.request) # cookie_jar中包含了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
        # 2. 向https://dig.chouti.com/login发送POST请求
        yield Request(
            url='https://dig.chouti.com/login',
            method='POST',
            body="phone=8615901492719&password=qwer1234&oneMonth=1",
            cookies=self.cookie_dict,
            headers={
                'user-agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
                'content-type':'application/x-www-form-urlencoded; charset=UTF-8',
            },
            callback=self.check_login
        )

    def check_login(self,response):
        print(response.text)
        yield Request(url='https://dig.chouti.com/',callback=self.index)

    def index(self,response):

        news_id_list = response.xpath('//div[@id="content-list"]//div[@class="part2"]/@share-linkid').extract()
        for news_id in news_id_list:
            # 赞
            """
            news_url = "https://dig.chouti.com/link/vote?linksId=%s" %(news_id,)
            yield Request(
                url=news_url,
                method='POST',
                cookies=self.cookie_dict,
                callback=self.output
            )
            """
            # 取消赞
            news_url = "https://dig.chouti.com/vote/cancel/vote.do"
            yield Request(
                url=news_url,
                method='POST',
                body="linksId=%s" %(news_id,),
                cookies=self.cookie_dict,
                headers={
                    'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
                },
                callback=self.output
            )

        # 2.1 获取页面
        page_list = response.xpath('//*[@id="dig_lcpage"]//a/@href').extract()
        for url in page_list:
            url = "https://dig.chouti.com" + url
            yield Request(url=url,callback=self.index)

    def output(self,response):
        print(response.text)

备注:爬取过程中的坑:请求头中,一定要携带content-type参数。请求过程中的url不能重复,尤其是和起始url。

我们可以使用urllib中的urlencode帮我们把数据转化为formdata格式的

from urllib.parse import urlencode

ret = {'name':'xxx','age':18}

print(urlencode(ret))

items

爬取的主要目的就是从非结构性的数据源提取结构性数据。Scrapy提供Item类来满足这样的需求。Item对象是种简单的容器,保存了爬取到的数据。其提供了类似于字典的API以及用于声明可用字段的简单语法。
声明Item

class Product(scrapy.Item):
    name=scrapy.Field()
    price=scrapy.Field()
    stock=scrapy.Field()
    last_updated=scrapy.Field(serializer=str)

Item字段
Field对象指明了每个字段的元数据。你可以为每个字段指明任何类型的元数据。设置Field对象的主要目的就是在一个地方定义好所有的元数据。
与Item配合
创建item

product = Product(name='Desktop PC', price=1000)
print product
Product(name='Desktop PC', price=1000)

获取字段值

> > > product['name']
> > > Desktop PC
> > > product.get('name')
> > > Desktop PC

> > > product['price']
> > > 1000

> > > product['last_updated']
> > > Traceback (most recent call last):
> > >     ...
> > > KeyError: 'last_updated'

> > > product.get('last_updated', 'not set')
> > > not set

> > > product['lala'] # getting unknown field
> > > Traceback (most recent call last):
> > >     ...
> > > KeyError: 'lala'

> > > product.get('lala', 'unknown field')
> > > 'unknown field'

> > > 'name' in product  # is name field populated?
> > > True

> > > 'last_updated' in product  # is last_updated populated?
> > > False

> > > 'last_updated' in product.fields  # is last_updated a declared field?
> > > True

> > > 'lala' in product.fields  # is lala a declared field?
> > > False

设置字段值

> > > product['last_updated'] = 'today'
> > > product['last_updated']
> > > today

> > > product['lala'] = 'test' # setting unknown field
> > > Traceback (most recent call last):
> > >     ...
> > > KeyError: 'Product does not support field: lala'

获取所有获取到的值

 print product.keys()
    # ['price', 'name']
    print product.items()
    # [('price', 1000), ('name', 'Desktop PC')]

复制item

product2=Product(product)
    print product2

{'name': 'Desktop PC', 'price': 1000}
product3=product2.copy()
print product3
{'name': 'Desktop PC', 'price': 1000}

根据item创建字典

print dict(product)
    # {'price': 1000, 'name': 'Desktop PC'}

根据字典创建item

print Product({'name': 'Laptop PC', 'price': 1500})
{'name': 'Laptop PC', 'price': 1500}

扩展Item
可以通过继承原始的Item来扩展item

class DiscountedProduct(Product):
    discount_percent = scrapy.Field(serializer=str)
    discount_expiration_date = scrapy.Field()

也可以通过使用原字段的元数据,添加新的值或修改原来的值来扩展字段元数据。

class SpecificProduct(Product):
    name = scrapy.Field(Product.fields['name'], serializer=my_serializer)

Item对象

class scrapy.item.Item([arg])

返回一个根据给定参数可选初始化的item,Item读值了标准的dict API。包括初始化函数也相同,为以额外添加的属性是:field,一个包含了所有声明的字段的字典,而不仅仅是货渠道的字段。

中间件

大多数时候裸奔的请求很容易被网站反爬技术识别,导致并不能获取到我们想要的数据,我们该怎么做呢?中间件就可以帮你解决这些事

下载中间件(Downloader middlewares)

Scrapy框架中的中间件主要分两类:蜘蛛中间件下载中间件。其中最重要的是下载中间件,反爬策略都是部署在下载中间件中的

蜘蛛中间件是介入到Scrapy的spider处理机制的钩子框架,可以添加代码来处理发送给 Spiders 的response及spider产生的item和request。

  1. 当蜘蛛传递请求和items给引擎的过程中,蜘蛛中间件可以对其进行处理(过滤出 URL 长度比 URLLENGTH_LIMIT 的 request。)
  2. 当引擎传递响应给蜘蛛的过程中,蜘蛛中间件可以对响应进行过滤(例如过滤出所有失败(错误)的 HTTP response)

下载中间件是处于引擎(Engine)和下载器(Downloader)之间的一层组件,可以有多个下载中间件被加载运行。

  1. 当引擎传递请求给下载器的过程中,下载中间件可以对请求进行处理 (例如增加http header信息,增加proxy信息等);
  2. 在下载器完成http请求,传递响应给引擎的过程中, 下载中间件可以对响应进行处理(例如进行gzip的解压等)
下载中间件三大函数

1. process_request(request, spider)——主要函数

当每个request通过下载中间件时,该方法被调用

需要传入的参数为:

  • request (Request 对象) – 处理的request
  • spider (Spider 对象) – 该request对应的spider

process_request() 必须返回其中之一: 返回 None 、返回一个 Response 对象、返回一个 Request 对象或raise IgnoreRequest

  • 如果其返回 None
    Scrapy将继续处理该request,执行其他的中间件的相应方法,直到合适的下载器处理函数(download handler)被调用, 该request被执行(其response被下载)
  • 如果其返回Response 对象
    Scrapy将不会调用任何其他的process_request()或 process_exception()方法,或相应的下载函数。其将返回该response,已安装的中间件的 process_response() 方法则会在每个response返回时被调用
  • 如果其返回 Request对象
    Scrapy则会停止调用 process_request方法并重新调度返回的request,也就是把request重新返回,进入调度器重新入队列
  • 如果其返回raise IgnoreRequest异常
    则安装的下载中间件的 process_exception()方法 会被调用。如果没有任何一个方法处理该异常, 则request的errback(Request.errback)方法会被调用。如果没有代码处理抛出的异常, 则该异常被忽略且不记录(不同于其他异常那样)

2. process_response(request, response, spider)——主要函数

当下载器完成http请求,传递response给引擎的时候,该方法被调用

需要传入的参数为:

  • request (Request 对象) – response所对应的request
  • response (Response 对象) – 被处理的response
  • spider (Spider 对象) – response所对应的spider

process_response() 必须返回以下之一:返回一个Response 对象、 返回一个Request 对象或raise IgnoreRequest 异常

  • 如果其返回一个 Response对象
    (可以与传入的response相同,也可以是全新的对象), 该response会被在链中的其他中间件的 process_response() 方法处理
  • 如果其返回一个 Request对象
    则中间件链停止, 返回的request会被重新调度下载。处理类似于 process_request() 返回request所做的那样
  • 如果其抛出一个IgnoreRequest异常
  • 则调用request的errback(Request.errback)。 如果没有代码处理抛出的异常,则该异常被忽略且不记录(不同于其他异常那样)
3. process_exception(request, exception, spider)

当下载处理器(download handler)或 process_request() (下载中间件)抛出异常(包括 IgnoreRequest 异常)时, Scrapy调用 process_exception()函数处理,但不处理process_response返回的异常

需要传入的参数为:

  • request (是 Request 对象) – 产生异常的request
  • exception (Exception 对象) – 抛出的异常
  • spider (Spider 对象) – request对应的spider

process_exception() 应该返回以下之一: 返回 None 、 一个 Response 对象、或者一个 Request 对象。

  • 如果其返回 None
    Scrapy将会继续处理该异常,接着调用已安装的其他中间件的 process_exception()方法,直到所有中间件都被调用完毕,则调用默认的异常处理
  • 如果其返回一个 Response 对象
    相当于异常被纠正了,则已安装的中间件链的 process_response()方法被调用。Scrapy将不会调用任何其他中间件的 process_exception()方法
  • 如果其返回一个 Request 对象
    则返回的request将会被重新调用下载。这将停止中间件的 process_exception() 方法执行,就如返回一个response的那样

UAMiddleware实例:request中加入随机User-Agent

爬虫神器——各大搜索引擎的User-Agent:
  • Chrome浏览器:‘Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36’,
  • 百度爬虫: ‘Mozilla/5.0 (compatible; Baiduspider/2.0; - +http://www.baidu.com/search/spider.html)’,
  • IE9浏览器: ‘Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)’,
  • 谷歌爬虫: ‘Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)’,
  • 必应爬虫: ‘Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)’,

本次实例依旧用的是上节课使用的58同城(city58)的示例代码:

1. city58_test:爬取两个页面的信息

import scrapy

from pyquery import PyQuery

from ..items import City58Item

class City58TestSpider(scrapy.Spider):

name = 'city58_test'

allowed_domains = ['58.com']

start_urls = ['http://bj.58.com/chuzu/',

'http://bj.58.com/chuzu/pn2/']


def parse(self, response):

jpy = PyQuery(response.text)

li_list = jpy('body > div.mainbox > div.main > div.content > div.listBox > ul > li').items()

for it in li_list:

a_tag = it(' div.des > h2 > a')

item = City58Item()

item['name'] = a_tag.text()

item['url'] = a_tag.attr('href')

item['price'] = it('div.listliright > div.money > b').text()

yield item

2. middleware.py:随机选取User-Agent,并把它赋值给传入进来的request

import random

class UAMiddleware(object):

*#定义一个User-Agent的List*

ua_list = [

'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 ',

'(KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36',

'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)',

'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)',

'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',

'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)',

]



def process_request(self, request, spider): *#对request进行拦截*

ua = random.choices(self.ua_list) 
#使用random模块,随机在ua_list中选取User-Agent

request.headers['User-Agent'] = ua *#把选取出来的User-Agent赋给request*

print(request.url) *#打印出request的url*

print(request.headers['User-Agent']) *#打印出request的headers*



def process_response(self, request, response, spider): 
#对response进行拦截

return response



def process_exception(self, request, exception, spider): 
#对process_request方法传出来的异常进行处理

pass

3. settings.py:在设置中开启UAMiddleware这个中间件

DOWNLOADER_MIDDLEWARES = {
‘city58.middlewares.UAMiddleware’: 543,
}

4. main.py:在main文件中运行爬虫,观察运行结果

系统默认提供的中间件

RetryMiddleware

该中间件将重试可能由于临时的问题,例如连接超时或者 HTTP 500 错误导致失败的页面。
process_response(self, request, response, spider)函数:判断是都有设置dont_retry以及判断response是否正常返回

def process_response(self, request, response, spider):

if request.meta.get('dont_retry', False): *#从meta中获取dont_retry关键字,如果为True,不重试,直接返回response;如果没有设置dont_retry关键字,则得到False值,继续执行下面判断。即默认重试*

return response

if response.status in self.retry_http_codes: *#查看response的返回码是否在重试返回码中*

reason = response_status_message(response.status) *#报错原因*

return self._retry(request, reason, spider) or response *#启用重试*

return response

process_exception(self, request, exception, spider):判断request异常

def process_exception(self, request, exception, spider):

if isinstance(exception, self.EXCEPTIONS_TO_RETRY) \

and not request.meta.get('dont_retry', False): *#判断process_request函数抛出的异常是否在EXCEPTIONS_TO_RETRY中,并且是否启动重试*

return self._retry(request, exception, spider)

_retry(self, request, reason, spider):重试函数

def _retry(self, request, reason, spider):

retries = request.meta.get('retry_times', 0) + 1



retry_times = self.max_retry_times *#最大重试次数*



if 'max_retry_times' in request.meta:

retry_times = request.meta['max_retry_times']



stats = spider.crawler.stats

if retries <= retry_times: *#判断是否达到最大重试次数*

logger.debug("Retrying %(request)s (failed %(retries)d times): %(reason)s",

{'request': request, 'retries': retries, 'reason': reason},

extra={'spider': spider}) *#重试日志*

retryreq = request.copy()

retryreq.meta['retry_times'] = retries *#累加重试次数*

retryreq.dont_filter = True *#设置不过滤*

retryreq.priority = request.priority + self.priority_adjust



if isinstance(reason, Exception):

reason = global_object_name(reason.__class__)



stats.inc_value('retry/count')

stats.inc_value('retry/reason_count/%s' % reason)

return retryreq

else:

stats.inc_value('retry/max_reached')

logger.debug("Gave up retrying %(request)s (failed %(retries)d times): %(reason)s",

{'request': request, 'retries': retries, 'reason': reason},

extra={'spider': spider})

CookiesMiddleware

Cookies的管理是通过CookiesMiddleware, 它属于DownloadMiddleware的一部分, 所有的requests和response都要经过它的处理。该中间件使得爬取需要cookie(例如使用session)的网站成为了可能。 其追踪了web server发送的cookie,并在之后的request中发送回去, 就如浏览器所做的那样。

Cookie和Session
  1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session.典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session
  2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户
  3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。所以,总结一下:Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式
设置cookies_enabled

COOKIES_ENABLED

默认: True
是否启用cookies middleware。如果关闭,cookies将不会发送给web server

COOKIES_DEBUG

默认: False
如果启用,Scrapy将记录所有在request(Cookie 请求头)发送的cookies及response接收到的cookies(Set-Cookie 接收头)

具体实现过程:

首先我们看处理request的部分
流程如下:

  1. 使用字典初始化多个cookies jar
  2. 把每个requests指定的cookies jar 提取出来
  3. 然后根据policy把requests中的cookies添加cookies jar
  4. 最后把cookies jar中合适的cookies添加到requests首部

代码:

class CookiesMiddleware(object):

"""This middleware enables working with sites that need cookies"""



def __init__(self, debug=False):

*# 用字典生成多个cookiesjar*

self.jars = defaultdict(CookieJar)

self.debug = debug







def process_request(self, request, spider):

if request.meta.get('dont_merge_cookies', False):

return

*# 每个cookiesjar的key都存储在 meta字典中*

cookiejarkey = request.meta.get("cookiejar")

jar = self.jars[cookiejarkey]

cookies = self._get_request_cookies(jar, request)

*# 把requests的cookies存储到cookiesjar中*

for cookie in cookies:

jar.set_cookie_if_ok(cookie, request)



*# set Cookie header*

*# 删除原有的cookies*

request.headers.pop('Cookie', None)

*# 添加cookiesjar中的cookies到requests header*

jar.add_cookie_header(request)

self._debug_cookie(request, spider)

接下来看看如何处理response中的cookies:

流程如下:

  1. 首先从cookies jar 字典中把requests对应的cookiesjar提取出来.
  2. 使用extract_cookies把response首部中的cookies添加到cookies jar
def process_response(self, request, response, spider):

if request.meta.get(*'dont_merge_cookies', False):*

return response



\# extract cookies from Set-Cookie and drop invalid/expired cookies

cookiejarkey = request.meta.get("cookiejar")

jar = self.jars[cookiejarkey]

jar.extract_cookies(response, request)

self._debug_set_cookie(response, spider)

return response

其他内置的downloader middleware

在这里插入图片描述

Item Pipeline

Item Pipeline简介

Item管道的主要责任是负责处理有蜘蛛从网页中抽取的Item,他的主要任务是清晰、验证和存储数据。
当页面被蜘蛛解析后,将被发送到Item管道,并经过几个特定的次序处理数据。
每个Item管道的组件都是有一个简单的方法组成的Python类。
他们获取了Item并执行他们的方法,同时他们还需要确定的是是否需要在Item管道中继续执行下一步或是直接丢弃掉不处理。

Item管道通常执行的过程有

清理HTML数据
验证解析到的数据(检查Item是否包含必要的字段)
检查是否是重复数据(如果重复就删除)
将解析到的数据存储到数据库中

编写自己的Item Pipeline

编写item管道其实是很容易的。
每个Item管道的组件都是由一个简单的方法组成的Python类:

process_item(item, spider)

每一个item管道组件都会调用该方法,并且必须返回一个item对象实例或raise DropItem异常。
被丢掉的item将不会在管道组件进行执行
此外,我们也可以在类中实现以下方法

open_spider(spider)

当spider执行的时候将调用该方法

close_spider(spider)

当spider关闭的时候将调用该方法
Item Pipeline例子

代码如下:

from scrapy.exceptions 
import DropItem
class PricePipeline( object ):
vat_factor =1.15
def process_item(self, item, spider):
if item['price']:
if item['price_excludes_vat']:
item['price'] =item['price'] 
self.vat_factor 
return item  
else:
raise DropItem("Missing price in %s"%item)

注:VAT:ValueAddedTax(增值税)

以上代码可以过滤那些没有价格的产品,并且对那些不包括增值税产品的价格进行调整

将抓取的items以json格式保存到文件中

从spider抓取到的items将被序列化为json格式,并且以每行一个item的形式被写入到items.jl文件中

代码:

import json 
class JsonWriterPipeline(object):
    def __init__(self):  
        self.file=open('items.jl', 'wb')
    def process_item(self, item, spider): 
        line =json.dumps(dict(item)) +"\n"
        self.file.write(line)
        return item

注:JsonWriterPipeline的目的是介绍如何编写项目管道。如果想要保存抓取的items到json文件中,推荐使用Feedexports

删除重复项

假设在spider中提取到的item有重复的id,那么我们就可以在process_item函数中进行过滤

如:

from scrapy.exceptions
import DropItem 
class  DuplicatesPipeline(object): 
def __init__(self): 
self.ids_seen =set() 
def process_item(self, item, spider): 
if item['id'] in self.ids_seen:
raise DropItem( "Duplicate item found: %s" % item) 
else: 
self.ids_seen.add(item['id'])
return item

激活ItemPipeline组件

在settings.py文件中,往ITEM_PIPELINES中添加项目管道的类名,就可以激活项目管道组件

如:

ITEM_PIPELINES ={ 'myproject.pipeline.PricePipeline': 300,   'myproject.pipeline.JsonWriterPipeline': 800, }

The integer values you assign to classes in this setting determine the order they run in- items go through pipelines from order number low to high

整数值通常设置在0-1000之间

**常用管道 **

1.图片下载管道:

获取指定的图片链接:

imgSrc = ul.xpath('.//img/@src2').extract()[0]

item['imgSrc'] = [imgSrc]

yield item

进入settings.py 里面,进行如下操作 :

ITEM_PIPELINES = {

'imageNet.pipelines.ImagenetPipeline': 300,

#scrapy中专门负责图片下载的管道
'scrapy.pipelines.images.ImagesPipeline':1
}

图片的存储路径

IMAGES_STORE = ‘imageDownLoad’

图片的下载地址 根据item中的字段来设置哪一个内容需要被下载

IMAGES_URLS_FIELD = ‘src’
2.小说下载管道:

找到指定的小说的下载链接,进行如下操作:

# 获取小说的下载地址、
     downloadUrl=response.xpath('//div[@class="showDown"]/ul/li[3]/script').extract_first('').split(',')[1].strip("'")
 print(downloadUrl)

 item['downloadUrl'] = [downloadUrl]

 yield item

进入settings.py:

ITEM_PIPELINES = {
   'qishu.pipelines.QishuPipeline': 300,
    #文件下载管道
    'scrapy.pipelines.files.FilesPipeline':1
}

FILES_STORE = 'file/book'

FILES_URLS_FIELD = 'downloadUrl'

scrapy-redis分布式

scrapy是python界出名的一个爬虫框架。Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。
虽然scrapy能做的事情很多,但是要做到大规模的分布式应用则捉襟见肘。有能人改变了scrapy的队列调度,将起始的网址从start_urls里分离出来,改为从redis读取,多个客户端可以同时读取同一个redis,从而实现了分布式的爬虫。

(一)scrapy-redis安装

安装:pip install scrapy-redis 官方站点:https://github.com/rolando/scrapy-redis

(二)scrapy-redis架构

在这里插入图片描述

(三)scrapy-Redis组件详解

如上图所示,scrapy-redis在scrapy的架构上增加了redis,基于redis的特性拓展了如下四种组件:Scheduler,Duplication Filter,Item Pipeline,Base Spider

1、Scheduler: scrapy改造了python本来的collection.deque(双向队列)形成了自己的Scrapy
queue,但是Scrapy多个spider不能共享待爬取队列Scrapy
queue,即Scrapy本身不支持爬虫分布式,scrapy-redis 的解决是把这个Scrapy
queue换成redis数据库(也是指redis队列),从同一个redis-server存放要爬取的request,便能让多个spider去同一个数据库里读取。Scrapy中跟“待爬队列”直接相关的就是调度器Scheduler,它负责对新的request进行入列操作(加入Scrapy
queue),取出下一个要爬取的request(从Scrapy
queue中取出)等操作。它把待爬队列按照优先级建立了一个字典结构,然后根据request中的优先级,来决定该入哪个队列,出列时则按优先级较小的优先出列。为了管理这个比较高级的队列字典,Scheduler需要提供一系列的方法。但是原来的Scheduler已经无法使用,所以使用Scrapy-redis的scheduler组件。

2、Duplication Filter

Scrapy中用集合实现这个request去重功能,Scrapy中把已经发送的request指纹放入到一个集合中,把下一个request的指纹拿到集合中比对,如果该指纹存在于集合中,说明这个request发送过了,如果没有则继续操作。这个核心的判重功能是这样实现的:

在这里插入图片描述

在scrapy-redis中去重是由Duplication Filter组件来实现的,它通过redis的set不重复的特性,巧妙的实现了DuplicationFilter去重。scrapy-redis调度器从引擎接受request,将request的指纹存入redis的set检查是否重复,并将不重复的request push写入redis的 request queue。

引擎请求request(Spider发出的)时,调度器从redis的request queue队列里根据优先级pop 出⼀个request 返回给引擎,引擎将此request发给spider处理。

3、Item Pipeline:

引擎将(Spider返回的)爬取到的Item给Item Pipeline,scrapy-redis 的Item Pipeline将爬取到的
Item 存入redis的 items queue。修改过Item Pipeline可以很方便的根据 key 从 items queue
提取item,从而实现 items processes集群。

4、Base Spider

不在使用scrapy原有的Spider类,重写的RedisSpider继承了Spider和RedisMixin这两个类,RedisMixin是用来从redis读取url的类。

当我们生成一个Spider继承RedisSpider时,调用setup_redis函数,这个函数会去连接redis数据库,然后会设置signals(信号):一个是当spider空闲时候的signal,会调用spider_idle函数,这个函数调用schedule_next_request函数,保证spider是一直活着的状态,并且抛出DontCloseSpider异常。一个是当抓到一个item时的signal,会调用item_scraped函数,这个函数会调用schedule_next_request函数,获取下一个request。

5、 总结

总结一下scrapy-redis的总体思路:这套组件通过重写scheduler和spider类,实现了调度、spider启动和redis的交互;

实现新的dupefilter和queue类,达到了判重和调度容器和redis的交互,因为每个主机上的爬虫进程都访问同一个redis数据库,所以调度和判重都统一进行统一管理,达到了分布式爬虫的目的;当spider被初始化时,同时会初始化一个对应的scheduler对象,这个调度器对象通过读取settings,配置好自己的调度容器queue和判重工具dupefilter;

每当一个spider产出一个request的时候,scrapy引擎会把这个reuqest递交给这个spider对应的scheduler对象进行调度,scheduler对象通过访问redis对request进行判重,如果不重复就把他添加进redis中的调度器队列里。当调度条件满足时,scheduler对象就从redis的调度器队列中取出一个request发送给spider,让他爬取;

当spider爬取的所有暂时可用url之后,scheduler发现这个spider对应的redis的调度器队列空了,于是触发信号spider_idle,spider收到这个信号之后,直接连接redis读取strart_url池,拿去新的一批url入口,然后再次重复上边的工作。

(四)从零搭建scrapy-redis分布式爬虫

1、scrapy-Redis分布式策略:

Slaver端从Master端拿任务(Request/url/ID)进行数据抓取,在抓取数据的同时也生成新任务,并将任务抛给Master。Master端只有一个Redis数据库,负责对Slaver提交的任务进行去重、加入待爬队列。

**优点:**scrapy-redis默认使用的就是这种策略,我们实现起来很简单,因为任务调度等工作scrapy-redis都已经帮我们做好了,我们只需要继承RedisSpider、指定redis_key就行了。

**缺点:**scrapy-redis调度的任务是Request对象,里面信息量比较大(不仅包含url,还有callback函数、headers等信息),导致的结果就是会降低爬虫速度、而且会占用Redis大量的存储空间。当然我们可以重写方法实现调度url。

在这里插入图片描述

2、安装Redis

下载redis:http://redis.io/download

安装完成后,拷贝一份Redis安装目录下的redis.conf到任意目录,建议保存到:/etc/redis/redis.conf 打开你的redis.conf配置文件,示例: 非Windows系统: sudo vim /etc/redis/redis.conf Master端redis.conf里注释bind 127.0.0.1,Slave端才能远程连接到Master端的Redis数据库。

3、创建项目

使用上面的scrapy的项目我们来修改一下,这个爬虫继承了RedisSpider,它能够支持分布式的抓取,采用的是basic spider,需要写parse函数。其次就是不再有start_urls了,取而代之的是redis_key,scrapy-redis将key从Redis里pop出来,成为请求的url地址。
修改spiders/hsw.py

在这里插入图片描述

修改settings.py

在这里插入图片描述

4、执行程序

通过runspider方法执行爬虫的py文件(也可以分次执行多条),爬虫(们)将处于等待准备状态:

scrapy runspider hsw.py

在Master端的redis-cli输入push指令,参考格式:

$redis > lpush myspider:start_urls http://finance.hsw.cn/hyxw/

在这里插入图片描述

5、获取数据

所有Slaver端将开始爬取数据,数据将保存在Redis数据库中,并共享Redis数据库的请求队列、请求指纹集合和数据队列。

在这里插入图片描述

实战学习

https://blog.csdn.net/killeri/article/category/7645130

Python爬虫scrapy-redis分布式实例

目标任务:将之前新浪网的Scrapy爬虫项目,修改为基于RedisSpider类的scrapy-redis分布式爬虫项目,将数据存入redis数据库。

一、item文件,和之前项目一样不需要改变

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

import scrapy
import sys
reload(sys)
sys.setdefaultencoding("utf-8")


class SinanewsItem(scrapy.Item):
    # 大类的标题和url
    parentTitle = scrapy.Field()
    parentUrls = scrapy.Field()

    # 小类的标题和子url
    subTitle = scrapy.Field()
    subUrls = scrapy.Field()

    # 小类目录存储路径
    subFilename = scrapy.Field()

    # 小类下的子链接
    sonUrls = scrapy.Field()

    # 文章标题和内容
    head = scrapy.Field()
    content = scrapy.Field()

二、spiders爬虫文件,使用RedisSpider类替换之前的Spider类,其余地方做些许改动即可,具体代码如下:

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

import scrapy
import os
from sinaNews.items import SinanewsItem
from scrapy_redis.spiders import RedisSpider
import sys
reload(sys)
sys.setdefaultencoding("utf-8")


class SinaSpider(RedisSpider):
    name = "sina"
    # 启动爬虫的命令
    redis_key = "sinaspider:strat_urls"
  # 动态定义爬虫爬取域范围
    def __init__(self, *args, **kwargs):
        domain = kwargs.pop('domain', '')
        self.allowed_domains = filter(None, domain.split(','))
        super(SinaSpider, self).__init__(*args, **kwargs)
      
    def parse(self, response):
        items= []
        # 所有大类的url 和 标题
        parentUrls = response.xpath('//div[@id="tab01"]/div/h3/a/@href').extract()
        parentTitle = response.xpath('//div[@id="tab01"]/div/h3/a/text()').extract()

        # 所有小类的ur 和 标题
        subUrls  = response.xpath('//div[@id="tab01"]/div/ul/li/a/@href').extract()
        subTitle = response.xpath('//div[@id="tab01"]/div/ul/li/a/text()').extract()

        #爬取所有大类
        for i in range(0, len(parentTitle)):
            
            # 爬取所有小类
            for j in range(0, len(subUrls)):
                item = SinanewsItem()

                # 保存大类的title和urls
                item['parentTitle'] = parentTitle[i]
                item['parentUrls'] = parentUrls[i]

                # 检查小类的url是否以同类别大类url开头,如果是返回True (sports.sina.com.cn 和 sports.sina.com.cn/nba)
                if_belong = subUrls[j].startswith(item['parentUrls'])

                # 如果属于本大类,将存储目录放在本大类目录下
                if(if_belong):
                    
                    # 存储 小类url、title和filename字段数据
                    item['subUrls'] = subUrls[j]
                    item['subTitle'] =subTitle[j]
                    items.append(item)

        #发送每个小类url的Request请求,得到Response连同包含meta数据 一同交给回调函数 second_parse 方法处理
        for item in items:
            yield scrapy.Request( url = item['subUrls'], meta={'meta_1': item}, callback=self.second_parse)

    #对于返回的小类的url,再进行递归请求
    def second_parse(self, response):
        # 提取每次Response的meta数据
        meta_1= response.meta['meta_1']

        # 取出小类里所有子链接
        sonUrls = response.xpath('//a/@href').extract()

        items= []
        for i in range(0, len(sonUrls)):
            # 检查每个链接是否以大类url开头、以.shtml结尾,如果是返回True
            if_belong = sonUrls[i].endswith('.shtml') and sonUrls[i].startswith(meta_1['parentUrls'])

            # 如果属于本大类,获取字段值放在同一个item下便于传输
            if(if_belong):
                item = SinanewsItem()
                item['parentTitle'] =meta_1['parentTitle']
                item['parentUrls'] =meta_1['parentUrls']
                item['subUrls'] = meta_1['subUrls']
                item['subTitle'] = meta_1['subTitle']
                item['sonUrls'] = sonUrls[i]
                items.append(item)

        #发送每个小类下子链接url的Request请求,得到Response后连同包含meta数据 一同交给回调函数 detail_parse 方法处理
        for item in items:
                yield scrapy.Request(url=item['sonUrls'], meta={'meta_2':item}, callback = self.detail_parse)

    # 数据解析方法,获取文章标题和内容
    def detail_parse(self, response):
        item = response.meta['meta_2']
        content = ""
        head = response.xpath('//h1[@id="main_title"]/text()')
        content_list = response.xpath('//div[@id="artibody"]/p/text()').extract()

        # 将p标签里的文本内容合并到一起
        for content_one in content_list:
            content += content_one

        item['head']= head[0] if len(head) > 0 else "NULL"
        item['content']= content

        yield item

三、settings文件设置

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

# 使用scrapy-redis里的去重组件,不使用scrapy默认的去重方式
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 使用scrapy-redis里的调度器组件,不使用默认的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 允许暂停,redis请求记录不丢失
SCHEDULER_PERSIST = True
# 默认的scrapy-redis请求队列形式(按优先级)
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
# 队列形式,请求先进先出
#SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue"
# 栈形式,请求先进后出
#SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack"

# 只是将数据放到redis数据库,不需要写pipelines文件
ITEM_PIPELINES = {
#    'Sina.pipelines.SinaPipeline': 300,
    'scrapy_redis.pipelines.RedisPipeline': 400,
}

# LOG_LEVEL = 'DEBUG'

# Introduce an artifical delay to make use of parallelism. to speed up the
# crawl.
DOWNLOAD_DELAY = 1
# 指定数据库的主机IP
REDIS_HOST = "192.168.13.26"
# 指定数据库的端口号
REDIS_PORT = 6379

执行命令:

本次直接使用本地的redis数据库,将settings文件中的REDIS_HOST和REDIS_PORT注释掉。

启动爬虫程序

scrapy runspider sina.py

执行程序后终端窗口显示如下:

img

表示程序处于等待状态,此时在redis数据库端执行如下命令:

redis-cli> lpush sinaspider:start_urls http://news.sina.com.cn/guide/

http://news.sina.com.cn/guide/为起始url,此时程序开始执行。

或:

import redis

# 将start_url 存储到redis中的redis_key中,让爬虫去爬取
redis_Host = "127.0.0.1"
redis_key = 'dangdang:start_urls'

# 创建redis数据库连接
rediscli = redis.Redis(host=redis_Host, port=6379, db="0")

# 先将redis中的requests全部清空
flushdbRes = rediscli.flushdb()
print("flushdbRes = {}".format(flushdbRes))
rediscli.lpush(redis_key, "http://book.dangdang.com/")

附录:命令行工具

创建项目

scrapy startproject myproject
该命令经会在myproject目录中创建一个Scrapy项目。进入到项目根目录,就可以使用scrapy命令来管理和控制你的项目了。
控制项目
有些Scrapy命令要求在Scrapy项目中运行。另外注意,有些命令在项目里运行时的效果有些区别。

scrapy -h
查看所有可用命令

scrapy -h
Scrapy提供了两种类型的命令。一种是必须在scrapy项目中运行,另一种则不与需要。

全局命令:

在这里插入图片描述

项目命令 |命令|语法|功能| |-|-| |crawl|scrapy crawl|使用spider进行爬取| |check|scrapy check [-l]|运行contract检查| |list|scrapy list|列出当前项目种所有可用的spider| |edit|scrapy edit|使用EDITOR种设定的编辑器给定的spider| |parse|scrapy parse [options]|获取给定的URL并使用响应的spider分析处理。| |genspider|scrapy genspider [-t template]|仅仅是创建spider的一种快捷方法。该方法可以用提前定义好的模板来生成spider,也可以自己创建spider的源码文件| |deploy|scrapy deploy [ | -l | -L ]|将项目部署到Scrapyd服务| |bench|scrapy bench|运行benchmark测试|
parse命令参数选项

–spider=SPIDER:跳过自动检测spider并强制使用特定的spider;
–a NAME=VALUE:设置spider的参数
–callback-cspider中用于解析返回的回调函数
–pipelines:在pipeline中处理item
–rules-r:使用CrawlSpider规则来发现用来解析返回的回调函数
–noitems不显示爬取道的item
–nolinks不显示爬取到的链接
–nocolour避免使用pygments对输出着色
–depth-d指定跟进链接诶请求的层数默认为1

BUG’

Introduce an artifical delay to make use of parallelism. to speed up the

crawl.

DOWNLOAD_DELAY = 1

指定数据库的主机IP

REDIS_HOST = “192.168.13.26”

指定数据库的端口号

REDIS_PORT = 6379


**执行命令:**

本次直接使用本地的redis数据库,将settings文件中的REDIS_HOST和REDIS_PORT注释掉。

**启动爬虫程序**

scrapy runspider sina.py


**执行程序后终端窗口显示如下:**

**[外链图片转存中...(img-cxHsPbHg-1567405272536)]**

**表示程序处于等待状态,此时在redis数据库端执行如下命令:**

redis-cli> lpush sinaspider:start_urls http://news.sina.com.cn/guide/


http://news.sina.com.cn/guide/为起始url,此时程序开始执行。

或:

import redis

将start_url 存储到redis中的redis_key中,让爬虫去爬取

redis_Host = “127.0.0.1”
redis_key = ‘dangdang:start_urls’

创建redis数据库连接

rediscli = redis.Redis(host=redis_Host, port=6379, db=“0”)

先将redis中的requests全部清空

flushdbRes = rediscli.flushdb()
print(“flushdbRes = {}”.format(flushdbRes))
rediscli.lpush(redis_key, “http://book.dangdang.com/”)




# 附录:命令行工具

创建项目

scrapy startproject myproject
该命令经会在myproject目录中创建一个Scrapy项目。进入到项目根目录,就可以使用scrapy命令来管理和控制你的项目了。
控制项目
有些Scrapy命令要求在Scrapy项目中运行。另外注意,有些命令在项目里运行时的效果有些区别。

scrapy <command> -h
查看所有可用命令

scrapy -h
Scrapy提供了两种类型的命令。一种是必须在scrapy项目中运行,另一种则不与需要。

全局命令:

[外链图片转存中...(img-J1QhO4oD-1567405272537)]

项目命令 |命令|语法|功能| |-|-| |crawl|`scrapy crawl `|使用spider进行爬取| |check|`scrapy check [-l] `|运行contract检查| |list|scrapy list|列出当前项目种所有可用的spider| |edit|`scrapy edit `|使用EDITOR种设定的编辑器给定的spider| |parse|`scrapy parse [options]`|获取给定的URL并使用响应的spider分析处理。| |genspider|`scrapy genspider [-t template] `|仅仅是创建spider的一种快捷方法。该方法可以用提前定义好的模板来生成spider,也可以自己创建spider的源码文件| |deploy|`scrapy deploy [ | -l | -L ]`|将项目部署到Scrapyd服务| |bench|`scrapy bench`|运行benchmark测试|
**parse命令参数选项**

–spider=SPIDER:跳过自动检测spider并强制使用特定的spider;
–a NAME=VALUE:设置spider的参数
–callback-cspider中用于解析返回的回调函数
–pipelines:在pipeline中处理item
–rules-r:使用CrawlSpider规则来发现用来解析返回的回调函数
–noitems不显示爬取道的item
–nolinks不显示爬取到的链接
–nocolour避免使用pygments对输出着色
–depth-d指定跟进链接诶请求的层数默认为1

–verbose-v显示每个请求的详细信息
  • 23
    点赞
  • 94
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aqiu12316

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值