尽管大多数应用层协议都是基于TCP的,但除了编写像QQ服务器那样的服务端应用,很少直接使用 TCP Socket 进行编程。一般都编写基于应用层网络协议(HTTP、FTP等)的应用都是直接使用封装相应协议的模块,这样开发效率会更好。例如,如果要使用HTTP或HTTPS开发Python应用,可以使用urllib3、twisted以及其他类似的模块,FTP、SMTP、POP、IMAP等常用协议也有对应的Python模块。
16.1 urllib3 模块
urllib3 是一个功能强大,条例清晰,用于编写HTTP客户端的Python库,许多Python的原生系统已经开始使用urllib3。urllib3提供了许多独有的特性:
- 线程安全
- 连接池
- 客户端SSL/TLS验证
- 使用multipart编码上传文件
- 协助处理重复请求和HTTP重定位
- 支持压缩编码
- 支持HTTP和SOCKS代理
- 100%测试覆盖率
urllib3 并不是Python语言的标准模块,因此,使用urllib3之前需要使用pip命令或conda命令安装 urllib3 :
pip install urllib3
或
conda install urllib3
16.1.1 发送 HTTP GET 请求
使用urllib3中的API向服务端发送HTTP 请求的过程: 首先需要引用urllib3模块;然后创建PoolManager类的实例,该类用于管理连接池;最后通过 request方法发送GET 请求,request方法的返回值就是服务端的响应结果,通过data属性直接可以获得服务端的响应数据。
当向服务端发送HTTP GET请求时,如果请求字段值包含中文、空格等字符,需要对其进行编码。在 urllib.parse模块中有一个urlencode函数,可以将一个字典形式的请求值作为参数传入urlencode函数,该函数返回编码结果。
print(urlencode({'wd':'涛哥最帅'}))
也可以直接使用fields关键字参数指定字典形式的GET请求字段。使用这种方式,request方法会自动对fields关键字参数指定的GET请求字段进行编码。
http.request('GET',url,fields={'wd':'涛哥最帅'})
示例:从百度网站获取搜索结果
from urllib3 import *
from urllib.parse import urlencode
# 调用disable_warnings函数可以阻止显示警告信息
disable_warnings()
# 创建PoolManager类的实例
http = PoolManager()
'''
# 组合url并访问
url = 'http://www.baidu.com/s?' + urlencode({'wd':'涛哥最帅'})
print(url)
response = http.request('GET', url)
'''
url = 'http://www.baidu.com/s'
# 使用关键字参数指定GET请求字段
response = http.request('GET', url,fields={'wd':'涛哥最帅'})
# 获取百度服务端的返回值(字节形式),并使用UTF-8格式对其进行解码
data = response.data.decode('UTF-8')
# 输出百度服务端返回的内容
print(data)
16.1.2 发送 HTTP POST 请求
向服务端发送比较复杂的数据时,通过HTTP GET 请求不太合适,因为HTTP GET 请求将要发送的数据都放到URL中。因此,当向服务端发送复杂数据时,建议使用HTTP POST请求。
HTTP POST 请求与HTTP GET请求的使用方法类似,只是在向服务端发送数据时,传递数据会跟在HTTP请求头后面,因此,可以使用HTTP POST请求发送任何类型的数据,包括二进制形式的文件(一般会将这样的文件使用Base64或其他编码格式进行编码)。
首先编写一个处理HTTP POST请求的 服务端程序(需要安装Flask模块)。
from flask import Flask, request
# 创建Flask对象,任何基于Flask模块的服务端应用都必须创建Flask对象
app = Flask(__name__)
# 设置路由/,默认处理GET请求
@app.route('/')
def helloworld():
return 'hello world'
# 设置路由/register,可处理POST请求
@app.route('/register', methods=['POST'])
def register():
# 输出名为name的请求字段的值
print(request.form.get('name'))
# 输出名为age的请求字段的值
print(request.form.get('age'))
# 客户端返回"注册成功"消息
return '注册成功'
if __name__ == '__main__':
# 开始运行服务端程序,默认端口是5000
app.run()
这里有一个路由的概念,其实路由就是在浏览器地址栏中输入的一个 Path (跟在域名或IP后面),Flask模块会将路由对应的 Path映射到服务端的一个函数,也就是说,如果在浏览器地址栏中输入特定的路由,Flask模块的相应API接收到这个请求,就会自动调用该路由对应的函数。如果不指定 methods,默认可以处理HTTP GET请求,如果要处理HTTP POST 请求,需要设置methods的值为[‘POST’]。Flask在处理HTTP POST的请求字段时,会将这些请求保存到字典中,form属性就是这个字典变量。
from urllib3 import *
disable_warnings()
http = PoolManager()
# 指定要提交的HTTP POST请求的URL,/register是路由
url = 'http://localhost:5000/register'
# 向服务端发送HTTP POST请求,用fields关键字参数指定HTTP POST请求字段名和值
response = http.request('POST', url,fields={'name':'李宁','age':18})
# 获取服务端返回的数据
data = response.data.decode('UTF-8')
# 输出服务端返回的数据
print(data)
16.1.3 HTTP 请求头
大多数服务端应用都会检测某些HTTP请求头,例如,为了阻止网络爬虫或其他的目的,通常会检测HTTP请求头的user-agent字段,该字段指定了用户代理,也就是用什么应用访问的服务端程序,如果是浏览器,如Chrome,会包含Mozilla/5.0或其他类似的内容,如果HTTP请求头不包含这个字段,或该字段的值不符合要求,那么服务端程序就会拒绝访问。还有一些服务端应用要求只有处于登录状态才可以访问某些数据,所以需要检测HTTP请求头的cookie字段,该字段会包含标识用户登录的信息。当然,服务端应用也可能会检测HTTP请求头的其他字段,不管服务端应用检测哪个HTTP请求头字段,都需要在访问URL时向服务端传递HTTP请求头。
通过PoolManager对象的request方法的headers关键字参数可以指定字典形式的HTTP请求头。
http.request('GET',url, headers = { 'headerl' : 'value1', 'header2': 'value2')
示例:通过request方法访问天猫商城的搜索功能,该搜索功能的服务端必须依赖 HTTP 请求头的 cookie 字段。
天猫的搜索页面只检测cookie字段,并未检测user-agent以及其他字段,所以可以复制所有的字段,也可以只复制cookie字段。将所有要传递的HTTP请求头都放在一个名为 headers.txt 的文件中。
headers.txt
注意:不要有空行,特别是最后一个空行
cookie:cna=FrmIEpCAkwYCAXApHRfpabZc; sm4=210100; UM_distinctid=15ff7162d4b4ea-0b674da1f2669c-173f6d55-384000-15ff7162d4cfca; hng=CN%7Czh-CN%7CCNY%7C156; _m_h5_tk=d3879a70c26d4b6106f2bdc5f279e637_1512913008614; _m_h5_tk_enc=cf1644d5258eebf260ca04b2243538b4; OZ_1U_2061=vid=va2d3b2861ab69.0&ctime=1512953122<ime=1512913703; t=2d2aa0a850be15e08be1eea671ba61a6; tracknick=androidguy; lgc=androidguy; _tb_token_=ee3bebe7e9155; cookie2=2fc662ebc663fa6aa9fa894ff475bfae; uc1=cookie14=UoTdeY238xtGyQ%3D%3D&lng=zh_CN&cookie16=UIHiLt3xCS3yM2h4eKHS9lpEOw%3D%3D&existShop=false&cookie21=UIHiLt3xTIkz&tag=8&cookie15=URm48syIIVrSKA%3D%3D&pas=0; uc3=sg2=B0FgoUrTyGXVbtIYqIcGIb35wx%2B3WQFvJAov33QyEkw%3D&nk2=An9XR83KKz8hIQ%3D%3D&id2=UUBc%2FEjO7bnB&vt3=F8dBzLWl9C%2BBDOsIZ7c%3D&lg2=U%2BGCWk%2F75gdr5Q%3D%3D; _l_g_=Ug%3D%3D; unb=284722917; cookie1=VyUIUmUXx75IMgtLbtay%2FhalyAkF2kcpJ%2FHn5XEfPnc%3D; login=true; cookie17=UUBc%2FEjO7bnB; _nk_=androidguy; uss=UU7yoRpba52c66tfkw4sW6aG4ZqIfteAisXkvAv1Rz9QBx1Ul53qlmuCHg%3D%3D; sg=y7f; isg=AqGhmWPLX17EwfOC7JToakURsGt75gpIh40chgN2magHasE8S54lEM-suKiX
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36
from urllib3 import *
import re
disable_warnings()
http = PoolManager()
# 定义天猫的搜索页面的URL
url = 'https://list.tmall.com/search_product.htm?spm=a220m.1000858.1000724.4.53ec3e72bTyQhM&q=%D0%D8%D5%D6&sort=d&style=g&from=mallfp..pc_1_searchbutton#J_Filter'
# 从headers.txt读取HTTP请求头,并将其转换为字典形式
def str2Headers(file):
headerDict = {}
f = open(file,'r')
# 读取headers.txt文件的所有内容
headersText = f.read()
headers = re.split('\n',headersText)
for header in headers:
result = re.split(':',header,maxsplit = 1)
headerDict[result[0]] = result[1]
f.close()
# 返回字典
return headerDict
# 请求天猫的搜索页面,并传递HTTP请求头
headers = str2Headers('headers.txt')
response = http.request('GET', url,headers=headers)
# 返回服务端的数据,并按utf-8格式解码,编码格式可以适当调整
data = response.data.decode('utf-8')
print(data)
16.1.4 HTTP 响应头
使用HTTPResponse.info方法可以非常容易地获取HTTP响应头的信息。其中,HTTPResponse对象是request方法的返回值。
示例:获取百度官网返回的HTTP响应头
from urllib3 import *
disable_warnings()
http = PoolManager()
url = 'https://www.baidu.com'
# url = 'http://httpbin.org/delay/3'
response = http.request('GET', url,timeout=Timeout(connect=2,read=4))
# 以字典的形式返回HTTP响应头信息
print(response.info())
print('------------')
# 输出HTTP响应头中的Content-Length字段的值
print(response.info()['Content-Length'])
16.1.5 上传文件
客户端浏览器向服务端发送HTTP请求时有一类特殊的请求,就是上传文件,为什么特殊呢?因为发送其他值时,可能是以字节为单位的,而上传文件时,可能是以KB或MB为单位的,发送的文件尺寸通常比较大,所以上传的文件内容会用multipart/form-data格式进行编码,然后再上传。urllib3对文件上传支持得非常好,只需要像设置普通的HTTP请求头一样在 request方法中使用fields关键字参数指定一个描述上传文件的HTTP请求头字段,然后再通过元组指定相关属性即可,例如,上传文件名、文件类型等。
首先,用Flask实现一个接收上传文件的服务端程序,该程序从客户端获取上传文件的内容,并将文件使用上传文件名保存到当前目录的uploads子目录中。
import os
from flask import Flask, request
# 定义服务端上传位置
UPLOAD_FOLDER = 'uploads'
app = Flask(__name__)
# 用于接收上传文件的路由需要使用POST方法
@app.route('/', methods=['POST'])
def upload_file():
# 获取上传文件内容
file = request.files['file']
if file:
# 将上传文件保存到uploads子目录
file.save(os.path.join(UPLOAD_FOLDER, file.filename))
return "文件上传成功"
if __name__ == '__main__':
app.run()
再编写一个上传文件的客户端程序。
from urllib3 import *
disable_warnings()
http = PoolManager()
# 定义上传文件的URL
url = 'http://localhost:5000'
while True:
# 输入上传文件的名字
filename = input('请输入要上传的文件名字(必须在当前目录下):')
# 如果什么都没有输入则推出循环
if not filename:
break
# 用二进制的方式打开要上传的文件名,然后读取文件的所有内容,使用with语句会自动关闭打开的文件
with open(filename,'rb') as fp:
fileData = fp.read()
# 上传文件
response = http.request('POST',url,fields={'file':(filename,fileData)})
# 输出服务端返回结果 "文件上传成功"
print(response.data.decode('utf-8'))
16.1.6 超时
由于HTTP底层是基于Socket 实现的,所以连接的过程中也可能会超时。Socket超时分为连接超时和读超时。
- 连接超时:是指在连接的过程中由于服务端的问题或域名(IP地址)弄错了而导致的无法连接服务器的情况,当客户端Socket尝试连接服务器超过给定时间后,还没有成功连接服务器,那么就会自动中断连接,通常会抛出超时异常。
- 读超时:是指在从服务器读取数据时由于服务器的问题,导致长时间无法正常读取数据而导致的异常。
使用urllib3模块中的API设置超时时间非常方便,只需要通过request方法的timeout关键字参数指定超时时间即可(单位是秒)。如果连接超时与读超时相同,可以直接将timeout关键字参数值设为一个浮点数,表示超时时间。如果连接超时与读超时不相同,需要使用Timeout对象分别设置。
# http是PoolManager类的实例
# 连接超时与读超时都是5s
http.request('GET', url1,timeout=5.0)
# 连接超时是2s,读超时是4s
http.request('GET ',urll,timeout=Timeout(connect=2.0 , read=4.0))
如果让所有网络操作的超时都相同,可以通过PoolManager类构造方法的timeout 关键字参数设置连接超时和读超时。
http = PoolManager(timeout=Timeout(connect=2.0, read=2.0 ) )
如果在request方法中仍然设置了timeout关键字参数,那么将覆盖通过PoolManager类构造方法设置的超时。
示例:测试一个错误的域名触发超时。同时可以控制让服务端延迟5s再返回数据,url = http://httpbin.org/delay/5
from urllib3 import *
disable_warnings()
# 通过PoolManager类的构造方法指定默认的连接超时和读超时
http = PoolManager(timeout=Timeout(connect=2,read=2))
url1 = 'https://www.baidu1122.com'
url2 = 'http://httpbin.org/delay/3'
try:
# 此处抛出异常后,仍然可以执行后边的代码
# 测试一个并不存在的网址,抛出异常
# 连接超时设置为2s
http.request('GET', url1,timeout=Timeout(connect=2.0,read=4))
except Exception as e:
print(e)
print('------------')
# 由于读超时为4s,而url2在3s后才返回数据,所以不会抛出异常
# 会正常输出服务端返回的结果
response = http.request('GET', url2,timeout=Timeout(connect=2,read=4))
print(response.info())
print('------------')
# 会在2s后抛出读超时异常
print(response.info()['Content-Length'])
http.request('GET', url2,timeout=Timeout(connect=2,read=2))
16.2 twisted 框架
twisted是一个完整的事件驱动的网络框架,利用这个框架可以开发出完整的异步网络应用程序。
twisted并不是Python的标准模块,所以在使用之前需要使用pip install twisted 安装 twisted模块,如果使用的是 Anaconda Python开发环境,也可以使用 conda install -c anaconda twisted 安装 twisted模块。
16.2.1 异步编程模型
目前常用的编程模型有三种:
1. 同步编程模型
如果所有的任务都在一个线程中完成,那么这种编程模型称为同步编程模型。线程中的任务都是顺序执行的,也就是说,只有当第1个任务执行完后,才会执行第2个任务,以此类推。
很显然,同步编程模型尽管简单,但执行效率比较低。如果出现阻塞,将无限期的等待下去。
2. 线程编程模型
如果要完成多个任务,比较有效的方式是将这些任务分解,然后启动多个线程,每个线程处理一部分任务,最后再将处理结果合并。这样做的好处是当一个任务被阻塞后,并不影响其他任务的执行。
如果是单CPU单核的计算机,那么多线程其实也是同步执行的,只是任何一个线程都无法长时间独占CPU的计算时间,所以多个线程会不断交替在CPU上执行,也就是说,每个线程都可能被分成若干个小的执行块,并根据某种调度算法获取CPU计算资源。但应该执行哪个线程、什么时间执行,都不是由程序员决定的,而通常是由操作系统的底层机制决定的,所以对于应用层的程序是无法干预的。当然,对于多CPU多核这样的高性能计算机,线程有可能同时运行。因此,多线程执行效率的高低在某种程度上取决于计算机是否有多颗 CPU,以及每颗CPU有多少个核。不管怎样,线程编程模型在运行效率上肯定会远远高于同步编程模型。
3. 异步编程模型
在多CPU上的异步编程模型类似多线程编程模型,它们的基本原理相同。
就运行效率来看,同步编程模型是最低的,线程编程模型是最高的,尤其是在多CPU的计算机上。异步编程模型也可以进行任务切换,但要等到任务被阻塞或执行结束才能切换到其他任务,因此,异步编程模型的运行效率介于同步编程模型和线程编程模型之间。同时在异步编程模型中调度任务是由程序员控制的。
线程编程模型效率高,为什么要使用异步编程模型?
- 线程编程模型使用起来有些复杂,通常需要使用一些锁机制。
- 如果有一两个子任务需要与用户交互,则使用异步编程模型可以立刻切换到其他任务,这一切都是可控的。
16.2.2 Reactor (反应堆)模式
异步编程模型之所以能监视所有任务的完成和阻塞情况,是因为通过循环用非阻塞模式执行完了所有的任务。例如,对于使用Socket访问多个服务器的任务。如果使用同步编程模型,会一个任务一个任务地顺序执行,而使用异步编程模型,执行的所有Socket方法都处于非阻塞的 (使用setblocking(0)设置),也就是说,使用异步编程模型需要在循环中执行所有的非阻塞Socket 任务,并利用select模块中的select方法监视所有的Socket是否有数据需要接收。
这种利用循环体来等待事件发生,然后处理发生的事件的模型被设计成了一个模式:Reactor(反应堆)模式。twisted就是使用了 Reactor 模式的异步网络框架。
16.2.3 HelloWorld (twisted 框架)
由于twisted框架是基于Reactor模式的,所以需要一个循环来处理所有的任务,不过这个循环并不需要我们写,twisted框架已经封装好了,只需要调用reactor模块中的run函数就可以通过Reactor模式以非阻塞方式运行所有的任务。
from twisted.internet import reactor
reactor.run()
这里调用了run函数,实质上是开始启动事件循环,也就是Reactor模式中的循环。
- twisted的Reactor模式必须通过run函数启动。
- Reactor循环是在开始的进程中运行的,也就是运行在主进程中。
- 一旦启动Reactor,就会一直运行下去。Reactor会在程序的控制之下。
- Reactor循环并不会消耗任何CPU 资源。
- 并不需要显式创建 Reactor循环,只要导入reactor模块即可。也就是说,Reactor是Singleton单件模式,即在一个程序中只能有一个Reactor。
twisted 可以使用不同的Reactor,但需要在导入 twisted.internet.reactor 之前安装它。例如,引用pollreactor的代码如下:
from twisted.internet import pollreactor
pollreactor.install()
如果在导入 twisted.internet.reactor 之前没有安装任何特殊的 Reactor,那么twisted会安装selectreactor。正因为如此,习惯性做法是不要在顶层的模块内引入Reactor以避免安装默认的Reactor,而是要使用Reactor的区域内安装。下面的代码安装了pollreactor,然后导入和运行Reactor。
from twisted.internet import pollreactor
# 安装pollreactor
pollreactor.install()
from twisted.internet import reactor
reactor.run()
其实上面的这段代码还是没做任何事情,只是使用了pollreactor作为当前的Reactor。下面这段代码在 Reactor循环开始后向终端输出一条消息。
def hello():
print('Hello,How are you ? ')
from twisted.internet import reactor
# 执行回调函数
reactor.callwhenRunning(hello)
print('starting the reactor.')
reactor.run()
通过调用Reactor的 callWhenRunning 函数,让Reactor启动后回调callWhenRunning 函数指定的回调函数。
16.2.4 用 twisted 实现时间戳客户端
服务端Socket,需要调用connectTCP函数,并且通过 giant 函数的参数指定host 和 port,以及一个工程对象,该工程对象对应的类必须是ClientFactory 的子类,并且设置了protocol等属性。protocol属性的类型是Protocol对象,Protocol相当于一个回调类,Protocol类的子类实现的很多父类的方法都会被回调。
from twisted.internet import protocol,reactor
host = 'localhost'
port = 9876
# 定义回调类
class MyProtocol(protocol.Protocol):
# 从控制台采集要发送给服务端的数据,按回车,即发送。
def sendData(self):
data = input('>')
if data:
print('...正在发送 %s' % data)
# 将数据发送给服务端
self.transport.write(data.encode(encoding='utf_8'))
else:
# 发生异常后,关闭连接
self.transport.loseConnection()
# 发送数据
def connectionMade(self):
self.sendData()
def dataReceived(self,data):
# 输出接收到的数据
print(data.decode('utf-8'))
# 调用sendData函数,从控制台采集要发送的数据
self.sendData()
# 工程类
class MyFactory(protocol.ClientFactory):
protocol = MyProtocol
clientConnectionLost = clientConnectionFailed = lambda self,connector,reason:reactor.stop()
# 连接host和port,以及MyFactory类的实例
reactor.connectTCP(host,port,MyFactory())
reactor.run()
16.2.5 用 twisted 实现时间戳服务端
用twisted编写服务端Socket程序与编写客户端Socket程序的步骤差不多,只是需要调用listenTCP监听端口。编写服务端Socket程序同样需要一个 Factory对象,以及一个从Protocol继承的类。
from twisted.internet import protocol,reactor
from time import ctime
port = 9876
class MyProtocol(protocol.Protocol):
# 当客户端连接到服务端后,调用该方法
def connectionMade(self):
# 获取客户端的IP
client = self.transport.getPeer().host
print('客户端',client,'已经连接')
def dataReceived(self,data):
# 接收到客户端发送过来的数据后,向客户端返回服务端的数据+时间
self.transport.write(ctime().encode(encoding='utf-8') + b' ' + data)
# 创建Factory对象
factory = protocol.Factory()
factory.protocol = MyProtocol
print('正在等待客户端连接')
# 监听端口,等待客户端的请求
reactor.listenTCP(port,factory)
reactor.run()
16.2.6 用 twisted 获取 Email 邮箱目录列表(略)
16.3 FTP 客户端
FTP是 File Transfer Protocol (文件传输协议)的缩写,与HTTP一样,都是非常常用的应用层协议,用于上传和下载文件。
Python语言中内置了很多模块,封装了各种应用层的协议,其中 ftplib模块封装了FTP。该模块中提供了若干个API,用于编写FTP客户端应用。
首先要有一个FTP服务器,Internet 上的或本地的都可以。连接FTP服务器首先要创建一个FTP类的实例,FTP服务器的IP或域名要通过FTP类构造方法的参数传入。在创建FTP对象后,就可以利用FTP对象的相关方法进行各种FTP操作。下面是几个常用的方法。
-
login(username,password):登录FTP服务器,如果FTP服务器不支持匿名登录,则需要输入用户名和密码。
-
cwd(dirname):改变当前的目录。
-
dir(callback):列出当前目录中所有的子目录和文件,如果不指定回调函数,dir方法会自己将所有的子目录和文件输出到终端。如果指定了回调函数,则每得到一个子目录或文件,都会调用回调函数进行处理。
-
mkd(dirname):在当前目录下建立子目录。
-
storlines(cmd,f):向FTP服务器上传文本文件,其中 cmd是 FTP命令,如STOR filename; f是一个文件对象,要用文本形式打开文件,如 open(filename, ‘r’)。
-
storbinary(cmd,f):向FTP服务器上传二进制文件,其中 cmd是 FTP命令,如 STOR filename;f是一个文件对象,要用二进制形式打开文件,如 open(filename, ‘rb’)。
-
retrlines(cmd,f):从FTP服务器下载文本文件,其中 cmd 是FTP命令,如RETR filename; f是一个文件对象,要用文本形式打开文件,如open(filename, ‘w’)。
-
retrbinary(cmd,f):从FTP服务器下载二进制文件,其中 cmd是 FTP命令,如RETR filename;f是一个文件对象,要用二进制形式打开文件,如open(filename, ‘wb’)。
-
quit():关闭FTP连接并退出。
FTP对象的其他方法以及这些方法的详细使用方式请到下面的页面查看官方文档:
https://docs.python.org/3/library/ftplib.html
import ftplib
# 定义ftp服务器的域名,这里使用的是本机,所以是localhost
host = 'localhost'
# 为dir方法定义回调函数,处理每一个子目录名和文件名
def dirCallback(dir):
# 按utf-8格式输出命令或文件名
print(dir.encode('ISO-8859-1').decode('utf-8'))
def main():
try:
# 连接ftp服务器
f = ftplib.FTP(host)
except Exception as e:
print(e)
return
print('FTP服务器已经成功连接')
try:
# 登录服务器,请将login方法的两个参数分别替换成真正的用户名和密码
f.login('用户名','密码')
except Exception as e:
print(e)
return
print('FTP服务器已经成功登录.')
# 将当前目录切换成Pictures
f.cwd('Pictures')
# 列出Pictures目录中的所有子目录和文件
f.dir(dirCallback)
print('当前工作目录:',f.pwd())
try:
# 在当前目录建立一个名为"新目录"的子目录
f.mkd('新目录'.encode('GBK').decode('ISO-8859-1'))
# 将当前目录切换到 Pictures/新目录
f.cwd('新目录'.encode('GBK').decode('ISO-8859-1'))
# 在当前目录建立一个名为dir1的子目录
f.mkd('dir1')
# 在当前目录建立一个名为dir2的子目录
f.mkd('dir2')
except:
f.cwd('新目录'.encode('utf-8').decode('ISO-8859-1'))
print('-----')
# 要上传的本地文件名
upload_file = '/Users/lining/Desktop/a.png'
# 打开要上传的本地文件
ff = open(upload_file,'rb')
# 上传本地文件,上传后的文件名为a.png,并输出一共传输了多少字节块
# 上传的方式是每次读若干字节一起上传,每次默认读取8192字节
print(f.storbinary('STOR %s' % 'a.png',ff))
# 列出当前目录中的子目录和文件名
f.dir(dirCallback)
# 将上传的a.png文件下载,保存成本地文件 xx.png
print(f.retrbinary('RETR %s' % 'a.png',open('/Users/lining/Desktop/xx.png','wb').write))
# 关闭FTP连接并退出
f.quit()
if __name__ == '__main__':
main()
16.4 Email 客户端(了解)
发送和接收Email需要使用很多协议,例如发送Email需要SMTP协议,接收Email需要使用POP3或IMAP4协议。当然,程序员一般并不需要对这些协议的底层实现有太深的了解,因为在Python 语言中很多原生的模块对这些Email 协议进行了很好的封装,只需要直接调用相关的API就可以轻松地发送和接收 Email。
16.4.1 使用 SMTP 发送简单的 Email
SMTP 是Simple Message Transfer Protocol(简单邮件传输协议)的缩写,是发送Email专用的协议。SMTP的基本原理就是将要发送的邮件传给SMTP服务器,然后SMTP服务器再将要发送的邮件发送给对方Email所在的SMTP服务器,最后,对方会通过POP3或IMAP4接收Email。也就是说,SMTP是负责在邮件服务器之间传递数据的协议。
在 Python 语言中发送Email需要导入smtplib模块,使用smtplib模块发送Email的步骤如下:
-
创建SMTP或 SMTP_SSL对象。SMTP分为明文数据传输与加密数据传输两种。SMTP 与HTTP一样,都是用明文传输数据的,如果想用加密的方式进行数据传输,需要创建SMTP_SSL 类的实例。SMTP的默认端口号是25,SMTP_SSL的默认端口号是465。
-
登录 Email。在发送Email之前,必须使用SMTP或SMTP_SSL对象的 login方法登录Email需要指定登录Email 的用户名和密码。
-
准备MIMEText对象。如果发送的是文本形式的 Email,需要创建 MIMEText对象,并设置相应的值。因为 Email 数据是基于MIME格式的(因特网消息内容标准)。要设置的值主要包括Email 正文内容、发送者的Email地址、接收者的 Email 地址、Email主题等。
-
发送 Email。前面所有的工作都准备就绪后,就可以使用sendmail方法发送Email了。
示例:实现一个SMTP客户端应用,可以从一个Email账号将邮件发送到另一个Email账号。
import smtplib
from email.mime.text import MIMEText
sender ='发送者的EMail地址'
password = '发送者的EMail密码'
to ='接收者的EMail地址'
def mail():
ret=True
try:
# 指定邮件正文,utf-8编码
msg=MIMEText('这是第一封email','plain','utf-8')
# 设置发送者的地址
msg['From']=sender
# 设置接受者的地址
msg['To']=to
# 设置邮件的主题
msg['Subject']="DBA最有内涵的帅哥,发送的一封邮件"
# SMTP("smtp.126.com", 25) # 使用不安全协议时放开
# 创建SMTP_SSL对象,并指定SMTP服务器
server=smtplib.SMTP_SSL("smtp.126.com", 465)
# 括号中对应的是发件人邮箱账号、邮箱密码
server.login(sender, password)
print(msg.as_string())
# 发送Email,sendmail方法的3个参数分别表示发送者地址、接收者地址(可以是多个)
# 以及Email头和正文(MIME编码格式)
# 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
server.sendmail(sender,[to,],msg.as_string())
# 关闭连接
server.quit()
# 如果 try 中的语句没有执行,则会执行下面的 ret=False
except Exception as e:
ret=False
print(e)
return ret
ret=mail()
if ret:
print("邮件发送成功")
else:
print("邮件发送失败")
16.4.2 使用 SMTP 发送待附件的 Email
使用SMTP发送带附件的 Email与发送文本形式的Email 的步骤类似,只是需要创建MIMEMultipart 对象,并用related定义内嵌资源的邮件体。也就是说,需要将附件以及HTML等富文本格式的内容嵌入到邮件体内。
import smtplib
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
sender ='发送者的EMail地址'
password = '发送者的EMail密码'
to ='接收者的EMail地址'
def mail():
ret=True
try:
msg = MIMEMultipart('related')
msg['From'] = sender
msg['To'] = to
msg['Subject'] = '涛哥(带附件)'
# 创建MIMEMultipart对象,内嵌HTML文档
msgAlternative = MIMEMultipart('alternative')
msg.attach(msgAlternative)
# 将msgAlternative内嵌在msg上
mail_msg = """
<p>涛哥更上一层楼.</p>
<p><a href="https://www.baidu.com">涛哥最帅</a></p>
<p>图片演示:</p>
<p><img src="cid:image1"></p>
"""
msgAlternative.attach(MIMEText(mail_msg, 'html', 'utf-8'))
# 将本地读片作为附件发送
fp = open('/Users/lining/Desktop/xx.png', 'rb')
# 创建MIMEImage对象内嵌图像文件数据
msgImage = MIMEImage(fp.read())
fp.close()
# 定义图片 ID,在 HTML 文本中引用
msgImage.add_header('Content-ID', '<image1>')
# 将图片作为msg的附件内嵌到邮件中
msg.attach(msgImage)
server=smtplib.SMTP_SSL("smtp.126.com", 465)
# 括号中对应的是发件人邮箱账号、邮箱密码
server.login(sender, password)
# 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
server.sendmail(sender,[to,],msg.as_string())
# 关闭连接
server.quit()
# 如果 try 中的语句没有执行,则会执行下面的 ret=False
except Exception as e:
ret=False
print(e)
return ret
ret=mail()
if ret:
print("邮件发送成功")
else:
print("邮件发送失败")
16.4.3 使用 POP3 接收 Email
POP是Post Office Protocol的缩写,是用于接收邮件的协议。POP3是 POP最新的版本(Version3 ),也是现在普通使用的版本,所以习惯上称POP 为 POP3。POP与SMTP一样,都是明文传输的,使用SSL加密的POP3称为POP3S。
在Python语言中使用POP3接收邮件需要导入poplib模块。接收邮件同样需要指定邮箱的用户名和密码,以及 POP3服务器,126邮箱的 POP3服务器是 pop.126.com,QQ邮箱的POP3服务器是pop.qq.com。
接收邮件相对于发送邮件要复杂一些,首先需要创建POP3(未加密)或POP3_SSL(加密),然后通过user和 pass_方法设置邮箱的用户名和密码,接下来就可以调用相应的方法完成各种 POP3支持的工作。
import poplib
import re
# pop3服务器地址
host = "pop.126.com"
# 用户名
username = "邮箱账号" # 如abcd@126.com
# 密码
password = "邮箱密码"
#pp = poplib.POP3(host)
pp = poplib.POP3_SSL(host)
# 设置调试模式,可以看到与服务器的交互信息
pp.set_debuglevel(1)
# 向服务器发送用户名
pp.user(username)
# 向服务器发送密码
pp.pass_(password)
# 获取服务器上信件信息,返回是一个列表,第一项是一共有多上封邮件,第二项是共有多少字节
ret = pp.stat()
# 需要取出所有信件的头部,信件id是从1开始的。
mailCount = ret[0]
print('一共',mailCount,'封邮件')
for i in range(1, mailCount):
# 取出信件头部。注意:top指定的行数是以信件头为基数的,也就是说当取0行,
# 其实是返回头部信息,取1行其实是返回头部信息之外再多1行。
try:
mlist = pp.top(i, 0)
print(mlist[1])
print(mlist[1][7])
if i > 20: break;
except:
pass
# 列出服务器上邮件信息,这个会对每一封邮件都输出id和大小。不象stat输出的是总的统计信息
ret = pp.list()
# 取第一封邮件完整信息,在返回值里,是按行存储在down[1]的列表里的。down[0]是返回的状态信息
#down = pp.retr(mailCount)
down = pp.retr(1)
# 输出邮件
charset = ''
for line in down[1]:
result = re.search('charset\s*=\s*"([^\"]*)"',line.decode('ISO-8859-1'))
if result != None:
charset = result.group(1)
print(charset)
if charset != '':
print(line.decode(charset))
# 退出
pp.quit()
16.4.4 使用 IMAP4 接收 Email
IMAP是Internet Message Access Protocol的缩写,中文的含义是“交互式数据消息访问协议”,目前最新的版本是4,所以习惯上称IMAP为IMAP4,也就是IMAP的第4个版本。
IMAP4与 POP3一样,都是用于接收Email的,那么这两个协议有什么区别呢?
POP3协议允许电子邮件客户端下载服务器上的邮件,但是在客户端的操作(如移动邮件、标记已读等),不会反馈到服务器上,比如通过客户端收取了邮箱中的三封邮件并移动到其他文件夹,邮箱服务器上的这些邮件是不会同时被移动的。
而IMAP4提供了Email服务端与电子邮件客户端之间的双向通信,客户端的操作都会反馈到服务器上。例如,在客户端移动邮件、删除邮件,邮件服务器的邮件也同时会被移动、删除。
当然,IMAP4也像POP3那样提供了方便的邮件下载服务,让用户能进行离线阅读。IMAP4提供的摘要浏览功能可以让用户在阅读完所有的邮件到达时间、主题、发件人、大小等信息后才做出是否下载的决定。此外,IMAP更好地支持了从多个不同设备中随时访问新邮件功能。
在 Python语言中使用IMAP4接收邮件需要导入 imaplib模块,然后可以使用IMAP4_SSL建立安全的IMAP连接,接下来就是调用一系列方法完成各种IMAP4支持的操作。另外要说明一下,并不是所有的Email 服务器都支持IMAP4,即使支持IMAP4,也可能支持的并不完整,所以如果使用的Email服务器对IMAP4 支持的有问题,应更换 Email服务器。
import imaplib
import base64
connection = imaplib.IMAP4_SSL('imap.qq.com', 993)
username = '邮箱用户名'
password = '邮箱密码'
# 登陆邮箱
try:
connection.login(username, password)
except Exception as err:
# 输出登陆失败的原因
print('登陆失败: :', err)
# 输出日志
connection.print_log()
# 列出所有的目录(如INBOX)
res,data = connection.list()
print('Response code:', data)
# 切换到INBOX目录
res, data = connection.select('INBOX')
print(res, data)
# 邮件数
print(data[0])
# 你也可以直接搜索邮件
res, msg_ids = connection.search(None, 'ALL')
print(res, msg_ids)
# 获取第1份邮件内容
res, msg_data = connection.fetch(data[0], '(UID BODY[TEXT])')
# 输出第1份邮件内容
print(msg_data)
# 退出邮箱
connection.logout()