学习爬虫,其基本的操作便是模拟浏览器向服务器发出请求,那么我们需要从哪个地方做起呢?请求需要我们自己构造吗? 我们需要关心请求这个数据结构怎么实现吗? 需要了解 HTTP、TCP、IP层的网络传输通信吗? 需要知道服务器如何响应以及响应的原理吗?
可能你无从下手,不过不用担心,Python的强大之处就是提供了功能齐全的类库来帮助我们实现这些需求。最基础的 HTTP 库有 urllib、requests、httpx等。(由于篇幅限制,本帖只讲解urllib库,Request和httpx后续会陆续更新)
拿 urllib 这个库来说,有了它,我们只需要关心请求的链接是什么,需要传递的参数是什么,以及如何设置可选的请求头,而无须深入到底层去了解到底是怎样传输和通信的。有了 urllib库,只用两行代码就可以完成一次请求和响应的处理过程,得到网页内容,是不是感觉方便极了?
目录
urllib的使用
1、发送请求
(1) urlopen
(2)Request
2、异常处理
3、解析链接
(1)urlparse
(2)urlunparse
(3)urlsplit
(4)urlunsplit
4、分析Robots协议
(1) Robots 协议
(2) robotparser
首先介绍一个 Python库,叫作urllib,利用它就可以实现HTTP请求的发送,而且不需要关心 HTTP 协议本身甚至更底层的实现,我们要做的是指定请求的 URL、请求头、请求体等信息。此外urllib还可以把服务器返回的响应转化为Python 对象,我们通过该对象便可以方便地获取响应的相关信息,如响应状态码、响应头、响应体等。
注意 :在Python 2 中, 有urllib和 urllib2两个库来实现 HTTP 请求的发送。 而在Python 3 中, urllib2库已经不存在了,统一为了 urllib。
首先,我们了解一下 urllib库的使用方法,它是 Python 内置的 HTTP 请求库,也就是说不需要额外安装,可直接使用。urllib 库包含如下4 个模块。
使用urllib库的 request模块,可以方便地发送请求并得到响应。urllib. request模块提供了最基本的构造 HTTP请求的方法,利用这个模块可以模拟浏览器的请求发起过程, 同时它还具有处理授权验证(Authentication )、重定向(Redirection)、浏览器Cookie 以及其他一些功能。
下面以百度为例,抓取该网页:
import urllib.request
url = 'https://www.baidu.com'
response = urllib.request.urlopen(url)
# 获取网页源代码
print("网页源代码:")
print(response.read().decode('utf-8'))
# 获取响应状态码
print("响应状态码:")
print(response.status)
# 获取响应头
print("响应头:")
print(response.getheaders())
运行结果:
网页源代码:
响应状态码:
200
响应头:
[('Accept-Ranges', 'bytes'), ('Cache-Control', 'no-cache'), ('Content-Length', '227'), ('Content-Security-Policy', "frame-ancestors 'self' https://chat.baidu.com http://mirror-chat.baidu.com https://fj-chat.baidu.com https://hba-chat.baidu.com https://hbe-chat.baidu.com https://njjs-chat.baidu.com https://nj-chat.baidu.com https://hna-chat.baidu.com https://hnb-chat.baidu.com http://debug.baidu-int.com;"), ('Content-Type', 'text/html'), ('Date', 'Tue, 20 Feb 2024 02:54:13 GMT'), ('P3p', 'CP=" OTI DSP COR IVA OUR IND COM "'), ('P3p', 'CP=" OTI DSP COR IVA OUR IND COM "'), ('Pragma', 'no-cache'), ('Server', 'BWS/1.1'), ('Set-Cookie', 'BD_NOT_HTTPS=1; path=/; Max-Age=300'), ('Set-Cookie', 'BIDUPSID=30561DE24C7F0F5FF9E66FE3886A5240; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com'), ('Set-Cookie', 'PSTM=1708397653; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com'), ('Set-Cookie', 'BAIDUID=30561DE24C7F0F5FFA6A431818DB782D:FG=1; max-age=31536000; expires=Wed, 19-Feb-25 02:54:13 GMT; domain=.baidu.com; path=/; version=1; comment=bd'), ('Traceid', '1708397653049913601018294765874260519959'), ('X-Ua-Compatible', 'IE=Edge,chrome=1'), ('X-Xss-Protection', '1;mode=block'), ('Connection', 'close')]
使用urloen方法已经可以完成对简单网页的GET请求抓取,该方法的API为:
urllib. request.urlopen(url, data=None, [timeout,]*, cafile=None, capath=None, cadefault=False, context=None)
可以发现,除了第一个参数用于传递URL 之外,我们还可以传递其他内容,例如 data(附加数据)、timeout (超时时间) 等。接下来就详细说明一下 urlopen 方法中几个参数的用法。
data参数是可选的。在添加该参数时,需要使用bytes方法将参数转化为字节流编码格式的内容,即bytes类型。另外,如果传递了这个参数,那么它的请求方式就不再是 GET,而是 POST了。
timeout 参数用于设置超时时间,单位为秒,意思是如果请求超出了设置的这个时间,还没有得到响应,就会抛出异常。如果不指定该参数,则会使用全局默认时间。这个参数支持HTTP、HTTPS、FTP 请求。
除了 data 参数和 timeout 参数, urlopen方法还有 context参数,该参数必须是 ss1. SSLContext类型,用来指定 SSL的设置。此外, cafile和 capath 这两个参数分别用来指定 CA证书和其路径, 这两个在请求 HTTPS 链接时会有用。cadefault参数现在已经弃用了, 其默认值为 False。
至此,我们讲解了 urlopen 方法的用法,通过这个最基本的方法,就可以完成简单的请求和网页抓取。
利用urlopen方法可以发起最基本的请求,但它那几个简单的参数并不足以构建一个完整的请求。如果需要往请求中加入Headers等信息,就得利用更强大的 Request 类来构建请求了。
首先,我们用实例感受一下 Request 类的用法:
import urllib. request
request= urllib.request.Request(' https://python.org')
response = urllib. request. urlopen(request)
print(response. read(). decode('utf-8'))
可以发现,我们依然是用urlopen方法来发送请求,只不过这次该方法的参数不再是 URL,而是一个Request 类型的对象。通过构造这个数据结构,一方面可以将请求独立成一个对象,另一方面可更加丰富和灵活地配置参数。
下面我们看一下可以通过怎样的参数来构造 Request 类,构造方法如下:
class urllib. request. Request(url, data=None, headers={},
origin req host=None, unverifiable=False, method=None)
下面我们传入多个参数尝试构建 Request 类:
from urllib import request, parse
url = 'https://www.httpbin.org/post'
headers = {
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)',
'Host': ' www.httpbin.org'
}
dict = {'name': 'germey'}
data = bytes(parse. urlencode(dict), encoding='utf-8')
req = request. Request(url=url, data=data, headers=headers, method='POST')
response = request. urlopen(req)
print(response. read(). decode('utf-8'))
运行结果:
{
"args": {},
"data": "",
"files": {},
"form": {
"name": "germey"
},
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "11",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "www.httpbin.org",
"User-Agent": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)",
"X-Amzn-Trace-Id": "Root=1-65d41be7-0337942e592a7e7a53c9107b"
},
"json": null,
"origin": "118.212.215.166",
"url": "https://www.httpbin.org/post"
}
观察结果可以发现, 我们成功设置了 data、headers 和 method。
通过 add header方法添加 headers 的方式如下:
req = request. Request(url=url, data=data, method='POST')
req add header('User Agent', 'Mozilla/4 0(compatible; MSIE 5 5; Windows NT)')
有了 Request类,我们就可以更加方便地构建请求,并实现请求的发送啦。
我们已经了解了如何发送请求,但是在网络不好的情况下,如果出现了异常,该怎么办呢? 这时要是不处理这些异常,程序很可能会因为报错而终止运行,所以异常处理还是十分有必要的。
urllib库中的 error模块定义了由 request模块产生的异常。当出现问题时, request模块便会抛出 error模块中定义的异常。
URLError类来自 urllib库的 error模块,继承自OSError类,是error异常模块的基类, 由 request 模块产生的异常都可以通过捕获这个类来处理。它具有一个属性reason,即返回错误的原因。
下面用一个实例来看一下:
from urllib import request, error
try:
response = request.urlopen(' https://cuiqingcai.com/404')
except error. URLError as e:
print(e. reason)
我们打开了一个不存在的页面,照理来说应该会报错,但是我们捕获了 URLError这个异常。
运行结果如下:
Not Found
程序没有直接报错,而是输出了错误原因,这样可以避免程序异常终止,同时异常得到了有效处理。
HTTPError是URLError的子类,专门用来处理HTTP请求错误,例如认证请求失败等。它有如下3个属性。
下面我们用几个实例来看看:
from urllib import request, error
try:
response = request.urlopen(' https://cuiqingcai.com/404')
except error. HTTPError as e:
print(e. reason, e. code, e. headers, sep='\n')
运行结果如下:
Not Found
404
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin: *
ETag: "64d39a40-24a3"
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'
x-proxy-cache: MISS
X-GitHub-Request-Id: C988:38DF71:2CD1E:3F291:65D4231A
Accept-Ranges: bytes
Date: Tue, 20 Feb 2024 03:57:15 GMT
Via: 1.1 varnish
Age: 0
X-Served-By: cache-icn1450083-ICN
X-Cache: MISS
X-Cache-Hits: 0
X-Timer: S1708401435.117679,VS0,VE185
Vary: Accept-Encoding
X-Fastly-Request-ID: ebbb650d9a42388fbffef33aa8977ba03d9fd797
X-Cache-Lookup: Cache Miss
Content-Length: 9379
X-NWS-LOG-UUID: 15553444116395185930
Connection: close
X-Cache-Lookup: Cache Miss
依然是打开同样的网址, 这里捕获了 HTTPError异常, 输出了 reason、code 和 headers 属性。
因为 URLError 是 HTTPError的父类,所以可以先选择捕获子类的错误,再捕获父类的错误。上述代码的更好写法如下:
from urllib import request, error
try:
response = request.urlopen(' https://cuiqingcai.com/404')
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')
这样就可以做到先捕获 HTTPError,获取它的错误原因、状态码、请求头等信息。如果不是HTTPError异常,就会捕获URLError异常,输出错误原因。最后,用else语句来处理正常的逻辑。这是一个较好的异常处理写法。有时候,reason属性返回的不一定是字符串,也可能是一个对象。再看下面的实例:
import socket
import urllib. request
import urllib. error
try:
response = urllib.request.urlopen(' https://www.baidu.com', timeout=0.01)
except urllib. error. URLError as e:
print(type(e. reason))
if isinstance(e. reason, socket. timeout):
print('TIME OUT')
这里我们直接设置超时时间来强制抛出 timeout异常。
运行结果如下:
TIME OUT
reason属性的结果是socket.timeout类。所以这里可以用isinstance方法来判断它的类型,做出更详细的异常判断。
前面说过,urllib库里还提供了 parse 模块,这个模块定义了处理URL的标准接口,例如实现URL 各部分的抽取、合并以及链接转换。它支持如下协议的 URL处理: file、ftp、gopher、hdl 、http、imap、 mailto、 mms、 news、 nntp、 prospero、 rsync、 rtsp、 rtspu、 sftp、 sip、 sips、 snews、 svn、 svn+ssh、telnet 和 wais。
下面我们将介绍parse 模块中的常用方法,看一下它的便捷之处。
该方法可以实现URL的识别和分段,这里先用一个实例来看一下:
from urllib. parse import urlparse
result = urlparse('https://www.baidu.com/index.html;user?id=5 #comment')
print(type(result))
print(result)
这里我们利用urlparse方法对一个 URL进行了解析,然后输出了解析结果的类型以及结果本身。
运行结果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5',fragment='comment')
可以看到,解析结果是一个ParseResult类型的对象, 包含6部分,分别是scheme、netloc、path、params、 query和 fragment。再观察一下上述实例中的 URL:
https://www.baidu.com/index.html;user?id=5 #comment
可以发现, urlparse 方法在解析 URL 时有特定的分隔符。例如:// 前面的内容就是 scheme, 代表协议。第一个/ 符号前面便是 netloc, 即域名; 后面是path, 即访问路径。分号;后面是 params,代表参数。问号?后面是查询条件 query, 一般用作 GET 类型的 URL。井号#后面是锚点 fragment,用于直接定位页面内部的下拉位置。
于是可以得出一个标准的链接格式,具体如下:
scheme://netloc/path;params? query#fragment
一个标准的URL 都会符合这个规则,利用urlparse方法就可以将它拆分开来。除了这种最基本的解析方式外,urlparse方法还有其他配置吗? 接下来,看一下它的 API用法:
urllib. parse. urlparse(urlstring, scheme='', allow fragments=True)
可以看到, urlparse 方法有3 个参数。
from urllib. parse import urlparse
result = urlparse('www.baidu.com/index.html;user?id=5 #comment', scheme='https')
print(result)
运行结果如下:
ParseResult(scheme='https', netloc='', path='www.baidu.com/index.html',params='user', query='id=5',fragment='comment')
可以发现,这里提供的URL 不包含最前面的协议信息,但是通过默认的 scheme 参数,返回了结果 https。
假设带上协议信息:
result = urlparse('http://www.baidu.com/index.html;user?id=5 #comment', scheme='https')
则结果如下:
ParseResult(scheme='http', netloc='www.baidu.com', path='/index.html', params='user', query='id=5',fragment='comment')
可见,scheme参数只有在 URL中不包含协议信息的时候才生效。如果URL中有,就会返回解析出的 scheme。
下面我们用实例来看一下:
from urllib. parse import urlparse
result = urlparse(' https://www.baidu.com/index.html;user?id=5#comment',allow fragments=False)
print(result)
运行结果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5#comment',fragment='')
假设 URL 中不包含 params 和 query, 我们再通过实例看一下:
from urllib. parse import urlparse
result = urlparse('https://www.baidu.com/index.html#comment', allow fragments=False)
print(result)
运行结果如下:
ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html#comment', params='', query='',fragment='')
可以发现, 此时 fragment 会被解析为 path的一部分。返回结果 ParseResult 实际上是一个元组,既可以用属性名获取其内容,也可以用索引来顺序获取。
实例如下:
from urllib. parse import urlparse
result = urlparse('https://www.baidu.com/index.html#comment', allowfragments=False)
print(result.scheme, result[0], result.netloc, result[1], sep='\n')
这里我们分别用属性名和索引获取了 scheme 和 netloc,运行结果如下:
https
https
www.baidu.com
www.baidu.com
可以发现,两种获取方式都可以成功获取,且结果是一致的。
有了 urlparse 方法, 相应就会有它的对立方法urlunparse, 用于构造 URL。这个方法接收的参数是一个可迭代对象,其长度必须是 6,否则会抛出参数数量不足或者过多的问题。先用一个实例看一下:
from urllib. parse import urlunparse
data = ['https', 'www.baidu.com', 'index.html', 'user', 'a=6', 'comment']
print(urlunparse(data))
运行结果如下:
https://www.baidu.com/index.html;user?a=6#comment
这样我们就成功实现了 URL的构造。
这个方法和urlparse 方法非常相似,只不过它不再单独解析 params 这一部分(params会合并到path中),只返回5个结果。实例如下:
from urllib. parse import urlsplit
result = urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result)
运行结果如下:
SplitResult(scheme='https', netloc='www.baidu.com', path='/index.html;user', query='id=5',
fragment='comment')
可以发现,返回结果是 SplitResult,这其实也是一个元组,既可以用属性名获取其值,也可以用索引获取。实例如下:
from urllib. parse import urlsplit
result = urlsplit('https://www.baidu.com/index.html;user?id=5#comment')
print(result. scheme, result[0])
运行结果如下:
https https
与urlumparse方法类似,这也是将链接各个部分组合成完整链接的方法,传入的参数也是一个可迭代对象,例如列表、元组等,唯一区别是这里参数的长度必须为5。实例如下:
from urllib. parse import urlunsplit
data = ['https', 'www.baidu.com', 'index.html', 'a=6', 'comment']
print(urlunsplit(data))
运行结果如下:
https://www.baidu.com/index.html?a=6#commenturljoin
urlunparse和 urlunsplit 方法都可以完成链接的合并,不过前提都是必须有特定长度的对象,链接的每一部分都要清晰分开。
除了这两种方法,还有一种生成链接的方法,是 urljoin。我们可以提供一个 base url(基础链接)作为该方法的第一个参数,将新的链接作为第二个参数。urljoin 方法会分析 base url的 scheme、netloc 和path这3个内容,并对新链接缺失的部分进行补充,最后返回结果。
下面通过几个实例看一下:
from urllib. parse import urljoin
print(urljoin('https://www.baidu.com', 'FAQ.html'))
print(urljoin('https://www.baidu.com','https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html','https://cuiqingcai.com/FAQ.html'))
print(urljoin('https://www.baidu.com/about.html', 'https://cuiqingcai.com/FAQ.html?question=2'))
print(urljoin('https://www.baidu.com?wd=abc', 'https://cuiqingcai.com/index.php'))
print(urljoin('https://www.baidu.com', '?category=2#comment'))
print(urljoin('www.baidu.com', '?category=2 #comment'))
print(urljoin('www.baidu.com#comment', '?category=2'))
运行结果如下:
https://www.baidu.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html
https://cuiqingcai.com/FAQ.html?question=2
https://cuiqingcai.com/index.php
https://www.baidu.com?category=2#comment
www.baidu.com?category=2#comment
www.baidu.com?category=2
可以发现, base url提供了三项内容: scheme、netloc和 path。如果新的链接里不存在这三项,就予以补充; 如果存在,就使用新的链接里面的,base url中的是不起作用的。通过urljoin 方法,我们可以轻松实现链接的解析、拼合与生成。
利用 urllib库的 robotparser 模块,可以分析网站的 Robots 协议。我们再来简单了解一下这个模块的用法。
Robots 协议也称作爬虫协议、机器人协议,全名为网络爬虫排除标准(Robots Exclusion Protocol),用来告诉爬虫和搜索引擎哪些页面可以抓取、哪些不可以。它通常是一个叫作 robots. txt的文本文件,一般放在网站的根目录下。
搜索爬虫在访问一个站点时,首先会检查这个站点根目录下是否存在 robots. txt文件,如果存在,就会根据其中定义的爬取范围来爬取。如果没有找到这个文件,搜索爬虫便会访问所有可直接访问的页面。
下面我们看一个 robots. txt的样例:
User-agent: *
Disallow: /
Allow: /public/
这限定了所有搜索爬虫只能爬取 public 目录。将上述内容保存成robots. txt文件,放在网站的根目录下, 和网站的入口文件(例如index. php、 index. html和 index. jsp等) 放在一起。上面样例中的User-agent描述了搜索爬虫的名称,这里将其设置为*,代表 Robots协议对所有爬取爬虫都有效。例如,我们可以这样设置:
User-agent: Baiduspider
这代表设置的规则对百度爬虫是有效的。如果有多条User-agent记录,则意味着有多个爬虫会受到爬取限制,但至少需要指定一条。
Disallow指定了不允许爬虫爬取的目录,上例设置为 /,代表不允许爬取所有页面。Allow一般不会单独使用,会和Disallow一起用,用来排除某些限制。上例中我们设置为/public/,结合Disallow的设置,表示所有页面都不允许爬取,但可以爬取 public 目录。
下面再来看几个例子。禁止所有爬虫访问所有目录的代码如下:
User-agent: *
Disallow: /
允许所有爬虫访问所有目录的代码如下:
User-agent: *
Disallow:
另外,直接把robots. txt文件留空也是可以的。禁止所有爬虫访问网站某些目录的代码如下:
User-agent: *
Disallow: /private/
Disallow: /tmp/
只允许某一个爬虫访问所有目录的代码如下:
User-agent: WebCrawler
Disallow:
User-agent: *
Disallow: /
以上是 robots. txt的一些常见写法。
了解 Robots协议之后, 就可以使用robotparser模块来解析robots. txt文件了。该模块提供了一个类 RobotFileParser,它可以根据某网站的 robots. txt文件判断一个爬取爬虫是否有权限爬取这个网页。该类用起来非常简单,只需要在构造方法里传入 robots. txt文件的链接即可。首先看一下它的声明:
urllib. robotparser. RobotFileParser(url='')
当然,也可以不在声明时传入 robots. txt文件的链接,就让其默认为空,最后再使用set url()方法设置一下也可以。下面列出了 RobotFileParser类的几个常用方法。
下面我们用实例来看一下:
from urllib. robotparser import RobotFileParser
rp = RobotFileParser()
rp.set url('https://www.baidu.com/robots.txt')
rp. read()
print(rp.can fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can fetch('Baiduspider', 'https://www.baidu.com/homepage/'))
print(rp.can fetch('Googlebot', 'https://www.baidu.com/homepage/'))
这里以百度为例, 首先创建了一个 RobotFileParser 对象 rp, 然后通过 set url 方法设置了robots. txt文件的链接。当然,要是不用set url方法,可以在声明对象时直接用如下方法设置:
rp = RobotFileParser('https://www.baidu.com/robots.txt')
接着利用 can fetch方法判断了网页是否可以被抓取。运行结果如下:
True
True
False
可以看到,这里我们利用Baiduspider可以抓取百度的首页以及 homepage页面,但是 Googlebot就不能抓取 homepage页面。打开百度的 robots. txt文件, 可以看到如下信息:
User-agent: Baiduspider
Disallow: /baidu
Disallow: /s?
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bh
User-agent: Googlebot
Disallow: /baidu
Disallow: /s?
Disallow: /shiften/
Disallow: /homepage/
Disallow: /cpro
Disallow: /ulink?
Disallow: /link?
Disallow: /home/news/data/
Disallow: /bh
不难看出, 百度的 robots. txt文件没有限制 Baiduspider 对百度 homepage 页面的抓取, 限制了Googlebot对 homepage页面的抓取。这里同样可以使用parse 方法执行对 robots. txt文件的读取和分析,实例如下:
from urllib. request import urlopen
from urllib. robotparser import RobotFileParser
rp= RobotFileParser()
rp.parse(urlopen(' https://www.baidu.com/robots.txt').read().decode('utf-8').split('\n'))
print(rp.can fetch('Baiduspider', 'https://www.baidu.com'))
print(rp.can fetch('Baiduspider', ' https://www.baidu.com/homepage/'))
print(rp.can fetch('Googlebot', ' https://www.baidu.com/homepage/'))
运行结果是一样的:
True
True
False
本节介绍了 robotparser 模块的基本用法和实例,利用此模块,我们可以方便地判断哪些页面能抓取、哪些页面不能。