Python爬虫
爬虫就是通过编写程序来获取互联网上的优秀资源
什么是爬虫
爬虫其实是一门计算机中的技术,它被广泛应用于搜索引擎。常见的搜索引擎有百度、Google、Bing等。搜索引擎的工作原理大致分为爬取信息、存储、建立索引、排序、检索等环节,其中第一阶段就是使用专用程序收集网页数据,这个程序通常称为蜘蛛(Spider)或爬虫(Crawler)。搜索引擎从已知的数据库出发,访问这些网页并抓取文件。搜索引擎通过这些爬虫从一个网站爬到另一个网站,跟踪网页中的链接,访问更多的网页,这个过程称为爬行,这些新的网址会被存入数据库等待搜索。简而言之,爬虫就是通过不间断地访问互联网,然后从中获取你指定的信息并返回给你,获取你所需要的数据。而我们的互联网上,随时都有无数的爬虫在爬取数据,并返回给使用者。除了搜索引擎,爬虫还可以完成很多别的事情。
爬虫合法么?
首先,爬虫在法律上是不被禁止的,也就是说法律上是允许爬虫存在的,但是爬虫也具有违法风险的。
robots.txt是一个协议,我们可以把它理解为一个网站的”管家”,它会告诉搜索引擎哪些页面可以访问,哪些页面不能访问。也可以规定哪些搜索引擎可以访问我们的网站而哪些搜索引擎不能爬取我们网站的信息等等,是网站管理者指定的”君子协议”。 当一个搜索机器人(有的叫搜索蜘蛛)访问一个站点时,它会首先检查该站点根目录下是否存在robots.txt,如果存在,搜索机器人就会按照该文件中的内容来确定访问的范围;如果该文件不存在,那么搜索机器人就沿着链接抓取。 另外,robots.txt必须放置在一个站点的根目录下,而且文件名必须全部小写。
在浏览器的网址搜索栏中,输入网站的根域名,然后再输入/robot.txt即可查看。比如,百度的robots.txt网址为 https://www.baidu.com/robots.txt
爬虫的原理
如果要获取网络上数据,我们要给爬虫一个网址(程序中通常叫URL),爬虫发送一个HTTP请求给目标网页的服务器,服务器返回数据给客户端(也就是我们的爬虫),爬虫再进行数据解析、保存等一系列操作。
Scrapy框架简介
Scrapy是一个为了爬取网站信息,提取结构性数据而编写的应用框架。Scrapy用途广泛,可用于数据挖掘、监测和自动化测试等。
Scrapy框架结构
Scrapy框架结构和流程设计遵循网络爬虫的基本原理。通过组件封装不同的功能模块;通过请求和响应类封装数据流;通过引擎指挥整个系统协调运行。
组件
下面简单介绍一下Scrapy框架结构中包含的组件。
- 引擎(Engine) 引擎犹如总指挥,是整个系统的“大脑”,指挥其他组件协同工作。
- 调度器(Scheduler) 调度器接收引擎发过来的请求,按照先后顺序,压入队列中,同时去除重复的请求。
- 下载器(Downloader) 下载器用于下载网页内容,并将网页内容返回给爬虫(Scrapy下载器是建立在twisted这个高效的异步模型上的)。
- 爬虫(Spiders) 爬虫作为最核心的组件,用于从特定的网页中提取需要的信息,即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面。
- 项目管道(Item Pipelines) 项目管道负责处理爬虫从网页中抽取的实体。主要的功能是持久化实体、验证实体的有效性、清除不需要的信息等。
- 下载器中间件(Downloader Middlewares) 下载器中间件介于引擎和下载器之间,主要处理Scrapy引擎与下载器之间的请求及响应。
- 爬虫中间件(Spider Middlewares) 爬虫中间件介于引擎和爬虫之间,主要工作是处理爬虫的响应输入和请求输出。
数据流
Scrapy框架结构中传递和处理的数据主要有以下3种:
- 向网站服务器发送的请求数据;
- 网站服务器返回的响应数据;
- 解析后的结构数据。
Scrapy中定义的Request和Response类,用于保存请求和响应数据;Item类保存解析后的结构数据。
Scrapy执行流程
- 爬虫(Spider)使用URL(要爬取页面的网址)构造一个请求(Request)对象,提交给引擎(Engine)。如果请求要伪装成浏览器,或者设置代理IP,可以先在爬虫中间件中设置,再发送给引擎。
- 引擎将请求安排给调度器,调度器根据请求的优先级确定执行顺序。
- 引擎从调度器获取即将要执行的请求。
- 引擎通过下载器中间件,将请求发送给下载器下载页面。
- 页面完成下载后,下载器会生成一个响应(Response)对象并将其发送给引擎。下载后的数据会保存于响应对象中。
- 引擎接收来自下载器的响应对象后,通过爬虫中间件,将其发送给爬虫(Spider)进行处理。
- 爬虫将抽取到的一条数据实体(Item)和新的请求(如下一页的链接)发送给引擎。
- 引擎将从爬虫获取到的Item发送给项目管道(Item Pipelines),项目管道实现数据持久化等功能。同时将新的请求发送给调度器,再从第②步开始重复执行,直到调度器中没有更多的请求,引擎关闭该网站。
第一个网络爬虫
Scrapy网络爬虫基础
首先介绍Scrapy中最重要的组件爬虫(Spider),它用于构建HTTP请求并从网页中提取数据;接着介绍使用Item封装数据;最后介绍使用Pipeline组件对数据进行处理,如数据清理、去重及持久化存储等。
使用Spider提取数据
Scrapy网络爬虫编程的核心就是爬虫(Spider)组件,它其实是一个继承于Spider的类,主要功能是封装一个发送给网站服务器的HTTP请求,解析网站返回的网页及提取数据。
下面从数据流的角度分析一下执行步骤:
- Spider生成初始页面请求(封装于Request对象中),提交给引擎。
- 引擎通知下载器按照Request的要求,下载网页文档,再将文档封装成Response对象作为参数传回给Spider。
- Spider解析Response中的网页内容,生成结构化数据(Item),或者产生新的请求(如爬取下一页),再次发送给引擎。
- 如果发送给引擎的是新的Request,就回到第(2)步继续往下执行。如果发送的是结构化数据(Item),则引擎通知其他组件处理该数据(保存到文件或数据库中)。
以实现起点中文网小说热销榜为例,打开Spiders目录下的qidian_hot_spider.py,实现代码如下:
#-*-coding:utf-8-*- from scrapy import Request from scrapy.spiders import Spider #导入Spider类 class HotSalesSpider(Spider): #定义爬虫名称 name = 'hot' #起始的URL列表 start_urls = ["https://www.qidian.com/rank/hotsales?style=1"] # 解析函数 def parse(self, response): #使用xpath定位到小说内容的div元素,保存到列表中 list_selector = response.xpath("//div[@class='book-mid-info']") #依次读取每部小说的元素,从中获取小说名称、作者、类型和形式 for one_selector in list_selector: #获取小说名称 name = one_selector.xpath("h4/a/text()").extract()[0] #获取作者 author = one_selector.xpath("p[1]/a[1]/text()").extract()[0] #获取类型 type = one_selector.xpath("p[1]/a[2]/text()").extract()[0] #获取形式(连载还是完本) form = one_selector.xpath("p[1]/span/text()").extract()[0] #将爬取到的一部小说保存到字典中 hot_dict = {"name":name, #小说名称 "author":author, #作者 "type":type, #类型 "form":form} #形式 #使用yield返回字典 yield hot_dict
首先从scrapy模块导入了两个模块:Request和Spider;然后定义了一个继承于Spider的类HotSalesSpider。该类的结构很简单,有两个属性name和start_urls,两个方法parse()和start_requests(),它们都是基类Spider的属性和方法。下面来看一下它们各自的功能。
- name:必填项。name是区分不同爬虫的唯一标识,因为一个Scrapy项目中允许有多个爬虫。不同的爬虫,name值不能相同。
- start_urls:存放要爬取的目标网页地址的列表。
- start_requests():爬虫启动时,引擎自动调用该方法,并且只会被调用一次,用于生成初始的请求对象(Request)。start_requests()方法读取start_urls列表中的URL并生成Request对象,发送给引擎。引擎再指挥其他组件向网站服务器发送请求,下载网页。代码中之所以没看到start_requests()方法,是因为我们没有重写它,直接使用了基类的功能。
- parse():Spider类的核心方法。引擎将下载好的页面作为参数传递给parse()方法,parse()方法执行从页面中解析数据的功能。
重写start_requests()方法
Spider的结构非常简单,不难理解,但是你一定有这样的疑问:
- 如何避免爬虫被网站识别出来导致被禁呢?
- 引擎是怎么知道要将下载好的页面发送给parse()方法而不是其他方法?能否自定义这个方法? 第一个问题的答案是可以重写(override)start_requests()方法,手动生成一个功能更强大的Request对象。因为伪装浏览器、自动登录等功能都是在Request对象中设置的。 第二个问题的答案是引擎之所以能自动定位,是因为在Request对象中,指定了解析数据的回调函数,而默认情况下,Request指定的解析函数就是parse()方法。
下面我们就来重写start_requests()方法,对起点中文网小说热销榜的功能做一些优化。优化内容有:
- 将爬虫伪装成浏览器。
- 设置新的解析数据的回调函数(不使用默认的parse())。
实现代码如下:
#-*-coding:utf-8-*-
from scrapy import Request
from scrapy.spiders import Spider#导入Spider类
class HotSalesSpider(Spider):
#定义爬虫名称
name = 'hot'
#设置用户代理(浏览器类型)
qidian_headers = {"User-Agent":"Mozilla/"
"5.0 (Windows NT 10.0; "
"Win64; x64) AppleWebKit/"
"537.36 (KHTML, like Gecko) Chrome/"
"68.0.3440.106 Safari/"
"537.36"}
#获取初始Request
def start_requests(self):
url = "https://www.qidian.com/rank/hotsales?style=1"
#生成请求对象,设置url,headers,callback
yield Request(url,headers=self.qidian_headers,callback=self.qidian_
parse)
# 解析函数
def qidian_parse(self, response):
……
在类HotSalesSpider中,新增一个字典型的属性qidian_headers,用于设置请求头信息。这里设置的User-Agent,就是用于伪装浏览器。 另外,代码中删除了属性start_urls,并重写了start_requests()方法,用于自定义Request对象。Request对象设置了3个参数:
- url:请求访问的网址。
- headers:请求头信息。
- callback:回调函数。这里确定解析数据的函数为qidian_parse()。引擎会将下载好的页面(Response对象)发送给该方法,执行数据解析功能。 解析函数由parse()改为qidian_parse(),实现代码未变。
Request对象
Request对象用来描述一个HTTP请求,它通常在Spider中生成并由下载器执行。
Request的定义形式为:
class scrapy.http.Request(url [,callback,method ='GET',headers,body,
cookies,meta,encoding ='utf-8',priority = 0,dont_filter = False,errback ])
其中,参数url为必填项,其他为选填项,下面逐个介绍这些参数。 以下参数用于设置向网站发送的HTTP请求的内容,你一定不会感到陌生。
- url:HTTP请求的网址,如https://baidu.com。
- method:HTTP请求的方法,如GET、POST、PUT等,默认为GET,必须大写英文字母。
- body:HTTP的请求体,类型为str或unicode。
- headers:HTTP的请求头,类型为字典型。请求头包含的内容可以参考2.1.3节HTTP请求。
- cookies:请求的Cookie值,类型为字典型或列表型,可以实现自动登录的效果,后面章节会具体讲解。
- encoding:请求的编码方式,默认为UTF-8。 以下参数设置Scrapy框架内部的事务。
- callback:指定回调函数,即确定页面解析函数,默认为parse()。页面下载完成后,回调函数将会被调用,如果在处理期间发生异常,则会调用errback()函数。
- meta:字典类型,用于数据的传递。它可以将数据传递给其他组件,也可以传递给Respose对象,本章的项目案例中会使用到该参数。
- priority:请求的优先级,默认为0,优先级高的请求会优先下载。
- dont_filter:如果对同一个url多次提交相同的请求,可以使用此项来忽略重复的请求,避免重复下载,其值默认为False。如果设置为True,即使是重复的请求,也会强制下载,例如爬取实时变化的股票信息数据。
- errback:在处理请求时引发任何异常时调用的函数,包括HTTP返回的404页面不存在的错误。 Request中的参数看上去有很多,但除了url外,其他参数都有默认值,大部分情况下不必设置。
使用选择器提取数据
Scrapy提取数据有自己的一套机制,被称做选择器(Selector类),它能够自由“选择”由XPath或CSS表达式指定的HTML文档的某些部分。Scrapy的选择器短小简洁、解析快、准确性高,使用其内置的方法可以快速地定位和提取数据。 下面就来了解一下选择器(Selector类)及选择器列表(SelectorList类,选择器对象的集合)内置的方法。 定位数据
- xpath(query):查找与XPath表达式匹配的节点,并返回一个SelectorList对象。SelectorList对象类似于一个列表,包含了所有匹配到的节点。参数query是XPath表达式的字符串。
- css(query):查找与CSS表达式匹配的节点,并返回一个SelectorList对象。参数query是CSS表达式的字符串。 提取数据
- extract():提取文本数据,返回unicode字符串列表。使用xpath()或css()方法将匹配到的节点包装为SelectorList对象后,可以使用extract()方法提取SelectorList对象中的文本,并将其存放于列表中。
- extract_first():SelectorList独有的方法,提取SelectorList对象中第一个文本数据,返回unicode字符串。
- re(regex):使用正则表达式提取数据,返回所有匹配的unicode字符串列表。
- re_first():SelectorList独有的方法,提取第一个与正则表达式匹配的字符串。
Response对象与XPath
我们完全没有必要手动构造一个选择器对象来实现对网页信息的查找与提取。因为Scrapy将下载下来的网页信息封装为Response对象传递给解析函数时,会自动构造一个选择器作为Response对象的属性,这样就能通过Response对象非常方便地查找与提取网页数据。
下面再来分析一下解析函数,函数框架如下:
# 解析函数
def qidian_parse(self, response):
……
参数response接收封装有网页信息的Response对象,这时就可以使用下面的方法实现对数据的定位。
- response.selector.xpath(query);
- response.selector.css(query)。 由于在Response中使用XPath和CSS查询十分普遍,因此Response对象提供了两个实用的快捷方式,它们能自动创建选择器并调用选择器的xpath()或css()方法来定位数据。简化后的方法如下:
- response.xpath(query);
- response.css(query)。
Response对象
Response用来描述一个HTTP响应,它只是一个基类。当下载器下载完网页后,下载器会根据HTTP响应头部的Content-Type自动创建Response的子类对象。子类主要有:
- TextResponse;
- HtmlResponse;
- XmlResponse。
其中,TextResponse是HtmlResponse和XmlResponse的子类。我们通常爬取的是网页,即HTML文档,下载器创建的便是HtmlResponse。
下面以HtmlResponse为例,介绍它的属性。
- url:响应的url,只读,如https://www.baidu.com。
- status:HTTP响应的状态码,如200、403、404。状态码可以参考2.1.4节HTTP响应。
- headers:HTTP的响应头,类型为字典型。具体内容可以参考2.1.4节HTTP响应。
- body:HTTP响应体。具体内容可以参考2.1.4节HTTP响应。
- meta:用于接收传递的数据。使用request.meta将数据传递出去后,可以使用response.meta获取数据。
多页数据的爬取
在解析函数中,提取完本页数据并提交给引擎后,设法提取到下一页的URL地址,使用这个URL地址生成一个新的Request对象,再提交给引擎。也就是说,解析本页的同时抛出一个下一页的请求,解析下一页时抛出下下页的请求,如此递进,直到最后一页。
#-*-coding:utf-8-*-
from scrapy import Request
from scrapy.spiders import Spider #导入Spider类
class HotSalesSpider(Spider):
#定义爬虫名称
name = 'hot'
current_page = 1 #设置当前页,起始为1
#获取初始Request
def start_requests(self):
url = "https://www.qidian.com/rank/hotsales?style=1"
#生成请求对象,设置url、headers和callback
yield Request(url,callback=self.qidian_parse)
#解析函数
def qidian_parse(self, response):
#使用xpath定位到小说内容的div元素,并保存到列表中
list_selector = response.xpath("//div[@class='book-mid-info']")
#依次读取每部小说的元素,从中获取小说名称、作者、类型和形式
for one_selector in list_selector:
#获取小说名称
name = one_selector.xpath("h4/a/text()").extract_first()
#获取作者
author = one_selector.xpath("p[1]/a[1]/text()").extract()[0]
#获取类型
type = one_selector.xpath("p[1]/a[2]/text()").extract()[0]
#获取形式(连载还是完本)
form = one_selector.xpath("p[1]/span/text()").extract()[0]
#将爬取到的一部小说保存到字典中
hot_dict = {"name":name, #小说名称
"author":author, #作者
"type":type, #类型
"form":form} #形式
#使用yield返回字典
yield hot_dict
#获取下一页URL,并生成Request请求,提交给引擎
#1.获取下一页URL
self.current_page+=1
if self.current_page<=25:
next_url = "https://www.qidian.com/rank/hotsales?style=1&page=
%d"%(self.current_page)
#2.根据URL生成Request,使用yield返回给引擎
yield Request(next_url,callback=self.qidian_parse)
以上代码看着多,其实仅增加了加粗代码部分,下面来分析一下这些代码。
- 属性current_page,用于记录当前的页码,初始值为1。
- 通过分析得知,第N页的URL地址为https://www.qidian.com/rank/hotsales?style=1&page=N,即只有page的值是变化(递增)的。获取下一页的URL就变得简单了。
- 根据下一页的URL,构建一个Request对象,构建方法和start_requests()中Request对象构建方法一样,仅仅是URL不同。
使用Item封装数据
Item对象是一个简单的容器,用于收集抓取到的数据,其提供了类似于字典(dictionary-like)的API,并具有用于声明可用字段的简单语法。
以起点中文网小说热销榜项目qidian_hot为例,在新建项目时,自动生成的items.py文件,就是用于封装数据的。之所以叫items,是因为源文件中可以定义多种Item,其原始代码为:
import scrapy
class QidianHotItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
pass
已知需要爬取的小说的字段有小说名称、作者、类型和形式。在类QidianHotItem中声明这几个字段,代码如下:
import scrapy
#保存小说热销榜字段数据
class QidianHotItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field() #小说名称
author = scrapy.Field() #作者
type = scrapy.Field() #类型
form = scrapy.Field() #形式
下面分析一下代码:
- 类QidianHotItem继承于Scrapy的Item类。
- name、author、type、form为小说的各个字段名。
- scrapy.Field()生成一个Field对象,赋值给各自的字段。
- Field对象用于指定每个字段的元数据,并且Field对象对接受的数据没有任何限制。因此,在定义属性字段时,无须考虑它的数据类型,使用起来非常方便。
下面修改HotSalesSpider类中的代码,使用QidianHotItem替代Python字典存储数据,实现代码如下:
#-*-coding:utf-8-*-
from scrapy import Request
from scrapy.spiders import Spider #导入Spider类
from qidian_hot.items import QidianHotItem #导入模块
class HotSalesSpider(Spider):
……
# 解析函数
def qidian_parse(self, response):
#使用xpath定位到小说内容的div元素,并保存到列表中
list_selector = response.xpath("//div[@class='book-mid-info']")
#依次读取每部小说的元素,从中获取小说名称、作者、类型和形式
for one_selector in list_selector:
#获取小说名称
name = one_selector.xpath("h4/a/text()").extract_first()
#获取作者
author = one_selector.xpath("p[1]/a[1]/text()").extract()[0]
#获取类型
type = one_selector.xpath("p[1]/a[2]/text()").extract()[0]
#获取形式(连载还是完本)
form = one_selector.xpath("p[1]/span/text()").extract()[0]
#将爬取到的一部小说保存到item中
item = QidianHotItem() #定义QidianHotItem对象
item["name"] = name #小说名称
item["author"] = author #作者
item["type"] = type #类型
item["form"] = form #形式
#使用yield返回item
yield item
#获取下一页URL,并生成Request请求提交给引擎
……
以上代码分析如下:
- 首先导入qidian_hot.items下的QidianHotItem模块。
- 生成QidianHotItem的对象item,用于保存一部小说信息。
- 将从页面中提取到的各个字段赋给item。赋值方法跟Pyton的字典一样,使用key-value的形式。key要与在QidianHotItem中定义的名称一致,否则会报错,value为各个字段值。Item复制了标准的字典API,因此可以按照字典的形式赋值。