13.3. urllib.request — 访问网络资源 | 互联网数据处理 |《python 3 标准库实例教程》| python 技术论坛-金年会app官方网
目标:一个用于访问 urls 资源的库,且可以通过自定义协议处理程序来扩展。
urllib.request
模块提供了一个使用由 urls 指定的网络资源的 api。它被设计成是可被用户应用扩展的,以便支持新的协议或者添加现存协议的变种(比如处理 http 基本认证)。
http get
注意
为测试本节的例子所使用的服务器由
http_server_get.py
实现,事实上这是为了介绍 模块而编写的几个例子。在一个终端开启这个服务器,然后就可以在另一个终端下尝试本节的例子了。
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.get)
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:
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
中实现,这主要是在介绍 模块时实现的几个例子。在一个终端开启这个服务器,然后在另一个终端测试本节的例子。
想要用 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 的 get
方法。一个实现上述要求的简单方案是创建 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.addinfo
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.get)
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
参考
- -- 可用于处理 url 字符串本身。
- -- 通过 http 表单来提交文件或大规模数据的 w3c 说明标准。
mimetypes
-- 名称到 mimetype 的映射。- -- 一个提供更多安全链接支持和更易用的 api 的第三方 http 库。python 核心开发组建议大多数开发人员使用
requests
,部分由于其比标准库更常得到安全方面的更新。
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 cc 协议,如果我们的工作有侵犯到您的权益,请及时联系金年会app官方网。