爬虫是网络信息收集最重要的工具之一,而我之前一直都用自己手写的爬虫,感觉挺好用也一直没学爬虫框架。由于研究需要,最近又要接触爬虫,于是索性学了一下爬虫框架Scrapy,看看为什么这么多人支持该框架,他又给我们编写爬虫带来了哪些便捷。
Scrapy简介
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。其最初是为了 页面抓取 (更确切来说, 网络抓取 )所设计的, 也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫。
Scrapy的安装
Scrapy是基于python实现的,因此推荐使用pip安装,具体语句参考:
sudo pip install virtualenv #安装虚拟环境工具
virtualenv ENV #创建一个虚拟环境目录
source ./ENV/bin/active #激活虚拟环境
pip install Scrapy
Scrapy整体架构
Scrapy由5个部分构成,分别是:
(1)Scrapy Engine,引擎,相当于整个爬虫框架的大脑,其连接着其他四个模块,控制着整个框架的数据流动、指令触发事件等等。
(2)Spiders,爬虫,这是由用户自己实现的部分,需要注意的是,在一个爬虫框架中可以存在很多爬虫,而每个爬虫都负责处理某个或某几个特定网站。
(3)Scheduler,调度器,负责管理由spider发起的request请求,将其入队,当引擎请求时将其传递。
(4)Downloader,下载器,负责获取页面数据并提供给引擎,而后提供给spider。
(5)Item Pipeline,管道,当页面被爬虫解析所需的数据存入Item后,将被发送到项目管道,并经过几个特定的次序处理数据,最后存入本地文件或存入数据库。
而一个完整的流程大概是这样的:Spider从自己的url列表中取出要爬取的url,将request请求传递给Scheduler,调度器经过调度将请求传递给Downlader,下载器会将整个页面的数据下载下来,并封装成应答包(Response),再回传给Spiders,爬虫将下载的数据整理为Item数据类型,最后交给Pipeline保存。整个过程中,引擎处理着每个模块之间的交互和数据流动,而调度器一般在多个爬虫同时传递request请求时才显现出其重要性。
其实在框架图中可以看到,还存在两个中间件:Downloader middlewares(下载中间件)和Spider middlewares(爬虫中间件),这两个中间件并不影响整个框架的运行,其为用户自定义的模块,可以不实现,而其存在的意义在于扩展Scrapy功能,比如IP代理池等等。
Sracpy代码目录结构
在编写爬虫前需要知道的是,模块中Engine、Scheduler、Downloader是Scrapy自己实现的,并不需要我们去编写,我们需要实现的是Spiders和Item Pipeline。
首先,使用命令创建一个Scrapy项目:
scrapy startproject malware
上面的命令会生成一个目录,目录结构如下:
malware/
scrapy.cfg #包含着整个项目的配置
malware/
__init__.py
items.py #Spiders的Item,定义了要存储数据的数据结构
pipelines.py #Item Pipeline的实现,处理Spider传递的Item
settings.py #Spider的设置文件
spiders/ #存放着所有的Spider
__init__.py
...
从零开始写第一个爬虫
1. 定义Item
写爬虫的第一步,先确定你要爬取数据的结构,Items是将要装载抓取的数据的容器,它工作方式像 python 里面的字典。以malware-traffic-analysis.net为例,我想要抓取他们主页的信息,包括url,title,name,那么定义如下:
from scrapy.item import Item, Field
class MalwareItem(Item):
name = Field() #存储站点名
title = Field() #存储主页标题
url = Field() #存储url
很简单的代码,但是这个Item会被Spider调用作为存储数据的容器并传递给管道,最后在管道中处理。
2. 编写Spider
spider是由用户自编写的爬虫具体实现代码,其中定义了初始url,如何跟踪链接,如何解析网站数据等等,一个简单的demo如下,首先我们在spiders目录中新建一个文件名为malware_spider的python文件,然后输入代码如下:
import scrapy
class malwareSpider(scrapy.Spider):
name = "malware"
allowed_domains = ["malware-traffic-analysis.net"]
start_urls = [
'http://www.malware-traffic-analysis.net/2018/index.html'
]
def parse(self, response):
filename = response.url.split("/")[-2]
with open(filename, 'wb') as f:
f.write(response.body)
简单解释一下上面的demo,首先定义了一个类去继承Spider类,并在这个类中实现具体的爬虫功能,其中参数解释如下:
(1)name:爬虫的名字,在启动爬虫的时候需要用到
(2)allowed_domains:爬虫可行域,定义了爬虫允许的爬取域
(3)start_urls:定义了初始url的列表,爬虫会依次爬取
而对于parse函数,这是Spider类中最重要的一个函数,其是一个回调函数,在每个下载器完成下载后,会将数据内容封装成一个Response,并将其传递给parse函数进行处理。在上面的demo中,parse就进行了一个简单的处理,截取url内容并以此为名保存在本地。我们在爬虫根目录使用下列命令启动爬虫看看效果:
scrapy crawl malware
主要注意的是,本命令需要在根目录执行,就是存在scrapy.cfg文件的目录,且最后一个参数是你爬虫的名字,如我定义的爬虫名字(name)就是malware,爬虫运行过程如下:
3. 在Spider中使用Item
在上面的例子中我们成功定义了Spider并让其爬取数据并保存,但是这种方式显得很直接粗漏,因此我们使用之前定义Item类来作为存储数据的容器。
3.1 提取数据
在将提出的数据存入Item之前,我们需要知道如何从下载的内容中提取出我们需要的数据。首先,我们能够得到初始的数据内容为页面源码,大概长相如下:
为了更方便地从页面源码中提取数据,Scrapy提供一个Selector类来辅助数据提取。Selector类主要使用XPath和CSS表达式来进行数据提取,其可用的方法如下:
- xpath(): 返回selectors列表, 每一个selector表示一个xpath参数表达式选择的节点.
- css(): 返回selectors列表, 每一个selector表示CSS参数表达式选择的节点
- extract(): 返回一个unicode字符串,该字符串为XPath选择器返回的数据
- re(): 返回unicode字符串列表,字符串作为参数由正则表达式提取出来
由于本文是对html源码进行数据提取,因此使用XPath方法,而XPath的语法请参考:XPath语法,使用Selector类与XPath对上面的demo进行修改:
import scrapy
class malwareSpider(scrapy.Spider):
name = "malware"
allowed_domains = ["malware-traffic-analysis.net"]
start_urls = [
'http://www.malware-traffic-analysis.net/2018/index.html'
]
def parse(self, response):
sel = scrapy.Selector(response)
url = response.url
title = sel.xpath('//title/text()').extract() #抓取title内容
name = self.allowed_domains[0]
3.2 使用Item
在上面的源码中,我们已经实现了数据的提取,现在我们要把它存储到Item类中,实现的具体代码如下:
import scrapy
from malware.items import MalwareItem
class malwareSpider(scrapy.Spider):
name = "malware"
allowed_domains = ["malware-traffic-analysis.net"]
start_urls = [
'http://www.malware-traffic-analysis.net/2018/index.html'
]
def parse(self, response):
sel = scrapy.Selector(response)
items = MalwareItem()
item['url'] = response.url
item['title'] = sel.xpath('//title/text()').extract()
item['name'] = self.allowed_domains[0]
return item
可以看到,我们调用了MalwareItem类,并将提取出的数据都存入了item容器中,最后将其返回。
4. 使用Item Pipeline
看到这里有些人会产生疑惑,上面的代码中,item被返回,那究竟返回到了哪呢?答案是Item Pipeline。值得一提的是,虽然Item Pipeline是五大模块之一,却并不是必须实现的模块,你可以直接在Spider的parse中直接进行数据存储等工作,且Spider默认状态下是关闭Item Pipeline的,你想要使用其必须在setting中加入一句代码来激活Item Pipeline:
ITEM_PIPELINES = {'malware.pipelines.MalwarePipeline': 1}
虽然可以直接在Spider的parse中直接进行数据存储等操作,但本文建议使用Item Pipeline进行数据操作,这样能够使整个框架分工更加明确与合理。在Item Pipeline中处理数据的代码如下:
class MalwarePipeline(object):
def process_item(self, item, spider):
filename = "test"
with open(filename, 'wb') as f:
f.write(item['url']+'\n')
f.write(item['name']+'\n')
f.write(item['title'][0])
return item
可以看到Pipeline主要运行函数是process_item,其会自动调用,而我们也主要在该函数中实现对数据的操作。上面的代码进行了一个简单的操作,新建一个test文件来存储从Spider传递过来的Item数据,运行的结果如下:
到这里,我们已经实现了一个完整的demo,从运行爬虫,抓取数据,提取数据最后处理数据。
进阶——实现一个通用实用型爬虫模板
上面的教程我们已经实现了一个最初级的爬虫,并成功运行,但是也遗留下了许多问题。首先,上面的爬虫只实现了一个网页的爬取以及数据提取,对于我们来说,我们肯定无法满足对单一网页的爬取,身为爬虫必须有跟踪链接与管理url的能力。
大部分网上的实现代码都是使用迭代的思想来实现跟踪链接(参考Here),url的管理也要单独实现,由于下载器不会自动调用,我们需要使用request方法来触发并实现回调。这无疑是十分麻烦的,我们使用爬虫框架的目的就是为了简化代码,少写多能。
为了实现一个通用实用型爬虫模板,本文并不准备采用迭代来实现,而是使用CrawlSpider类来实现,由于其实现了自动管理url与跟踪,无疑简化了我们代码的实现。
1. Item实现
首先明确一下需要提取的数据,从malware-traffic-analysis.net站点的目录开始,跟踪每个页面,从页面中提取可以下载的pcap文件链接。因此,Item的定义如下:
from scrapy.item import Item, Field
class MalwareItem(Item):
title = Field()
downloadurl = Field()
url = Field()
2. Item Pipeline实现
Item Pipeline中进行数据的保存,我将其保存在一个txt文件中,实际上可以进行更多操作。
class MalwarePipeline(object):
def __init__(self):
self.file = open('malware.txt', mode='wb')
def process_item(self, item, spider):
self.file.write(item['title'])
self.file.write("\n")
self.file.write(item['url'])
self.file.write("\n")
self.file.write(item['downloadurl'])
self.file.write("\n")
return item
3. CrawlSpider实现
在编写CrawlSipder前,我们先了解一下CrawlSpider是怎么进行url管理和跟踪的。CrawlSpider使用rules来定义抓取url的规则,其可以包含多个Rule对象,每个Rule对象对应一条规则。简单来说,CrawlSpider中定义了一个名为rules的属性,其中包含了许多规则,只要页面中有url命中了其中一条规则,就会被添加入待爬取的url列表,而CrawlSpider对于url的管理和去重等操作都是自动化的。
看一下目录中要跟踪url的结构:
大概是”日期+index.html”或”日期+index+数字.html”,所以写出规则:
rules = [
Rule(LinkExtractor(allow=('\d{2}\/\d{2}\/index(\d)?\.html')),
callback='parse_item',
follow=True)
]
但是这里有个问题,我们可以发现我们抓取到的都是相对路径,对于相对路径,其实CrawlSpider会进行自动填充,但我们也可以自行定义url补充,这里用到了rules的process_links参数,具体优化如下:
rules = [
Rule(LinkExtractor(allow=('\d{2}\/\d{2}\/index(\d)?\.html')),
callback='parse_item',
process_links = start_urls[0],
follow=True)
]
接下来查看一下需要爬取的文件路径源码:
因此,我们根据特征写出xpath语句:
xpath('//ul/li/a[@class="menu_link"]/@href')
编写了url抓取规则和xpath的语句,我们接着编写CrawlSpider的总体代码,值得注意的是,CrawlSpider不能使用parse,不然rules规则会被覆盖,因此需要在rules中添加一个回调参数callback来替代parse,具体的实现代码如下:
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.selector import Selector
from malware.items import MalwareItem
class malwareSpider(CrawlSpider):
name = 'malware'
download_delay = 1
start_urls = ['http://www.malware-traffic-analysis.net/2018/']
rules = [
Rule(LinkExtractor(allow=('\d{2}\/\d{2}\/index(\d)?\.html')),
callback='parse_item',
process_links = start_urls[0],
follow=True)
]
#这是初始url的回调函数,这里我们不实现
def parse_start_url(self, response):
pass
def parse_item(self, response):
item = MalwareItem()
sel = Selector(response)
try:
item['title'] = (sel.xpath('//title/text()').extract())[0]
item['url'] = response.url
item['downloadurl'] = (sel.xpath('//ul/li/a[@class="menu_link"]/@href').extract())[1]
yield item
except:
pass
在根目录下启动爬虫,爬虫能够成功运行并进行数据抓取:
4. 爬虫的优化与调整
在完成上述代码后,依旧存在一些需要解决的问题,首先,源网站待爬取的数据一共有169条,却一共进行了174次爬取,查看记录文件后发现爬虫爬到其他域去了,因此在爬虫加上作用域的限制:
allowed_domains = ['malware-traffic-analysis.net']
查看一下爬到其他域的原因,发现是规则不严谨造成的,于是优化规则内容,避免类似情况发生,Rule的allow是采用正则匹配,我们添加xpath来使规则更加完善,修改后的规则如下:
rules = [
Rule(LinkExtractor(allow=('\d{2}\/\d{2}\/index(\d)?\.html'), restrict_xpaths=('//ul/li/a[@href]')),
process_links = start_urls[0],
callback='parse_item',
follow=True)
]
即必须在ul/li/a的href属性中的链接才会被提取跟踪,修改后爬取结果如下:
虽然爬取规则正确了,但是查看存储的内容时发现,与预期出现了很大的偏差,主要是在downloadurl这一项上,预期认为总是列表的第二项,实际上许多地方出现了偏差,出现在了列表的第一项或第三项,于是我们需要优化数据筛选规则,使用单一的xpath并无法满足我们,于是我们调用re()方法来添加正则匹配:
sel.xpath('//ul/li/a[@class="menu_link"]/@href').re('(.)+\.pcap\.zip').extract()
上面的代码在实际运行时报错,这里有一个坑,当我调用re()方法时,我下意识地认为这是一个添加正则规则的函数,后来我才re和extract不能同时调用,因为re已经相当于正则+extract。因为第一次接触Spider的人很容易犯这个错误,所以我在这里写出来。实际上,在xpath语法中结合正则表达是一个更好的选择,写法如下:
sel.xpath('//ul/li/a[@class="menu_link" and contains(@href, ".pcap.zip")]/@href').extract()
最后一个问题就是考虑到最后下载文件时需要一个完整的url,而我们抓到都是只是文件名,因此使用urljoin来拼接url,修改pipeline如下:
import urlparse
class MalwarePipeline(object):
def __init__(self):
self.file = open('malware.txt', mode='wb')
def process_item(self, item, spider):
self.file.write(item['title'])
self.file.write("\n")
self.file.write(item['url'])
self.file.write("\n")
self.file.write(urlparse.urljoin(item['url'], item['downloadurl']))
self.file.write("\n")
return item
而修改后完整的爬虫Spider代码如下:
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
from scrapy.selector import Selector
from malware.items import MalwareItem
class malwareSpider(CrawlSpider):
name = 'malware'
download_delay = 1
allowed_domains = ['malware-traffic-analysis.net']
start_urls = ['http://www.malware-traffic-analysis.net/2018/']
rules = [
Rule(LinkExtractor(allow=('\d{2}\/\d{2}\/index(\d)?\.html'), restrict_xpaths=('//ul/li/a[@href]')),
process_links = start_urls[0],
callback='parse_item',
follow=True)
]
def parse_start_url(self, response):
pass
def parse_item(self, response):
item = MalwareItem()
sel = Selector(response)
try:
item['title'] = (sel.xpath('//title/text()').extract())[0]
item['url'] = response.url
item['downloadurl'] = (sel.xpath('//ul/li/a[@class="menu_link" and contains(@href, ".pcap.zip")]/@href').extract())[0]
yield item
except:
pass
爬取成功的数据存储图:
到此为止,一个完整的爬虫模板已经完成,在今后需要重新编写爬虫时,我们的修改流程大致如下:
(1)修改Item,决定数据存储的容器结构
(2)修改Pipeline,对数据进行操作或存储
(3)修改爬虫可行域、初始url
(4)修改rules,决定url抓取规则
(5)修改xpath,决定数据提取规则
总结
源码地址:Here。