【爬虫大世界】
学习爬虫,最初的操作便是模拟浏览器向服务器发出请求。至于怎么做,不必感到无从下手,Python提供了功能齐全的类库来帮助我们完成这一操作
最基础的HTTP库有urllib、httplib2、request、treq等
【3.1使用urllib】
在Python2中,有urllib和urllib2两个库来实现请求的发送;而在Python3中,已经不存在urllib2了,统一为urllib,其官方文档为:https://docs.python.org/3/library/urllib.html
urllib库是Python内置的HTTP请求库,它包含4个模块:
△request:它是最基本的HTTP请求模块,用来模拟发送请求。只需给库方法传入URL以及额外的参数,就可以模拟[在浏览器输入网址,按下回车]这一过程
△error:异常处理模块,如果出现请求错误,我们可以捕捉这些异常,然后进行重试或其他操作,保证程序不会意外终止
parse /pɑ:rs/ v.从语法上描述或分析(词句等)
△parse:一个工具模块,提供了许多URL处理方法,比如拆分、解析、合并等
△robotparser:主要用来识别网站的robots.txt文件,然后判断哪些网站可以爬,哪些不可以
【3.1.1发送请求】
1、urlopen( )
urllib.request模块提供了最基本的构造HTTP请求的方法,利用它可以模拟浏览器的一个请求发起过程。以抓取Python官网为例:
import urllib.request response = urllib.request.urlopen('https://www.python.org') print(response.read().decode('utf-8'))
运行这两行代码,可以得到:
这里输出了网页的源代码
△如果在上述代码中不添加decode( )方法,源代码中的一些转义字符将无法被转义,比如上图中的空行是通过'\n'实现的,如果删去了decode( )方法,将会直接显示\n,而不是空行
如果删去decode( )方法,除了不转义之外,还有就是返回的数据类型为bytes,而不是str。由于返回的数据过多,需要将终端窗口扩充才能看到完整的数据,也就是在原有的str数据添加:b' ',用以表示bytes类型的数据
【bytes(字节流)类型数据】
事实上,一切数据在传输时都是bytes类型,原因在于为了更好地传输非英文系的数据(诸如汉字、韩文等),需要根据Unicode将这些字进行encode(编码),比如汉字'中',它对应的bytes类型数据为b'\xe4\xb8\xad',进行编码后,汉字'中'就能够进行数据传输了
为了统一,原本不需要进行编码的英文系数据也进行了编码,比如字母'a',它对应的bytes类型就直接是b'a'。这种编码只是形式上的,'a'与b'a'并没有一目了然的区别
我们基本上不需要关心bytes类型数据的编码和解码,因为这一切都是计算机自动完成的,普通用户不需要了解这么深,但对于学计算机的人必须了解,因为往往在写代码时进行数据操作,需要在bytes的编码和解码之间来回转换
原本服务器传回的HTML代码的解码工作由浏览器自动完成,但这里我们通过Python类库模拟浏览器向服务器发送请求,并没有浏览器,因此但凡是之前浏览器的工作,都需要我们自行完成
下面来看看它返回的到底是什么:
import urllib.request response = urllib.request.urlopen('https://www.python.org') print(type(response))
利用type( )方法输出响应的类型,得到如下结果:
<class 'http.client.HTTPResponse'>
可以发现,它是一个HTTPResponse类型的对象,再通过help(response),可以得知HTTPResponse类型的对象主要包含read( )、readinto( )、getheader(name)、getheaders( )、fileno( )等方法,以及msg、version、status、reason、debuglevel、closed等属性
将这个对象赋值为response变量后,就可以调用这些方法和属性了
read( )方法可以得到返回的网页内容,我们可以试试调用其他的方法和属性:比如status属性可以得到返回结果的状态码,最常见的200代表请求成功、404代表网页未找到
import urllib.request response = urllib.request.urlopen('https://www.python.org') print(response.stauts) print(response.getheaders()) print(response.getheader('Server'))
得到输出:
nginx(engine X) n.一款轻量级的Web服务器/反向代理服务器/电子邮件代理服务器
调用status属性直接输出了响应状态码200;然后调用getheaders( )方法,输出了响应的头信息,显示为一个元组列表(a sequence of two-element tuples);最后通过getheader( )方法并传递一个'Server'参数获取响应头中的值【为什么是元组列表而不是字典?】
利用最基本的urlopen( )方法,可以完成最基本的简单网页的GET请求抓取
【API(Application Programming Interface),应用程序编程接口】
API是一些预先定义的函数,目的是“提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节”
如果我们要给链接传递一些参数,该如何实现?首先看一下urlopen( )函数的API:
urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)
直接在Python官网中搜索'urllib.request.urlopen'即可查看该函数的API,可以发现,除了第一个参数可以传递URL外,我们还可以传递其他内容,比如data(附加数据)、timeout(超时时间)
●data参数
data参数是可选的,对于urlopen( )函数而言,data参数必须是字节流编码格式的内容,即bytes类型,因此我们使用Python的bytes( )方法进行转化
另外,如果传递了这个参数,则它的请求就不再是GET方式,而是POST方式
import urllib.request import urllib.parse data = bytes(urllib.parse.urlencode({'word' : 'hello'}), encoding = 'utf-8') response = urllib.request.urlopen('http://httpbin.org/post', data = data) print(response.read().encode('utf-8'))
首先我们假设要传递{'word' : 'hello'}这个数据,先是通过parse模块中的urlencode( )方法将字典转化为字符串;然后由于data参数类型必须是bytes,再用bytes( )函数,且通过第二个参数encoding指定编码格式,将原本为str类型数据的data转换为bytes类型数据
【urlencode( )】
留意一下,常见的URL中如果包含参数,它们出现在URL中的形式不会是键值对,而是类似,即通过'&'字符连接两个键值对,而键值对的键与值之间直接用'='连接,整体是一个字符串
urlencode( )方法就是帮助我们把键值对转化为上述这种形式的:
这里请求的站点是httpbin.org,它提供HTTP请求测试;这里请求的URL为http://httpbin.org/post,这个链接可以用来测试POST请求,输出请求的一些信息,其中包含我们传递的data参数:
我们传递的参数出现在了form字段中,这表明是模拟了表单提交的方式,以POST方式传输数据
●timeout参数
timeout参数用于设置超时时间,单位为秒。当请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,就会使用全局默认时间。
它支持HTTP、HTTPS、FTP请求
import urllib.request response = urllib.request.urlopen('http://httpbin.org/get', timeout = 0.1) print(reponse.read().decode('utf-8'))
输出结果为:
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File ''xxx.py'' , line 3 , in <module>
reponse = urllib.request.urlopen('http://httpbin.org/get' , timeout = 1)
...
urllib.error.URLError: <urlopen error timed out>
这里我们设置超时时间是0.1秒。程序过0.1秒后,服务器依然没有响应,就会抛出URLError异常。该异常属于urllib.error模块,错误原因是超时
因此,可以通过设置这个超时时间来控制一个网页如果长时间未响应,就跳过它的抓取。可以利用try except语句来实现:
socket (原)n.孔,插座 (计)n.套接字
网络上两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket
import socket import urllib.request import urllib.error try: response = urllib.request.urlopen('http://httpbin.org/get', timeout = 0.1) except urllib.error.URLError as e: if isinstance(e.reason, socket.timeout): print('Time Out!')
我们请求了http://httpbin.org/get测试链接,设置超时时间为0.1秒,然后捕获了URLError异常
一开始导入的socket模块暂时难以理解透彻,只需知道,socket模块中有timeout属性,如果要判断一个异常的发生是否真的是由于timeout,那么就需要通过isinstance( )函数来判断
对于所有的异常,都有一个reason属性,用以显示该异常的具体情况
就上述代码而言:
可以看到,其显示异常的具体情况为'socket.timeout'
按照常理来说,0.1秒内基本不可能得到服务器的响应,因此上述代码中成功输出了'Time Out!'
●其他参数
我们回顾urlopen( )函数的其他API:
urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)
除了data和timeout参数,还有其他参数,下面来简要概述:
context n.语境,上下文,背景,环境
△context参数:用来指定SSL设置,它必须是ssl.SSLContext类型
△cafile、capath参数:分别指定CA证书和它的路径,应用于请求HTTPS链接
△cadefault参数:现已弃用,默认值为False
综上,便是urlopen( )方法的用法,详见https://docs.python.org/3/library/urllib.request.html
2、Request
我们可以利用urlopen( )函数实现最基本请求的发起,但查看urlopen( )函数的API,似乎这几个参数并不足以构建一个完整的请求
如果请求中需要加入Headers等信息,就可以利用更强大的Request类来构建
import urllib.request request = urllib.request.Request('https://python.org') response = urllib.request.urlopen(request) print(reponse.read().decode('utf-8'))
以上述代码为例,我们依然使用urlopen( )来发送这个请求,只是这次的参数不再是URL,而是一个Request类型的对象
通过构造这个数据结构,一方面可以将请求独立成一个对象,另一方面实现灵活且丰富地配置参数
查看Request的构造方法:【为什么官网上没有相关资料?】
urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None)
Request存在的意义便是在发送请求时附加上一些信息,而仅靠urlopen( )则不能完成
下面来分析一下各个参数:
□ url:为用于请求的URL,是必传参数,其它都是可选参数
□ data:该参数与urlopen( )函数中的data参数一样,限定为bytes类型数据;且如果是字典,需用urllib.parse模块中的urlencode( )函数编码
□ headers:该参数为一个字典,它就是请求头
通过该函数,可以修改原本请求头中的信息,比如User-Agent,默认的User-Agent是Python-urllib,如果要伪装成火狐浏览器,可以把它设置为:
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11
从而实现爬虫伪装
我们可以在构建请求时通过headers参数直接构造,也可以通过请求实例的add_header( )方法
□ origin_req_host:请求方的host名称或IP地址
verify /ˈverɪfaɪ/ v.核实,证明,判定
□ unverifiable:默认为False,表示这个请求是否是无法验证的,意思是用户没有足够的权限来选择接收这个结果的请求
“例如,我们请求一个HTML文档中的图片,但是我们没有自动抓取图像的权限,此时unverifiable的值就是True”
□ method:该参数为一个字符串,用来指示请求使用的方法,如'GET'、'POST'和'PUT'等
from urllib import request , parse url = 'http://httpbin.org/post' headers = { 'User-Agent' : 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' , 'Host' : 'httpbin.org' } dict = {'name' : 'Examine2'} data = bytes(parse.urlencode(dict), encoding = 'utf-8') req = request.Request(url = url, headers = headers, data = data, method = 'POST') response = request.urlopen(req) print(response.read().decode('utf-8'))
我们尝试传入多个参数来构建一个请求,url指定请求URL,headers中指定User-Agent和Host,参数data用urlencode( )和bytes( )转换成字节流,指定请求方式为POST
执行上面示例代码,可得到输出:
可以看到,除却data中的数据成功发送外,我们还设置了headers和method
之前有提及,headers修改请求头中的信息“可以通过请求实例的add_header( )方法”,即:
req = request.Request(url = url, data = data, method = 'POST') req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')
注意add_header( )方法接收两个字符串
3、高级用法
在上面的过程中,我们虽然可以构造请求,但对于一些更高级的操作(如Cookies处理、代理设置等)还是无法实现
为此,我们需要更强大的工具【Handler】
Handler可以理解为各种处理器,有专门处理登录验证的,有处理Cookies的,有处理代理设置的...
首先介绍一下urllib.request模块中的BaseHandler类,它是所有其他Handler的父类,它提供了最基本的方法,如default_open( )、protocol_request( )等
接下来就有各种Handler子类继承这个BaseHandler类,如:
□ HTTPDefaultErrorHandler:用于处理HTTP响应错误,错误都会抛出HTTPError类型的异常
□ HTTPRedirectHandler:用于处理重定向
□ HTTPCookieProcessor:用于处理Cookies
□ ProxyHandler:用于设置代理,默认代理为空
Mgr → manager
□ HTTPPasswordMgr:用于管理密码,它维护了用户名和密码的表
Auth → authentication /ɔ:ˌθentɪ'keɪʃn/ n.认证
authentic /ɔ:ˈθentɪk/ adj.真的,可信的,认证了的
□ HTTPBasicAuthHandler:用于管理认证(链接打开时可能需要认证)
其余Handler请查阅:https://docs.python.org/3/library/urllib.request.html#urllib.request.BaseHandler
另外一个比较重要的类是【OpenerDirector】,简称为【Opener】;我们之前用过的urlopen( )方法就是urllib为我们提供的一个Opener
为什么要引入Opener?
之前使用的Request和urlopen( )相当于类库为你封装好了极其常用的请求方法,利用它们可以完成最基本的请求,但为了实现更高级的功能,我们需要深入一层进行配置,使用更底层的实例来完成操作
Opener可以使用open( )方法,返回的类型和urlopen( )如出一辙(记住urlopen( )也是Opener)
Opener与Handler的关系是:利用Handler来构建Opener
下面来看看几个实例:(可暂时不过于深究,待需要时回来复习)
●验证
有时打开某些网页,会出现以下对话框:
倘若要请求这样的页面,就需要借助HTTPBasicAuthHandler
realm /rɛlm]/ n.王国,领域
from urllib.request import HTTPBasciAuthHandler, HTTPPasswordMgrWithDefaultRealm, build_opener from urllib.error import URLError url = 'http://localhost:5000/'#←此为书上示范网址,未知原因无法打开 username = 'username' password = 'password' p = HTTPPasswordMgrWithDefaultRealm() p.add_password(None, url, username, password) auth_handler = HTTPBasicAuthHandler(p) opener = build_opener(auth_handler) try: result = opener.open(url) print(result.read().decode('utf-8')) except URLError as e: print(e.reason)
这里实例化了HTTPBasicAuthHandler对象,其参数是HTTPPasswordMgrWithDefaultRealm对象,它利用add_password( )添加用户名和密码,这样就建立了一个处理验证的Handler
然后利用这个Handler通过build_opener( )构建一个Opener,使用Opener的open( )方法打开链接,调用open( )就相当于调用了urlopen( ),剩下步骤相似
integrate /ˈɪntɪgreɪt/ v.使一体化,合并 adj.整体的、完整的
IDE → Integrated Development Environment 集成开发环境,是用于程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具
实践过程中始终出现Error:[WinError 10061] 由于目标计算机积极拒绝,无法连接。
尝试了许多方法,如:http://tieba.baidu.com/p/5995082291?pid=123475984092&cid=0#123475984092
其给出的方案是将服务器代码和客户端代码分别置于两个终端命令窗口执行;尝试了将范例中的代码搬运,依瓢画葫芦成功打开http://localhost:5000/网址,但依旧爬取失败,原因未知
●代理
做爬虫时,免不了要使用代理,如果要添加代理,可以这样做:
from urllib.error import URLError from urllib.request import ProxyHandler, build_opener proxy_handler = ProxyHandler({ 'http' : 'http://127.0.0.1:9743' , 'https' : 'https://127.0.0.1:9743' }) opener = build_opener(proxy_handler) try: result = opener.open('https://www.baidu.com') print(result.read().decode('utf-8')) except URLError as e: print(e.reason)
此次爬取同样出现Error:[WinError 10061] 由于目标计算机积极拒绝,无法连接。
“这里我们搭建了一个本地代理,它运行在9743端口上” 【?】
这里使用的ProxyHandler,其参数是一个字典,键名是协议类型(如HTTP或HTTPS等),键值是代理链接,可以添加多个代理
然后利用这个Handler加build_opener( )方法构建Opener,发送请求即可
●Cookies
jar /dʒɑr/ n.罐子、缸、杯 v.猛然震动,不一致
Mozilla /məuzilə/ Mosaic+Godzilla中文名称摩斯拉,Mozilla FireFox(火狐浏览器)的生产厂商
Cookies的处理就需要相关的Handler了
首先看看如何将网站的Cookies获取下来:
import http.cookiejar , urllib.request cookie = http.cookiejar.CookieJar() handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) response = opener.open('http://www.baidu.com') for item in cookie: print(item.name +'='+ item.value)
首先我们创建了一个CookieJar对象实例,然后利用HTTPCookieProcessor来构建一个Handler,最后构建出Opener,执行函数open( )即可
执行open( )函数后,目标网址上的Cookies就被获取,并存储到变量cookie中,结果输出:
这里可以看到输出了每条Cookie的名称和值
此外,获取的Cookies存储在变量cookie中,并非是单纯地以字典的形式储存,而是CookieJar类型,我们在上述代码后面添加print(cookie),可以看到:
因此不能简单地通过for key , value in cookie.items( ):来输出
不过,既然能输出,那就有可能输出成文件格式。Cookies实际上也是以文本形式保存的。
firename = 'cookies.txt' cookie = http.cookiejar.MozillaCookieJar(filename) handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) response = opener.open('http://www.baidu.com') cookie.save(ignore_discard = True, ignore_expires = True)
这时的CookieJar就需要换成MozillaCookieJar了
【CookieJar子类】
CookieJar本身是一种类型对象,用于
①管理HTTP Cookie值
②存储HTTP请求生成的Cookie
③向传出的HTTP请求添加Cookie对象
【FileCookieJar(filename , delayload = None , policy = None)】:
从CookieJar派生而来,用来创建FileCookieJar实例,检索Cookie信息并将Cookie存储到文件中
filename是存储cookie的文件名;delayload为True时,支持延时访问文件,即只有在需要时才读取文件或在文件中储存数据
【MozillaCookieJar(filename , delayload = None , policy = None)】:
从FileCookieJar派生而来,创建与Mozilla浏览器cookie.txt兼容的FileCookieJar实例
【LWPCookieJar(filename , delayload = None , policy = None)】:
从FileCookieJar派生而来,创建与libwww-perl标准的Set-Cookie3格式兼容的FileCookieJar实例
大多数情况下,我们只用CookieJar;如果需要和本地文件交互,就用MozillaCookieJar或LWPCookieJar
上述代码将CookieJar替换成MozillaCookieJar,并调用了save( )函数:save(ignore_discard = True , ignore_expires = True),成功将Cookies保存成Mozilla型浏览器的Cookies格式
discard /dɪsˈkɑ:d/ v.丢弃,解雇,出牌 n.被抛弃的人,丢出的牌
【Mozilla型浏览器】
之外我们介绍Request类型对象时,介绍到了headers,而使用Request对象作为urlopen( )参数而不是单纯的URL的根本原因是,Request能在请求时发送一些附加信息,如headers
我们有说到“代理”,headers中的User-Agent是方便我们伪装成浏览器的,默认为Python-urllib;我们之前伪装成火狐浏览器,就是将User-Agent修改为
Mozilla/5.0 (X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11
我们查阅一下其他浏览器的User-Agent,发现:
搜狗浏览器 1.x
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET CLR 2.0.50727; SE 2.X MetaSr 1.0)
360浏览器
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)
世界之窗(The World) 3.x
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)
IE 9.0
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0
你会发现,几乎所有的浏览器的User-Agent都带有'Mozilla'字符串,这也是为什么MozillaCookieJar会成为常用的Cookie存储格式
至于为什么都会带有'Mozilla'字符串,详细请参阅:https://blog.csdn.net/puppylpg/article/details/47319401
总结下来就是,
最初的Mozilla浏览器功能卓越,拥有Mozilla Frame(框架);当其他浏览器被研发出来时,不想放弃Mozilla Frame,通通就在User-Agent上声明自己支持Mozilla(事实上是伪装成Mozilla)
而当时伪装成Mozilla的其他浏览器又拥有各自的优点,当后续浏览器被研发,为了兼容其他浏览器的优点,也总是在User-Agent上伪装成其他浏览器,追根溯源,导致绝大部分浏览器都在User-Agent上标注为'Mozilla'。所谓User-Agent,也变得混乱不堪,失去了原本的意义
尽管现在Mozilla浏览器已经被淘汰(继承的是Mozilla FireFox浏览器),但就是这个已经不存在的浏览器,“在形式上”存活于其他各大浏览器的User-Agent中
这就是Mozilla型浏览器
题外话到此结束
上述代码将CookieJar修改为MozillaCookieJar,运行代码,会生成一个cookie.txt文件,内容如下:
这也就是Mozilla型浏览器储存Cookies的格式
生成文件是通过在创建MozillaCookieJar实例时传递文件名、调用save( )函数实现的,除此之外,步骤与普通的CookieJar一样;事实上,如果这时调用for item in cookie: print(item.name +'='+ item.value),仍可输出原来的结果
这表明MozillaCookieJar生成的cookie实例在后续操作中产生的变化与原来的CookieJar是一样(除了实例类型从CookieJar变成MozillaCookieJar),只是在保存成文本时修改了格式,与Mozilla浏览器兼容
另外,上述在介绍CookieJar子类时提及到了LWPCookieJar
LWP是Library for WWW access Perl的缩写,Perl是一门编程语言,LWP是访问Web服务器的Perl包,利用这个包,可以很方便地在Perl脚本里面访问外部Web服务器上的资源
因此将Cookie保存成libwww-perl(LWP)格式的Cookies文件有好处
要生成LWP格式的Cookies文件,只需修改:
cookie = http.CookieJar.LWPCookieJar(filename)
可以得到一个新的cookie.txt文件:
生成了Cookies文件可以对其进行读取并利用(以LWPCookieJar格式为例,MozillaCookieJar格式的同样适用),当请求的网站需要Cookies时:
import http.cookiejar , urllib.reqeust cookie = http.cookiejar.LWPCookieJar() cookie.load('cookie.txt') handler = urllib.request.HTTPCookieProcessor(cookie) opener = urllib.request.build_opener(handler) response = opener.open('http://www.baidu.com') print(response.read().decode('utf-8'))
这里,在已经生成了LWPCookieJar格式的Cookies并保存成文件的前提下,通过调用load( )方法来读取本地的Cookies文件,获取到Cookies的内容
通过同样的方法构建Handler和Opener后,即可输出百度网页的源码
【3.1.2处理异常】
前一节我们了解了请求的发送过程,但某些时候会出现各种异常,倘若不处理这些异常,程序很可能因报错而终止运行。因此我们很有必要学会异常处理
urllib的error模块定义了由request模块产生的异常;如果出现了问题,request模块就会抛出error模块中定义的异常
1、URLError
“URLError类来自urllib库的error模块,它继承自OSError,是error异常模块的基类,由request模块产生的异常都可以通过捕获这个类来处理”
它具有一个reason属性,即返回错误的原因
from urllib import request, error try: response = request.urlopen('https://cuiqingcai.com/index.htm') except error.URLError as e: print(e.reason)
我们尝试打开一个不存在的页面,按理说应该会报错,但这时我们捕获了URLError这个异常,成功输出了'Not Found'字样
所谓不存在的页面,是服务器成功响应(状态码200)后,但服务器检测到该域名下并不存在的网页;而在地址栏上随便打上不存在的URL(诸如baaidu.com),属于根本没有作出响应;两种情况不同
同样的还有https://www.52pojie.cn/thread-666632-1-1.htmlnn
程序输出了e.reason而没有直接报错,就避免了程序异常终止,同时有效处理异常
2、HTTPError
HTTPError是URLError的子类,专门用来处理HTTP请求错误,比如认证请求失败等
它有3个属性:
□ code:返回HTTP状态码,404网页不存在、500服务器内部错误...
□ reason:继承自父类URLError,同样返回错误的原因
□ headers:返回请求头
from urllib import request, error try: response = request.urlopen('https://cuiqingcai.com/index.htm') except error.HTTPError as e: print(e.reason, e.code, e.headers, sep = '\n')
sep → separator,默认为' ',替换print( )函数中的逗号
输出为:
依旧是同样的网址,这里捕获了HTTPError,并输出了reason、code和headers属性
因为URLError是HTTPError的父类,所以一般推荐先选择捕获子类的错误,再去捕获父类的错误:
from urllib import request, error try: response = request.urlopen('https://cuiqingcai.com/index.htm') except error.HTTPError as e: print(e.reason, e.code, e.headers, sep = '\n') except error.URLError as e: print(e.reason) else: print('Request Successfully!')
有时候,reason返回的是诸如'Not Found'的字符串,但有时候返回的也可能是一个对象
我们在学习urlopen( )的timeout参数时,有代码行if isinstance(e.reason , socket.timeout),如果这时输入代码print(type(e.reason)),结果会输出<class 'socket.timeout'>
【3.1.3解析链接】
前面有提及urllib的parse模块,它定义了处理URL的标准接口,实现URL各部分的抽取、合并以及链接转换等
它支持如下协议的URL处理:file、ftp、gopher、hdl、http、https、imap、mailto、mms、news、nntp、prospero、rsync、rtsp、rtspu、sftp、sip、sips、snews、svn、svn+ssh、telnet和wais
①、urlparse( )
该方法可以实现URL的识别和分段:
from urllib import parse result = urllib.parse.urlparse('http://www.baidu.com/index.html;user?id=5#comment') print(type(result), result)
利用urlparse( )对一个URL进行解析,输出解析结果的类型以及解析结果:
可以看到,返回的结果是一个ParseResult类型的对象,它包含6个部分,分别是scheme、netloc、path、params、query和fragment
scheme /ski:m/ v./n.计划,图谋 n.体系 位于'://'之前,代表协议
netloc → network locality 代表域名,也即服务器位置,位于'/'后面、';'或'?'前面的全部
path 访问路径,也即网页文件在服务器中的位置
params → parameters n.形参(复) 位于';'后面,是可选参数
query /ˈkwɪəri/ v./n.询问,疑问 n.问题,问号 位于'?'后面的查询条件,一般用作GET类型的URL,用'&'连接键值对
fragment /ˈfrægmənt/ v.破碎、破裂 n.碎片,片段,分段,未完成的部分 位于'#'后面,是锚点,用于直接定位页面内部的下拉位置
所以可以得出一个标准的链接格式:
scheme://netloc/path;params?query#fragment
一个标准的URL都会符合这个规则,利用urlparse( )将其拆分出来
除却最基本的解析方式外,urlparse( )还拥有其他配置。查阅它的API用法:
urllib.parse.urlparse(urlstring, scheme='', allow_fragments=True)
□ urlstring:为必填项,即待解析的URL
□ scheme:它是默认的协议。倘若这个URL没有带协议信息,会将这个作为默认的协议
from urllib.parse import urlparse result = urlparse('www.baidu.com/index.html;user?id=5#comment', scheme = 'https') print(result)
可以看到,URL没有包含最前面的scheme信息,但返回了指定默认的scheme参数
【URL不携带scheme属性时,原本netloc的值为什么搬运至path处?】
假如我们带上scheme:
from urllib.parse import urlparse result = urlparse('http://www.baidu.com/index.html;user?id=5#comment', scheme = 'https') print(result)
可见,scheme参数只有在URL中不包含scheme信息时才会生效
□ allow_fragments:即是否允许fragment,默认为True。当它被设置为False时,原fragment部分会被忽略,被解析为path、params或者query的一部分,fragment部分为空
from urllib.parse import urlparse reslut = urlparse('http://www.baidu.com/index.html;user?id=5#comment', allow_fragments = False) print(result)
如果URL中不包含params和query:
from urllib.parse import urlparse result = urlparse('http://www.baidu.com/index.html#comment', allow_fragments = False) print(result)
当URL中不包含params和query,fragment会被解析为path的一部分
实际上返回的ParseResult是一个元组,我们可以用索引来获取,也可以用属性名来获取:
from urllib.parse import urlparse result = urlparse('http://www.baidu.com/index.html#comment', allow_fragments = False) print(result.scheme, result[0], result.netloc, result[1], sep = '\n')
得到输出:
通过索引和属性名来获取,其结果是一致的
②、urlunparse( )
有了urlparse( ),就有它的对立方法urlunparse( )
urlunprase( )接受的参数是一个可迭代对象(诸如list、tuple等),但它的长度必须是6,否则会抛出参数不足或过多的问题
from urllib.parse import urlunparse data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment'] print(urlunparse(data))
这样我们就成功实现了URL的构造
③、urlsplit( )
这个方法与urlparse( )非常相似,只不过它不再单独解析params部分,而是将params合并到path中,只返回5个结果:
from urllib.parse import urlsplit result = urlsplit('http://www.baidu.com/index.html;user?id=5#comment') print(result)
返回的SplitResult类型对象同样也是一个元组,可以通过之前提到的两种方式来获取
④、urlunsplit( )
传入的参数必须是长度为5的可迭代对象,params部分提前合并到path中,详细参考urlunparse( )
⑤、urljoin( )
对于如何生成链接,我们可以使用urlunparse( )和urlunsplit( ),但前提是必须有特定长度的对象,且链接的每一部分都要清晰分开
生成链接还有另外一个方法,那就是urljoin( ),在学习urljoin( )之前,我们先回顾join( )用法:
【join( )】(sep).join(seq) → (separator).join(sequence)
join( )不能像print( )一样单独调用,它需要一个'sep',即分隔符(separator);而join( )接受一个序列
▷'分'.join(['1' , '2' , '3']) ==> '1分2分3'
▷''.join(['1' , '2' , '3']) ==> '123'
可以看到,join( )用于将序列中的元素按指定的连接符合并成一个新字符串;当sep为空时,就是纯粹地将序列中的元素连接成一个新字符串
既然是合并字符串,那么原有的序列中的元素也必须是字符串:
▷'分'.join([1 , 2 , 3])
这样会抛出异常TypeError: sequence item 0: excepted str instance, int found
urljoin( )的API为urljoin(base, url, allow_fragment=True),它接收两个参数,base_url(基础链接)为第一个参数,新的URL作为第二个参数;该方法会分析base_url的scheme、netloc和path这三个内容,并对新链接缺失的部分进行补充
通过实例可以看出,urljoin( )将base_url解析,剥离出scheme、netloc和path三项内容(如果有),检查url参数是否在这三项内容上有残缺,如果有,将会用剥离出的scheme、netloc或path来修补;如果url参数本身就含有scheme、netloc或path,将保持自身的内容
base_url中的params、query和fragment将不起作用
⑥、urlencode( )
该方法在之前已经有提前学习,简而言之,其作用就是将字典转换为符合URL的字符串
有时为了更加方便地构造参数,我们会事先将参数用字典来表示,然后调用urlencode( )来构造URL
from urllib.parse import urlencode params = {'name' : 'Examine2', 'age' : '19'} base_url = 'http://www.baidu.com?' url = base_url + urlencode(params) print(url)
结果输出:
参数就成功地由字典转化为GET请求参数了
⑦、parse_qs( )
qs是query string的缩写,特指URL中的query参数,而一般在URL中,query参数表现为:
事实上parse_qs( )是反序列化,旨在将GET请求参数转化回字典,与urlencode( )相反,你甚至可以把parse_qs( )看作是urldecode( )
from urllib.parse import parse_qs query = 'name=Examine2&age=19' print(parse_qs(query))
代表query参数的字符串成功转化为字典,而由于字典的值有时候并非只有一个元素,因此parse_qs( )返回的字典中,值全部为含有str类型数据的list
⑧、parse_qsl( )
qsl是query string list的缩写,该方法与parse_qs( )十分相似,只是返回的结果不再是dict,而是由tuple组成的list
from urllib.parse import parse_qsl query = 'name=Examine2&age=19' print(parse_qsl(query))
鉴于list中每个元素都是tuple,故可以通过parse_qsl(query)[0][1]的方式来读取数据
⑨、quote( )
quote /kwəʊt/ v.引用、引述,报价
如果传递给URL中文参数,有时可能导致乱码,quote( )就是将中文字符转化为URL编码的
from ullib.parse import quote keyword = '真实' url = 'https://www.baidu.com/s?wd=' + quote(keyword) print(url)
可以看到,中文字符'真实'被编码成了'%E7%9C%9F%E5%AE%9E',而这也其实是搜索关键字在网页中存在的真正形式:在百度搜索栏中输入'真实',尽管URL中显示出中文字符:
但打开网页源文件,会发现这时变成了
quote( )默认的编码方式为UTF-8
⑩、unquote( )
quote( )的对立方法,能够对URL中隐藏的中文字符进行解码
from urllib.parse import unquote print(unquote('https:///www.baidu.com/s?wd=%E7%9C%9F%E5%AE%9E'))
【3.1.4分析Robots协议】
1、Robots协议
exclude /ɪkˈsklu:d/ v.排除,排斥,驱除
Robots协议(爬虫协议、机器人协议),全名为网络爬虫排除标准(Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以爬取、哪些不可以
它通常是一个叫作robots.txt的文本文件,一般放在网站的根目录下
当搜索爬虫访问一个站点时,它首先会检查这个站点根目录下是否存在robots.txt文件,如果存在,搜索爬虫会根据其中定义的爬取范围来爬取;如果没有找到这个文件,搜索爬虫便会访问所有可直接访问的页面
下面来看一个robots.txt的样例:
User-agent: *
Disallow: /
Allow: /public/
上述robots.txt限制了所有爬虫只可以爬取public目录的功能
将上述内容保存成robots.txt文件,放在网站的根目录下,和网站的入口文件(比如index.php、index.html和index.jsp等)放在一起
上面的User-agent描述了搜索爬虫的名称,这里设置为*表示该协议对任何爬取爬虫都有效
比如,设置为User-agent: Baiduspider代表我们设置的规则对百度爬虫是有效的;如果有多条User-agent记录,则会有多个爬取爬虫受到限制
Disallow指定了不可以抓取的页面,以上例子设置为/则代表不允许爬取任何页面
Allow通常与Disallow一起使用,一般不会单独使用,它用来排除某些限制。现在设置为/public/,则表示所有页面不允许抓取,但可以抓取public目录
下面来看几个例子:
①禁止所有爬虫访问任何目录:
User-agent: *
Disallow: /
②允许所有爬虫访问任何目录:
User-agent: *
Disallow:
(或者直接把robots.txt文件留空)
③禁止所有爬虫访问网站某些目录:
User-agent: *
Disallow: /private/
Disallow: /tmp/
tmp → temporary .tmp文件是临时文件,很多都没有什么价值
④只允许某个爬虫访问:
User-agent: WebCrawler
Disallow:
User-agent: *
Disallow: /
2、爬虫名称
事实上,各个网站的爬虫都有了固定的名字,比如:
爬虫名称 |
名称 |
网站 |
BaiduSpider |
百度 |
|
Googlebot |
谷歌 |
|
360Spider |
360搜索 |
|
YodaoBot |
有道 |
|
ia_archiver |
Alexa |
|
Scooter |
altavista |
更多的可以在网上进行搜索
3、robotparser
了解了Robots协议后,我们可以使用robotparser模块来解析robots.txt了
robotparser模块只提供了一个类RobotFileParser,它可以根据某网站的robots.txt文件来判断一个爬取爬虫是否有权限来爬取这个网页
该类的API为:urllib.robotparser.RobotFileParser(url=' '),构造RobotFileParser的实例只需传入robots.txt的链接即可;当然,可以在声明时不传入,使其按默认的空URL创建一个实例,然后调用实例的set_url( )方法
这个类的几个常用方法:
□ set_url( ):用来设置robots.txt文件的链接。如果在创建RobotFileParser对象时就已经传入了链接,则不需要再使用这个方法设置了
□ read( ):读取robots.txt文件并进行分析。注意这个方法执行了一个读取和分析的操作,如果不调用这个方法,接下来的判断都为False,所以一定要记得调用这个方法。该方法不会返回任何内容,但执行了读取操作
□ parse( ):用来解析robots.txt文件,传入的参数是robots.txt某些行的内容,它会按照robots.txt的语法规则来分析这些内容。该方法允许直接分析已有的robots.txt文件,可以看作是read( )的替补
fetch /fetʃ/ v.取来、拿来,售得 n.诡计,风浪区
□ can_fetch( ):该方法传入两个参数,第一个是User-agent(可以是代表所有爬虫的'*'),第二个是要抓取的URL(该URL必须存在于构造时传入的URL的子目录下)。它返回True或False,代表该搜索引擎是否可以爬取这个URL
□ mtime( ):'m'的含义未知,我的理解是'modified time',将返回上次抓取和分析robots.txt的时间。这个方法对于长时间分析和抓取的爬虫是很有必要的,你可能需要定期检查来抓取最新的robots.txt
□ modified( ):将当前时间设置为上次抓取和分析robots.txt的时间【作用?】
from urllib.robotparser import RobotFileParser rp = RobotFileParser() rp.set_url('http://www.jianshu.com/robots.txt') rp.read() print(rp.can_fetch('*', 'http://www.jianshu.com/p/b67554025d7d')) print(rp.can_fetch('*', 'http://www.jianshu.com/search?q=python&page=1&type=collections'))
上述代码中,我们首先创建了一个RobotFileParser对象,通过set_url( )方法设置了robots.txt的链接,接着利用can_fetch( )方法判断了网页是否可以被爬取:
上面的代码是通过set_url( )和read( )来获取即将要分析的robots.txt,其实也等价于:
rp.parse(urlopen('http://www.jianshu.com/robots.txt').read( ).decode('utf-8').split('\n'))
以上便是robotparser模块的基本用法和实例,利用它判断出哪些页面时可以爬取、哪些不可以
''''''''''''''