Scrapy是用Python实现的一个爬取网站数据、提取结构性数据而编写的应用框架,通常我们可以很简单的通过 Scrapy 框架实现一个爬虫,抓取指定网站的内容或图片。

构建项目

在使用前,需要创建一个新的Scrapy项目,进入到我们创建的项目目录,运行以下命令:

scrapy startproject mySpider

之后我们会看到一个mySpider文件夹,目录结构如下

mySpider/
    scrapy.cfg              # 项目配置文件
    mySpider/               # 项目的Python模块,会从这里引入代码
        __init__.py         
        items.py            # 项目的目标文件
        pipelines.py        # 项目的管道文件
        settings.py         # 项目的设置文件
        spiders/            # 存储爬虫代码目录
            __init__.py

定义Item

Items是将要装在爬取数据的容器,它工作方式像python里面的字典,但它提供更多的保护,比如对未定义的字段填充以防止拼写错误。

它是通过创建一个scrapy.item.Item类来生命,定义属性未scrapy.item.Field对象,就像是一个对象关系映射(ORM)

我们通过将需要到的item模型化,来控制从站点获取的数据,比如标题、作者、特色图片等。我们需要编辑items.py文件:

from scrapy.item import Item,Field
class webItem(Item):
    title = Field()
    author = Field()
    pic = Field()

编写第一个爬虫

Spider是用户编写的类,用于从一个域中抓取信息,定义了用于下载URL的初步列表,如何跟踪链接,以及如何来解析网页内容提取items。

要建立一个Spider,必须要未scrapy.Spider创建一个子类,并确定三个必要属性和一个方法:

  • name:爬虫的识别名,它是唯一的,在不同的爬虫中必须定义不同的名字
  • allow_domains = []:搜索的域名范围,即爬虫的约束区域
  • start_urls:爬虫开始爬的一个url列表,爬虫会从这个列表开始爬取数据,所以第一次下载数据也会从这些urls开始,其他的子url将会从这些起始url中继承性生成。
  • parse():爬虫的方法,调用时传入从每一个url传回的Response对象作为参数,且response是该方法的唯一参数。这个方法负责解析返回的数据(response.body),提取结构化数据(生成item)并生成需要下一页的url请求

下面我们来写第一个爬虫代码,我们将其命名未first_spider.py,于myspider/spiders/目录下

import scrapy

class firstSpider(scrapy.Spider):
    name = "first"
    allowed_domains = ["blog.ikedong.cn"]
    start_urls = (
        'https://blog.ikedong.cn/',
    )

    def parse(self, response):
        filename = "ikedong.html"
        open(filename,'wb').write(response.body) # 将响应内容传入filename

尝试运行,在mySpider目录下执行:

scrapy crawl first

结果:

2021-12-30 10:37:13 [scrapy.utils.log] INFO: Scrapy 2.5.1 started (bot: mySpider)
2021-12-30 10:37:13 [scrapy.utils.log] INFO: Versions: lxml 4.7.1.0, libxml2 2.9.12, cssselect 1.1.0, parsel 1.6.0, w3lib 1.22.0, Twisted 21.7.0, Python 3.9.9 (tags/v3.9.9:ccb0e6a, Nov 15 2021, 18:08:50) [MSC v.1929 64 bit (AMD64)], pyOpenSSL 21.0.0 (OpenSSL 1.1.1l  24 Aug 2021), cryptography 36.0.0, Platform Windows-10-10.0.22000-SP0
2021-12-30 10:37:13 [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.selectreactor.SelectReactor
2021-12-30 10:37:13 [scrapy.crawler] INFO: Overridden settings:
{'BOT_NAME': 'mySpider',
 'NEWSPIDER_MODULE': 'mySpider.spiders',
 'ROBOTSTXT_OBEY': True,
 'SPIDER_MODULES': ['mySpider.spiders']}
2021-12-30 10:37:13 [scrapy.extensions.telnet] INFO: Telnet Password: 4ba0245d2d5ecf2a
2021-12-30 10:37:13 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
 'scrapy.extensions.telnet.TelnetConsole',
 'scrapy.extensions.logstats.LogStats']
2021-12-30 10:37:14 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware',
 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
 'scrapy.downloadermiddlewares.retry.RetryMiddleware',
 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
 'scrapy.downloadermiddlewares.stats.DownloaderStats']
2021-12-30 10:37:14 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
 'scrapy.spidermiddlewares.referer.RefererMiddleware',
 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
 'scrapy.spidermiddlewares.depth.DepthMiddleware']
2021-12-30 10:37:14 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2021-12-30 10:37:14 [scrapy.core.engine] INFO: Spider opened
2021-12-30 10:37:14 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2021-12-30 10:37:14 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
2021-12-30 10:37:14 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://blog.ikedong.cn/robots.txt> (referer: None)
2021-12-30 10:37:14 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://blog.ikedong.cn/> (referer: None)
2021-12-30 10:37:14 [scrapy.core.engine] INFO: Closing spider (finished)
2021-12-30 10:37:14 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 440,
 'downloader/request_count': 2,
 'downloader/request_method_count/GET': 2,
 'downloader/response_bytes': 13904,
 'downloader/response_count': 2,
 'downloader/response_status_count/200': 2,
 'elapsed_time_seconds': 0.772987,
 'finish_reason': 'finished',
 'finish_time': datetime.datetime(2021, 12, 30, 2, 37, 14, 856820),
 'httpcompression/response_bytes': 58607,
 'httpcompression/response_count': 2,
 'log_count/DEBUG': 2,
 'log_count/INFO': 10,
 'response_received_count': 2,
 'robotstxt/request_count': 1,
 'robotstxt/response_count': 1,
 'robotstxt/response_status_count/200': 1,
 'scheduler/dequeued': 1,
 'scheduler/dequeued/memory': 1,
 'scheduler/enqueued': 1,
 'scheduler/enqueued/memory': 1,
 'start_time': datetime.datetime(2021, 12, 30, 2, 37, 14, 83833)}
2021-12-30 10:37:14 [scrapy.core.engine] INFO: Spider closed (finished)

我们会在mySpider文件夹下发现parse()方法下filename的同名文件

image-20211230104022423

我们打开它,发现是我们所爬取的完整页面:

image-20211230104137844

这时有人会问了,马老师,发生甚么事了?

Scrapy未爬虫的start_urls属性中的每一个url创建了一个scrpay.http.Request对象,并将爬虫的parse()方法指定为回调函数,这些Request首先被调度,然后执行,通过parse()方法,scrapy.http.Response对象被返回,结果也被反馈给爬虫。

提取数据

Scrapy常常会使用Xpath的方法提取数据,例如我们来获取标题

import scrapy

class firstSpider(scrapy.Spider):
    name = "first"
    allowed_domains = ["blog.ikedong.cn"]
    start_urls = (
        'https://blog.ikedong.cn/',
    )

    def parse(self, response): # 提取数据
        # 获取标题
        context = response.xpath('/html/head/title/text()')
        # 提取标题
        title = context.extract()
        print(title)
        pass

结果:

image-20211230111210147

我们之前在items.py中定义了一个webItem的类,我们将它引进到代码中。

from mySpider.items import webItem

然后我们便可以将获取到的数据封装到webItem对象中,这次我们将文章的标题、作者、特色图片保存到该对象中。

from typing import ItemsView
import scrapy

from mySpider.items import webItem

class firstSpider(scrapy.Spider):
    name = "first"
    allowed_domains = ["blog.ikedong.cn"]
    start_urls = (
        'https://blog.ikedong.cn/',
    )

    def parse(self, response):

        # 创建存放信息的集合
        items = []

        for each in response.xpath('//article'):
            # 获取webItem对象
            item = webItem()
            # extract()方法返回的是unicode字符串
            title = each.xpath("div[2]/div/a/h3/text()").extract()
            author = each.xpath("div[2]/div/div[2]/span[1]/a[2]/text()").extract()
            pic = each.xpath("div[1]/a/img/@data-src").extract()

            # 传入到item中
            item['title'] = title
            item['author'] = author
            item['pic'] = pic 

            items.append(item)

        return items
            # yield item

结果:

image-20220104103019739

保存数据

Scrapy保存信息最简单的方法有四种,-o输出指定格式的文件:

scrapy crawl first -o webData.json # json格式
scrapy crawl first -o webData.json # json lines格式,默认未Unicode编码
scrapy crawl first -o webData.csv # csv格式
scrapy crawl first -o webData.xml # xml格式

管道文件

管道文件是用于处理item数据,它可以用来清洗、验证和存储数据。

保存到本地

Scrapy提供换门处理和下载的pipeline,包括文件下载、图片下载。

我们可以首先在setting.py中定义一个IMAGES_STORE变量:

IMAGE_STORE = './images‘ # 存放至项目的images文件夹中

内置的ImagesPipeline会默认读取Item的image_urls字段,并认为该字段是一个列表形式,他会遍历item的image_urls字段,并提取每个url下载

但是我们所生成的图片链接并非用image_urls字段表示,也不是列表形式,而是单个url,所以为了实现下载,我们需要自定义ImagePipeline继承ImagesPipeline

import scrapy
from scrapy.exceptions import DropItem
from scrapy.pipelines.images import ImagesPipeline

class ImagePipeline(ImagesPipeline):
    def file_path(self, request, response, info): 
    # request就是当前下载的Request对象,这个方法用来返回保存的文件名
        pic = request.pic
        file_name = pic.split('/')[-1]
        return file_name

    def item_completed(self, results, item, info): 
    # 保存图片
        image_paths = [x['path'] for ok, x in results if ok]
        # for ok, x in results:
        # if ok:
        #     print(x['path'])
        if not image_paths:
            raise DropItem('Image Downloaded Failed')

        return item

    def get_media_requests(self, item, info):
    # 获取媒体资源,通过Item第项将pic提取出来并生成Request对象    
        yield scrapy.Request(item['pic'])

保存到Mysql

代码如下:

import scrapy
import pymysql

class MysqlPipeline():
    def __init__(self, host, database, user, password, port):
        self.host = host
        self.database = database
        self.user = user
        self.password = password
        self.port = port

    @classmethod   # 无需实例化,使用cls来访问变量
    def from_crawler(cls, crawler):
        return cls(
            host = crawler.settings.get('MYSQL_HOST'),
            database = crawler.settings.get('MYSQL_DATABASE'),
            user = crawler.settings.get('MYSQL_USER'),
            password = crawler.settings.get('MYSQL_PASSWORD'),
            port = crawler.settings.get('MYSQL_PORT')
        )

    def open_spider(self, spider):
        self.db = pymysql.connect(host=self.host, user=self.user, password=self.password, database=self.database, charset='utf8', port=self.port)
        self.cursor = self.db.cursor()

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

    def process_item(self, item, spider):
        data = dict(item) # 将item变为字典形式
        keys = ",".join(data.keys()) # 将字典的键值变成用,分割的字符串
        values = ",".join(['%s']*len(data)) # 根据字典的长度建立对应长数的%s
        sql = "insert into %s (%s) values (%s)" % ('testDB', keys, values)
        self.cursor.execute(sql, tuple(data.values()))
        self.db.commit()
        return item

同时在setting.py中添加几个变量:

MYSQL_HOST = 'localhoset'
MYSQL_DATABSE = 'python'
MYSQL_PORT = 3306
MYSQL_USER = 'python'
MYSQL_PASSWORD = '123456'

要注意的是,管道默认是关闭状态,需要到setting.py中打开

image-20220104115323677