前言 最近学习了基于Python的网络爬虫,都说写博客是对自己所学知识进行再次总结与梳理的很好的方式,因此在完成了学习后,我希望能在日常学习之余,找出一部分时间再来回顾一下我的Python爬虫学习之路,既能进一步巩固的爬虫知识,又能让我对Python相关知识体系有更清晰的认识。下面就让我们开始Python网络爬虫的学习笔记专栏吧.

我的Python爬虫学习路线是这样的:先学习Python网络爬虫的相关知识,并在随后进一步学习基于Python的数据分析,最后根据PyQt库制作出一个具有较好的UI设计成熟程序。我们的学习之路将从Python网络爬虫的基本知识开始。众所周知,编程技能的学习更多的是依赖实际的代码训练项目,而非简单的阅读教材与观看视频,所以我也会将我在更新Python爬虫笔记过程中写的代码在我的GitHub上同步更新,希望大家多多关注和支持。

学习爬虫,首先需要掌握一系列基本的库的使用,在今天的这篇文章里我们将一同学习urllib这个基本的库。

urllib是Python内置的http请求库,它有下面四个模块:

  • request:模拟发送请求,通过给库方法传入url以及其它额外的参数,就可以模拟这个过程
  • error:异常处理模块,当出现请求错误时,利用此模块捕获异常并重新尝试或者进行其他操作保证程序不会出现异常
  • parse:提供了拆分、解析、合并等URL处理方法
  • robotparser:用来识别网站robots.txt文件

下面我们将分别介绍这几个模块

request模块——发送请求并得到响应

urlopen()

1
2
3
#urlopen()函数原型
urllib.request.urlopen(url,data=None,[timeout,]*,cafile=None,capath=None,cadefault=False,context=None)
#data为附加数据,timeout为超时时间

这是一种最基本的构造HTTP请求的方法,可以用来模拟浏览器的一个请求发起过程,还可以处理授权验证、重定向、浏览器Cookies和其他内容.利用此方法可以实现最基本的简单网页GET请求抓取。

1
2
3
4
5
6
7
import urllib.request
response=urllib.request.urlopen('http://www.dingzhi.ga') #定义HTTPResponse类型对象response
print(response.read().decode('utf-8')) #使用HTTPResponse类型对象的read()方法并利用decode()方法
print(type(response)) #返回response的类型HTTPResponse
print(response.status) #返回response的状态码,此处为200,代表请求成功,例如404代表网页未找到
print(response.getheaders()) #返回response的响应头信息
print(response.getheader('Server')) #返回response的响应头信息中Server项

下面是urlopen()方法的几个其他可选参数的使用方法.

data参数

如果添加该参数,并且它是字节流编码格式的内容,即bytes类型,则需要bytes()方法转化,另外,如果传递了这个参数,那么请求方式就不再是GET方式,而是POST方式。下面是一个示例代码:

1
2
3
4
5
import urllib.parse
import urllib.request
data = bytes(urllib.parse.urlencode({'word': 'hello'}), encoding='utf8') #使用bytes()方法转字节流,该方法需要的第一个参数是str类型,需要使用urllib.parse.urlencode()方法将参数字典转为字符串;第二个参数指定编码格式,这里定为utf8
response = urllib.request.urlopen('http://httpbin.org/post', data=data) #传递了参数word,值为hello,它需要被转码成bytes类型
print(response.read())

运行上述例程后可以在运行结果中看到我们传递的参数,证明这模拟了表单提交方式,以POST方式传输数据。

timeout参数

设置超时时间,单位为秒。如果请求超出了设置的这个时间仍然没有响应就会抛出异常。下面是一段示例程序:

1
2
3
import urllib.request
response = urllib.request.urlopen('http://httpbin.org/get', timeout=1)
print(response.read())

运行时系统抛出了URLError异常,错误原因是超时.

在实际应用中,这个参数可以用来控制一个网页如果长时间没有响应,就跳过爬取这个网站,可以通过try-except语句实现,也就是实现超时处理。相关代码如下:

1
2
3
4
5
6
7
8
9
> 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')
>

上面的示例程序在最终输出了TIME OUT。这在实际应用中有时很有用处。

Request

利用Request可以实现在请求中加入Headers等信息的需求。构造方法如下:

1
2
3
4
5
6
7
class urllib.request.Request(url,data=NNone,headers={},origin_req_host=None,unverifiable=False,method=None)
#参数说明:
#data:必须传入bytes类型,如果它是字典,需要先使用urllib.parse.urlencode()进行编码
#headers:请求头是字典类型,可以通过headers参数直接构造请求,也可以利用add_header()方法添加,这个参数常用来在爬取时应对部分网站反爬虫措施时伪装浏览器,下面将会详细展开
#origin_req_host:请求放host名称或IP
#unverifiable:这个请求是否无法验证
#method:指示请求使用的方法,如POST、GET和PUT等

下面我们来实际练习一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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': 'Germey'
}
data = bytes(parse.urlencode(dict), encoding='utf8')
req = request.Request(url=url, data=data, headers=headers, method='POST')
response = request.urlopen(req)
print(response.read().decode('utf-8'))

观察运行结果可以发现我们成功设置了data、headers、method,另外我们也可以用add_header()方法添加headers,并方便的构造请求:

1
2
req = request.Request(url=url, data=data, method='POST')
req.add_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)')

我们常常利用爬虫模拟浏览器上网的方式来实现绕过很多网站反爬虫机制进行网页爬取,下面我们来详细介绍相关内容。这个方法的核心思想是利用Request伪装headers头文件,进而将爬虫伪装成一个浏览器。
而获取正常浏览器浏览时的headers的方法很简单:首先打开浏览器审查元素界面,点击Network选项卡

img

接下来刷新网页:

1567426658777

点击第一项dingzhi.ga后右边会显示Headers选项如下所示:

1567426699516

在最后面就有User-Agent的信息,我们即可直接复制这段信息进入我们的代码。

1
2
3
4
5
6
7
> from urllib import request
> url = "http://www.dingzhi.ga"
> header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36'}
> req = request.Request(url, headers = header)
> text = request.urlopen(req).read().decode()
> print(text)
>

这样就能成功的爬取到那些采取了反爬虫措施的网页信息了。

高级操作——Opener

Opener可以使用open()方法,返回类型和urlopen()如出一辙,我们可以利用Handler构建Opener,实现高级功能(如验证、代理、Cookies等),此处我们略去不表,在未来用到时再做展开。

error模块——处理异常

error模块定义了由request模块产生的异常,如果出了问题,request模块就会抛出error模块定义的异常.

URLError

这个类来自error模块,继承自OSError类,是error异常模块的基类,request模块生成的异常都可以通过捕获这个类来处理,它具有一个属性reason,即返回错误的原因.下面是一个实例:

1
2
3
4
5
from urllib import request, error
try:
response = request.urlopen('http://www.dingzhi.ga/index.php')
except error.URLError as e:
print(e.reason)

我们打开一个不存在的页面,照理来说应该报错,但是由于捕获了URLError这个异常,运行后显示Not Found.这样我们就可以避免程序异常终止,并且在发生异常时进行有效处理.

HTTPError

URLError的子类,专门用来处理HTTP请求错误,具有如下三个属性:

  • code:返回HTTP状态码,如404表示网站不存在,500表示服务器内部错误等
  • reason:如同父类一样,用于返回错误的原因
  • headers:返回请求头

我们下面结合一个示例代码进行分析:

1
2
3
4
5
from urllib import request,error
try:
response = request.urlopen('http://www.dingzhi.ga/index.php')
except error.HTTPError as e:
print(e.reason, e.code, e.headers, sep='\n')

在上面的程序中由于捕获了HTTPError异常,因而正常输出了reason、code、headers等属性.

考察到URLErrorHTTPError的父类,所以可以先捕获子类的错误,再捕获父类的错误,上述代码在这种思路下可以进行优化,改写成为如下的代码,这也代表一种程序书写的范式与思路:

1
2
3
4
5
6
7
8
9
10
> from urllib import request, error
> try:
> response = request.urlopen('http://www.dingzhi.ga/index.php')
> 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属性返回的并不一定是字符串,也有可能是一个对象,下面我们看一段实例:

1
2
3
4
5
6
7
8
9
import socket
import urllib.request
import urllib.error
try:
response = urllib.request.urlopen('https://www.dingzhi.ga', timeout=0.001)
except urllib.error.URLError as e:
print(type(e.reason))
if isinstance(e.reason, socket.timeout):
print('TIME OUT')

通过输出结果我们可以看到,reason属性的结果是socket.timeout类,所以可以用isinstance()方法判断其类型,进而做出更详细的异常判断.

parse模块——解析处理URL链接

这个模块可以实现URL各部分的抽取、合并以及链接转换等,支持各种常见协议的URL处理,如ftp、http、https等,下面我们将介绍这个模块的常用方法.

urlparse()——实现URL识别与分段

我们通过考察下面一个实例的方式来分析这个方法的用途.

1
2
3
from urllib.parse import urlparse
result = urlparse('http://www.baidu.com/index.html;user?id=5#comment')
print(type(result), result)

读者不妨自己在电脑上尝试运行这段代码,我们不难观察出,返回的结果为ParseResult类型的对象,包含scheme、netloc、path、params、query、fragment六个部分.结合我们输入的URL链接进行对照,为我们得到了以下的公式,事实上任何一个标准的URL都会符合这个规则,我们可以利用urlparse()方法将之进行拆分:

1
scheme://netloc/path;params?query#fragment

下面我们考察urlparse()的API.

1
urllib.parse.urlparse(urlstring,scheme='',allow_fragments=True)
  • urlstring:必填项,即待解析URL

  • scheme:默认的协议(如httphttps)加入这个链接没有带协议信息,将会用这个作为默认协议,如下所示

    1
    2
    3
    from urllib.parse import urlparse
    result = urlparse('www.baidu.com/index.html;user?id=5#comment', scheme='https')
    print(result)

    从运行结果上看,scheme被指定为了https,但需要注意,当需要解析的链接带上了scheme时,最终返回的scheme以链接中的scheme为准,与API中传入的scheme无关,例如在下面的案例中,最终返回scheme为http而不是https:

    1
    2
    3
    from urllib.parse import urlparse
    result = urlparse('http://www.baidu.com/index.html;user?id=5#comment', scheme='https')
    print(result)
  • allow_fragments:即是否忽略fragment.如果被设置为False,fragment就会被自动忽略,它会被解析为path、parameters或者query中的一部分,而fragment部分为空.

urlunparse()——实现URL的整合与构造

是前述方法的对立方法,我们直接通过下面的实例介绍其使用方法而不再过多文字说明:

1
2
3
4
from urllib.parse import urlunparse
data = ['http', 'www.baidu.com', 'index.html', 'user', 'a=5', 'comment']
print(urlunparse(data))
#运行结果为http://www.baidu.com/index.html;user?a=5#comment

urlsplit()——实现URL识别与分段

urlparse()类似但是不解析params,只返回5个结果.返回结果是SplitResult类型,其实这是一个元组类型,既可以用属性获取值,也可以用索引获取值.

urlunsplit()——实现URL的整合与构造

是前述方法的对立方法,我们直接通过下面的实例介绍其使用方法而不再过多文字说明:

1
2
3
4
from urllib.parse import urlunsplit
data = ['http', 'www.baidu.com', 'index.html', 'a=6', 'comment']
print(urlunsplit(data))
#运行结果为http://www.baidu.com/index.html;user?a=6#comment

urljoin()——实现URL的解析、拼合与生成

利用这个方法,我们可以提供一个base_url作为第一个参数,将新的链接作为第二个参数,该方法会分析base_url的结构并对新链接缺失的部分进行补充并最终返回结果.下面是一个案例:

1
2
3
4
5
6
7
8
9
from urllib.parse import urljoin
print(urljoin('http://www.baidu.com', 'FAQ.html'))
print(urljoin('http://www.baidu.com', 'http://www.dingzhi.ga'))
print(urljoin('http://www.baidu.com/about.html', 'http://www.dingzhi.ga/index.html'))
print(urljoin('http://www.baidu.com/about.html', 'http://www.dingzhi.ga/articles/Python-DataScience-1/#%E5%9B%BE%E5%83%8F%E7%9A%84%E5%8F%98%E6%8D%A2'))
print(urljoin('http://www.baidu.com?wd=abc', 'http://www.dingzhi.ga/index.html'))
print(urljoin('http://www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com#comment', '?category=2'))

可以发现,base_url提供了scheme、netloc、path三项内容,如果这三项内容在新的链接里面不存在就进行补充,如果新的链接里面存在就是用新的链接的部分,而base_url中params、query、fragment是不起作用的.

urlencode()——实现GET请求参数的构造

下面我们通过一段示例代码进行介绍:

1
2
3
4
5
6
7
8
9
from urllib.parse import urlencode
params = {
'name': 'dingzhi',
'age': 20
} #声明了一个字典将参数进行表示
base_url = 'http://www.baidu.com?'
url = base_url + urlencode(params) #调用urlencode()方法将其序列化为GET请求参数
print(url)
#运行结果:http://www.baidu.com?name=dingzhi&age=20

可以看到,参数成功的由字典类型转换为GET 请求参数了,这个方法非常常用,有时为了更加方便的构造参数,我们会事先用字典表示,要转化为URL的参数时,只需要调用该方法即可.

parse_qs()——实现GET请求参数的反序列化

利用parse_qs()方法可以将GET请求参数转为字典.下面是一个案例

1
2
3
4
from urllib.parse import parse_qs
query = 'name=dingzhi&age=20'
print(parse_qs(query))
#运行结果:{'name':['dingzhi'],'age':['20']}

parse_qsl()——实现GET请求参数到元组列表的转化

1
2
3
4
from urllib.parse import parse_qsl
query = 'name=dingzhi&age=20'
print(parse_qsl(query))
#运行结果:[{('name','dingzhi'),('age','20')]

quote()——实现内容到URL编码的转化

可以将内容转化为URL编码的格式,当URL带有中文参数时,有时可能会导致乱码问题,可以利用这个方法将中文字符转换为URL编码:

1
2
3
4
from urllib.parse import quote
keyword = '浙江大学'
url = 'https://www.baidu.com/s?wd=' + quote(keyword)
print(url)

unquote()——实现URL编码到内容的转化

1
2
3
from urllib.parse import unquote
url = 'https://search.bilibili.com/all?keyword=%E6%B5%99%E6%B1%9F%E5%A4%A7%E5%AD%A6&from_source=banner_search'
print(unquote(url))

robotparser模块——分析Robots协议

Robots协议概述

robots是网站跟爬虫间的协议,用简单直接的txt格式文本方式告诉对应的爬虫被允许的权限,也就是说robots.txt是搜索引擎中访问网站的时候要查看的第一个文件。当一个搜索蜘蛛访问一个站点时,它会首先检查该站点根目录下是否存在robots.txt,如果存在,搜索机器人就会按照该文件中的内容来确定访问的范围;如果该文件不存在,所有的搜索蜘蛛将能够访问网站上所有没有被口令保护的页面。

robots.txt文件的格式

“robots.txt”文件包含一条或更多的记录,这些记录通过空行分开(以CR,CR/NL, or NL作为结束符),每一条记录的格式如下所示:

1
"<field>:<optionalspace><value><optionalspace>"。

在该文件中可以使用#进行注解,具体使用方法和UNIX中的惯例一样。该文件中的记录通常以一行或多行User-agent开始,后面加上若干Disallow行,详细情况如下:

User-agent:该项的值用于描述搜索引擎robot的名字,在”robots.txt”文件中,如果有多条User-agent记录说明有多个robot会受到该协议的限制,对该文件来说,至少要有一条User-agent记录。如果该项的值设为,则该协议对任何机器人均有效,在”robots.txt”文件中,”User-agent:“这样的记录只能有一条。

Disallow:该项的值用于描述不希望被访问到的一个URL,这个URL可以是一条完整的路径,也可以是部分的,任何以Disallow开头的URL均不会被robot访问到。例如”Disallow:/help”对/help.html 和/help/index.html都不允许搜索引擎访问,而”Disallow:/help/“则允许robot访问/help.html,而不能访问/help/index.html。任何一条Disallow记录为空,说明该网站的所有部分都允许被访问,在”/robots.txt”文件中,至少要有一条Disallow记录。如果”/robots.txt”是一个空文件,则对于所有的搜索引擎robot,该网站都是开放的。

Allow:该项的值用于描述希望被访问的一组URL,与Disallow项相似,这个值可以是一条完整的路径,也可以是路径的前缀,以Allow项的值开头的URL是允许robot访问的。例如”Allow:/hibaidu”允许robot访问/hibaidu.htm、/hibaiducom.html、/hibaidu/com.html。一个网站的所有URL默认是Allow的,所以Allow通常与Disallow搭配使用,实现允许访问一部分网页同时禁止访问其它所有URL的功能。

需要特别注意的是Disallow与Allow行的顺序是有意义的,robot会根据第一个匹配成功的Allow或Disallow行确定是否访问某个URL。

使用”*”和”$”:

robots支持使用通配符”*”和”$”来模糊匹配url:

“$” 匹配行结束符。

“*” 匹配0或多个任意字符。

robots.txt语法
  • 允许所有SE收录本站:robots.txt为空就可以,什么都不要写。

  • 禁止所有SE收录网站的某些目录:

    1
    2
    3
    4
    User-agent: *
    Disallow: /目录名1/
    Disallow: /目录名2/
    Disallow: /目录名3/
  • 禁止某个SE收录本站,例如禁止百度:

    1
    2
    User-agent: Baiduspider
    Disallow: /
  • 禁止所有SE收录本站:

    1
    2
    User-agent: *
    Disallow: /
  • 加入sitemap.xml路径

robotparser

该模块提供了一个类RobotFileParser,可以根据某个网站的robots.txt文件判断一个爬去爬虫是否有权限爬取这个网页.这个类使用十分简单,只需要在构造方法里传入robots.txt的链接即可,其声明如下:

1
urllib.robotparser.RobotFileParser(url='')

当然也可以在声明时不传入而默认为空,最后再使用set_url()方法设置一下即可,下面列出这个类的常用的几个方法:

  • set_url():用来设置robots.txt文件链接
  • read():读取robots.txt文件并进行分析,注意,这个方法执行一个读取和分析操作,如果不调用这个方法,接下来判断都会为False,所以一定要调用这个方法,这个方法不会返回任何内容,但是执行了读取操作
  • parse():用来解析robots.txt文件,传入的参数是robots.txt某些行的内容,按照robots.txt的语法规则分析这些内容
  • can_fetch():该方法传入两个参数,第一个是User-Agent,第二个是要抓取的URL,返回的内容是该搜索引擎是否可以抓取这个URL,返回结果是True或者False.
  • mtime():返回的是上次抓取和分析robots.txt的时间,这对于长时间分析和抓取的搜索爬虫很有必要,你可能需要定期检查抓取最新的robots.txt
  • modified():对长时间分析和抓取的搜索爬虫很有帮助,将当前时间设置为上次抓取和分析robots.txt的时间.

下面来看一个案例

1
2
3
4
5
6
7
8
9
10
11
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"))
'''
运行结果:
True
False
'''

也可以使用先前的parse方法实现分析,代码如下:

1
2
3
4
5
6
7
8
9
10
11
from urllib.robotparser import RobotFileParser
from urllib.request import urlopen
rp = RobotFileParser()
rp.parse(urlopen('http://www.jianshu.com/robots.txt').read().decode('utf-8').split('\n'))
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"))
'''
运行结果:
True
False
'''

后记

在这一节中我们主要探讨了Python爬虫基本库urllib的使用,这个库共有四个模块——request(发送请求并得到响应)、parse(解析处理URL链接)、error(处理异常)、robotparser(分析Robots协议),我们通过一系列案例分析了这四个模块的作用,但是在使用urllib库时,有一些不方便的地方,如使用Cookies时需要利用Opener和Handler来实现,而借助更强大的requests库,我们可以更方便的实现Cookies、登录验证、代理设置等操作,我们将在下次博客中进一步介绍关于requests库的相关知识和使用方式,一起感受它的强大之处.

学习爬虫,首先需要掌握一系列基本的库的使用,在今天的这篇文章里我们将一同学习urllib这个基本的库。