13.3. urllib.request — 访问网络资源
目标:一个用于访问 URLs 资源的库,且可以通过自定义协议处理程序来扩展。
urllib.request
模块提供了一个使用由 URLs 指定的网络资源的 API。它被设计成是可被用户应用扩展的,以便支持新的协议或者添加现存协议的变种(比如处理 HTTP 基本认证)。
HTTP GET
注意
为测试本节的例子所使用的服务器由
http_server_GET.py
实现,事实上这是为了介绍http.server
模块而编写的几个例子。在一个终端开启这个服务器,然后就可以在另一个终端下尝试本节的例子了。
HTTP GET 操作是 urllib.request
最简单的一个用法。通过给 urlopen()
函数传递一个 URL 地址获得一个「类文件」句柄来操作远程资料。
urllib_request_urlopen.py
from urllib import request
response = request.urlopen('http://localhost:8080/')
print('RESPONSE:', response)
print('URL :', response.geturl())
headers = response.info()
print('DATE :', headers['date'])
print('HEADERS :')
print('---------')
print(headers)
data = response.read().decode('utf-8')
print('LENGTH :', len(data))
print('DATA :')
print('---------')
print(data)
我们的范例服务器接收输入,然后返回一串文本作为应答。从 urlopen()
返回的对象的方法 info()
让我们得以访问 HTTP 服务器的响应头信息,而远程资源的其他数据则可以通过 read()
和 readlines()
方法取得。
$ python3 urllib_request_urlopen.py
RESPONSE: <http.client.HTTPResponse object at 0x101744d68>
URL : http://localhost:8080/
DATE : Sat, 08 Oct 2016 18:08:54 GMT
HEADERS :
---------
Server: BaseHTTP/0.6 Python/3.5.2
Date: Sat, 08 Oct 2016 18:08:54 GMT
Content-Type: text/plain; charset=utf-8
LENGTH : 349
DATA :
---------
CLIENT VALUES:
client_address=('127.0.0.1', 58420) (127.0.0.1)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1
SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0
HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=Python-urllib/3.5
这个由 urlopen()
返回的类文件对象是可迭代的:
urllib_request_urlopen_iterator.py
from urllib import request
response = request.urlopen('http://localhost:8080/')
for line in response:
print(line.decode('utf-8').rstrip())
这个例子在打印输出之前去掉了每一行末尾的换行符与回车符。
$ python3 urllib_request_urlopen_iterator.py
CLIENT VALUES:
client_address=('127.0.0.1', 58444) (127.0.0.1)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1
SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0
HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=Python-urllib/3.5
编码参数
参数用 urllib.parse.urlencode()
编码后就可以添加到 URL ,然后传递给服务器。
urllib_request_http_get_args.py
from urllib import parse
from urllib import request
query_args = {'q': 'query string', 'foo': 'bar'}
encoded_args = parse.urlencode(query_args)
print('Encoded:', encoded_args)
url = 'http://localhost:8080/?' + encoded_args
print(request.urlopen(url).read().decode('utf-8'))
以上例子返回的输出列表 CLIENT VALUES 包含编码后的查询参数。
$ python urllib_request_http_get_args.py
Encoded: q=query+string&foo=bar
CLIENT VALUES:
client_address=('127.0.0.1', 58455) (127.0.0.1)
command=GET
path=/?q=query+string&foo=bar
real path=/
query=q=query+string&foo=bar
request_version=HTTP/1.1
SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0
HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=Python-urllib/3.5
HTTP POST
注意
测试例子所用的服务器在
http_server_POST.py
中实现,这主要是在介绍http.server
模块时实现的几个例子。在一个终端开启这个服务器,然后在另一个终端测试本节的例子。
想要用 POST 的方式而不是 GET 方式提交形式编码后的数据到远端的服务器,则需要将编码后的查询参数作为数据传递给 urlopen()
函数。
urllib_request_urlopen_post.py
from urllib import parse
from urllib import request
query_args = {'q': 'query string', 'foo': 'bar'}
encoded_args = parse.urlencode(query_args).encode('utf-8')
url = 'http://localhost:8080/'
print(request.urlopen(url, encoded_args).read().decode('utf-8'))
服务器可以解码形式编码后的数据并按名称获取对应的值。
$ python3 urllib_request_urlopen_post.py
Client: ('127.0.0.1', 58568)
User-agent: Python-urllib/3.5
Path: /
Form data:
foo=bar
q=query string
添加输出信息的头部
urlopen()
是一个方便我们使用的函数,该函数包装了一些请求 (request) 是如何创建和操作的细节。更精准的控制可以直接用 Request
的实例来实现。比如说,在输出信息中添加自定义头部来控制返回数据的格式,指出本地缓存文件的版本,并告知远端服务器正在交互的客户端软件的名字。
由前面的例子的输出可以看出,头部中默认的 User-agent 值是由常量 Python-urllib
紧跟 Python 解释器版本组成的。当你开发的应用需要访问属于其他人的网络资源时,出于礼貌,应该在请求中包含实际的 用户 agent 信息,以便对方可以更容易地识别访问源。使用自定义的 agent 也使得对方可以用 robots.txt
文件(参考 http.robotparser
模块)来对爬虫进行控制。
urllib_request_request_header.py
from urllib import request
r = request.Request('http://localhost:8080/')
r.add_header(
'User-agent',
'PyMOTW (https://pymotw.com/)',
)
response = request.urlopen(r)
data = response.read().decode('utf-8')
print(data)
在创建了一个 Request
对象后,并在发送这个请求前,请使用 add_header()
来设定用户 agent 的值。最后一行的输出展示了我们自定义的值。
$ python3 urllib_request_request_header.py
CLIENT VALUES:
client_address=('127.0.0.1', 58585) (127.0.0.1)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1
SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0
HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=PyMOTW (https://pymotw.com/)
用 Request 以 POST 方式发送表单数据
需要被传递的数据可以在构建 Request
时特别指定,以便用 POST 方法发送给服务器。
urllib_request_request_post.py
from urllib import parse
from urllib import request
query_args = {'q': 'query string', 'foo': 'bar'}
r = request.Request(
url='http://localhost:8080/',
data=parse.urlencode(query_args).encode('utf-8'),
)
print('Request method :', r.get_method())
r.add_header(
'User-agent',
'PyMOTW (https://pymotw.com/)',
)
print()
print('OUTGOING DATA:')
print(r.data)
print()
print('SERVER RESPONSE:')
print(request.urlopen(r).read().decode('utf-8'))
在指定添加数据以后, Request
所使用的 HTTP 方法自动从 GET 变为 POST 。
$ python3 urllib_request_request_post.py
Request method : POST
OUTGOING DATA:
b'q=query+string&foo=bar'
SERVER RESPONSE:
Client: ('127.0.0.1', 58613)
User-agent: PyMOTW (https://pymotw.com/)
Path: /
Form data:
foo=bar
q=query string
上传文件
编码并上传文件比操作简单表单的工作要多一些。一个完整的 MIME 消息需要在 request 体内构建,以便服务器可以区分表单字段和要上传的文件。
urllib_request_upload_files.py
import io
import mimetypes
from urllib import request
import uuid
class MultiPartForm:
"""积累数据以便在 POST 一个表单的时候用"""
def __init__(self):
self.form_fields = []
self.files = []
# 使用一个大随机字节串来划分 MIME 数据的各部分。
self.boundary = uuid.uuid4().hex.encode('utf-8')
return
def get_content_type(self):
return 'multipart/form-data; boundary={}'.format(
self.boundary.decode('utf-8'))
def add_field(self, name, value):
"""向表单数据增加一个简单字段。"""
self.form_fields.append((name, value))
def add_file(self, fieldname, filename, fileHandle,
mimetype=None):
"""添加一个要上传的文件。"""
body = fileHandle.read()
if mimetype is None:
mimetype = (
mimetypes.guess_type(filename)[0] or
'application/octet-stream'
)
self.files.append((fieldname, filename, mimetype, body))
return
@staticmethod
def _form_data(name):
return ('Content-Disposition: form-data; '
'name="{}"\r\n').format(name).encode('utf-8')
@staticmethod
def _attached_file(name, filename):
return ('Content-Disposition: file; '
'name="{}"; filename="{}"\r\n').format(
name, filename).encode('utf-8')
@staticmethod
def _content_type(ct):
return 'Content-Type: {}\r\n'.format(ct).encode('utf-8')
def __bytes__(self):
"""返回一个表示表单数据的字节串,包括附加的文件。"""
buffer = io.BytesIO()
boundary = b'--' + self.boundary + b'\r\n'
# 添加表单字段
for name, value in self.form_fields:
buffer.write(boundary)
buffer.write(self._form_data(name))
buffer.write(b'\r\n')
buffer.write(value.encode('utf-8'))
buffer.write(b'\r\n')
# 添加要上传的文件
for f_name, filename, f_content_type, body in self.files:
buffer.write(boundary)
buffer.write(self._attached_file(f_name, filename))
buffer.write(self._content_type(f_content_type))
buffer.write(b'\r\n')
buffer.write(body)
buffer.write(b'\r\n')
buffer.write(b'--' + self.boundary + b'--\r\n')
return buffer.getvalue()
if __name__ == '__main__':
# 创建带有简单字段的表单
form = MultiPartForm()
form.add_field('firstname', 'Doug')
form.add_field('lastname', 'Hellmann')
# 添加一个伪文件
form.add_file(
'biography', 'bio.txt',
fileHandle=io.BytesIO(b'Python developer and blogger.'))
# 构建一个要提交 (POST) 的字节串数据的请求 (request) 。
data = bytes(form)
r = request.Request('http://localhost:8080/', data=data)
r.add_header(
'User-agent',
'PyMOTW (https://pymotw.com/)',
)
r.add_header('Content-type', form.get_content_type())
r.add_header('Content-length', len(data))
print()
print('OUTGOING DATA:')
for name, value in r.header_items():
print('{}: {}'.format(name, value))
print()
print(r.data.decode('utf-8'))
print()
print('SERVER RESPONSE:')
print(request.urlopen(r).read().decode('utf-8'))
MultiPartForm
类可以将任意一个表单表示成一个附有文件的具有多个部分的 MIME 消息。
$ python3 urllib_request_upload_files.py
OUTGOING DATA:
User-agent: PyMOTW (https://pymotw.com/)
Content-type: multipart/form-data;
boundary=d99b5dc60871491b9d63352eb24972b4
Content-length: 389
--d99b5dc60871491b9d63352eb24972b4
Content-Disposition: form-data; name="firstname"
Doug
--d99b5dc60871491b9d63352eb24972b4
Content-Disposition: form-data; name="lastname"
Hellmann
--d99b5dc60871491b9d63352eb24972b4
Content-Disposition: file; name="biography";
filename="bio.txt"
Content-Type: text/plain
Python developer and blogger.
--d99b5dc60871491b9d63352eb24972b4--
SERVER RESPONSE:
Client: ('127.0.0.1', 59310)
User-agent: PyMOTW (https://pymotw.com/)
Path: /
Form data:
Uploaded biography as 'bio.txt' (29 bytes)
firstname=Doug
lastname=Hellmann
创建自定义协议处理器
urllib.request
內建支持访问 HTTP(S) ,FTP ,和本地文件。要添加对其他 URL 类型的支持,就得先注册另一个协议处理器。比如说,要支持用 URLs 来指向远端 NFS 服务器上的任意文件,又不需要用户在访问文件前挂载路径,就需要创建一个从 BaseHandler
派生的,定义有 nfs_open()
方法的子类。
按协议指定的 open()
方法仅有一个参数,即一个 Request
实例,该方法返回一个对象, 该对象须定义有一个读取数据的 read()
方法,一个返回回复头部信息的 info()
方法, 和一个返回被读取文件实际所在 URL 的 geturl()
方法。一个实现上述要求的简单方案是创建 urllib.response.addinfourl
的一个实例,并将头部 (headers) ,URL 和文件开启句柄 (open file handle) 传递给该实例的构建函数。
urllib_request_nfs_handler.py
import io
import mimetypes
import os
import tempfile
from urllib import request
from urllib import response
class NFSFile:
def __init__(self, tempdir, filename):
self.tempdir = tempdir
self.filename = filename
with open(os.path.join(tempdir, filename), 'rb') as f:
self.buffer = io.BytesIO(f.read())
def read(self, *args):
return self.buffer.read(*args)
def readline(self, *args):
return self.buffer.readline(*args)
def close(self):
print('\nNFSFile:')
print(' unmounting {}'.format(
os.path.basename(self.tempdir)))
print(' when {} is closed'.format(
os.path.basename(self.filename)))
class FauxNFSHandler(request.BaseHandler):
def __init__(self, tempdir):
self.tempdir = tempdir
super().__init__()
def nfs_open(self, req):
url = req.full_url
directory_name, file_name = os.path.split(url)
server_name = req.host
print('FauxNFSHandler simulating mount:')
print(' Remote path: {}'.format(directory_name))
print(' Server : {}'.format(server_name))
print(' Local path : {}'.format(
os.path.basename(tempdir)))
print(' Filename : {}'.format(file_name))
local_file = os.path.join(tempdir, file_name)
fp = NFSFile(tempdir, file_name)
content_type = (
mimetypes.guess_type(file_name)[0] or
'application/octet-stream'
)
stats = os.stat(local_file)
size = stats.st_size
headers = {
'Content-type': content_type,
'Content-length': size,
}
return response.addinfourl(fp, headers,
req.get_full_url())
if __name__ == '__main__':
with tempfile.TemporaryDirectory() as tempdir:
# 创建一个临时文件来测试
filename = os.path.join(tempdir, 'file.txt')
with open(filename, 'w', encoding='utf-8') as f:
f.write('Contents of file.txt')
# 用我们的 NFS 处理器构建一个开启者
# 并将其注册为默认的开启者。
opener = request.build_opener(FauxNFSHandler(tempdir))
request.install_opener(opener)
# 通过 URL 打开这个文件。
resp = request.urlopen(
'nfs://remote_server/path/to/the/file.txt'
)
print()
print('READ CONTENTS:', resp.read())
print('URL :', resp.geturl())
print('HEADERS:')
for name, value in sorted(resp.info().items()):
print(' {:<15} = {}'.format(name, value))
resp.close()
FauxNFSHandler
和 NFSFile
类分别打印信息来演示实现中实际呼叫挂载和卸载的地方。 由于这仅仅是一个模拟,我们仅仅向 FauxNFSHandler
提供临时文件夹的路径,它需要查看其中所有的文件。
$ python3 urllib_request_nfs_handler.py
FauxNFSHandler simulating mount:
Remote path: nfs://remote_server/path/to/the
Server : remote_server
Local path : tmprucom5sb
Filename : file.txt
READ CONTENTS: b'Contents of file.txt'
URL : nfs://remote_server/path/to/the/file.txt
HEADERS:
Content-length = 20
Content-type = text/plain
NFSFile:
unmounting tmprucom5sb
when file.txt is closed
参考
- 标准库 urllib.request 文档
urllib.parse
-- 可用于处理 URL 字符串本身。- Form content types -- 通过 HTTP 表单来提交文件或大规模数据的 W3C 说明标准。
mimetypes
-- 名称到 mimetype 的映射。- requests -- 一个提供更多安全链接支持和更易用的 API 的第三方 HTTP 库。Python 核心开发组建议大多数开发人员使用
requests
,部分由于其比标准库更常得到安全方面的更新。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。