Scrapy,Python开发的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据。Scrapy用途广泛,可以用于数据挖掘、监测和自动化测试。
Scrapy吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。它也提供了多种类型爬虫的基类,如BaseSpider、sitemap爬虫等,最新版本又提供了web2.0爬虫的支持。在之前的博文当中,简要介绍了Scrapy相关工具命令的介绍,以及Scrapy爬虫项目的基本结构。现在,我们需要自己编写一个Scrapy爬虫项目,从之前介绍Scrapy项目结构中可以知道,一个简单的爬虫,只需要对spider
、items.py
以及pipelines.py
做一些相关操作即可,因为它们分别代表着爬虫部分相关的代码,爬虫项目的数据容器以及对爬虫数据的相关处理,这三个步骤对于爬虫项目是最为重要的!!!
Items的编写
使用Scrapy中的Item对象可以保存爬取到的数据,相当于存储爬取到的数据的容器。一般来说,互联网网页中的信息比较庞大,基本上都是非结构化信息,这样的非结构化信息不太利于我们对信息的管理,所以此时,我们可以定义自己所关注的结构化信息,然后从庞大的互联网信息体系中提取出我们关注的结构化信息,这样可以更利于我们对数据的管理,提取之后,这些数据信息需要一个存储的地方,此时,可以将提取到的结构化数据存储到Item对象中。
定义结构化数据信息的格式如下:
结构化数据名 = scrapy.Field()
所以,若要对结构化数据网页标题、网页地址等进行定义。我们打开之前创建的爬虫项目myfirstspider
,可以将items.py的代码修改为如下:
import scrapy
class MyfirstspiderItem(scrapy.Item):
title = scrapy.Field()
url = scrapy.Field()
可以看到,要定义一个结构化数据,只需要将Scrapy下的Field类实例化即可。
我们可以通过Python Shell来实际使用以下Items
,更深入地理解Items
。进入Python Shell,将上面的Items
代码写上,如下:
>>> import scrapy
>>> class MyFirstspiderItem(scrapy.Item):
title = scrapy.Field()
url = scrapy.Field()
>>> item = MyFirstspiderItem(title='测试', url='http://www.baidu.com')
>>> print(item)
{'title': '测试', 'url': 'http://www.baidu.com'}
我们实例化了MyFirstspiderItem
类,并初始化了参数,并且将MyFirstspiderItem
的实例化对象打印了出来。可以发现,对应的数据会以字典的形式存储,原数据项会转变为字典中的字段名,原数据项对应的值会转变为字典中相应字段名对应的值。
如果我们要单独取某个字段对应的值,可以通过:对象名['字段名']
的方式实现。比如,想单独输出对象中的title字段对应的值,可以通过如下方式实现:
>>> item['title']
'测试'
切记,不可以使用.
的方式获取字段的值,否则将引发错误,如下:
>>> item.title
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "/Users/xxx/PycharmProjects/demo/venv/lib/python3.7/site-packages/scrapy/item.py", line 92, in __getattr__
raise AttributeError(f"Use item[{name!r}] to get field value")
AttributeError: Use item['title'] to get field value
如果你想要获取该对象中所有的字段名,可以通过如下方式:
>>> item.keys()
dict_keys(['title', 'url'])
如果想获取此时对象中的项目视图,可以通过如下方式:
>>> item.items()
ItemsView({'title': '测试', 'url': 'http://www.baidu.com'})
Item的复制:
>>> item.copy()
{'title': '测试', 'url': 'http://www.baidu.com'}
dict与item的转化
>>> dict_item = dict(item)
>>> type(dict_item)
<class 'dict'>
>>> item = MyFirstspiderItem(dict_item)
>>> type(item)
<class '__main__.MyFirstspiderItem'>
Spider的编写
Spider类是Scrapy中与爬虫相关的基类,所有的爬虫文件都必须继承该类(scrapy.Spider)。在一个爬虫项目中,爬虫文件是一个极其重要的部分,爬虫锁进行的爬取动作以及数据提取等操作都是在该文件中进行定义和编写的,通过爬虫文件,可以定义如何对站站进行相应的爬取。比如,可以在爬虫项目中通过genspider
命令创建一个爬虫文件,然后对该爬虫文件进行相应的修改与编写。下面通过编辑器打开之前创建爬虫文件secondspider.py
,默认代码如下所示:
class SecondspiderSpider(scrapy.Spider):
name = 'secondspider'
allowed_domains = ['baidu.com']
start_urls = ['http://baidu.com/']
def parse(self, response, **kwargs):
pass
可以看到,首先在爬虫文件中需要导入scrapy模块,然后创建了一个爬虫类SecondspiderSpider,该类继承了scrapy.Spider
基类。同时,name
属性的值为secondspider,代表的是爬虫名称,所以此时爬虫名称为secondspider。allowed_domains
属性代表的是允许爬取的域名,如果启动了OffsiteMiddleware
,非允许的域名对应的网址则会自动过滤掉。start_urls
属性代表的是爬取的起始网址,如果没有特别指定爬取的URL网址,则会从该属性中定义的网易开始进行爬取,在该属性中,我们可以定义多个起始网址。在这里,还拥有一个名为parse
的方法,如果没有特别指定回调函数,该方法是处理Scrapy爬虫爬取到的网页响应的默认方法,通过该方法,可以对响应进行处理并返回处理后的数据,同时该方法也负责链接的跟进。
除了这些默认生成的属性和方法之外,在Scrapy的spider中海油一些常用的属性和方法,具体如下:
名称 | 属性或方法 | 含义 |
---|---|---|
start_requests() | 方法 | 该方法会默认读取start_urls属性中定义的网址,为每一个网址生成一个Request请求对象,并返回可迭代对象 |
make_requests_from_url(url) | 方法 | 该方法会被start_requests() 调用,该方法负责实现生成Request请求对象 |
closed(reason) | 方法 | 关闭Spider时,该方法会被调用 |
log(message, level=logging.DEBUG, **kw) | 方法 | 使用该方法可以实现在Spider中添加log |
init() | 方法 | 该方法主要负责爬虫的初始化,为构造函数 |
下面,我们将刚刚那个爬虫文件secondspider.py
进行相应的修改,如下所示:
class SecondspiderSpider(scrapy.Spider):
name = 'secondspider'
allowed_domains = ['baidu.com']
start_urls = ["https://www.baidu.com"]
def parse(self, response, **kwargs):
item = MyfirstspiderItem()
item["title"] = response.xpath('/html/head/title/text()').extract()
print(item["title"])
在此,除了导入scrapy模块之外,我们还导入了items.py中的MyfirstspiderItem类,也就是上面所写的Items。写好程序之后,使用如下命令运行该爬虫:
scrapy crawl secondspider --nolog
执行完毕,得到相应的爬取结果,如下所示:
['百度一下,你就知道']
可以看到,此时已经成功将这个网页中的标题提取出来并输出。上方的代码中,为什么定义了start_urls属性就默认定义了起始网址呢?我们是否可以用其他的变量表示起始网址呢?当然是可以的。start_urls属性是默认的设置起始网址属性,如果我们想用其他的变量(属性)来作为设置起始网址的属性,可以通过重写start_requests()
方法来实现。下面,我们将刚刚那个爬虫文件secondspider.py
进行修改,如下所示:
class SecondspiderSpider(scrapy.Spider):
name = 'secondspider'
start_urls = ["https://www.baidu.com"]
my_urls = ["http://www.qq.com/"]
def start_requests(self):
for url in self.my_urls:
yield self.make_requests_from_url(url)
def parse(self, response, **kwargs):
item = MyfirstspiderItem()
item["title"] = response.xpath('/html/head/title/text()').extract()
print(item["title"])
写好文件之后,我们重新运行下该爬虫,得到结果如下所示:
['腾讯首页']
可以看到,此时会爬取我们新属性my_urls中设置的网址的标题,在爬虫文件中,我们通过重写start_requests()
方法,将起始网址设置为了my_urls属性中读取,并且使用for循环遍历,每一次循环读取my_urls属性中的一个网址,并调用Scrapy中默认的make_requests_from_url(url)
方法来实现生产Request请求对象,将生成的请求对象通过yield返回。
如果我不想在代码中写死我想要爬取的网址,希望可以通过传参的方式来动态的爬取我想要爬取的网址,那么我们又该如何实现呢?首先,我们可以在爬虫文件中重写构造方法__init__()
,在构造方法中设置一个变量用于接收用户在执行该爬虫文件时传递过来的参数,接收到参数后,就可以使用用户传进来的值了。然后,我们在运行时,我们只需要通过-a
选项执行对应的参数名和参数值即可实现参数的传递,非常方便。下面,我们将刚刚那个爬虫文件secondspider.py
进行修改,如下所示:
class SecondspiderSpider(scrapy.Spider):
name = 'secondspider'
start_urls = ["https://www.baidu.com"]
def __init__(self,url):
self.start_urls = ["%s"%url]
def parse(self, response, **kwargs):
item = MyfirstspiderItem()
item["title"] = response.xpath('/html/head/title/text()').extract()
print(item["title"])
修改好文件之后,运行如下命令:
scrapy crawl secondspider -a url='http://www.sina.cn' --nolog
而后运行该爬虫,得到结果如下所示:
['手机新浪网']
可以看到,在运行的时候,通过-a
选项将url参数的值设置为了新浪的网页地址,执行时成功接收到该参数,并且提取除了传递进来的网址的标题信息。
如果我们需要通过传递参数的方式来爬取多个网址,应该怎么实现呢?首先我们说一下其中的一种思路,我们可以这么做,我们可以通过-a
选项在形式上指定一个参数后,但是在该参数中包含多个实际参数的信息,但是设置好各实际参数信息之间的间隔符号,随后在Spider
文件中通过str.split()
将各真实参数的信息分割出来,这样就可以使用多个参数了。我们将上面爬虫文件修改如下:
class SecondspiderSpider(scrapy.Spider):
name = 'secondspider'
start_urls = ["https://www.baidu.com"]
def __init__(self, url):
url_list = url.split('|')
self.start_urls = url_list
def parse(self, response, **kwargs):
item = MyfirstspiderItem()
item["title"] = response.xpath('/html/head/title/text()').extract()
print(item["title"])
通过代码分析得知,参数是通过|
来进行分割成多个url。然后我们通过以下代码来运行以上程序:
scrapy crawl secondspider -a url='http://www.sina.cn|http://www.csdn.net' --nolog