scrapy学习记录

介绍

近期一直都有关注数据的采集方面的开发,之前也用Python(urllib+BeautifulSoup)写过“爬虫”但是效果不是很好,表现在内存占用过高和做出来的东西不够通用,很多周边的东西(图片下载、缩略图等)都需要自己来实现。

Scrapy 概述

Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试

Scrapy 架构

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

图片

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

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)传给下载器,下载器把资源下载下来,并封装成应答包(Response)
3 爬虫解析Response 4 若是解析出实体(Item),则交给实体管道进行进一步的处理;若是解析出的是链接(URL),则把URL交给Scheduler等待抓取

默认情况下,Scrapy使用 LIFO 队列来存储等待的请求。简单的说,就是 深度优先顺序 。如果想要 广度优先顺序 进行爬取,需要进行设定。

Scrapy 存在的问题

爬虫是一个很依赖于网络io的应用,单机的处理能力有限,很快就变成瓶颈。而scrapy并不是一个分布式的设计,在需要大规模爬取的情况下就很成问题。当然可以通过修改Request队列来实现分布式爬取,而且工作量也不算特别大。

  • scrapy的并行度不高。力图在爬虫里做一些计算性的操作就会影响抓取的速率。这主要是python里的线程机制造成的,因为Python使用了GIL(和Ruby一样),多线程并不会带来太多速度上的提升(除非用Python的C扩展实现自己的模块,这样绕过了GIL)。Summary:Use Python threads if you need to run IO operations in parallel. Do not if you need to run computations in parallel.
  • scrapy的内存消耗很快。可能是出于性能方面的考虑,pending requests并不是序列化存储在硬盘中,而是放在内存中的(毕竟IO很费时),而且所有Request都放在内存中。你抓取到 百万网页的时候,考虑到单个网页时产生很多链接的,pending request很可能就近千万了,加上脚本语言里的对象本来就有额外成本,再考虑到GC不会立即释放内存,内存占用就相当可观了。
  • 归根到底,这两个问题是根植于语言之中的。

Scrapy 实例

新建项目 (Project)

1
scrapy startproject news_scrapy

输入以上命令之后,就会看见命令行运行的目录下多了一个名为 news_scrapy的目录,目录的结构如下:

|—- news_scrapy
| |—- news_scrapy
| |—- init.py
| |—- items.py #用来存储爬下来的数据结构(字典形式)
| |—- pipelines.py #用来对爬出来的item进行后续处理,如存入数据库等
| |—- settings.py #爬虫配置文件
| |—- spiders #此目录用来存放创建的新爬虫文件(爬虫主体)
| |—- init.py
| |—- scrapy.cfg #项目配置文件

定义目标(Items)

Items是装载抓取的数据的容器,工作方式像 python 里面的字典,但它提供更多的保护,比如对未定义的字段填充以防止拼写错误
通过创建scrapy.Item类, 并且定义类型为 scrapy.Field 的类属性来声明一个Item,通过将需要的item模型化,来控制站点数据。
编辑 items.py

1
2
3
4
5
6
7
8
9
# -*- coding: utf-8 -*-
import scrapy
class NewsScrapyItem(scrapy.Item):
# define the fields for your item here like:

category = scrapy.Field()
url = scrapy.Field()
secondary_title = scrapy.Field()
secondary_url = scrapy.Field()
#text = Field()

制作爬虫(Spider)

Spider 定义了用于下载的URL列表、跟踪链接的方案、解析网页内容的方式,以此来提取items。
要建立一个Spider,你必须用scrapy.spider.BaseSpider创建一个子类,并确定三个强制的属性:

  • name:爬虫的识别名称,必须是唯一的,在不同的爬虫中你必须定义不同的名字。
  • start_urls:爬取的URL列表。爬虫从这里开始抓取数据,所以,第一次下载的数据将会从这些urls开始。其他子URL将会从这些起始URL中继承性生成。
  • parse():解析的方法,调用的时候传入从每一个URL传回的Response对象作为唯一参数,负责解析并匹配抓取的数据(解析为item),跟踪更多的URL。
    在 spiders 目录下新建 Wynews.py,代码如下。利用 yield Request(url=item[‘url’],meta={‘item_1’: item},callback=self.second_parse) 来进行第二层爬取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class WynewsSpider(BaseSpider):
name = "Wynews"
start_urls = ['http://news.163.com/rank/']

def parse(self,response):
html = HtmlXPathSelector(response)
page = html.xpath('//div[@class="subNav"]/a')
for i in page:
item = dict()
item['category'] = i.xpath('text()').extract_first()
item['url'] = i.xpath('@href').extract_first()
print item['category'],item['url']
yield Request(url=item['url'],meta={'item_1': item},callback=self.second_parse)

def second_parse(self,response):
item_1= response.meta['item_1']
html = HtmlXPathSelector(response)
#print 'response ',response
page = html.xpath('//tr/td/a')
#print 'page ',page
items = []
for i in page:
item = DidiScrapyItem()
item['category'] = item_1['category'].encode('utf8')
item['url'] = item_1['url'].encode('utf8')
item['secondary_title'] = i.xpath('text()').extract_first().encode('utf8')
item['secondary_url'] = i.xpath('@href').extract_first().encode('utf8')
#print i.xpath('text()').extract(),i.xpath('@href').extract()
items.append(item)
return items

存储结果(Pipeline)

Item pipeline 的主要责任是负责处理 spider 抽取的 Item,主要任务是清理、验证和存储数据。当页面被 spider 解析后,将被发送到 pipeline,每个 pipeline 的组件都是由一个简单的方法组成的Python类。pipeline 获取Item,执行相应的方法,并确定是否需要在 pipeline中继续执行下一步或是直接丢弃掉不处理。

执行过程

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

主要方法

  • process_item(item, spider)
    每一个item管道组件都会调用该方法,并且必须返回一个item对象实例或raise DropItem异常。
    被丢掉的item将不会在管道组件进行执行

  • open_spider(spider)
    当spider执行的时候将调用该方法

  • close_spider(spider)
    当spider关闭的时候将调用该方法

编写自己的 Pipeline

编辑 pipelines.py。把抓取的 items 保存到 json 文件中。

1
2
3
4
5
6
7
8
import json
class NewsScrapyPipeline(object):
def __init__(self):
self.file = open('items.json', 'w')
def process_item(self, item, spider):
line = json.dumps(dict(item),ensure_ascii=False) + "\n"
self.file.write(line)
return item

另外,如果不考虑编码(没有中文),可以在运行爬虫的时候直接通过下面的命令导出结果。

dump到JSON文件:

1
scrapy crawl myspider -o items.json

dump到CSV文件:

1
scrapy crawl myspider -o items.csv

dump到XML文件:

1
scrapy crawl myspider -o items.xml

激活Item Pipeline组件

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

1
2
3
ITEM_PIPELINES = {
'news_scrapy.pipelines.NewsScrapyPipeline': 300,
}

开启爬虫 (Crawl)

1
scrapy crawl Wynews

可能出现的问题 (Problem)

打开 items.json 文件,中文可能会出现文件乱码问题

1
[{"category": "\u93c2\u4f34\u6908", "url": "http://news.163.com/special/0001386F/rank_news.html", "secondary_title": "\u934b\u950b\u9422\u5cf0\u30b3\u95c3\u8e6d\u7b09\u9473\u6ec8\u69fb\u951b\u5c7e\u5d0f\u6fc2\u7a3f\u5dfb\u9359\u53c9\u7c2e\u6769\u6ec4\u7966\u95c0", "secondary_url": "http://caozhi.news.163.com/16/0615/09/BPJG6SB60001544E.html"},

这一行代码就能解决。

1
line = json.dumps(dict(item),ensure_ascii=False) + "\n"

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "A股闯关MSCI再度失败 索罗斯们押注对冲胜出", "secondary_url": "http://money.163.com/16/0615/06/BPJ4T69300253B0H.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "湖北副省长担心房价下跌:泡沫若破裂后果很严重", "secondary_url": "http://money.163.com/16/0615/08/BPJBM36U00252G50.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "马云:假货质量超过正品 打假很复杂", "secondary_url": "http://money.163.com/16/0615/08/BPJAIOVI00253G87.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "A股闯关未成功 纳入MSCI新兴市场指数被延迟", "secondary_url": "http://money.163.com/16/0615/07/BPJ7260D00252G50.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "马云称许多假货比真品好 网友:怪不得淘宝假货多", "secondary_url": "http://money.163.com/16/0615/08/BPJC437N002526O3.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "贪官示意家人低价买地 拆迁后获赔近亿元", "secondary_url": "http://money.163.com/16/0615/08/BPJAT58400252G50.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "又是毒胶囊:浙江查获1亿多粒毒胶囊 6人被捕", "secondary_url": "http://money.163.com/16/0615/07/BPJ8NMRG00253B0H.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "还不起了?委内瑞拉寻求宽限1年偿还中国贷款", "secondary_url": "http://money.163.com/16/0615/07/BPJ9IH3400252C1E.html"}
{"category": "财经", "url": "http://money.163.com/special/002526BH/rank.html", "secondary_title": "A股频现清仓式减持 上半年十大减持王曝光", "secondary_url": "http://money.163.com/16/0615/07/BPJ7Q9BC00254IU4.html"}
{"category": "汽车", "url": "http://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "《装X购车指南》 30-50万都能买到啥车?", "secondary_url": "http://auto.163.com/16/0615/07/BPJ6U1J900084TUP.html"}
{"category": "汽车", "url": "http://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "看挡杆还以为是A8L 新款哈弗H9内饰曝光", "secondary_url": "http://auto.163.com/16/0615/00/BPIGTP4B00084TUO.html"}
{"category": "汽车", "url": "http://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "前脸/尾灯有变 新款捷达搭1.5L油耗更低", "secondary_url": "http://auto.163.com/16/0615/00/BPIGMEHE00084TUO.html"}
{"category": "汽车", "url": "http://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "主打车型不超10万良心价 远景SUV将8月上市", "secondary_url": "http://auto.163.com/16/0615/00/BPIHR2A500084TUO.html"}
{"category": "汽车", "url": "http://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "Macan并不是我真姓 众泰SR8搭2.0T/D", "secondary_url": "http://auto.163.com/16/0613/00/BPDBPB0J00084TUO.html"}
{"category": "汽车", "url": "http://news.163.com/special/0001386F/rank_auto.html", "secondary_title": "上海福特翼搏优惠1.5万元", "secondary_url": "http://auto.163.com/16/0615/00/BPIHH8FF000857M6.html"}

添加命令行参数

第一种方法,在命令行用crawl控制spider爬取的时候,加上-a选项,如

1
scrapy crawl WangyiSpider -a category=打车

然后在 spider 的构造函数里加上带入的参数

1
2
3
4
5
6
7
8
import scrapy
class WangyiSpider(BaseSpider):
name = "Wangyi"
def __init__(self, category=None, *args, **kwargs):
super(WangyiSpider, self).__init__(*args, **kwargs)
self.base_url = 'http://news.yodao.com/'
self.start_urls = ['http://news.yodao.com/search?q=' +
category]

默认情况当你每次执行scrapy crawl命令时会创建一个新的进程。但我们可以使用核心API在同一个进程中同时运行多个spider,如下,在 settings.py 的同级目录下编辑 run.py,导入编写的 spider 类如 JingdongSpider, SuningSpider。

Scrapy 调优

提高并发能力

增加并发

并发是指同时处理的request的数量。其有全局限制和局部(每个网站)的限制。Scrapy 默认的全局并发限制(16)对同时爬取大量网站的情况并不适用,因此需要增加这个值。 增加多少取决于爬虫能占用多少CPU。 一般开始可以设置为 100 。不过最好的方式是做一些测试,获得 Scrapy 进程占取CPU与并发数的关系。选择一个能使CPU占用率在80%-90%的并发数比较恰当。

1
2
# 增加全局并发数
CONCURRENT_REQUESTS = 100

降低log级别

为了减少CPU使用率(及记录log存储的要求), 当调试程序完毕后,可以不使用 DEBUG log级别。

1
2
# 设置Log级别:
LOG_LEVEL = 'INFO'

禁止cookies

禁止cookies能减少CPU使用率及Scrapy爬虫在内存中记录的踪迹,提高性能。

1
2
# 禁止cookies:
COOKIES_ENABLED = False

禁止重试

对失败的HTTP请求进行重试会减慢爬取的效率,尤其是当站点响应很慢(甚至失败)时, 访问这样的站点会造成超时并重试多次。这是不必要的,同时也占用了爬虫爬取其他站点的能力。

1
2
# 禁止重试:
RETRY_ENABLED = False

减小下载超时

对一个非常慢的连接进行爬取(一般对通用爬虫来说并不重要), 减小下载超时能让卡住的连接能被快速的放弃并解放处理其他站点的能力。

1
2
3
4
5
# 减小下载超时:
DOWNLOAD_TIMEOUT = 15

# 可能会引发的错误
TimeoutError: User timeout caused connection failure: Getting http://homea.people.com.cn/n1/2016/0628/c69176-28504657.html took longer than 15.0 seconds..

通过如上配置,我的爬虫每分钟响应的request是之前的4倍,然而值得注意的是,这些设置并不是在所有场景都适用,需要通过具体场景试验,具体问题具体分析。

避免被禁止(ban)

有些网站实现了特定的机制,以一定规则来避免被爬虫爬取。下面是些处理这些站点的建议(tips):

  • 使用user agent池,轮流选择之一来作为user agent。池中包含常见的浏览器的user agent(google一下一大堆)
  • 禁止cookies(参考 COOKIES_ENABLED),有些站点会使用cookies来发现爬虫的轨迹。
  • 设置下载延迟(2或更高)。参考 DOWNLOAD_DELAY 设置。
  • 如果可行,使用 Google cache 来爬取数据,而不是直接访问站点。
  • 使用IP池。例如免费的 Tor项目 或付费服务(ProxyMesh)。
  • 使用高度分布式的下载器(downloader)来绕过禁止(ban),就只需要专注分析处理页面。这样的例子有: Crawlera
    如果仍然无法避免被ban,考虑商业支持.
文章目录
  1. 1. 介绍
  2. 2. Scrapy 概述
  3. 3. Scrapy 架构
  4. 4. Scrapy 组件
  5. 5. Scrapy 运行流程
  6. 6. Scrapy 存在的问题
  7. 7. Scrapy 实例
    1. 7.1. 新建项目 (Project)
    2. 7.2. 定义目标(Items)
    3. 7.3. 制作爬虫(Spider)
    4. 7.4. 存储结果(Pipeline)
    5. 7.5. 执行过程
    6. 7.6. 主要方法
    7. 7.7. 编写自己的 Pipeline
    8. 7.8. 激活Item Pipeline组件
    9. 7.9. 开启爬虫 (Crawl)
    10. 7.10. 可能出现的问题 (Problem)
    11. 7.11. 添加命令行参数
  8. 8. Scrapy 调优
    1. 8.1. 提高并发能力
      1. 8.1.1. 增加并发
      2. 8.1.2. 降低log级别
      3. 8.1.3. 禁止cookies
      4. 8.1.4. 禁止重试
      5. 8.1.5. 减小下载超时
      6. 8.1.6. 避免被禁止(ban)
|