16. 网络编程
网络通信是两台计算机上的两个进程之间的通信,网络编程就是如何在程序中实现两台计算机的通信。
1. TCP/IP简介
互联网协议包含了上百种协议标准,最重要的两个协议是TCP和IP协议,因此,大家把互联网的协议简称TCP/IP
协议。
互联网上每个计算机的唯一标识是IP地址,如果一台计算机同时接入两个或更多的网络,比如路由器,它就有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。
IP协议: 把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块,然后通过IP包发送出去。由于互联网链路复杂,两台计算机之间经常有多条线路,因此,路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送,但不保证能到达,也不保证顺序到达。
IP地址: 实际上是一个32位整数(简称IPV4),以字符串表示的IP地址如192.168.0.1
实际是把32位整数按8位分组后的数字表示,目的是便于阅读。
IPV6地址: 实际上是一个128位整数,是目前使用的IPV4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334
。
TCP协议是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。
TCP协议会通过 握手 建立连接,然后对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。
浏览器的HTTP协议、发送邮件的SMTP协议等这些更高级的协议都是建立在TCP协议基础上的。
一个TCP报文包含:
- 要传输的数据
- 源IP地址和目标IP地址
- 源端口和目标端口
端口 :
- 作用:一个TCP报文传输到计算机时,端口区分该报文传给哪个具体的进程。两个进程在两台计算机之间建立网络连接需要各自的IP地址和各自的端口号
- 一些公共端口:(小于1024的是Internet标准服务的端口,端口号大于1024的可任意使用)
- Web服务的标准端口:
80
- SMTP服务:
25
- FTP服务:
21
- MySQL端口:
3306
- Oracle端口:
1521
- Web服务的标准端口:
一个进程也可能同时和多个计算机建立链接,因此它会申请很多端口。
2. TCP编程
Socket是网络编程的一个抽象概念。通常用一个Socket表示"打开了一个网络连接",而打开一个Socket需要知道目标计算机的IP地址和端口号,再指定协议类型即可。
大多数链接都是可靠的TCP连接。创建TCP连接时,主动发起连接的叫客户端,被动响应连接的叫服务器。
1. 客户端
客户端要主动发起TCP连接,必须知道服务器的IP地址和端口号。
客户端与服务器基于TCP的socket交互步骤:
-
导入socket库:
import socket
-
创建一个socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
与某服务器建立连接(以连接百度服务器为例)。
connect(tuple)
s.connect(('www.baidu.com', 80)) # connect() 接收一个tuple,指定地址和端口号
-
发送数据。
send()
s.send(b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n')
-
接收数据。
recv(n)
n指定接收的最大字节数。由于接收的数据可能过大,一次性读取耗费内存,因此分段接收buffer = [] while True: d = s.recv(1024) if d: buffer.append(d) else: break data = b''.join(buffer)
-
接收数据代码解释:
- 创建保存数据的容器
buffer
while True
是为了一直按每次最多1024
字节接收数据,直到没有数据(d为空)后break
- 利用字符串
join()
方法,将buffer的内容赋给data
- 创建保存数据的容器
-
关闭连接
s.close()
-
处理接收到的数据(接收到的数据包含HTTP头和网页本身)
header, html = data.split(b'\r\n\r\n', 1) # 对data切割一次
-
打印header,注意编码格式
h = header.decode('utf-8') print(h)
-
将html写入文件
with open('baidu.html', 'wb') as f: f.write(html)
-
这样就在当前目录下生成一个baidu.html文件,通过浏览器就打开百度首页
示例:创建一个基于TCP连接的Socket
# 导入socket库
import socket
# 创建一个socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 与百度服务器建立连接(connect()的参数是一个`tuple`,包含地址和端口号)
s.connect(('www.baidu.com', 80))
# 发送数据
s.send(b'GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n')
# 接收数据
buffer = []
while True:
# 每次最多接收1k字节
d = s.recv(1024)
if d:
buffer.append(d)
else:
break
data = b''.join(buffer)
# 关闭连接
s.close()
# data数据包含HTTP头和网页本身,对data进行切割,打印header、保存html的信息到文件
header, html = data.split(b'\r\n\r\n', 1)
# 打印header信息
print(header.decode('utf-8'))
# 把接收的数据写入文件
with open('baidu.html', 'wb') as f:
f.write(html)
- 说明:
- 创建
Socket
时,AF_INET
指定IPV4协议,如果要用更先进的IPV6,就指定为AF_INET6
SOCK_STREAM
指定使用面向流的TCP协议,这样一个Socket
对象就创建成功,但还没建立连接- TCP连接创建的是双向通道,双方都可以同时给对方发送数据。但谁先发谁后发、怎么协调,要根据具体的协议来决定
- 比如:HTTP协议规定客户端必须先发送请求给服务器,服务器收到后才发送数据给客户端
- 发送的文本格式必须符合HTTP标准,如果格式没问题,接下来就可以接收百度服务器返回的数据
- 接收数据时,调用
recv(max)
方法,一次最多接收指定的字节数。因此在一个while
循环中反复接收,直到recv()
返回空数据,表示接收完毕,退出循环 - 接收完数据后,调用
close()
方法关闭Socket,这样一次完整的网络通信就结束了 - 收到的数据包括HTTP头和网页本身,对其进行切割,然后网页内容写入到文件,在浏览器中打开
baidu.html
文件即可看到百度首页
- 创建
2. 服务端
服务端编程要比客户端复杂一些。
服务端编程步骤:
- 服务端进程绑定一个固定端口(比如
80
)监听来自其他客户端的连接 - 客户端连接过来,服务器就与该客户端建立Socket连接,随后的通信就靠这个Socket连接
- 服务器会有大量来自客户端的连接,所以服务器要能够区分一个Socket连接是和哪个客户端绑定的
- 一个Socket依赖4项来唯一确定一个Socket:
- 服务器地址
- 服务器端口
- 客户端地址
- 客户端端口
- 一个Socket依赖4项来唯一确定一个Socket:
- 服务器还需要同时响应多个客户端的请求,所以每个连接都需要一个新的进程或者新的线程来处理。否则服务器一次就只能服务一个客户端了
步骤:
-
testServer.py
-
首先创建一个基于IPV4和TCP协议的Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
绑定监听的地址和端口。服务器可能有多块网卡,可绑定到某一块网卡的IP地址上;也可用
0.0.0.0
绑定到所有的网络地址;还可用127.0.0.1
绑定到本机地址,127.0.0.1
是一个特殊的IP地址,表示本机地址,如果绑定到这个地址,客户端必须同时在本机运行才能连接,外部计算机无法连接进来。端口号需预先指定。自定义的非标准服务,大于1024
的随便指定s.bind(('127.0.0.1', 9999)) # bind()接收一个tuple,包含地址和端口号
-
调用
listen()
方法开始监听端口,传入的参数指定等待连接的最大数量s.listen(5)
-
每个服务器的请求连接都必须创建新线程(或进程)来处理,否则单线程在处理连接的过程中,无法接受其他客户端的连接
def tcplink(sock, addr): sock.send(b'Welcome!') while True: data = sock.recv(1024) time.sleep(1) if not data or data.decode('utf-8') == 'exit': break sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8')) sock.close() print('Connection from %s:%s closed.' % addr)
-
服务器通过一个永久循环来接受来自客户端的连接,
accept()
会等待并返回一个客户端的连接while True: # 接受一个新连接 sock, addr = s.accept() # 创建新线程来处理TCP连接 t = threading.Thread(target=tcplink, args=(sock, addr)) t.start()
-
这样建立连接后,服务器首先发一条欢迎消息,然后等待客户端数据,并加上
Hello
在发送给客户端;若客户端发送了exit
字符串,就直接关闭连接 -
编写一个客户端程序用来测试
testClient.py
:s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 建立连接: s.connect(('127.0.0.1', 9999)) # 接收欢迎消息: print(s.recv(1024).decode('utf-8')) for data in [b'Michael', b'Tracy', b'Sarah']: # 发送数据: s.send(data) print(s.recv(1024).decode('utf-8')) s.send(b'exit') s.close()
-
然后打开两个命令行窗口,先启动服务端,再启动客户端,即可看到效果。
-
需要注意的是:客户端程序运行完毕就退出了,服务器程序会永远运行下去,必须按Ctrl+c退出程序
示例:编写一个简单的服务器程序,它接收客户端连接,把客户端发过来的字符串加上Hello
再发回去。代码如下:
import socket
import threading
import time
# 创建一个基于TIPV4和CP协议的Socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定监听的地址和端口
s.bind(('127.0.0.1', 9999))
# 调用listen()方法开始监听端口
s.listen(5)
print('Waiting for connection...')
# 连接创建新线程(或进程)来处理
def tcplink(sock, addr):
print('Accept new connection from %s:%s...' % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
time.sleep(1)
if not data or data.decode('utf-8') == 'exit':
break
sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
sock.close()
print('Connection from %s:%s closed.' % addr)
# 服务器通过一个永久循环来接受来自客户端的连接
while True:
# 接受一个新连接
sock, addr = s.accept()
# 创建新线程来处理TCP连接
t = threading.Thread(target=tcplink, args=(sock, addr))
t.start()
- 测试结果:
两个客户端测试结果:
小结
1. 用TCP协议进行Socket编程在Python中十分简单:
- 对于客户端,要主动连接服务器的IP和指定端口
- 对于服务器,首先监听指定端口,然后对每个新的连接,创建一个线程或进程来处理。通常,服务器程序会无限运行下去
2. 注意:同一个端口,被一个Socket绑定之后,就不能被别的Socket绑定了。
3. UDP编程
TCP是建立可靠连接,并且通信双方都可以以 流 的形式发送数据。相对TCP,UDP是面向无连接的协议。
使用UDP协议时,不需要建立连接,只要知道对方的IP地址和端口号,就可以直接发数据包,但能不能到达就不知道了。
服务器绑定UDP端口和TCP端口互不冲突,可指定相同的端口。
虽然UDP数据传输不可靠,但它的优点是和TCP比,速度快,对于不要求可靠到达的数据,就可以使用UDP协议。
使用UDP协议传输数据,其通信双方也分为客户端和服务器。
步骤:
-
testServer.py
-
创建
socket
对象,使用SOCK_DGRAM
来指定UDP协议s = socket.socket(socket.AF_INET, socket.DGRAM)
-
服务器首先需要绑定端口:
s.bind(('127.0.0.1', 9999))
-
UDP不需要
listen()
方法,直接接收来自任何客户端的数据while True: # 接收数据 recvfrom()方法返回数据和客户端的地址与端口 data, addr = s.recvfrom(1024) print('Received from %s:%s.' addr) # sendto()方法把数据用UDP发给客户端 s.sendto(b'Hello, %s!' % data, addr')
-
testClient.py
-
客户端使用UDP时,先创建基于UDP的
Socket
,然后不需要调用connect()
,直接通过sendto()
给服务器发数据s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for data in [b'Jason', b'Bob', b'Alice']: # 发送数据 sendto(data, (ip, port)) s.sendto(data, ('127.0.0.1', 9999)) # 接收数据 recv(max) print(s.recv(1024).decode('utf-8')) s.close()
代码如下:
- 服务端
testServer.py
import socket
# 创建连接对象,SOCK_DGRAM 指定该socket的类型是UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 首先绑定端口
s.bind(('127.0.0.1', 9999))
# 不需要调用listen()方法,直接接收来自任何客户端的数据
print('Bind UDP on 9999...')
while True:
# 接收数据,recvfrom()方法返回数据和客户端的地址和端口
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)
- 客户端
testClient.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'Jason', b'Bob', b'Alice']:
# 发送数据
s.sendto(data, ('127.0.0.1', 9999))
# 接收数据
print(s.recv(1024).decode('utf-8'))
s.close()
- 测试结果:
17. 电子邮件
电子邮件软件(比如Outlook
、Foxmail
等)被称为MUA:Mail User Agent——邮件用户代理。
电子邮件发送流程:我网易邮箱me.163.com
→ 朋友新浪邮箱friend@sina.com
- 我编辑邮件,使用MUA发出,到达网易的MTA:Mail Transfer Agent——邮件传输代理(Email服务提供商,如网易、新浪等)
- 网易的MTA把邮件发到新浪的服务商(即新浪的MTA)。该过程可能还会经过别的MTA,但无需关心,只关心速度
- 新浪的MTA把邮件投递到邮件的最终目的地 MDA:Mail Delivery Agent——邮件投递代理。Email到达MDA后就存放在新浪的某个服务器的某个文件或特殊的数据库里,这个长期保存邮件的地方称之为电子邮箱
- 朋友通过MUA从MDA上把邮件取到自己的电脑上
总结来说:发件人 → MUA → MTA → MTA → 若干个MTA → MDA ← MUA ← 收件人
编写程序来发送和接收邮件,本质上是:
- 编写MUA把邮件发到MTA
- 编写MUA从MDA上收邮件
发邮件时,MUA和MTA使用的协议就是SMTP:Simple Mail Transfer Protocol,后面的MTA到另一个MTA也是用SMTP协议。
收邮件时,MUA和MDA使用的协议有两种:POP:Post Office Protocol;IMAP:Internet Message Access Protocol。
邮件客户端软件在发邮件时,先配置SMTP
服务器,即要发到哪个MTA
上,假设使用的163邮箱
,就不能直接发到新浪的MTA
上,因为它只服务于新浪的用户。因此需要填163提供的SMTP服务器地址:smtp.163.com
,还需要填写你的邮箱地址和口令,这样163的MUA
才能正常把Email通过SMTP协议
发送到MTA
。收邮件时也类似。
1. SMTP发送邮件
1. 使用SMTP发送邮件
SMTP是发送邮件的协议,Python内置对SMTP的支持,可发送 纯文本邮件、HTML邮件以及带附件的邮件。
Python对SMTP支持有smtplib
和email
两个模块,email
负责构造邮件,smtplib
负责发送邮件。
发送邮件步骤:( 可忽略,看下边的详细邮件步骤 )
-
导入相应模块
from email.mime.text import MIMEText
-
构造一个最简单的纯文本邮件
MIMEText
对象:第一个参数是邮件正文- 第二个参数是MIME的subtype,传入
'plain'
表示纯文本 - 最终的MIME就是
'text/plain'
- 最后一定要用
utf-8
编码保证多语言兼容性
msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')
-
通过
SMTP
发出去set_debuglevel(1)
打印和SMTP服务器交互的所有信息SMTP
协议就是简单的文本命令和响应login()
方法用来登录SMTP服务器sendmail()
方法发邮件。因为可以一次发给多个人,所以传入一个list
,邮件正文是一个str
as_string()
把MIMRText
对象变成str
# 输入发件人Email地址和口令 from_addr = input('From: ') password = input('Password: ') # 输入收件人地址 to_addr = input('To: ') # 输入SMTP服务器地址 网易:smtp.163.com QQ:smtp.qq.com smtp_server = input('SMTP server: ') # 该SMTP是发件人邮箱的SMTP服务器地址 import smtplib # SMTP协议默认端口是25 server = smtplib.SMTP(smtp_server, 25) # 打印出和SMTP服务器交互的所有信息 server.set_debuglevel(1) # 登录SMTP服务器 server.login(from_addr, password) # 发邮件 sendmail() server.sendmail(from_addr, [to_addr], msg.as_string()) # 退出 server.quit()
代码如下:
from email.mime.text import MIMEText
# 构造一个最简单的纯文本邮件
msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')
# 通过SMTP发出去
# 输入Email地址和口令
# from_addr = input('From: ')
from_addr = '577155188@qq.com'
# password = input('Password: ')
password = 'zsaivahrhmipbccg'
# 输入收件人地址
# to_addr = input('To: ')
to_addr = '13436603789@163.com'
# 输入SMTP服务器地址
# smtp_server = input('SMTP server: ') # smtp.163.com 或 smtp.qq.com
smtp_server = 'smtp.qq.com'
import smtplib
# SMTP协议默认端口是25
server = smtplib.SMTP(smtp_server, 25)
# 打印出和SMTP服务器交互的所有信息
server.set_debuglevel(1)
# 登录SMTP服务器
server.login(from_addr, password)
# 发邮件 sendmail()
server.sendmail(from_addr, [to_addr], msg.as_string())
# 退出
server.quit()
执行输入相关信息后,会打印出详细的与服务器交互的信息,并且在收件人邮箱里会有该邮件:
仔细观察,发现如下问题:
- 邮件没有主题
- 收件人的名字没有显示为友好的名字,收件人是 (无)
- 明明收到了邮件,却提示不在收件人中
这是因为邮件主题、如何显示发件人、收件人等信息并不通过SMTP协议发给MTA,而是包含在发给MTA的文本中。因此,需要把From
、To
和subject
添加到MIMEText
中,才是一封完整的邮件:
发送完整邮件的步骤:
-
编写格式化一个邮件地址的函数。不能简单的传入
name <addr@example.com>
,因为若包含中文,需要通过Header
对象进行编码from email.header import Header from email.mime.text import MIMEText from email.utils import parseaddr, formataddr import smtplib def _format_addr(s): name, addr = parseaddr(s) return formataddr((Header(name, 'utf-8').encode(), addr))
-
获取输入的相关信息:发件人邮箱和口令、收件人邮箱地址、SMTP服务器地址
from_addr = input('From: ') password = input('Password: ') to_addr = input('To: ') smtp_server = input('SMTP server: ')
-
编写msg的相关信息,msg是一个email的class
msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8') msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr) # `msg['To']`接收的是字符串而不是list,如果有多个邮件地址,用`,`分隔即可 msg['To'] = _format_addr('管理员 <%s>' % to_addr) msg['Subject'] = Header('来自SMTP的问候...', 'utf-8').encode()
-
编写SMTP发送设置
server = smtplib.SMTP(smtp_server, 25) server.set_debuglevel(1) # 打印与服务器交互的信息可不写 server.login(from_addr, password) server.sendmail(from_addr, [to_addr], msg.as_string()) server.quit()
具体代码:
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib
# 编写格式化一个邮件地址的函数。不能简单的传入`name <addr@example.com>`,因为若包含中文,需要通过`Header`对象进行编码
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# 获取输入的相关信息:发件人邮箱和口令、收件人邮箱地址、SMTP服务器地址
from_addr = input('From: ')
password = input('Password: ')
to_addr = input('To: ')
smtp_server = input('SMTP server: ')
msg = MIMEText('Hello, send by Python...', 'plain', 'utf-8')
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候...', 'utf-8').encode()
server = smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
执行代码,输入相关信息,成功后会在163的邮箱里看到该邮件:
2. 发送HTML邮件
发送HTML邮件(不是普通的纯文本文件)的方法:在构造MIMEText
对象时,把HTML字符串传进去,再把第二个参数由plain
变为html
就可以了。
msg = MIMEText('<html><body><h1>Hello</h1>' +
'<p>send by <a href="http://www.python.org">Python</a>...</p>' +
'</body></html>', 'html', 'utf-8')
其他不变。再发送一遍邮件,你将看到以HTML显示的邮件:
- 点击
Python
的话,会转向Python的官网。
3. 发送附件
Email中加入附件:构造一个MIMEMultipart
对象代表邮件本身,然后往里面加一个MIMEText
作为邮件正文,再继续往里面加表示附件的MIMEBase
对象即可。
代码如下:
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import smtplib
# 编写格式化一个邮件地址的函数。不能简单的传入`name <addr@example.com>`,因为若包含中文,需要通过`Header`对象进行编码
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
# 获取输入的相关信息:发件人邮箱和口令、收件人邮箱地址、SMTP服务器地址
from_addr = input('From: ')
password = input('Password: ')
to_addr = input('To: ')
smtp_server = input('SMTP server: ')
# 邮件对象
msg = MIMEMultipart()
msg['From'] = _format_addr('Python爱好者 <%s>' % from_addr)
msg['To'] = _format_addr('管理员 <%s>' % to_addr)
msg['Subject'] = Header('来自SMTP的问候...', 'utf-8').encode()
# 邮件正文是MIMEText
msg.attach(MIMEText('send with file...', 'plain', 'utf-8'))
# 添加附件就是加上一个MIMEBase,从本地读取一个图片
with open('image.png', 'rb') as f:
# 设置附件的MIME和文件名,这里是png类型
mime = MIMEBase('image', 'png', filename='image.png')
# 加上必要的头信息
mime.add_header('Content-Disposition', 'attachment', filename='test.png')
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来
mime.set_payload(f.read())
# 用Base64编码
encoders.encode_base64(mime)
# 添加到MIMEMultipart
msg.attach(mime)
# 发送邮件
server = smtplib.SMTP(smtp_server, 25)
server.set_debuglevel(1)
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
然后,按正常发送流程把msg
(注意类型已变为MIMEMultipart
)发送出去,就可以收到如下带附件的邮件:
4. 发送图片
把一个图片嵌入到邮件正文中:按照发送附件的方式,先把邮件作为附件添加进去,然后在HTML中通过引用src="cid:0"
就可以把附件作为图片嵌入。如果有多个图片,就给它们依次编号,引用不同的cid:x
即可。
- 直接在HTML邮件中链接图片是不可以的,因为大部分邮件服务商都会自动屏蔽带有外链的图片,他们不知道这些链接是否指向恶意网站。
发送附件的代码中,MIMEMultipart
的MIMEText
从plain
改为html
,然后修改msg.attach()
中的第一个参数为HTML格式即可。需要修改的代码如下:
msg.attach(MIMEText('<html><body><h1>Hello</h1>' +
'<p><img src="cid:0"></p>' +
'</body></html>', 'html', 'utf-8'))
然后重新执行后可收到邮件:
5. 同时支持HTML和Plain格式
发送HTML邮件,收件人通过浏览器或Outlook之类的软件可正常浏览邮件内容,但收件人使用的设备太古老,查看不了HTML邮件,处理方法:在发送HTML的同时再附件一个纯文本,如果收件人无法查看HTML格式的邮件,就可以自动降级查看纯文本邮件。
使用MIMEMultipart
可组合一个HTML和Plain,注意指定subtype
是alternative
。
修改的代码如下:
# 邮件对象指定为 alternative
msg = MIMEMultipart('alternative')
# 编写两个邮件正文对象,这样保证无法查看HTML格式邮件的古老设备能收到纯文本的邮件内容
msg.attach(MIMEText('Hello,I am Jason', 'plain', 'utf-8'))
msg.attach(MIMEText('<html><body><h1>Hello</h1></body></html>', 'html', 'utf-8'))
因为现在的设备基本都支持HTML格式的邮件,所以收到的是第二个邮件对象的内容,第一个则过滤掉了:
6. 加密SMTP
使用标准的25
端口连接SMTP
服务器时,使用的是明文传输,发送邮件的整个过程可能会被窃听。要更安全的发送邮件,方法是:加密SMTP对话:先创建SSL安全连接,然后再使用SMTP协议发送邮件。
举例:邮件服务商Gmail,提供的SMTP服务必须要加密传输,方法为:在创建SMTP对象后,立刻调用starttls()
方法,就创建了安全连接。
smtp_server = 'smtp.gmail.com'
smtp_port = 587 # Gmail的SMTP端口是587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls() # 加密SMTP
- 除了以上代码,其他都一样
小结
- 使用Python的
smtplib
发送邮件十分简单,只要掌握了各种邮件类型的构造方法,正确设置好邮件头,就可以顺利发出 - 构造一个邮件对象就是一个
Message
对象 - 构造一个
MIMEText
对象就表示一个文本邮件对象 - 构造一个
MIMEImage
对象就表示一个作为附件的图片 - 要把多个对象组合起来,就用
MIMEMultipart
对象 MIMEBase
可表示任何对象- 以上的继承关系如下:(这种嵌套关系可构造出任意复杂的邮件)
Message
← MIMEBase
← MIMEMultipart
← MIMENonMultipart
← MIMEMessage
← MIMEText
← MIMEImage
2. POP3收取邮件
收取邮件:编写一个MUA作为客户端,从MDA把邮件获取到用户的电脑或手机上。
收取邮件最常用的协议是POP协议,目前版本是3,俗称POP3。
收取邮件步骤:
- 使用Python内置的
poplib
模块把邮件的原始文本下载到本地poplib
模块,实现了POP3协议,可直接用来收邮件
- 使用
email
解析原始文本,还原为邮件对象email
模块提供的各种类解析原始文本,把邮件变成可阅读的邮件对象
1. 通过POP3下载邮件
步骤说明:
-
导入包
import poplib from email.parser import Parser
-
获取邮件地址、口令、pop3服务器地址(pop3地址如:
pop3.qq.com
)email = input('Email:') password = input('Password:') pop3_server = input('POP3 server:')
-
连接到pop3服务器
server = poplib.POP3(pop3_server)
-
pop3欢迎语,可打印可不打印
welcome_msg = server.getwelcome().decode('utf-8')
-
身份认证
server.user(email) server.pass_(password)
-
邮件数量
count
和占用空间size
,都在server.stat()
中count, size = server.stat()
-
server.list()
包含三部分:邮件数量与占用空间resp
、邮件列表mails
、octets
暂不用管resp, mails, octets = server.list()
-
邮件数量:
len(server.list()[1]
或len(mails)
count = len(server.list()[1])
-
获取某封邮件的内容
server.retr(index)
,index
从1
开始。要获取所有邮件,只需要循环使用retr()
把每一封邮件内容拿到即可resp, lines, octets = server.retr(index)
-
lines
存储邮件的原始文本的每一行,可获得整个邮件的原始文本msg_content = b'\r\n'.join(lines).decode('utf-8')
-
解析出邮件
msg = Parser().parsestr(msg_content)
-
根据邮件索引号删除邮件
server.dele(index)
server.dele(20) # 把从最久的邮件开始算,序号为20的邮件被删除
-
关闭连接
server.quit()
代码如下:
import poplib
from email.parser import Parser
# 输入邮件地址、口令和POP3服务器地址
email = input('Email:')
password = input('Password:')
# 举例:pop3.qq.com、pop3.sina.com、pop3.163.com
pop3_server = input('POP3 server:')
# 连接到POP3服务器
server = poplib.POP3(pop3_server)
# 可以打开或关闭调试信息
# server.set_debuglevel(1)
# 可选:打印POP3服务器的欢迎文字
print(server.getwelcome().decode('utf-8'))
# 身份认证
server.user(email)
server.pass_(password)
# stat() 返回邮件数量和占用空间
print('Message: %s. Size: %s' % server.stat())
# list() 返回所有邮件的编号
resp, mails, octets = server.list()
# resp:总邮件数和总容量,类似于 server.stat()
# mails:列表形式返回所有邮件信息 [b'1 82923', b'2 2184', ...]
# octets:不用关心
print('Total:\nresp: {}\nmails: {}\noctets: {}'.format(resp, mails, octets))
# 获取最新一封邮件,注意索引号从1开始
index = len(mails)
resp2, lines, octets2 = server.retr(index)
# lines:邮件信息;resp2和octets2:不用关心
print('Last:\nresp: {}\nlines: {}\noctets: {}'.format(resp2, lines, octets2))
# lines存储了邮件的原始文本的每一行,可获得整个邮件的原始文本
msg_content = b'\r\n'.join(lines).decode('utf-8')
# 稍后解析出邮件
msg = Parser().parsestr(msg_content)
# 可以根据邮件索引号直接从服务器删除邮件
server.dele(22) # 执行后就删掉了该index的邮件,邮箱可看
# 关闭连接
server.quit()
# 欢迎语 print(server.getwelcome().decode('utf-8')) 打印:
'''
+OK Welcome to coremail Mail Pop3 Server (163coms[10774b260cc7a37d26d71b52404dcf5cs])
'''
# print('Message: %s. Size: %s' % server.stat()) 打印:
'''
Message: 25. Size: 9006000
'''
# print('Total:\nresp: {}\nmails: {}\noctets: {}'.format(resp, mails, octets)) 打印:
'''
Total:
resp: b'+OK 22 8945506'
mails: [b'1 2330', b'2 2350', b'3 2360', b'4 2421', b'5 1726532', b'6 1726617', b'7 2712', b'8 2747', b'9 2823', b'10 1726567', b'11 1727191', b'12 1727240', b'13 29500', b'14 29488', b'15 29506', b'16 29506', b'17 29313', b'18 28949', b'19 29313', b'20 29192', b'21 29317', b'22 29532']
octets: 214
'''
# print('Last:\nresp: {}\nlines: {}\noctets: {}'.format(resp2, lines, octets2)) 打印:
'''
Last:
resp: b'+OK 29532 octets'
lines: [b'Received: from out180-163-24-185.mail.qq.com (unknown [180.163.24.185])', b'\tby mx19 (Coremail) with SMTP id RcCowADX6aCvc7hgl0v9FA--.38935S3;', b'\tThu, 03 Jun 2021 14:16:15 +0800 (CST)', b'DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=qq.com; s=s201512;', 。。。 dki', b'\tm=pass header.i=@qq.com', b'X-Coremail-Antispam: 1Uf129KBjDUn29KB7ZKAUJUUUUU529EdanIXcx71UUUUU7v73', 。。。', b'', b'--===============3847937056631975014==--']
octets: 29532
'''
2. 解析邮件
解析邮件的过程和上一节构造邮件正好相反,过程如下:
- 导入必要模块:
from email.parser import Parser
from email.header import decode_header
from email.utils import parseaddr
import smtplib
- 将邮件内容解析为
Message
对象:
msg = Parser().parsestr(msg_content)
- 这个
Message
对象本身可能是一个MIMEMultipart
对象,即包含嵌套的其他MIMEBase
对象,嵌套可能还不止一层,因此要递归的打印出Message
对象的层次结构:
def print_info(msg, indent=0):
# indent 表示缩进显示
if indent == 0:
for header in ['From', 'To', 'Subject']:
value = msg.get(header, '')
if value:
if header == 'Subject':
value = decode_str(value)
else:
hdr, addr = parseaddr(value)
name = decode_str(hdr)
value = u'%s <%s>' % (name, addr)
print('%s%s: %s' % (' ' * indent, header, value))
if (msg.is_multipart()):
parts = msg.get_payload()
for n, part in enumerate(parts):
print('%spart %s' % (' ' * indent, n))
print('%s--------------------' % (' ' * indent))
print_info(part, indent + 1)
else:
content_type = msg.get_content_type()
if content_type == 'text/plain' or content_type == 'text/html':
content = msg.get_payload(decode=True)
charset = guess_charset(msg)
if charset:
content = content.decode(charset)
print('%sText: %s' % (' ' * indent, content + '...'))
else:
print('%sAttachment: %s' % (' ' * indent, content_type))
- 邮件的Subject或者Email中包含的名字都是经过编码后的str,要正常显示,就必须decode:
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
-
decode_header()
返回一个list,因为像Cc
、Bcc
这样的字段可能包含多个邮件地址,所以解析出来的会有多个元素。上面的代码只取了第一个元素。 -
文本邮件的内容也是str,还需要检测编码,否则,非utf-8编码的邮件都无法正常显示:
def guess_charset(msg):
charset = msg.get_charset()
if charset is None:
content_type = msg.get('Content-Type', '').lower()
pos = content_type.find('charset=')
if pos >= 0:
charset = content_type[pos + 8:].strip()
return charset
- 此时可以试试收取一封邮件。先往自己邮箱发一封邮件,然后用浏览器登录邮箱,看看邮件收到没,如果收到了,就用Python程序把它收到本地:
运行程序,结果如下:
+OK Welcome to coremail Mail Pop3 Server (163coms[...])
Messages: 126. Size: 27228317
From: Test <xxxxxx@qq.com>
To: Python爱好者 <xxxxxx@163.com>
Subject: 用POP3收取邮件
part 0
--------------------
part 0
--------------------
Text: Python可以使用POP3收取邮件……...
part 1
--------------------
Text: Python可以<a href="...">使用POP3</a>收取邮件……...
part 1
--------------------
Attachment: application/octet-stream
我们从打印的结构可以看出,这封邮件是一个MIMEMultipart
,它包含两部分:第一部分又是一个MIMEMultipart
,第二部分是一个附件。而内嵌的MIMEMultipart
是一个alternative
类型,它包含一个纯文本格式的MIMEText
和一个HTML
格式的MIMEText
。
小结
用Python的poplib
模块收取邮件分两步:第一步是用POP3协议
把邮件获取到本地,第二步是用email模块
把原始邮件解析为Message对象
,然后,用适当的形式把邮件内容展示给用户即可。