urllib库的结构
urllib库包含以下四个模块:
- request: 基本的HTTP请求模块
- error: 异常处理模块
- parse: 工具模块
- robotparser: 识别robots.txt的模块
urlopen方法
使用urlopen方法可以发送简单请求
API
urllib.request.urlopen(url, data=None, [timeout,] *, cafile=None, capath=None, cadefault=False, context=None)
- url: 要请求的URL
- data: 请求携带的参数, 如果设置了此参数, 请求方式将变为POST, 而不是GET
- timeout: 超时时间, 单位为秒, 超时抛出URLError异常
- cafile: CA证书
- cspath: CA证书的路径
- cadefault: 已弃用, 默认False
- context: 用来指定SSL设置, 值必须是ssl.SSLContext类的对象
另外, urlopen方法还可以接受一个Request对象作为参数, 详见后文
发送GET请求
from urllib.request import urlopen
url = 'https://www.python.org'
resp = urlopen(url=url)
print(resp.read().decode('utf-8')) # read()方法返回的数据是字节, 需要手动解码
发送POST请求
from urllib.request import urlopen
from urllib.parse import urlencode
url = 'https://www.httpbin.org/post'
data = {'name': 'germey'}
# 使用urlencode将数据编码, 再由bytes转为字节
data = bytes(urlencode(data), encoding='utf-8')
# 携带data之后, 请求方式变为POST
resp = urlopen(url=url, data=data)
print(resp.read().decode('utf-8'))
处理超时
import socket
from urllib.request import urlopen
from urllib.error import URLError
url = 'https://www.httpbin.org/get'
try:
resp = urlopen(url=url, timeout=0.1) # timeout单位为秒
html = resp.read().decode('utf-8')
print(html)
except URLError as e: # 超时抛出URLError异常
if isinstance(e.reason, socket.timeout): # 判断异常具体类型
print('TIME OUT')
Request类
Request类能够添加更多的请求信息, 例如请求头信息, 请求方式等
API
class urllib.request.Request(url, data=None, headers={}, origin_rep_host=None, unverifiable=False, method=None)
- url: 要请求的URL
- data: 要传递的数据, 必须是bytes类型
- headers: 请求头信息, 类型是字典, 请求头信息可以通过headers参数传递, 也可以通过Request对象的add_header方法传递
- origin_req_host: 请求方名称或IP地址
- unverifiable: 请求是否是无法验证的
- method: 请求方式
使用方法
from urllib.request import Request
from urllib.request import urlopen
from urllib.parse import urlencode
url = 'https://www.httpbin.org/post'
data = bytes(urlencode({'name': 'germey'}), encoding='utf-8')
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36',
'host': 'www.httpbin.org',
}
req = Request(url=url, data=data, headers=headers, method='POST')
resp = urlopen(req) # 仍然使用urlopen发送请求, 传入Request对象作为参数
print(resp.read().decode('utf-8'))
使用Handler
使用Handler可以处理一些请求过程中的特殊情况, 比如登录验证, Cookie, 代理等
基类urllib.request.BaseHandler提供最基本的方法, 比如default_open, protocol_request等
各种Handler子类继承了BaseHandler, 处理各种情况:
- HTTPDefaultErrorHandler: 处理HTTP相应错误, 抛出HTTPError异常
- HTTPRedirectHandler: 处理重定向
- HTTPCookieProcessor: 处理Cookie
- ProxyHandler: 设置代理, 默认为空
- HTTPPasswordMgr: 管理密码, 维护着用户名密码的对照表
- HTTPBasicAuthHandler: 处理认证问题
处理登录验证
from urllib.request import HTTPPasswordMgrWithDefaultRealm
from urllib.request import HTTPBasicAuthHandler
from urllib.request import build_opener
from urllib.error import URLError
url = 'https://ssr3.scrape.center/'
username = 'admin'
password = 'admin'
pwd_mgr = HTTPPasswordMgrWithDefaultRealm() # 创建密码管理器实例
pwd_mgr.add_password(None, url, username, password) # 添加用户名密码
auth_handler = HTTPBasicAuthHandler(pwd_mgr) # 使用密码管理器对象, 创建认证处理器对象
opener = build_opener(auth_handler) # 使用认证处理器对象, 构建opener, 类似于urlopen, 用于发送请求
try: # 使用opener.open方法发送请求, 就会携带上面配置的账户信息
resp = opener.open(url)
html = resp.read().decode('utf-8')
print(html)
except URLError as e:
print(e.reason)
处理代理
from urllib.error import URLError
from urllib.request import ProxyHandler
from urllib.request import build_opener
url = 'https://www.baidu.com'
proxy_handler = ProxyHandler({ # 创建代理处理器
'http': 'http://118.190.244.234:3128',
'https': 'https://118.190.244.234:3128'
})
opener = build_opener(proxy_handler) # 创建opener
try: # 发送请求
resp = opener.open(url)
html = resp.read().decode('utf-8')
print(html)
except URLError as e:
print(e.reason)
处理Cookie
# 直接输出Cookie
import http.cookiejar
import urllib.request
url = 'https://www.baidu.com'
cookies = http.cookiejar.CookieJar() # 创建CookieJar对象
handler = urllib.request.HTTPCookieProcessor(cookies) # 使用CookieJar对象创建handler对象
opener = urllib.request.build_opener(handler) # 创建opener
resp = opener.open(url)
for cookie in cookies: # 通过CookieJar对象即可拿到Cookie信息, 这个对象类似于列表
# 获取Cookie对象的name和value属性
print(cookie.name, '=', cookie.value)
# 将Mozilla格式Cookie信息写入文件
import http.cookiejar
import urllib.request
url = 'https://www.baidu.com'
filename = 'bd_m_cookie.txt' # 要保存Cookie信息的文件名
# MozillaCookieJar可以处理Cookie信息的文件读写, 支持Mozilla格式的Cookie文件
cookies = http.cookiejar.MozillaCookieJar(filename=filename)
handler = urllib.request.HTTPCookieProcessor(cookiejar=cookies)
opener = urllib.request.build_opener(handler)
resp = opener.open(url)
# 将Cookie信息存入文件
cookies.save(ignore_discard=True, ignore_expires=True)
"""文件内容
# Netscape HTTP Cookie File
# http://curl.haxx.se/rfc/cookie_spec.html
# This is a generated file! Do not edit.
.baidu.com TRUE / FALSE 1675640364 BAIDUID 48B3F4D3CCDDB7205C471C7941363BCE:FG=1
.baidu.com TRUE / FALSE 3791588011 BIDUPSID 48B3F4D3CCDDB72072B89C5EEAF3C1AE
.baidu.com TRUE / FALSE 3791588011 PSTM 1644104364
www.baidu.com FALSE / FALSE 1644104664 BD_NOT_HTTPS 1
"""
# 将LWP格式的Cookie信息写入文件
import http.cookiejar
import urllib.request
url = 'https://www.baidu.com'
filename = 'bd_lwp_cookie.txt'
# 这里需要换成LWPCookieJar, 其它都一样
cookies = http.cookiejar.LWPCookieJar(filename=filename)
handler = urllib.request.HTTPCookieProcessor(cookiejar=cookies)
opener = urllib.request.build_opener(handler)
resp = opener.open(url)
cookies.save(ignore_expires=True, ignore_discard=True)
"""文件内容
#LWP-Cookies-2.0
Set-Cookie3: BAIDUID="519E24A62494ECF40B4A6244CFFA07C3:FG=1"; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2023-02-06 00:13:16Z"; comment=bd; version=0
Set-Cookie3: BIDUPSID=519E24A62494ECF45DB636DC550D8CA7; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2090-02-24 03:27:23Z"; version=0
Set-Cookie3: PSTM=1644106396; path="/"; domain=".baidu.com"; path_spec; domain_dot; expires="2090-02-24 03:27:23Z"; version=0
Set-Cookie3: BD_NOT_HTTPS=1; path="/"; domain="www.baidu.com"; path_spec; expires="2022-02-06 00:18:16Z"; version=0
"""
# 从文件中读取Cookie信息
import urllib.request
import http.cookiejar
url = 'https://www.baidu.com'
filename = 'bd_lwp_cookie.txt'
cookies = http.cookiejar.LWPCookieJar()
# 加载之前保存的文件
cookies.load(filename=filename, ignore_discard=True, ignore_expires=True)
handler = urllib.request.HTTPCookieProcessor(cookiejar=cookies)
opener = urllib.request.build_opener(handler)
resp = opener.open(url)
html = resp.read().decode('utf-8')
print(html)
处理异常
URLError类
URLError类继承自OSError类, 是urllib.error模块中的基类
在使用urllib发送请求的过程中出现的异常, 都可以用URLError来捕获
URLError有一个reason属性, 表示出错原因
reason可能返回一个字符串, 也可能返回一个Error对象(比如超时的时候返回<class ‘socket.timeout’> 对象)
HTTPError类
HTTPError类是URLError类的子类, 专门处理HTTP请求错误
包含三个属性:
- code: 响应状态码
- reason: 出错原因, 可能是字符串, 也可能是对象
- headers: 请求头信息
代码
import urllib.request
from urllib.error import URLError, HTTPError
url = 'https://cuiqingcai.com/404'
try:
resp = urllib.request.urlopen(url, timeout=1)
html = resp.read().decode('utf-8')
print(html)
except HTTPError as e:
print(e.reason, e.headers, e.url, e.fp, e.code, sep='\n')
except URLError as e:
print(type(e.reason), '\n', e.reason)
else:
print('success')
"""
Not Found
Server: GitHub.com
Content-Type: text/html; charset=utf-8
Access-Control-Allow-Origin: *
ETag: "60789243-247b"
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; connect-src 'self'
x-proxy-cache: MISS
X-GitHub-Request-Id: E15A:6107:132CB29:158E796:61FF1AA9
Accept-Ranges: bytes
Date: Sun, 06 Feb 2022 00:55:58 GMT
Via: 1.1 varnish
Age: 501
X-Served-By: cache-hkg17931-HKG
X-Cache: HIT
X-Cache-Hits: 1
X-Timer: S1644108959.779112,VS0,VE1
Vary: Accept-Encoding
X-Fastly-Request-ID: cce2ac7f081b0d937fe93e90656fce56b5e6cc03
X-Cache-Lookup: Cache Miss
X-Cache-Lookup: Cache Miss
X-Cache-Lookup: Cache Miss
Content-Length: 9339
X-NWS-LOG-UUID: 17106243714350687226
Connection: close
X-Cache-Lookup: Cache Miss
https://cuiqingcai.com/404
<http.client.HTTPResponse object at 0x0000019F340B1B80>
404
"""
parse模块中的常用方法
urllib.parse模块中提供了很多方法来处理URL
urlparse
urllib.parse.urlparse(url=url, scheme=’’, allow_fragments=True)
- url: 待解析的URL
- scheme: 默认的协议, 如果URL不带协议信息, 就用这个
- allow_fragments: 是否将fragment部分单独拆分出来, 如果设置成False, fragment会跟在其它部分后面
# 利用urlparse将一个URL拆开
from urllib.parse import urlparse
url = 'https://www.baidu.com/index.html;user?id=5#comment'
result = urlparse(url=url, scheme='https', allow_fragments=True)
print(result) # 返回值类似于一个命名元组, 可以使用索引取值, 也可以使用属性取值
# ParseResult(scheme='https', netloc='www.baidu.com', path='/index.html', params='user', query='id=5', fragment='comment')
print(result.scheme) # 使用属性取值
# https
print(result[1]) # 使用索引取值
# www.baidu.com
urlunparse
与urlparse相反, urlunparse是将URL的各个部分组装起来得到一个完整的URL
urllib.parse.urlunparse(components)
- components: URL的组件, 接收一个长度固定是6的可迭代对象
# 利用urlunparse拼接URL
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
urlsplit
urlsplit与urlparse类似, 只是将params并入path中
urllib.parse.urlsplit(url, scheme=’’, allow_fragments=True)
# 利用urlsplit拆分URL
from urllib.parse import urlsplit
url = 'https://www.baidu.com/index.html;user?id=5#comment'
print(urlsplit(url))
# SplitResult(scheme='https', netloc='www.baidu.com', path='/index.html;user', query='id=5', fragment='comment')
urlunsplit
urlunsplit与urlunparse类似, 只是传入的组件可迭代对象的长度必须为5
# 利用urlunsplit拼接URL
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#comment
urljoin
urljoin接收一个base_url和另一个url(一般是相对URL), 可以自动分析base_url的scheme, netloc, path部分, 与相对url拼接在一起, 得到一个完整的URL
urllib.parse.urljoin(base, url, allow_fragments=True)
- base: 基础URL
- url: 要和基础URL拼接的相对URL
- allow_fragments: 是否单独拼接fragment部分
from urllib.parse import urljoin
print(urljoin('https://www.baidu.com', 'FAQ.html'))
# https://www.baidu.com/FAQ.html
urlencode
urlencode能将字典形式的参数序列化为字符串形式(“name=germey&age=25”)
from urllib.parse import urlencode
params = {
'user': 'germey',
'age':25
}
base_url = 'https://www.baidu.com?' # 注意自己处理?的问题
url = base_url + urlencode(params)
print(url)
# https://www.baidu.com?user=germey&age=25
parse_qs
parse_qs反序列化GET请求参数字符串, 返回字典形式的参数
from urllib.parse import parse_qs
query = 'name=mergey&age=25'
print(parse_qs(query))
# {'name': ['mergey'], 'age': ['25']}
parse_qsl
parse_qsl的功能与parse_qs类似, 只是返回元组的列表
from urllib.parse import parse_qsl
query = 'name=mergey&age=25'
print(parse_qsl(query))
# [('name', 'mergey'), ('age', '25')]
quote
quote可以将中文字符串转换为带%的16进制数的字符串形式, 以便作为URL中的参数使用
from urllib.parse import quote
keyword = '高清壁纸'
base_url = 'https://www.baidu.com/s?wd='
url = base_url + quote(keyword)
print(url)
# https://www.baidu.com/s?wd=%E9%AB%98%E6%B8%85%E5%A3%81%E7%BA%B8
# 实际上底层的做法就是将中文转换为bytes, 再把每个字节转换为16进制, 再在前面加上%
bs = bytes(keyword, encoding='utf-8')
b_list = []
for b in bs:
b_list.append(hex(b)[-2:].upper())
b_str = '%' + '%'.join(b_list)
print(b_str)
# %E9%AB%98%E6%B8%85%E5%A3%81%E7%BA%B8
unquote
unquote的功能与quote相反
from urllib.parse import unquote
url = 'https://www.baidu.com/s?wd=%E9%AB%98%E6%B8%85%E5%A3%81%E7%BA%B8'
print(unquote(url))
# https://www.baidu.com/s?wd=高清壁纸
Robots协议
Robots协议也称作爬虫协议, 机器人协议, 全称爬虫排除协议(Robots Exclusion, Protocol), 用来告诉爬虫, 哪些页面可以抓取, 哪些不可以
它通常是一个名为robots.txt的文件, 放在网站的根目录
robots.txt文件中一般有三种类型的条目:
- Use-agent: 爬虫的名称
- Disallow: 不允许抓取的路径
- Allow: 允许抓取的路径
示例
- 禁止所有爬虫访问所有目录
User-agent: *
Disallow: / - 允许所有爬虫访问所有目录(也可以将robots.txt文件留作空白)
User-agent: *
Disallow: - 禁止所有爬虫访问某些目录
User-agent: *
Disallow: /private/
Disallow: /tmp/ - 只允许某一个爬虫访问所有目录
User-agent: WebCrawler
Disallow:
User-agent: *
Disallow: /
常见爬虫名称
爬虫名称 | 网站名称 |
---|---|
BaiduSpider | 百度 |
Googlebot | 谷歌 |
360Sipder | 360搜索 |
YodaoBot | 有道 |
ia_archiver | Alexa |
Scooter | altavista |
Bingbot | 必应 |
解析Robots协议
使用urllib.robotparser模块中的RobotFileParser类可以读取并解析Robots协议
RobotFileParser类有几个常用方法:
- set_url: 设置robots.txt文件的链接, 如果在实例化RobotFileParser类的时候传入了url参数, 就不需要这个方法了
- read: 读取robots.txt文件, 在解析之前必须先读取, 否则会解析失败, 全部返回False
- parse: 解析robots.txt文件, 参数是文件中某些行的内容
- can_fetch: 判断指定的User-agent是否能抓取指定的URL
- mtime: 返回上次解析robots.txt文件的时间
- modified: 设置解析robots.txt文件的时间
from urllib.robotparser import RobotFileParser
parser = RobotFileParser()
parser.set_url('https://www.baidu.com/robots.txt')
parser.read() # 解析之前先读取
print(parser.can_fetch('Baiduspider', 'https://www.baidu.com')) # True
print(parser.can_fetch('Baiduspider', 'https://www.baidu.com/homepage/')) # True
print(parser.can_fetch('Googlebot', 'https://www.baidu.com/homepage/')) # False