Loading

scrapy-基础知识

一个分布式爬虫

https://github.com/gnemoug/distribute_crawler

开源的框架:
scrapy

Cola

https%3A//github.com/chineking/cola

PySpider

portia

spidy

garb

MechanicalSoup

Crawley

scrapy

官方文档 : https://docs.scrapy.org/en/latest/

官方示例 :  https://docs.scrapy.org/en/latest/_static/selectors-sample1.html

抓取示例网站:
http://quotes.toscrape.com/   

http://books.toscrape.com/

精通 scrap 网络爬虫

示例代码库

https://github.com/fangweiren/Scrapy_Book_Code

scrapy运行原理

旧版原理图:

引擎(Scrapy Engine),用来处理整个系统的数据流处理,触发事务。

调度器(Scheduler),用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。

下载器(Downloader),用于下载网页内容,并将网页内容返回给蜘蛛。

蜘蛛(Spiders),蜘蛛是主要干活的,用它来制订特定域名或网页的解析规则。编写用于分析response并提取item(即获取到的item)或额外跟进的URL的类。 每个spider负责处理一个特定(或一些)网站。

项目管道(Item Pipeline),负责处理有蜘蛛从网页中抽取的项目,他的主要任务是清晰、验证和存储数据。当页面被蜘蛛解析后,将被发送到项目管道,并经过几个特定的次序处理数据。

下载器中间件(Downloader Middlewares),位于Scrapy引擎和下载器之间的钩子框架,主要是处理Scrapy引擎与下载器之间的请求及响应。

蜘蛛中间件(Spider Middlewares),介于Scrapy引擎和蜘蛛之间的钩子框架,主要工作是处理蜘蛛的响应输入和请求输出。

调度中间件(Scheduler Middlewares),介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

  • Spiders(爬虫):负责处理所有Responses,从中分析提取数据,获取Items字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器)。
  • Engine(引擎):负责Spider、Item Pipeline、Downloader、Scheduler中间的通讯、信号以及数据传递等。
  • Scheduler(调度器):负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列和入队,当引擎需要时,交还给引擎。
  • Downloader(下载器):负责下载引擎发送的所有Requests请求,并将其获取到的Responses交还给引擎,由引擎交给Spider来处理。
  • Item Pipeline(管道):负责处理Spider中获取到的Items,并进行后期处理(如详细分析、过滤、存储等)。
  • Downloader Middlewares(下载中间件):一个可以自定义下载功能的组件。
  • Spider Middlewares(Spider中间件):一个可以自定义引擎和Spider交互的组件。
  • 通信的功能组件:如进入Spider的Responses和从Spider出去的Requests。

爬取流程:

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

新版原理图:

当对象在框架内的流动方式:

1,当Spider 要爬取某URL时,需要使用该URL构造一个Request对象,提交给ENGINE

2,REQUEST 对象随后进入到scheduler按某种算法进行排队,之后的某个时刻schdule 将其出队,送往downloader

3,downloader 根据request对象中的url 地址发送一次请求到目的网站服务器,之后用服务器的返回的http 响应构建一个Response 对象,其中包含了页面的html 文本

4,response 对象最终会送往spider 的页面解析函数,(构造request对象时指定),进行处理,页面解析函数从页面中提取数据,封装成item后提交给engine , item 随后被送往itempiplines 中进行处理, 最终可能由exporter 以某种数据格式写入文件, 另一方面,页面解析函数还可以从页面中提取链接URL ,构造出新的request 对象并提交给engine

数据流:

Scrapy中的数据流由执行引擎控制:

REQUEST RESPONSE ITEM

  1. 引擎打开一个网站(open a domain),找到处理该网站的Spider并向该spider请求第一个要爬取的URL(s)。
  2. 引擎从Spider中获取到第一个要爬取的URL并在调度器(Scheduler)以Request调度。
  3. 引擎向调度器请求下一个要爬取的URL。
  4. 调度器返回下一个要爬取的URL给引擎,引擎将URL通过下载中间件(请求(request)方向)转发给下载器(Downloader)。
  5. 一旦页面下载完毕,下载器生成一个该页面的Response,并将其通过下载中间件(返回(response)方向)发送给引擎。
  6. 引擎从下载器中接收到Response并通过Spider中间件(输入方向)发送给Spider处理。
  7. Spider处理Response并返回爬取到的Item及(跟进的)新的Request给引擎。
  8. 引擎将(Spider返回的)爬取到的Item给Item Pipeline,将(Spider返回的)Request给调度器。
  9. (从第二步)重复直到调度器中没有更多地request,引擎关闭该网站。

组件介绍

  • Scrapy Engine(引擎):引擎负责控制数据流在所有组件中的流动,并在相应动作发生时触发事件。 框架的核心,其他所有组件在起控制下协同工作
  • Scheduler(调度器):调度器从引擎接受Request并将他们入队,以便之后引擎请求他们时提供给引擎。 负责对spider 提交的下载请求进行调度
  • Downloader(下载器):下载器负责获取页面数据并提供给引擎,而后提供给Spiders。 负责下载页面
  • Spiders(爬虫):Spiders是用户编写用于分析Response并提取items(即获取到的items)或额外跟进的URL的类。 每个spider负责处理一个特定(或一些)网站。 对新页面产生下载请求
  • Item Pipeline(管道):Item Pipeline负责处理被Spider提取出来的items。典型的处理有清理、 验证及持久化(例如存取到数据库中)。 更多内容查看 Item Pipeline
  • Downloader Middlewares(下载器中间件):下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的Response(也包括引擎传递给下载器的Request),即处理下载请求。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看Downloader Middleware
  • Spider middlewares(爬虫中间件):Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理Spider的输入(Response)和输出(Items及Requests),即处理解析。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。更多内容请看 Spider Middleware

形象的数据流程:

1、引擎:怎么样,爬虫老弟,搞起来啊!
2、Spider:好啊,老哥,来来来,开始吧。今天就爬xxx网站怎么样
3、引擎:没问题,入口URL发过来!
4、Spider:呐,入口URL是https://ww.xxx.com。
5、引擎:调度器老弟,我这有request请求你帮我排序入队一下吧。
6、调度器:引擎老哥,这是我处理好的request。
7、引擎:下载器老弟,你按照下载中间件的设置帮我下载一下这个request请求。
8、下载器:可以了,这是下载好的东西。(如果失败:sorry,这个request下载失败了。然后引擎告诉调度器,这个request下载失败了,你记录一下,我们待会儿再下载)
9、引擎:爬虫老弟,这是下载好的东西,下载器已经按照下载中间件处理过了,你自己处理一下吧。
10、Spider:引擎老哥,我的数据处理完毕了,这里有两个结果,这个是我需要跟进的URL,还有这个是我获取到的Item数据。
11、引擎:管道老弟,我这儿有个item你帮我处理一下!
12、引擎:调度器老弟,这是需要跟进URL你帮我处理下。(然后从第四步开始循环,直到获取完需要全部信息)

Middlewares中间件

Spider Middlewares: 处理解析Items的相关逻辑修正,比如数据不完整要添加默认,增加其他额外信息等

  • process_spider_input(response, spider):当Response通过spider中间件时,该方法被调用,处理该Response。
  • process_spider_output(response, result, spider):当Spider处理Response返回result时,该方法被调用。
  • process_spider_exception(response, exception, spider):当spider或(其他spider中间件的) process_spider_input()抛出异常时, 该方法被调用。

Downloader Middlewares:处理发出去的请求(Request)和返回结果(Response)的一些回调

  • process_request(request, spider):当每个request通过下载中间件时,该方法被调用,这里可以修改UA,代理,Refferer
  • process_response(request, response, spider): 这里可以看返回是否是200加入重试机制
  • process_exception(request, exception, spider):这里可以处理超时

Request 对象

Request对像用来描述一个http请求, 构造器的方法:

Request(url[, callback, method='GET', headers, body, cookies, meta,
 encoding='utf-8', priority=0, dont_filter=False, errback])

Response 对象

Scrapy的开发流程

项目创建流程

$ scrapy   startproject  book 
  • scrapy.cfg
    项目的配置文件,带有这个文件的那个目录作为scrapy项目的根目录
  • items.py
    定义你所要抓取的字段
  • pipelines.py
    管道文件,当spider抓取到内容(item)以后,会被送到这里,这些信息(item)在这里会被清洗,去重,保存到文件或者数据库。
  • middlewares.py
    中间件,主要是对功能的拓展,你可以添加一些自定义的功能,比如添加随机user-agent, 添加proxy。
  • settings.py
    设置文件,用来设置爬虫的默认信息,相关功能开启与否,比如是否遵循robots协议,设置默认的headers,设置文件的路径,中间件的执行顺序等等。
  • spiders/
    在这个文件夹下面,编写你自定义的spider。

genspider**

genspider用于生成爬虫,与startproject不同的是,它只是生成爬虫模块文件,而startproject是生成整个scrapy项目。默认使用basic模板,使用-l参数可以查看所有可用的模板

scrapy genspider -l

scrapy genspider taobao taobao.com

创建爬虫模板的, 淘宝是新建爬虫唯一可识别的名称,example.com是start_urls

创建爬虫文件是根据scrapy的母版来创建的

创建一个基础母版爬虫,其他同理

scrapy genspider  -t 母版名称 爬虫文件名称 要爬取的域名 创建一个基础母版爬虫,其他同理
如:scrapy genspider -t basic filter baidu.com

注:此处创建的蜘蛛filter 可以不加-t basic,默认使用basic模板

scrapy check 爬虫文件名称 测试一个爬虫文件是否合规

如:scrapy check filter

scrapy crawl 爬虫名称 执行爬虫文件,显示日志

scrapy crawl 爬虫名称 --nolog 执行爬虫文件,不显示日志

https://www.jianshu.com/p/b8739df295f4 xpath

常用命令

## 全局命令
scrapy startproject     <project_name>  #当前目录创建一个项目
scrapy settings [options]               # 获取项目设定值
scrapy runspider <spider_file.py>     #未创建项目的情况下运行一个编写的spider 
scrapy shell http://www.baidu.com     #调试用, 一般会加上设置 user_agent 
scrapy  shell   'https://book.douban.com/tag/%E5%B0%8F%E8%AF%B4?start=660&type=T'  --set=USER_AGENT='Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'


scrapy fetch <url>    # 使用Scrapy下载器(downloader)下载给定的URL,并将获取到的内容送到标准输出。简单的来说,就是打印url的html代码
scrapy view  <url>    #在你的默认浏览器中打开给定的URL,并以Scrapy spider获取到的形式展现。 有些时候spider获取到的页面和普通用户看到的并不相同,一些动态加载的内容是看不到的, 因此该命令可以用来检查spider所获取到的页面
scrapy version [-v]  # 输出Scrapy版本。配合 -v 运行时,该命令同时输出Python, Twisted以及平台的信息

##项目命令
scrapy crawl <spider_name>  -o file_name    --nolog   # 启动项目 scrapy支持json/csv/xml等格式的文件
crapy check [-l] <spider>    # 运行contract检查,检查你项目中的错误之处
scrapy list					 # 列出当前项目中所有可用的spider。每行输出一个spider
scrapy genspider [-t template] <name> <domain>  # 在当前项目中创建spider。该方法可以使用提前定义好的模板来生成spider。您也可以自己创建spider的源码文件

setting .py配置

ROBOTSTXT_OBEY = False   #不遵循robots协议

#去掉下面的这个注释,以设置请求头信息,伪造浏览器headers,并手动添加一个user-agent
DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
  #user-agent新添加
  'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}


# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'

抓取分析

scrapy框架需要用户在spider子类中的操作:

  1. 爬虫从哪些页面开始爬取
  2. 对一个已经下载的页面,提取其中的那些数据
  3. 爬取当前页面后,接下来需要爬取哪个或哪些页面
# -*- coding:utf-8 -*-
import scrapy


class BooksSpider(scrapy.Spider):
    # 每一个爬虫的唯一标识
    name = 'books'

    # 定义爬虫爬取的起始点,起始点可以是多个,这里只有一个
    start_urls = ['http://books.toscrape.com/']

    def parse(self, response):
        # 提取数据
        # 每一本书的信息在<article class="product_pod">中,我们使用
        # css()方法找到所有这样的 article 元素,并依次迭代
        for book in response.css('article.product_pod'):
            name = book.xpath('./h3/a/@title').extract_first()
            price = book.css('p.price_color::text').extract_first()
            yield {
                'name': name,
                'price': price,
            }

        # 提取链接
        # 下一页的 url 在 ul.pager > li.next > a 里面
        # 例如:<li class="next"><a href="catelogue/page-2.html">next</a><li>
        next_url = response.css('ul.pager li.next a::attr(href)').extract_first()
        if next_url:
            # 如果找到下一页的 URL,得到绝对路径,构造新的 Request 对象
            next_url = response.urljoin(next_url)
            yield scrapy.Request(next_url, callback=self.parse)

理解:

爬虫会指定初始入口地址,scrapy 根据初始地址请求后的返回调用解析函数, 在解析函数里判断是抓取数据还是再次进行下一层级的爬取,然后调用解析函数 该解析函数的最后获取下一页的地址, 继续循环调用

实现一个spider 需要:

1,继承scrapy.Spider
2,为Spider 取名
3,设定起始的爬取点
4,实现页面解析函数

Spider类实现了:

供Scrapy 引擎调用的接口,,例如用来创建Spider 实例的类方法from_crawler
供用户使用的实用工具函数,例如可以调用log方法将调试信息输出到日志
供用户访问的属性,例如可以通过settings 属性访问配置文件的配置

命名

name = "books"

执行scrap crawl 命令就用到此标识,唯一性设置 ,告诉scrapy 使用哪个Spider 进行爬取

设定起始爬取点

通过start_urls 设定,通常为一个列表,其中放入所有的起始爬取点的url, 执行后,Spider 基类的start_request 函数会自动调用该变量,循环产生http请求,返回执行后的response

class Spider(object_ref):
 ...
 def start_requests(self):
 	for url in self.start_urls:
	     yield self.make_requests_from_url(url)
            
 def make_requests_from_url(self, url):
 	return Request(url, dont_filter=True)
 def parse(self, response):
	 raise NotImplementedError
 ...

请求原理:

1,对于起始爬取点的下载请求是由Scrapy 引擎调用Spider 对象的start_requests方法提交的,由于BookSpider 类中没有实现start_requests 的方法,因此引擎会调用spider 基类的start_requests 方法,

2,在start_requests 方法中,self.start_urls 是我们定义的起始爬取列表,通过访问实例属性,对其进行迭代, 用迭代出来的每个url 作为参数调用make_requests_from_url方法

3,make_requests_from_url方法中,有真正构造请求的Request对象的代码

4,由于构造Request对象时并没有传递callback参数指定页面的解析,,因此默认将prase 方法作为页面的解析函数, spider子类必须提供praser 方法,否则调用基类的该方法,从而抛出异常

5,起始爬取点可能有多个,start_requests 方法需要返回一个可迭代的对象(列表,生成器)其中每一个元素是一个Request对象,

由于爬取点的下载请求是由引擎调用Spider 对象的start_requests 方法产生的,我们可以在我们自己的Spider 子类中实现这个方法,即覆盖基类的start_request方法,直接构造并提交起始爬取点的Request对象,这样可以在Request 中添加特定的HTTP 请求头部和指定的页面解析函数

class BooksSpider(scrapy.Spider):
 # start_urls = ['http://books.toscrape.com/']
 # 实现start_requests 方法, 替代start_urls类属性
 def start_requests(self):
 	yield scrapy.Request('http://books.toscrape.com/',
 		callback=self.parse_book,
 		headers={'User-Agent': 'Mozilla/5.0'},
 		dont_filter=True)
 # 改用parse_book 作为回调函数
 def parse_book(response):

总结:

为爬虫设定起始爬取点的两种方式:
1,定义start_urls属性
2,实现start_requests 方法

实现页面解析函数

页面解析函数是构造Request对象时通过callback 参数指定的回调函数(或是默认的prase方法), 页面解析函数是实现Spider中最核心的部分,需要完成:

1,使用选择器提取页面中的数据,将数据封装后(Item,或字典)提交给Scrapy 引擎
2,使用选择器或LinkExport提取页面中的链接,用其构造新的Request对象并提交给Scrapy 引擎

使用ITEM 封装数据

注意可能抽取数据失败的情况, 如xpath 或css

class BooksSpider(scrapy.Spider):
 ...
 def parse(self, response):
 for sel in response.css('article.product_pod'):
 name = sel.xpath('./h3/a/@title').extract_first()
 price = sel.css('p.price_color::text').extract_first()
 yield {
 'name': name,
 'price': price,
 }
 ...

使用字典封装数据的缺点:

1,无法一目了然的了解数据中包含那些字段,影响代码可读性
2,缺乏对字段名字的检测,容易出错
3,不便于携带元数据,传递给其他组件的信息

scrapy 提供基类:

Item 基类: 自定义数据类
Field 类: 用来描述自定义数据类包含了哪些字段 (如name, price)

## items.py 文件中
from scrapy import Item, Field
class BookItem(Item):
     name = Field()
     price = Field()
    
# spider 类中
from ..items import BookItem
class BooksSpider(scrapy.Spider):
     ...
     def parse(self, response):
     for sel in response.css('article.product_pod'):
         book = BookItem()
         book['name'] = sel.xpath('./h3/a/@title').extract_first()  ##注意可能抽取失败的情况
         book['price'] = sel.css('p.price_color::text').extract_first()
     yield book
 

item 元数据的处理:

item pipline 处理数据

提取数据和封装数据后,需要对获取的数据进行处理 item pipline 是处理数据的组件, 一个Item Pipline 就是一个包含特定接口的类,通常只负责一种数据功能的数据处理,在一个项目中可以同时启用多个Item Pipline , 他们按次序级联起来,形成一条数据处理流水线

Item Pipeline 的几种典型应用:
1,清洗数据
2,验证数据的有效性
3,过滤掉重复的数据
4,将数据存入数据库

示例代码:

# -*- 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
from scrapy.item import Item
import pymongo


class PriceConverterPipeline(object):

    # 英镑兑换人民币汇率
    exchange_rate = 8.8345

    def process_item(self, item, spider):
        # 提取 item 的 price 字段(如£53.74)
        # 去掉前面英镑符号£,转换为 float 类型,乘以汇率
        price = float(item['price'][1:]) * self.exchange_rate

        # 保留 2 位小数,赋值回 item 的 price 字段
        item['price']= '¥%.2f' % price

        return item


class DuplicatesPipeline(object):

    def __init__(self):
        self.book_set = set()

    def process_item(self, item, spider):
        name = item['name']
        if name in self.book_set:
            raise DropItem("Duplicate book found:%s" % item)

        self.book_set.add(name)
        return item


class MongoDBPipeline(object):
    @classmethod
    def from_crawler(cls, crawler):
        cls.DB_URI = crawler.settings.get('MONGO_DB_URI', 'mongodb://localhost:27017/')
        cls.DB_NAME = crawler.settings.get('MONGO_DB_NAME', 'scrapy_data')

        return cls()

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.DB_URI)
        self.db = self.client[self.DB_NAME]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        collection = self.db[spider.name]
        post = dict(item) if isinstance(item, Item) else item
        collection.insert_one(post)

        return item
    
    
# mongodb 配置解释:
# 在Spider 整个爬取过程中,数据库的连接和关闭只需要操作一次, 应在开始处理数据前连接数据库,在处理完所有数据后关闭数据库, 因此实现以下两个方法,open_spider(self, spider):  close_spider(self, spider):
# 
# 在processor_item 中实现Mongodb数据库的写入操作, 使用self.db 和spider.name 获取一个集合, 然后将数据插入集合,集合对象的insert_one 方法需要传入一个字典对象(不能传入Item 对象) 因此在调用前先对item 的类型进行判断,如果是Item 对象,就将其转化为字典.


##注意此处获取的 mongodb 的路径地址是根据配置文件中设置获得

代码的解释:

一个pipline 不需要继承特定的基类,只需要实现某些特定方法, 例如 process_item open_sipder , close_spider
一个item pipline 必须实现一个process_item(item,spider)方法,该方法用来处理每一项由Spider爬取到的数据, 其中两个参数:

Item 爬取到的每一项数据, item 或字典
Spider 爬取此项数据的Spider 对象

process_item方法是Item Pipeline 的核心, 补充2点:

如果process_item 在处理某项item 时返回了一项数据(item 或字典), 返回的数据会递送给下一级Item pipline (如果有),继续处理
如果process_item 在处理某项item 时抛出DropItem 异常,该项item 就会被抛弃,不再递送给后面的ItemPipline继续处理,也不会导出到文件, 通常在检测到无效数据或是想要过滤的数据时,抛出DropItem 异常

必须实现的process_item 方法外,还有3 个常见的方法

open_spider(self, spider )
Spider 打开时(处理数据前)回调该方法,通常该方法用于在开始处理数据前完成某种初始化工作,如连接数据库

close_spider(self, spider )
Spider 关闭时(处理数据后)回调该方法,通常该方法用于在处理完数据后完成某种清理工作,如关闭数据库

from_crawler(cls,crawler)
创建ItemPipline 对象时回调该类函数, 通常,在该方法中通过crawler.settings 读取配置,根据配置创建ItemPipline 对象

启用Item Pipline

在Scrapy 中,Itempipline 是可选的组件, 想要启用某个(或某些)ItemPipline需要在配置文件settings.py配置

ITEM_PIPELINES = {
 'example.pipelines.PriceConverterPipeline': 300,
 }

## example 是创建的项目名字, 即是项目根目录下的目录, piplines 指该目录下的文件名字,pipelines.py   
# PriceConverterPipeline 指的是创建的item pipline 类

ITEM_PIPELINES 是一个字典,把想要启用的Item Pipline 添加到这个字典中,其中每一项的键是每一个Item Pipline 类的导入路径, 值是一个0~100 的数字, 同时启用多个Item Pipline时,Scrapy 根据这些数值决定Item Pipline 处理数据的先后次序,数值小的在前.

LinkExtractor 链接提取

抽取页面内的链接

示例:

from scrapy.linkextractors import LinkExtractor 
 # 提取链接
 # 下一页的url在 ul.pager > li.next >a 
 # 例如: <li class="next"><a href="catalogue/page-2.html">next</a></li>
 le = LinkExtractor(restrict_css='ul.pager li.next')
 links = le.extract_links(response)
 if links:
 next_url = links[0].url
 yield scrapy.Request(next_url, callback=self.parse)

创建流程:

导入LinkExtractor,位于scrapy.linkextractor 模块中
创建一个LinkExtractor对象,使用一个或多个构造器参数描述规则,这里传递的restrict_css是一个css选择器,描述了下一页链接所在的区域,
调用LinkExtractor对象的extract_links方法传入一个Response 对象,该方法一句创建对象时所设置的提取规则,在Response对象所包含的页面中提取链接, 最终返回一个列表,其中每一个元素都是一个Link 对象, 即提取到的一个链接
页面中的下一页链接只有一个,因此用links[0]获取LInk对象,该对象的url 属性就是链接页面的绝对url,用其构造Request 对象并提交

LinkExtractor构造器参数

构造器参数指明获取如何获取链接url

  • allow
    接收一个正则表达式或一个正则表达式列表,提取绝对url与正则表达式匹配的链接, 如果该参数为空(默认),则提取全部链接

    pattern = '/intro/.+\.html$'
    le = LinkExtractor(allow=pattern)
    links = le.extract_links(response1)
    [link.url for link in links]
    
  • deny
    接收一个正则表达式或一个正则表达式列表, 与allow相反,排除绝对 url 与正则表达式匹配的链接

    from urllib.parse import urlparse
    pattern = '^' + urlparse(response1.url).geturl()
    le = LinkExtractor(deny=pattern)
    links = le.extract_links(response1)
    [link.url for link in links]
    
  • allow_domains
    接收一个域名或一个域名列表,提取指定域的链接

    domains = ['github.com', 'stackoverflow.com']  # 提取这两个域名的链接
    le = LinkExtractor(allow_domains=domains)
    links = le.extract_links(response1)
    [link.url for link in links]
    
  • deny_domains
    接收一个域名或一个域名列表,与allow_domains 相反

    le = LinkExtractor(deny_domains='github.com')
    links = le.extract_links(response1)
    [link.url for link in links]
    
  • restrict_xpaths
    接收一个Xpath 表达式或者一个xpath表达式列表,提取该表达式选中区域下的链接

    le = LinkExtractor(restrict_xpaths='//div[@id="top"]')
    links = le.extract_links(response1)
    [link.url for link in links]
    
  • restric_css
    接收css选择器

    le = LinkExtractor(restrict_css='div#bottom')   # <div id='bottom'>下的链接
    links = le.extract_links(response1)
    [link.url for link in links]
    
  • tags
    接收一个标签(字符串)或一个标签列表,提取指定标签内的链接,默认为['a','area']

  • attrs
    接收一个属性(字符串)或一个属性列表,提取指定属性内的链接,默认['href']

    le = LinkExtractor(tags='script', attrs='src')   
    links = le.extract_links(response2)
    [link.url for link in links]
    

    所以,如果一个LinkExtractor对象如果构造时没有传入任何参数,那么默认获取页面内的所有a标签的href 属性值

  • process_value
    接收一个形如func(vlaue)回调函数,若果传递了该参数,LinkExtractor 将调用该回调函数对提取的每一个链接(如a的href)进行处理,回调函数正常情况下应该返回一个字符串(即处理结果),如果想要抛弃所处理的链接时,返回None

    import re
    def prosess(value):
        m = re.search("javascript:goToPage\('(.*?)'", value)
        if m:
            value = m.group(1)
        return value
    
    le = LinkExtractor(process_value=prosess)   
    links = le.extract_links(response2)
    [link.url for link in links]
    

示例操作

body1 = '''
<!-- example1.html -->
 <html>
 <body>
 <div id="top">
 <p>下面一些站内的链接</p>
 <a class="internal" href="/intro/install.html">Installation guide</a>
 <a class="internal" href="/intro/tutorial.html">Tutorial</a>
 <a class="internal" href="../examples.html">Examples</a>
 </div>
 <div id="bottom">
 <p>下面一些站外的链接</p>
 <a href="http://stackoverflow.com/tags/scrapy/info">StackOverflow</a>
 <a href="https://github.com/scrapy/scrapy">Fork on Github</a>
 </div>
 </body>
 </html>
 <!-- example2.html -->
 <html>
'''

body2 = '''
<html>
<head>
 <script type='text/javascript' src='/js/app1.js'/>
 <script type='text/javascript' src='/js/app2.js'/>
 </head>
 <body>
 <a href="/home.html">主页</a>
 <a href="javascript:goToPage('/doc.html'); return false">᮷ẓ</a>
 <a href="javascript:goToPage('/example.html'); return false">Ṹ例</a>
 </body>
 </html>
'''
from scrapy.linkextractors import LinkExtractor
from scrapy.http import HtmlResponse
response1 = HtmlResponse(url='http://example1.com', body=body1, encoding='utf8')
response2 = HtmlResponse(url='http://example1.com', body=body2, encoding='utf8')

1,默认提取所有的链接

le = LinkExtractor()           #创建一个对象,如果构造器没有任何规则,那么就提取页面内的所有链接
links = le.extract_links(response1)
[link.url for link in links]
['http://example1.com/intro/install.html',
 'http://example1.com/intro/tutorial.html',
 'http://example1.com/examples.html',
 'http://stackoverflow.com/tags/scrapy/info',
 'https://github.com/scrapy/scrapy']

2,提取匹配的正则表达式

pattern = '/intro/.+\.html$'
le = LinkExtractor(allow=pattern)
links = le.extract_links(response1)
[link.url for link in links]
['http://example1.com/intro/install.html',
 'http://example1.com/intro/tutorial.html']

3,正则,拒绝该模式

from urllib.parse import urlparse
pattern = '^' + urlparse(response1.url).geturl()
le = LinkExtractor(deny=pattern)
links = le.extract_links(response1)
[link.url for link in links]
['http://stackoverflow.com/tags/scrapy/info',
 'https://github.com/scrapy/scrapy']

4,允许的域匹配

domains = ['github.com', 'stackoverflow.com']
le = LinkExtractor(allow_domains=domains)
links = le.extract_links(response1)
[link.url for link in links]
['http://stackoverflow.com/tags/scrapy/info',
 'https://github.com/scrapy/scrapy']

5,域的拒绝匹配

le = LinkExtractor(deny_domains='github.com')
links = le.extract_links(response1)
[link.url for link in links]
['http://example1.com/intro/install.html',
 'http://example1.com/intro/tutorial.html',
 'http://example1.com/examples.html',
 'http://stackoverflow.com/tags/scrapy/info']

6,xpath提取指定标签内链接

le = LinkExtractor(restrict_xpaths='//div[@id="top"]')
links = le.extract_links(response1)
[link.url for link in links]
['http://example1.com/intro/install.html',
 'http://example1.com/intro/tutorial.html',
 'http://example1.com/examples.html']

7,css 提取指定标签

le = LinkExtractor(restrict_css='div#bottom')   # <div id='bottom'>下的链接
links = le.extract_links(response1)
[link.url for link in links]
['http://stackoverflow.com/tags/scrapy/info',
 'https://github.com/scrapy/scrapy']

8,自定义标签内属性的提取

le = LinkExtractor(tags='script', attrs='src')   
links = le.extract_links(response2)
[link.url for link in links]
['http://example1.com/js/app1.js', 'http://example1.com/js/app2.js']
le = LinkExtractor()   
links = le.extract_links(response1)
[link.url for link in links]
['http://example1.com/intro/install.html',
 'http://example1.com/intro/tutorial.html',
 'http://example1.com/examples.html',
 'http://stackoverflow.com/tags/scrapy/info',
 'https://github.com/scrapy/scrapy']

9,自定义函数内抽取

自定义提取是以已经提取的数据的url 链接为参数,对每一个url 进行处理,如果符合处理逻辑就保存,否则抛弃

import re
def prosess(value):
    m = re.search("javascript:goToPage\('(.*?)'", value)
    if m:
        value = m.group(1)
    return value
le = LinkExtractor(process_value=prosess)   
links = le.extract_links(response2)
[link.url for link in links]
['http://example1.com/home.html',
 'http://example1.com/doc.html',
 'http://example1.com/example.html']

相对链接的提取

有些链接写的是相对链接,使用该方法后,不用再对url 进行组合,程序会自动识别并请求

<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
    </li>
</ul>

解析地址

>>> response.css('li.next a').get()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'

>>> response.css('li.next a::attr(href)').get()
'/page/2/'

>>> response.css('li.next a').attrib['href']
'/page/2/'

使用join组合url

import scrapy
class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            next_page = response.urljoin(next_page)  ##重点
            yield scrapy.Request(next_page, callback=self.parse)

使用response.follow

import scrapy
class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('span small::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)   ##重点      

通过一个可迭代的url 列表批量构建多个请求

anchors = response.css('ul.pager a')
yield from response.follow_all(anchors, callback=self.parse)

#或者
yield from response.follow_all(css='ul.pager a', callback=self.parse)

更多的示例:

import scrapy
class AuthorSpider(scrapy.Spider):
    name = 'author'

    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        author_page_links = response.css('.author + a')
        yield from response.follow_all(author_page_links, self.parse_author)

        pagination_links = response.css('li.next a')
        yield from response.follow_all(pagination_links, self.parse)

    def parse_author(self, response):
        def extract_with_css(query):
            return response.css(query).get(default='').strip()   ##如果没有数据则使用默认值

        yield {
            'name': extract_with_css('h3.author-title::text'),
            'birthdate': extract_with_css('.author-born-date::text'),
            'bio': extract_with_css('.author-description::text'),
        }   

代码示例

import scrapy


class QuotesSpider(scrapy.Spider):
    name = 'quotes'
    start_urls = [
        'http://quotes.toscrape.com/tag/humor/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'author': quote.xpath('span/small/text()').get(),
                'text': quote.css('span.text::text').get(),
            }

        next_page = response.css('li.next a::attr("href")').get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

执行

scrapy runspider quotes_spider.py -o quotes.json

运行机制

scrapy 寻找在内部定义的spider ,并使用crawl engine 运行它

通过对start_urls的 请求,抓取开始了,抓取成功后将请求的响应作为参数,调用当前的解析方法

在解析方法中,还获取下一此需要请求的地址,如果地址部位空,则继续调用请求,请求后发返回继续调用本解析完成

scrapy 还提供的功能:

内置的从 html 获取数据,CSS selectors and XPath expressions

安装

pip install Scrapy

scrapy 由Python 编写,并依靠数个关键的包

  • lxml, an efficient XML and HTML parser
  • parsel, an HTML/XML data extraction library written on top of lxml,
  • w3lib, a multi-purpose helper for dealing with URLs and web page encodings
  • twisted, an asynchronous networking framework
  • cryptography and pyOpenSSL, to deal with various network-level security needs

Scrapy Tutorial

首先创建一个爬去项目, 进入一个目录

scrapy startproject tutorial

#所创建文件的作用
tutorial/
    scrapy.cfg            # deploy configuration file
    tutorial/             # project's Python module, you'll import your code from here
        __init__.py
        items.py          # project items definition file
        middlewares.py    # project middlewares file
        pipelines.py      # project pipelines file
        settings.py       # project settings file
        spiders/          # a directory where you'll later put your spiders
            __init__.py

第一个抓取Spider

spider 是定义的类,通过它scrapy 从一个站点来抓取信息

在目录spiders/ 下创建抓取文件,quotes_spider.py

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"  #唯一性的标示了一个抓取蜘蛛,

    def start_requests(self):  #  必须返回可迭代的请求,
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response): #将要被调用的解析方法,用来解析访问后返回的请求,抓取数据,获取新的url,并继续构建新的请求
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)

运行方式scrapy crawl quotes #蜘蛛的唯一性名字

背后运行的模式分析:

Scrapy schedules the scrapy.Request objects returned by the start_requests method of the Spider. Upon receiving a response for each one, it instantiates Response objects and calls the callback method associated with the request (in this case, the parse method) passing the response as argument.

这种方式是重写了spider类的方法,另外一种方式是只提供起始需要抓取的URLs 并配置解析函数, start_requests() 函数使用默认spider 类自己的方法

示例如下:

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
scrapy shell 'http://quotes.toscrape.com/page/1/'

>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]

>>> response.css('title::text').getall()
['Quotes to Scrape']

>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']

>>> response.css('title::text').get()
'Quotes to Scrape'

>>> response.css('title::text')[0].get()
'Quotes to Scrape'


>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']

XPath: a brief intro

>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'

抽取数据示例

Extracting quotes and authors

$ scrapy shell 'http://quotes.toscrape.com'

>>> response.css("div.quote")
[<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 <Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
 ...]
 
 
>>> quote = response.css("div.quote")[0]

>>> text = quote.css("span.text::text").get()
>>> text
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").get()
>>> author
'Albert Einstein'


>>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']


>>> for quote in response.css("div.quote"):
...     text = quote.css("span.text::text").get()
...     author = quote.css("small.author::text").get()
...     tags = quote.css("div.tags a.tag::text").getall()
...     print(dict(text=text, author=author, tags=tags))
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”', 'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'tags': ['abilities', 'choices']}
...



Extracting data in our spider

import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

存储数据

scrapy crawl quotes -o quotes.json

Using spider arguments

根据执行时传递的请求来执行程序

scrapy crawl quotes -o quotes-humor.json -a tag=humor
import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        url = 'http://quotes.toscrape.com/'
        tag = getattr(self, 'tag', None)
        if tag is not None:
            url = url + 'tag/' + tag
        yield scrapy.Request(url, self.parse)

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
            }

        next_page = response.css('li.next a::attr(href)').get()
        if next_page is not None:
            yield response.follow(next_page, self.parse)

If you pass the tag=humor argument to this spider, you’ll notice that it will only visit URLs from the humor tag, such as http://quotes.toscrape.com/tag/humor

spider

蜘蛛是定义如何刮除某个站点(或一组站点)的类,包括如何执行爬网(即跟踪链接)以及如何从其页面中提取结构化数据(即抓取项)。 换句话说,Spider是您定义自定义行为的地方,该行为用于爬网和解析特定站点(或在某些情况下为一组站点)的页面。

对于Spider,抓取周期将经历如下过程:

  1. 首先,生成初始请求以抓取第一个URL,然后指定要使用从这些请求下载的响应进行调用的回调函数。要执行的第一个请求是通过调用start_requests()方法获得的,该方法(默认情况下)会生成对start_urls中指定的URL的请求,并使用parse方法作为Requests的回调函数。
  2. 在回调函数中,您解析响应(网页)并返回项目对象,Request对象或这些对象的可迭代对象。这些请求还将包含一个回调(可能相同),然后由Scrapy下载,然后由指定的回调处理响应。
  3. 在回调函数中,通常使用选择器(但也可以使用BeautifulSoup,lxml或您喜欢的任何机制)来解析页面内容,并使用已解析的数据生成项目。
  4. 最后,通常将从蜘蛛返回的项目保存到数据库(在某些项目管道中)或使用Feed导出写入文件。即使此周期(或多或少)适用于任何种类的蜘蛛,Scrapy中还是捆绑了不同种类的默认蜘蛛以用于不同目的。我们将在这里讨论这些类型。

Spider类定义了如何爬取某个网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item),Spider就是你定义爬取的动作及分析某个网页(或者是有些网页)的地方

常用的类:

  • scrapy.spiders.Spider
  • scrapy.spiders.CrawlSpider

scrapy.Spider

name
定义spider名字的字符串(string)。spider的名字定义了Scrapy如何定位(并初始化)spider,所以其必须是唯一的。name是spider最重要的属性,而且必须

allowed_domains
可选。包含了spider允许爬取的域名(domain)列表(list)。 当 OffsiteMiddleware 启用时, 域名不在列表中的URL不会被跟进。

start_urls
URL列表。当没有制定特定的URL时,spider将从该列表中开始进行爬取。 因此,第一个被获取到的页面的URL将是该列表之一。 后续的URL将会从获取到的数据中提取

start_requests()
该方法必须返回一个可迭代对象(iterable)。该对象包含了spider用于爬取的第一个Request。该方法仅仅会被Scrapy调用一次,可以将其实现为生成器。
该方法的默认实现是使用 start_urls 的url生成Request。我们可以重写该方法来实现定制

class MySpider(scrapy.Spider):
    name = 'myname'

    def start_requests(self):
        return [scrapy.FormRequest("http://www.example.com/login",
                                   formdata={'user': 'john', 'pass': 'secret'},
                                   callback=self.logged_in)]

    def logged_in(self, response):
        print('登陆成功')
        pass

parse(response)
当response没有指定回调函数时,该方法是Scrapy处理下载的response的默认方法。parse 负责处理response并返回处理的数据以及(/或)跟进的URL。
该方法及其他的Request回调函数必须返回一个包含 Request、dict 或 Item 的可迭代的对象。简单的来说,所谓的回调函数,其实就是告诉spider,在拿到了网站的response以后,交给谁来进行处理后面的页面的解析工作

示例:

import scrapy
from myproject.items import MyItem

class MySpider(scrapy.Spider):
    name = 'cnblog'
    allowed_domains = ['cnblogs.com']

    start_urls = ['http://www.cnblogs.com.com/123.html',
                  'http://www.cnblogs.com.com/234.html', 
                  'http://www.cnblogs.com.com/345.html'
                  ]
                  
    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        for h3 in response.xpath('//h3').extract():
            item = MyItem()
            item['title'] = h3
            yield item

        for url in response.xpath('//a/@href').extract():
            yield scrapy.Request(url, callback=self.parse)

scrapy.CrawlSpider

class scrapy.spiders.CrawlSpider

主要处理的是 url 比较一致的情况.

http://www.cnblogs.com/cnkai/p/7397421.html

,http://www.cnblogs.com/cnkai/p/7396835.html比如我们想爬取我的博客内容,会发现,除了最后的几个数字不同,其他的内容是相同的,可以通过这个类来进行自动抓取相似的链接

CrawlSpider类定义了如下的属性和方法。

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

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

  • link_extractor
    这是一个 Link Extractor 对象。 其定义了如何从爬取到的页面提取链接。
  • callback
    这是一个callable或string(该spider中同名的函数将会被调用)。 从link_extractor中每获取到链接时将会调用该函数。该回调函数接受一个response作为其第一个参数, 并返回一个包含 Item 以及(或) Request 对象(或者这两者的子类)的列表(list)
  • cb_kwargs
    包含传递给回调函数的参数(keyword argument)的字典。
  • follow
    是一个布尔(boolean)值,指定了根据该规则从response提取的链接是否需要跟进。 如果 callback 为None, follow 默认设置为 True ,否则默认为 False 。
  • process_links
    是一个callable或string(该spider中同名的函数将会被调用)。 从link_extractor中获取到链接列表时将会调用该函数。该方法主要用来过滤。
  • process_request
    是一个callable或string(该spider中同名的函数将会被调用)。 该规则提取到每个request时都会调用该函数。该函数必须返回一个request或者None。 (用来过滤request)
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor

class MySpider(CrawlSpider):
    name = 'cnblog'
    allowed_domains = []
    start_urls = ['http://www.cnblogs.com']

    rules = (
        Rule(LinkExtractor(allow=('http://www.cnblogs.com/\w+/p/\d+.html', )), callback='parse_item', follow=True),
    )

    def parse_item(self, response):
        print(response.url)

Selectors

抓取网页后抽取数据的方式

示例网页代码

<html>
 <head>
  <base href='http://example.com/' />
  <title>Example website</title>
 </head>
 <body>
  <div id='images'>
   <a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
   <a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
   <a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
   <a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
   <a href='image5.html'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
  </div>
 </body>
</html>

打开shell 执行:

scrapy shell https://docs.scrapy.org/en/latest/_static/selectors-sample1.html

构造一个XPATH来选择内部的文本

文本也算是一个节点, html 标签内如果没有文本, 那么获取的该标签内的文本则为 \n

示例获取:

scrapy 请求后返回的response 结果默认含有抽取数据的xpath 方法和 css 方法

>>> response.xpath('//title/text()')
[<Selector xpath='//title/text()' data='Example website'>]

>>> response.xpath('//title/text()').getall()
['Example website']
>>> response.xpath('//title/text()').get()
'Example website'

selector .get() or .getall() methods get 获取单个元素(列表内的第一个元素), getall 获取所有元素

css 选择器

>>> response.css('title::text').get()
'Example website'

也可以嵌套使用选择器
>>> response.css('img').xpath('@src').getall()
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']


>>> response.xpath('//div[@id="images"]/a/text()').get()
'Name: My image 1 '

##  如果获取的数据不存在
>>> response.xpath('//div[@id="not-exists"]/text()').get() is None
True


>>> response.xpath('//div[@id="not-exists"]/text()').get(default='not-found')
'not-found'


##使用属性获取标签内的属性值
>>> [img.attrib['src'] for img in response.css('img')]
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']
 
>>> response.css('img').attrib['src']
'image1_thumb.jpg'

>>> response.css('base').attrib['href']
'http://example.com/'

>>> response.xpath('//base/@href').get()
'http://example.com/'

>>> response.css('base::attr(href)').get()
'http://example.com/'


>>> response.css('base').attrib['href']
'http://example.com/'


>>> response.xpath('//a[contains(@href, "image")]/@href').getall()
['image1.html',
 'image2.html',
 'image3.html',
 'image4.html',
 'image5.html']
 
 
>>> response.css('a[href*=image]::attr(href)').getall()
['image1.html',
 'image2.html',
 'image3.html',
 'image4.html',
 'image5.html']
 

>>> response.xpath('//a[contains(@href, "image")]/img/@src').getall()
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']
 
 
>>> response.css('a[href*=image] img::attr(src)').getall()
['image1_thumb.jpg',
 'image2_thumb.jpg',
 'image3_thumb.jpg',
 'image4_thumb.jpg',
 'image5_thumb.jpg']
 
 
 

扩展的CSS 选择器

to select text nodes, use ::text

to select attribute values, use ::attr(name) where name is the name of the attribute that you want the value of

示例:

title::text selects children text nodes of a descendant <title> element:

>>> response.css('title::text').get()
'Example website'

*::text selects all descendant text nodes of the current selector context:

>>> response.css('#images *::text').getall()
['\n   ',
 'Name: My image 1 ',
 '\n   ',
 'Name: My image 2 ',
 '\n   ',
 'Name: My image 3 ',
 '\n   ',
 'Name: My image 4 ',
 '\n   ',
 'Name: My image 5 ',
 '\n  ']

foo::text returns no results if foo element exists, but contains no text (i.e. text is empty):

>>> response.css('img::text').getall()
[]

>>> response.css('img::text').get()
>>> response.css('img::text').get(default='')
''

a::attr(href) selects the href attribute value of descendant links:

>>> response.css('a::attr(href)').getall()
['image1.html',
 'image2.html',
 'image3.html',
 'image4.html',
 'image5.html']

嵌套选择器

选择方法(.xpath或.css)返回相同类型的选择器列表,因此您也可以为这些选择器调用选择方法。

>>> links = response.xpath('//a[contains(@href, "image")]')
>>> links.getall()
['<a href="image1.html">Name: My image 1 <br><img src="image1_thumb.jpg"></a>',
 '<a href="image2.html">Name: My image 2 <br><img src="image2_thumb.jpg"></a>',
 '<a href="image3.html">Name: My image 3 <br><img src="image3_thumb.jpg"></a>',
 '<a href="image4.html">Name: My image 4 <br><img src="image4_thumb.jpg"></a>',
 '<a href="image5.html">Name: My image 5 <br><img src="image5_thumb.jpg"></a>']
 
 
>>> for index, link in enumerate(links):
...     args = (index, link.xpath('@href').get(), link.xpath('img/@src').get())
...     print('Link number %d points to url %r and image %r' % args)
Link number 0 points to url 'image1.html' and image 'image1_thumb.jpg'
Link number 1 points to url 'image2.html' and image 'image2_thumb.jpg'
Link number 2 points to url 'image3.html' and image 'image3_thumb.jpg'
Link number 3 points to url 'image4.html' and image 'image4_thumb.jpg'
Link number 4 points to url 'image5.html' and image 'image5_thumb.jpg'
 

获取元素的属性值

使用xpath

>>> response.xpath("//a/@href").getall()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

使用css

>>> response.css('a::attr(href)').getall()
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']


response.css('li.next a::attr(href)').get()
response.css('li.next a').attrib['href'] 

而且选择器具有attri 属性

>>> [a.attrib['href'] for a in response.css('a')]
['image1.html', 'image2.html', 'image3.html', 'image4.html', 'image5.html']

>>> response.css('base').attrib
{'href': 'http://example.com/'}
>>> response.css('base').attrib['href']
'http://example.com/'

#如果选择器为空
>>> response.css('foo').attrib
{}

带正则的选择器

# 原始的html
 <a href='image1.html'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
 

>>> response.xpath('//a[contains(@href, "image")]/text()').re(r'Name:\s*(.*)')
['My image 1',
 'My image 2',
 'My image 3',
 'My image 4',
 'My image 5']
 
 
如果获取的文本在正则中匹配了指定的模式,那么就会得到指定的匹配项

>>> response.xpath('//a[contains(@href, "image")]/text()').re_first(r'Name:\s*(.*)')
'My image 1'

XPaths

xpath 的语法

表达式 描述
/ 选中文档的根
. 选中当前节点
.. 选中当前节点的父节点
ELEMENT 选中子节点中所有ELEMENT 元素节点
//ELEMENT 选中后代节点中所有ELEMENT 元素节点
* 所有元素节点
text() 选中所有文本子节点
@ATTR 选中名为ATTR的属性节点
@* 选中所有属性节点
[谓语] 谓语用来查找某个特定特点或者包含某个特定值的节点

演示示例:

示例模式 含义
/ 从根路径开始 .xpath('/html')
E1/E2 选中子节点中的所有E2 xpath('/html/body/div/a')
//E 选中文档中的所有E ,无论在什么位置 xpath('//a')
E1//E2: E1后代节点中的所有E2节点, 无论后代节点在什么位置 xpath('/html/body//img')
E/text(): 选中E节点的文本子节点 xpath('//a/text()')
E/*:选中E的所有元素子节点 xpath('/html/*')
*/E 孙节点中的所有E xpath('//div/*/img')
E/@ATTR E的ATTR 属性的值 .xpath('//img/@src')
//@ATTR 文档中的所有ATTR 属性 .xpath('//@href')
E/@*' E的所有属性 .xpath('//a[1]/img/@*')
. 当前节点 路径下 .xpath('.//img')
.. 当前节点的父节点
node[谓语]: 谓语用来查找某个特定的节点或者包含某个特定值的节点 xpath('//a[last()]') xpath('//a[position()<=3]')
xpath('//div[@id]') 所有含id属性的 div
xpath('//div[@id="images"]') 具有 id属性且值为images
       selector = Selector(response=response)
        title = selector.xpath('//a[@class="titlelnk"]/text()').extract()
        link = selector.xpath('//a[@class="titlelnk"]/@href').extract()
        read = selector.xpath('//span[@class="article_comment"]/a/text()').extract()
        comment = selector.xpath('//span[@class="article_view"]/a/text()').extract()
        = response.xpath('//span[@class="article_view"]/a/text()').re('\d+')
        response.xpath('//span[@class="article_comment"]/a/text()').re('\d+')

css 语法

表达式 描述 示例
* 选中所有元素 *
E 选中E元素 p
E1,E2 选中E1,E2元素 div,pre
E1 E2 选中E1后代元素中的E2元素 div p
E1>E2 选中E1子元素中的E2元素 div>p
E1+E2 选中E1兄弟元素中的E2元素 p+strong
.CLASS 选中CLASS 属性中包含CLASS 的元素 .info
#ID 选中id属性为ID 的元素 #main
[ATTR] 选中包含ATTR属性的元素 [href]
[ATTR=VALUE] 选中包含ATTR属性且值为VLAUE的元素 [method=post]
[ATTR~=VALUE] 选中包含ATTR属性且值包含VLAUE的元素 [method~=post]
E:nth-child(n) 选中E元素,且该元素必须是其父元素的(倒数) 第n个元素 a:nth-child(1)
E:nth-last-child(n) 选中E元素,且该元素必须是其父元素的(倒数) 第n个元素 a:nth-last-child(2)
E:first-child 选中E元素,且该元素必须是其父元素的(倒数) 第1个元素 a:first-child
E:last-child 选中E元素,且该元素必须是其父元素的(倒数) 第1个元素 a:last-child
E:empty 选中没有子元素的E元素 div:emp
E::text 选中E元素的文本节点 TEXT NODE p::text

示例:

response.css('img')  #选中元素  html 标签
response.css('[style]')
response.css('[id=images-2]')
# 选中每个div的第一个a
response.css('div>a:nth-child(1)')
# 选中第二个div的第一个a
response.css('div:nth-child(2)>a:nth-child(1)')
# 选中第一个div的最后一个a
response.css('div:first-child>a:last-child')
# 选中所有a的文本
sel = response.css('a::text')



#特别的示例
## xpath常用函数
# string(arg): 返回参数的字符串值

text='<a href="#">Click here to go to the <strong>Next Page</strong></a>'
sel = Selector(text=text)
 # 如果想得a 中整个字符串'Click here to go to the Next Page',
 # 使用text()就不行了, 因为Click here to go to the઼和Next Page  不在同一个元素下
 # 以下做法将得两个子串
sel.xpath('/html/body/a//text()').extract()
# 这种情况可以使用string() 函数
sel.xpath('string(/html/body/a)').extract()


## contains(str1, str2): 判断str1 中是否包含str2,返回布尔值
text = '''
<div>
<p class="small info">hello world</p>
<p class="normal info">hello scrapy</p>
</div>
'''

sel.xpath('//p[contains(@class, "small")]') #选择class属性中包含small 的p 元素
sel.xpath('//p[contains(@class, "info")]') # 选择class 属性中包含info"的p元素

slector相对路径

示例:

目的:想要获取div 下的所有p

>>> divs = response.xpath('//div')
>>> for p in divs.xpath('//p'):  # this is wrong - gets all <p> from the whole document
...     print(p.get())

#正确方式
>>> for p in divs.xpath('.//p'):  # extracts all <p> inside
...     print(p.get())

>>> for p in divs.xpath('p'):
...     print(p.get())


以class查询时,使用CSS

>>> from scrapy import Selector
>>> sel = Selector(text='<div class="hero shout"><time datetime="2014-07-23 19:00">Special date</time></div>')
>>> sel.css('.shout').xpath('./time/@datetime').getall()
['2014-07-23 19:00']

//node[1] and (//node)[1]区别

//node[1] selects all the nodes occurring first under their respective parents. 选择所有在其各自父代下首先出现的节点 有多个

(//node)[1] selects all the nodes in the document, and then gets only the first of them. 选择文档中的所有节点,然后仅获取它们中的第一个 只有一个

>>> from scrapy import Selector
>>> sel = Selector(text="""
....:     <ul class="list">
....:         <li>1</li>
....:         <li>2</li>
....:         <li>3</li>
....:     </ul>
....:     <ul class="list">
....:         <li>4</li>
....:         <li>5</li>
....:         <li>6</li>
....:     </ul>""")
>>> xp = lambda x: sel.xpath(x).getall()


>>> xp("//li[1]")
['<li>1</li>', '<li>4</li>']
>>> xp("(//li)[1]")
['<li>1</li>']

获取文本节点

示例:

>>> from scrapy import Selector
>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')
>>> sel.xpath('//a//text()').getall() # take a peek at the node-set
['Click here to go to the ', 'Next Page']
>>> sel.xpath("string(//a[1]//text())").getall() # convert it to string
['Click here to go to the ']


>>> sel.xpath("//a[1]").getall() # select the first node
['<a href="#">Click here to go to the <strong>Next Page</strong></a>']
>>> sel.xpath("string(//a[1])").getall() # convert it to string
['Click here to go to the Next Page']

Regular expressions

Set operations

数据存储

首先定义item

# vim  item.py
import scrapy
class CnblogItem(scrapy.Item):
    title = scrapy.Field()
    link = scrapy.Field()

编写spider

import scrapy
from cnblog.items import CnblogItem   #新添加

class Cnblog_Spider(scrapy.Spider):
    name = "cnblog"
    allowed_domains = ["cnblogs.com"]
    start_urls = [
     'https://www.cnblogs.com/',
    ]

    def parse(self, response):
        item = CnblogItem()    #新添加
        item['title'] = response.xpath('//a[@class="titlelnk"]/text()').extract()   #修改
        item['link'] = response.xpath('//a[@class="titlelnk"]/@href').extract()     #修改
        yield item   #新添加

修改pipline 实现保存

mport pymongo   #别忘了导入这个模块

class FilePipeline(object):
'''
实现保存到txt文件的类,类名这个地方为了区分,做了修改,
当然这个类名是什么并不重要,你只要能区分就可以,
请注意,这个类名待会是要写到settings.py文件里面的。
'''
    def process_item(self, item, spider):
        with open('cnblog.txt', 'w', encoding='utf-8') as f:
            titles = item['title']
            links = item['link']
            for i,j in zip(titles, links):
                f.wrire(i + ':' + j + '\n')
        return item        
        

class MongoPipeline(object):
'''
实现保存到mongo数据库的类,
'''

    collection = 'cnblog'  #mongo数据库的collection名字,随便

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
    '''
    scrapy为我们访问settings提供了这样的一个方法,这里,
    我们需要从settings.py文件中,取得数据库的URI和数据库名称
    '''
        return cls(
            mongo_uri = crawler.settings.get('MONGO_URI'),
            mongo_db = crawler.settings.get('MONGO_DB')
        )

    def open_spider(self, spider):                  #爬虫一旦开启,就会实现这个方法,连接到数据库
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):            #爬虫一旦关闭,就会实现这个方法,关闭数据库连接
        self.client.close()

    def process_item(self, item, spider):  
        '''
        每个实现保存的类里面必须都要有这个方法,且名字固定,用来具体实现怎么保存
        '''
        
        titles = item['title']
        links = item['link']
        table = self.db[self.collection]
        for i, j in zip(titles, links):
            data = {}
            data['文章:链接'] = i + ':' + j
            table.insert_one(data)
        return item
                

修改setting.py 启用已经配置好的pipline

ROBOTSTXT_OBEY = False

DEFAULT_REQUEST_HEADERS = {
  'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en',
  'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}
#新修改
ITEM_PIPELINES = {      
    'cnblog.pipelines.FilePipeline': 300,    #实现保存到txt文件
    'cnblog.pipelines.MongoPipeline': 400,     #实现保存到mongo数据库
}
#新添加数据库的URI和DB
MONGO_URI = 'mongodb://localhost:27017'  D
MONGO_DB = "cnblog"     

pipline

当Item在Spider中被收集之后,它将会被传递到Item Pipeline,一些组件会按照一定的顺序执行对Item的处理。
每个item pipeline组件(有时称之为“Item Pipeline”)是实现了简单方法的Python类。他们接收到Item并通过它执行一些行为,同时也决定此Item是否继续通过pipeline,或是被丢弃而不再进行处理。

以下是item pipeline的一些典型应用:

  • 清理HTML数据
  • 验证爬取的数据(检查item包含某些字段)
  • 查重(并丢弃)
  • 将爬取结果保存到数据库或者文件中

每个item pipeline组件是一个独立的Python类,同时必须实现以下方法:

  • process_item(self, item, spider)

    每个item pipeline组件都需要调用该方法,这个方法必须返回一个具有数据的dict,或是 Item (或任何继承类)对象, 或是抛出 DropItem 异常,被丢弃的item将不会被之后的pipeline组件所处理。

    参数:

    • item (Item 对象或者一个dict) – 被爬取的item
    • spider (Spider 对象) – 爬取该item的spider
  • open_spider(self, spider)

    当spider被开启时,这个方法被调用。

    参数:

    • spider (Spider 对象) – 被开启的spider
  • close_spider(self, spider)

    当spider被关闭时,这个方法被调用

    参数:

    • spider (Spider 对象) – 被关闭的spider
  • from_crawler(cls, crawler)
    如果给出,这个类方法将会被调用从Crawler创建一个pipeline实例,它必须返回一个pipeline的新的实例,Crawler对象提供了调用scrapy所有的核心组件的权限,比如你可以调用settings里面的设置项

pipline 去重

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

在setting .py 中设置启用pipline

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

分配给每个类的整型值,确定了他们运行的顺序,item按数字从低到高的顺序,通过pipeline,通常将这些数字定义在0-1000范围内

settings.py

https://www.cnblogs.com/cnkai/p/7399573.html

调试工具

pysnooper 可以看变量

pdb

pudb

不使用scrapy 执行爬虫

在项目根目录里,即有scrapy .cfg 的目录下新建文件

$ vim   main.py
from scrapy import cmdline
cmdline.execute("scrapy crawl project".split())

################################################################
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from scrapy import cmdline
# 方式一:注意execute的参数类型为一个列表
cmdline.execute('scrapy crawl spidername'.split())
# 方式二:注意execute的参数类型为一个列表
cmdline.execute(['scrapy', 'crawl', 'spidername'])


###########################################
#也可以执行
在终端通过 python   main.py

另外一种运行方式

同样在项目的目录下创建文件

import scrapy
from scrapy.crawler import CrawlerProcess
from baidu_com import BaiduComSpider

# 创建一个CrawlerProcess对象
process = CrawlerProcess() # 括号中可以添加参数

process.crawl(BaiduComSpider)
process.start()

scrapy 的配置文件

https://www.cnblogs.com/yoyowin/p/12165538.html

posted @ 2020-08-05 18:12  Lust4Life  阅读(723)  评论(0)    收藏  举报