前言

从大二开始接触python,到现在已经是第三个年头了;随着入职腾讯,进入云原生行业后,python已经不再是我的主要开发语言,我转而收养了golang小地鼠成为了一名gopher

python依然是我的工具人好伙伴(日常生活中一旦有自动化的念头也会直接想到python),并且作为数据工作者,对于python的数据处理能力还是挺依赖的,golang的生态也没有好到能面面俱到

鄙人大二时课设写过一个小小的b站爬虫(基于bs4, reselenium等简单写的),最后也只是草草爬了几十万的用户数据以及几百万的视频数据,做了做没有什么意义的词频分析,而scrapy作为我一定会忘记的爬虫必会知识,还是有必要写一篇小笔记record一下的

需要了解的词

  • 网络爬虫:泛指获取网页信息,提取有用信息的行为
  • selenium: web自动化测试工具集,但在爬虫工程中也经常使用,模拟人的点击操作驱动浏览器来获取网页信息

Scrapy是什么

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。也是高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。
Scrapy吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。它也提供了多种类型爬虫的基类,如BaseSpider,sitemap爬虫等

架构

Scrapy使用了 Twisted异步网络库来处理网络通讯,整体架构大致如下:

各组件的作用

Scrapy Engine

引擎负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件。 详细内容查看下面的数据流(Data Flow)部分。

此组件相当于爬虫的“大脑”,是整个爬虫的调度中心

调度器(Scheduler)

调度器从引擎接受request并将他们入队,以便之后引擎请求他们时提供给引擎。

初始的爬取URL和后续在页面中获取的待爬取的URL将放入调度器中,等待爬取。同时调度器会自动去除重复的URL(如果特定的URL不需要去重也可以通过设置实现,如post请求的URL)

下载器(Downloader)

下载器负责获取页面数据并提供给引擎,而后提供给spider

Spiders

Spider是Scrapy用户编写用于分析response并提取item(即获取到的item)或额外跟进的URL的类。 每个spider负责处理一个特定(或一些)网站。

Item Pipeline

Item Pipeline负责处理被spider提取出来的item。典型的处理有清理、 验证及持久化(例如存取到数据库中)

当页面被爬虫解析所需的数据存入Item后,将被发送到项目管道(Pipeline),并经过几个特定的次序处理数据,最后进行数据持久化

下载器中间件(Downloader middlewares)

下载器中间件是在引擎及下载器之间的特定钩子(specific hook),处理Downloader传递给引擎的response。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。

通过设置下载器中间件可以实现爬虫自动更换user-agent, IP等功能

Spider中间件(Spider middlewares)

Spider中间件是在引擎及Spider之间的特定钩子(specific hook),处理spider的输入(response)和输出(items及requests)。 其提供了一个简便的机制,通过插入自定义代码来扩展Scrapy功能。

数据流(Data flow)

scrapy爬取数据时的数据流如下:

  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,引擎关闭该网站

hello world in scrapy

创建scrapy项目

在项目目录下shell执行:

scrapy startproject tutorial

创建后目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

定义目标类

​ Scrapy spider可以以python的dict来返回提取的数据.虽然dict很方便,并且用起来也熟悉,但是其缺少结构性,容易打错字段的名字或者返回不一致的数据,尤其在具有多个spider的大项目中。
为了定义常用的输出数据,Scrapy提供了 Item 类。 Item 对象是种简单的容器,保存了爬取到得数据。 其提供了 类似于词典(dictionary-like) 的API以及用于声明可用字段的简单语法。
许多Scrapy组件使用了Item提供的额外信息: exporter根据Item声明的字段来导出数据、 序列化可以通过Item字段的元数据(metadata)来定义、 trackref 追踪Item实例来帮助寻找内存泄露 (see 使用 trackref 调试内存泄露) 等等。

以我的习惯我喜欢先定好爬取目标,因为爬虫的主要目标就是从非结构性数据源中提取结构性信息,所以这里我们先在items.py中定义我们的目标数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

import scrapy


class TutorialItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
title = scrapy.Field()
price = scrapy.Field()
comment = scrapy.Field()
product_id = scrapy.Field()

制作爬虫

制作爬虫,总体来说分为两步:先爬再取

也就是说,首先你要获取整个网页的所有内容,然后再取出其中对你有用的部分

要建立一个Spider,你必须用scrapy.spider.BaseSpider创建一个子类,并确定三个强制的属性:

  • name:爬虫的识别名称,必须是唯一的,在不同的爬虫中你必须定义不同的名字
  • start_urls:爬取的URL列表;爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始,其他子URL将会从这些起始URL中继承性生成
  • parse():解析的方法,调用的时候传入从每一个URL传回的Response对象作为唯一参数,负责解析并匹配抓取的数据(解析为item),跟踪更多的URL

常规使用scrapy.Request来递归地创建Response进行爬取(这种形式下也可以使用bs4, xpath等工具来构建url):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)

其中20, 21行又可以用response.follow简化为:

1
2
if next_page is not None:
yield response.follow(next_page, callback=self.parse)

也可以直接将Selector传递给response.follow:

1
2
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, callback=self.parse)

至此我们就得到了我们的目标items,之后我们可以选择直接输出到文件或者pipelines.py`中做数据清洗 / 验证以及数据的持久化存储了

总结

scrapy整体看下来是一个完整但偏笨重的爬虫框架,其优势是支持并发,而且集成了 HTTP 请求、下载、解析、调度等爬虫程序中常见的功能模块,让爬虫工程师只专注于页面解析和制定抓取规则

但高度的抽象模块们让整个爬虫项目显得比较臃肿,每个爬虫项目都需要按照相应的模版生成好几个文件,这一点上可以类比django,可能在一些简单web应用上我就会选择flask;而对于爬虫来说,基于golangcolly就是一个非常轻便的爬虫框架,并发控制等在golang中也非常简单,在这里埋一个colly爬虫框架的文章坑吧hh