scrapy 快速上手

Scrapy快速上手

scrapy简介

scrapy是一款爬虫框架,相比于一般的基于requests 自行编写的爬虫,其特点主要包括:

  • 系统化&结构化:这意味着你编写的代码能够有很高的扩展性,维护更加容易,对于不擅长设计项目架构的童鞋帮助很大
  • 高效率scrapy 让你在编写程序的同时不需要考虑许多性能上的东西(例如多线程),这些scrapy 都已为你考虑好了,你真正需要关注的是爬虫的解析部分
  • 一步到位:这里指的是一个完整的爬虫工作,包括页面下载->解析->过滤->存储几个基本步骤,scrapy都存在相应的模块进行处理,并且这些步骤之间的衔接也由scrapy完成,让你摆脱数据在不同阶段传输的焦虑

scrapy官方文档对于scrapy有着非常鲜明的解释了,包括一个基础的教程各个模块具体的功能,以及一些高级操作,本文档的目的旨在:快速上手,略过一些不必要的细节,实现让一个小白也能根据文档快速实现利用scrapy实现一个完整的爬虫。

一个简单的例子

快速上手一个项目的方式就是跑通一个小例子,一方面简洁的程序能更容易理解,另一方面跑通程序能够提高我们对项目的研究兴趣,下面的例子实现的功能是访问请求:http://quotes.toscrape.com/page/1/,将该页面每个方框内的**文字、作者、标签**信息提取出来,并存储成`json lines`的形式。

  • 创建一个scrapy项目: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 genspider quotes quotes.toscrape.com,这会在spiders下创建一个新的文件,包含以下内容:
import scrapy


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

    def parse(self, response):
        pass

这里有一些需要解释的,start_urls 是一个列表,包含了我们需要爬取的网址,同时也是起始网址,可以理解为爬虫入口,很多爬虫会从起始网址开始,一步步提取出更多的网址并进一步爬取,但是在这个例子中,我们只考虑单个网址。parse 是一个解析方法,它有一个参数response ,就是爬取start_urls 列表中的网址后得到的结果,你可以理解为response=requests.get(url=start_requests[i]), i=0,1,2...注意,爬虫的核心代码就是从网页中解析出我们需要的数据,因此在你的爬虫文件中编写的其他函数(例如parse_authro(self, response)),应该也遵照这个思想:接收response,解析数据,yield 新请求。有点扯远了,继续下一步

  • 我们需要从response中解析出方框的内容,这来源于我们对于网页源代码的观察,如下图:

image-20210203181416969

容易发现每个方框都是一个class="quote"div标签,因此我们可以用如下的代码去解析其中的数据(代码运用了css解析,后面会详解,这里了解一下就好)

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(),
            }

上面的代码让我们遍历所有div标签,并从中提取出三个字段的内容,至此,我们的爬虫解析部分就写好了

  • 运行我们的爬虫很容易,直接执行代码:scrapy crawl quotes -o res.jl就可以看到我们的数据了

image-20210203182053647

scrapy运作机制

pass

scrapy选择器:css与xpath使用

css&xpath是scrapy中response解析提取数据的两种主要方式,在此之前,你一定用过例如Beautifulsoup 进行网页文本数据的解析,相比之下,scrapy内置的解析器效率要更高,因此强烈建议将这两种解析方法都掌握。下面的列表不会像官网一样一步步教你解析,更多的是一种功能式的查询,即回答我该怎么样获取我想要的数据的问题。

查找指定标签文本

response.css('a::text')     
response.xpath('//a/text()')   # 有多个标签返回多个

查找指定标签的属性

response.css('a::attr(href)')
response.xpath('//a/@href')

查找指定属性为指定值的标签

response.css('a[href="xxx"]')   # 获取属性href=xxx的a标签
response.xpath('//a[@href="xxx"]')

查找指定属性包含指定值的标签

response.css('a[href*="xxx"]')
response.xpath('a[contains(@href, "xxx")]')

查找文本为指定值的标签

response.xpath('//a[text()="xxx"]')

查找文本包含指定值的标签

response.xpath('a[contains(text(), "xxx")]')

当获取到标签之后,可以通过 get() , getall() 等方法提取需要的信息,或者在此基础上再进行标签的获取

文件&图片下载

有些时候我们希望爬虫不仅爬取文字信息,也要下载图片&文件内容到本地,这个时候就需要用到scrapy.pipelines.FilesPipelinescrapy.pipelines.ImagePipeline 了,它们分别定义了文件管道和图片管道。传统的下载方式是在parse 的时候加入下载链接到item中,然后调用下载函数(如cptools.process.download_file)指定下载并存储,但这种下载方式是阻塞式的,用pipeline的好处是可以异步下载,提高效率,主要的步骤如下:

  1. 定义一个Item(可以是原本已有的),添加两个字段files,file_urls,后者是下载链接的列表(即一次可传入多个下载链接),前者是文件下载完成后存储的下载相关信息(如下载路径、url、校验码等)
  2. settings.py 中设置变量FILES_STORE即存储路径,之后下载的内容都会存储在这个路径下,最终结果是FILES_STORE/full/3afec3b4765f8f0a07b78f98c07b83f013567a0a(最后一串是文件的sha1值命名的文件)
  3. settings.py中启动pipeline:即scrapy.pipelines.files.FilesPipeline:1

下面是一个例子:

# items.py
class SFItem(scrapy.Item):
    name = scrapy.Field()  # 固件名称
    # 下载信息存储,下面字段必要
    file_urls = scrapy.Field()
    files = scrapy.Field()

# spiders
def parse(self, response):
		item = SFItem()
    item['file_urls'] = ["https://download_url.zip"]   # 注意要列表传入
    yield item
    
# settings.py
ITEM_PIPELINES = {
    'IndustryInfoCrawler.pipelines.SFFilesPipeline': 1,
   'IndustryInfoCrawler.pipelines.MongoPipeline': 300,
}
FILES_STORE = 'root'

上面的方式基本满足了对文件的下载操作,但有两点问题:

  1. 下载路径我们只定义了根目录,如果我们相对文件分类,那怎么办

  2. 文件名sha1值是为了不重复,但可读性很差,我们希望能够自定义文件名

解决办法通过重写Pipeline实现,样例如下:

from scrapy.pipelines.images import FilesPipeline

class SFFilesPipeline(FilesPipeline):
    """
    自定义下载管道
    """
    def get_media_requests(self, item, info):
        for file_url in item['file_urls']:
            yield scrapy.Request(image_url)  # 请求下载

    def file_path(self, request, response=None, info=None, *, item=None): 
        # parent = super().file_path(request, response, info)  # 获取父类目录
        SF_path =  'SF' # 在这里自定义存储的目录,也可以根据不同文件类型,自己设置分类目录
        filename = 'test.zip'
        return os.path.join(SF_path, filename)  # 返回结果为文件存储路径,最终结果是在根目录下存储`FILES_STORE/SF_PATH/filename`
  • 文件到期:很多时候我们不希望重复对本地已有的文件进行下载操作,可以自定义下载管道中请求的时候检查本地文件是否存在,也可以通过设置文件到期时间(未到期的文件不会重复下载)
# settings.py
# 120 days of delay for files expiration
FILES_EXPIRES = 120

# 30 days of delay for images expiration
IMAGES_EXPIRES = 30

# 如果存在自定义的文件管道,如上面的SFFilesPipeline,可以对单个子类管道设置到期时间,以子类名称大写开头
SFFILESPIPELINE_FILES_EXPIRES = 120   # 120天内不重复下载通过SFFilesPipeline的文件

还有一个注意点是,当下载文件过大,超出scrapy预期的话,会报警告,可以通过在settings.py中设置DOWNLOAD_WARNSIZE = 0去除