第3章 因特网客户端编程

覆水难收。同理,上传到网上的信息也是无法彻底删除的。

——Joe Garrelli,1996年3月

本章内容:

因特网客户端简介;

文件传输;

网络新闻;

电子邮件;

相关模块。

第2章介绍了使用套接字的底层网络通信协议。这种类型的网络是当今因特网中大部分客户端/服务器协议的核心。这些网络协议分别用于文件传输(FTP、SCP 等)、阅读 Usenet新闻组(NNTP)、发送电子邮件(SMTP)、从服务器上下载电子邮件(POP3、IMAP)等。协议的工作方式与第 2 章介绍的客户端/服务器的例子相似。唯一区别在于现在使用 TCP/IP这样底层的协议创建了新的、有专门用途的协议,以此来实现刚刚介绍的高层服务。

3.1 因特网客户端简介

在介绍研究这些协议之前,先要弄清楚“因特网客户端到底是什么”。为了回答这个问题,这里将因特网理解为用来传输数据的地方,数据在服务提供者和服务使用者之间传输。在某些情况下称为“生产者-消费者”(虽然这个概念一般用于描述操作系统方面的内容)。服务 器就是生产者,提供服务,而客户端使用服务。对特定的服务,一般只有一个服务器(即进程或主机等),但有多个消费者(就像之前看的客户端/服务器模型那样)。虽然现在不再使用底层的套接字创建因特网客户端,但模型是完全相同的。

本章将介绍多个因特网协议,并创建相应的客户端程序。通过这些程序会发现这些协议的API非常相似。这些相似性在设计之初就考虑到了,因为保持接口的一致性有很大的好处。更重要的是,还学会了如何为这些协议创建真正的客户端程序。虽然本章只会详细介绍其中三个协议,但在学习完本章后,读者会有足够的信心和能力写出任何因特网协议的客户端程序。

3.2 文件传输

3.2.1 文件传输因特网协议

因特网中最常见的事情就是传输文件。文件传输每时每刻都在发生。有很多协议可以用于在因特网上传输文件。最流行的包括文件传输协议(FTP)、UNIX 到 UNIX 复制协议(UUCP)、用于Web的超文本传输协议(HTTP)。另外,还有(UNIX下的)远程文件复制命令rcp(以及更安全、更灵活的scp和rsync)。

在当下,HTTP、FTP、scp/rsync的应用仍然非常广泛。HTTP主要用于基于Web的文件下载以及访问 Web 服务,一般客户端无须登录就可以访问服务器上的文件和服务。大部分HTTP文件传输请求都用于获取网页(即将网页文件下载到本地)。

而scp和rsync需要用户登录到服务器主机。在传输文件之前必须验证客户端的身份,否则不能上传或下载文件。FTP与scp/rsync相同,它也可以上传或下载文件,并采用了UNIX的多用户概念,用户需要输入有效的用户名和密码。但FTP也允许匿名登录。现在来深入了解FTP。

3.2.2 文件传输协议

文件传输协议(File Transfer Protocol,FTP)由已故的Jon Postel和Joyce Reynolds开发,记录在RFC(Request for Comment)959号文档中,于1985年10月发布。FTP主要用于匿名下载公共文件,也可以用于在两台计算机之间传输文件,特别是在使用Windows进行工作,而文件存储系统使用UNIX的情况下。早在Web流行之前,FTP就是在因特网上进行文件传输以及下载软件和源代码的主要手段之一。

前面提到过,FTP要求输入用户名和密码才能访问远程FTP服务器,但也允许没有账号的用户匿名登录。不过管理员要先设置FTP服务器以允许匿名用户登录。这时,匿名用户的用户名是“anonymous”,密码一般是用户的电子邮件地址。与向特定的登录用户传输文件不同,这相当于公开某些目录让大家访问。但与登录用户相比,匿名用户只能使用有限的几个FTP命令。

图3-1展示了这个协议,其工作流程如下。

1.客户端连接远程主机上的FTP服务器。

2.客户端输入用户名和密码(或“anonymous”和电子邮件地址)。

3.客户端进行各种文件传输和信息查询操作。

4.客户端从远程FTP 服务器退出,结束传输。

当然,这只是一般情况下的流程。有时,由于网络两边计算机的崩溃或网络的问题,会导致整个传输在完成之前就中断。如果客户端超过15分钟(900秒)还没有响应,FTP连接就会超时并中断。

在底层,FTP只使用TCP(见第2章),而不使用UDP。另外,可以将FTP看作客户端/服务器编程中的特殊情况。因为这里的客户端和服务器都使用两个套接字来通信:一个是控制和命令端口(21号端口),另一个是数据端口(有时是20号端口),如图3-1所示。

第3章 因特网客户端编程 - 图1 图3-1 因特网上的FTP客户端和服务器。客户端与服务器在命令与控制端口通过FTP协议通信,而数据通过数据端口传输

前面说“有时”是因为FTP有两种模式:主动和被动。只有在主动模式下服务器才使用数据端口。在服务器把20号端口设置为数据端口后,它“主动”连接客户端的数据端口。而在被动模式下,服务器只是告诉客户端随机的数据端口号,客户端必须主动建立数据连接。在这种模式下,FTP服务器在建立数据连接时是“被动”的。最后,现在已经有了一种扩展的被动模式来支持第6版本的因特网协议(IPv6)地址——详见RFC 2428。

Python 已经支持了包括 FTP 在内的大多数据因特网协议。可以在 http://docs.python.org/lib/internet.html中找到支持各个协议的客户端模块。现在看看用Python 创建因特网客户端程序有多么容易。

3.2.3 Python和FTP

那么如何用 Python 编写 FTP 客户端程序呢?其实之前已经提到过一些了,现在还要添加相应的Python模块导入和调用操作。再回顾一下流程。

1.连接到服务器。

2.登录。

3.发出服务请求(希望能得到响应)。

4.退出。

在使用Python 的FTP支持时,所需要做的只是导入ftplib模块,并实例化一个ftplib.FTP类对象。所有的FTP操作(如登录、传输文件和注销等)都要使用这个对象完成。

下面是一段Pytho伪代码。

from ftplib import FTP

f = FTP('some.ftp.server')

f.login('anonymous', 'your@email.address')

:

f.quit()

在看真实的例子之前,先熟悉一下代码中会用到的ftplib.FTP 类的方法。

3.2.4 ftplib.FTP类的方法

表3-1列出了最常用的方法,这个表并不全面(要了解所有的方法,请参阅模块源代码),但这里列出的方法涵盖了Python中进行FTP客户端编程所需的API。也就是说,其他方法不是必需的,因为其他方法要么提供辅助或管理功能,要么提供这些API使用。

表3-1 FTP对象的方法 第3章 因特网客户端编程 - 图2

(续表) 第3章 因特网客户端编程 - 图3

在一般的FTP事务中,要使用到的指令有login()、cwd()、dir()、pwd()、stor()、retr()和quit()。表3-1中没有列出的一些FTP对象方法也很有用。关于FTP对象的更多信息,请参阅http://docs.python.org/library/ftplib#ftp-objects中的Python文档。

3.2.5 交互式FTP示例

在 Python 中使用 FTP 非常简单,甚至都不用写脚本,直接在交互式解释器中就能实时地看到操作步骤和输出。下面这个示例会话是在几年前python.org还支持FTP服务器的时候做的。现在这个示例已经无法工作,只是用来演示与正在运行的FTP服务器进行交互的情形。

>>> from ftplib import FTP

>>> f = FTP('ftp.python.org')

>>> f.login('anonymous', 'guido@python.org')

'230 Guest login ok, access restrictions apply.'

>>> f.dir()

total 38

drwxrwxr-x 10 1075   4127    512 May 17 2000 .

drwxrwxr-x 10 1075   4127    512 May 17 2000 ..

drwxr-xr-x 3  root   wheel    512 May 19 1998 bin

drwxr-sr-x 3  root   1400    512 Jun 9 1997 dev

drwxr-xr-x 3  root   wheel    512 May 19 1998 etc

lrwxrwxrwx 1  root   bin      7 Jun 29 1999 lib -> usr/lib

-r—r—r— 1  guido  4127     52 Mar 24 2000 motd

drwxrwsr-x 8  1122   4127    512 May 17 2000 pub

drwxr-xr-x 5  root   wheel    512 May 19 1998 usr

>>> f.retrlines('RETR motd')

Sun Microsystems Inc.  SunOS 5.6    Generic August 1997

'226 Transfer complete.

>>> f.quit()

'221 Goodbye.'

3.2.6 客户端FTP程序示例

前面提到过,如果直接在交互环境中使用FTP就无须编写脚本。但下面还是编写一段脚本,用来从Mozilla的网站下载最新的Bugzilla代码。示例3-1就用来完成这个工作。虽然这里在尝试编写一个应用程序,但读者也可以交互式地运行这段代码。这个程序使用FTP库下载文件,其中也包含一些错误检查。

示例3-1 FTP下载示例(getLatestFTP.py)

这个程序用于下载网站中最新版本的文件。读者可以修改这个程序,用来下载其他内容。

第3章 因特网客户端编程 - 图4

不过脚本并不会自动运行,需要手动运行才会下载代码。如果使用的是类UNIX系统,可以设定一个cron作业来自动下载。另一个问题是,如果需要下载的文件的文件名或目录名被修改了,程序就无法正常工作。

如果运行脚本时没有出错,则会得到如下输出。

$ getLatestFTP.py

* Connected to host "ftp.mozilla.org"

* Logged in as "anonymous"

* Changed to "pub/mozilla.org/webtools" folder

* Downloaded "bugzilla-LATEST.tar.gz" to CWD

$

逐行解释

第1~9行

代码前几行导入要用的模块(主要用于抓取异常对象),并设置一些常量。

第11~44行

main()函数分为以下几步:创建一个FTP对象,尝试连接到FTP服务器(第12~17行),然后返回。如果发生任何错误就退出。接着尝试用“anonymous”登录,如果不行就结束(第19~25行)。下一步就是转到发布目录(第27~33行),最后下载文件(第35~44行)。

在第14行和本书中其他的异常处理程序中,需要保存异常实例e。对于Python 2.5或更老的版本,需要将as改为逗号,因为这里使用的是从Python 2.6引入的新语法。Python 3只会理解如第14行所示的新语法。

在第35~36行,向retrbinary()传递了一个回调函数,每接收到一块二进制数据的时候都会调用这个回调函数。这个函数就是创建文件的本地版本时需要用到的文件对象的write()方法。传输结束时,Python解释器会自动关闭这个文件对象,因此不会丢失数据。虽然很方便,但最好还是不要这样做,作为一个程序员,要尽量做到在资源不再被使用的时候就立即释放,而不是依赖其他代码来完成释放操作。这里应该把开放的文件对象保存到一个变量(如变量loc),然后把loc.write传给ftp.retrbinary()。

完成传输后,调用loc.close()。如果由于某些原因无法保存文件,则移除空的文件来避免弄乱文件系统(第40行)。在os.unlink(FILE)两侧添加一些错误检查代码,以应对文件不存在的情况。最后,为了避免另外两行(第43~44行)关闭FTP连接并返回,使用了else语句(第35~42行)。

第46~47行

这是运行独立脚本的惯用方法。

3.2.7 FTP的其他内容

Python同时支持主动和被动模式。注意,在Python 2.0及以前版本中,被动模式默认是关闭的;在Python 2.1及以后版本中,默认是打开的。

以下是一些典型的FTP客户端类型。

命令行客户端程序:使用一些FTP客户端程序(如/bin/ftp或NcFTP)进行FTP传输,用户可以在命令行中交互式执行FTP传输。

GUI客户端程序:与命令行客户端程序相似,但它是一个GUI程序,如WS_FTP、Filezilla、CuteFTP、Fetch、SmartFTP。

Web浏览器:除了使用HTTP之外,大多数Web浏览器(也称为客户端)可以进行FTP传输。URL/URI的第一部分就用来表示所使用的协议,如“http://blahblah”。这就告诉浏览器要使用HTTP作为与指定网站传输数据的协议。通过修改协议部分,就可以发送使用FTP的请求,如“ftp://blahblah”,这与使用HTTP的网页URL很像(当然,“ftp://”后面的“blahblah”可以展开为“host/path?attributes”)。如果要登录,用户可以把登录信息(以明文方式)放在URL里,如:“ftp://user:passwd@host /path?attr1=val1&attr2=val2…”。

自定义应用程序:自己编写的用于FTP文件传输的程序。这些是用于特殊目的的应用程序,一般这种程序不允许用户与服务器交互。

这4种客户端类型都可以用Python编写。前面用ftplib来创建了一个自定义应用,但读者也可以创建一个交互式的命令行应用程序。在命令行的基础上,还可以使用一些GUI工具包,如Tk、wxWidgets、GTK+、Qt、MFC,甚至Swing(要导入相应的Python或Jython的接口模块)来创建一个完整的GUI程序。最后,可以使用Python的urllib模块来解析FTP的URL并进行FTP传输。在urllib的内部也导入并使用了ftplib,因此urllib也是ftplib的客户端。

FTP不仅可以用于下载应用程序,还可以用于在不同系统之间传输文件。比如,如果读者是一个工程师或系统管理员,需要传输文件。在跨网络的时候,显然可以使用scp或rsync命令,或者把文件放到一个能从外部访问的服务器上。不过,在一个安全网络的内部机器之间移动大量的日志或数据库文件时,这种方法的开销就太大了,因为需要考虑安全性、加密、压缩、解压缩等因素。如果只是想写一个FTP程序来在下班后自动移动文件,那么使用Python是一个非常好的主意。

在 FTP 协议定义/规范(RFC 959)中,可以得到关于 FTP 的更多信息,地址为http://tools.ietf.org/html/rfc959和www.network sorcery.com/enp/protocol/ftp.htm。其他相关的RFC包括2228、2389、2428、2577、2640、4217。关于Python的更多FTP支持,可以访问这个页面:http://docs.python.org/library/ftplib。

3.3 网络新闻

3.3.1 Usenet与新闻组

Usenet 新闻系统是一个全球存档的“电子公告板”。各种主题的新闻组一应俱全,从诗歌到政治,从自然语言学到计算机语言,从软件到硬件,从种植到烹饪、招聘/应聘、音乐、魔术、相亲等。新闻组可以面向全球,也可以只面向某个特定区域。

整个系统是一个由大量计算机组成的庞大的全球网络,计算机之间共享 Usenet 上的帖子。如果某个用户发了一个帖子到本地的 Usenet 计算机上,这个帖子会被传播到其他相连的计算机上,再由这些计算机传到与它们相连的计算机上,直到这个帖子传播到了全世界,每个人都收到这个帖子为止。帖子在Usenet上的存活时间是有限的,这个时间可以由Usenet系统管理员来指定,也可以为帖子指定一个过期的日期/时间。

每个系统都有一个已“订阅”的新闻组列表,系统只接收感兴趣的新闻组里的帖子,而不是接收服务器上所有新闻组的帖子。Usenet新闻组的内容由提供者安排,很多服务都是公开的。但也有一些服务只允许特定用户使用,例如付费用户、特定大学的学生等。Usenet系统管理员可能会进行一些设置来要求用户输入用户名和密码,管理员也可以设置是否只能上传或只能下载。

Usenet正在逐渐退出人们的视线,主要被在线论坛替代。但依然值得在这里提及,特别是它的网络协议。

老的Usenet使用UUCP作为其网络传输机制,在20世纪80年代中期出现了另一个网络协议TCP/IP,之后大部分网络流量转向使用TCP/IP。下一节将介绍这个新的协议。

3.3.2 网络新闻传输协议

用户使用网络新闻传输协议(NNTP)在新闻组中下载或发表帖子。该协议由Brain Kantor (加州大学圣地亚哥分校)和Phil Lapsley(加州大学伯克利分校)创建并记录在RFC 977中,于1986年2月公布。其后在2000年10月公布的RFC 2980中对该协议进行了更新。

作为客户端/服务器架构的另一个例子,NNTP 与 FTP 的操作方式相似,但更简单。在FTP中,登录、传输数据和控制需要使用不同的端口,而NNTP只使用一个标准端口119来通信。用户向服务器发送一个请求,服务器就做出相应的响应,如图3-2所示。

第3章 因特网客户端编程 - 图5 图3-2 因特网上的NNTP客户端和服务器。客户端主要阅读新闻,有时也发帖子。文章会在服务器之间做同步

3.3.3 Python和NNTP

由于之前已经有了Python和FTP的经验,读者也许可以猜到,有一个nntplib库和一个需要实例化的nntplib.NNTP类。与FTP一样,所要做的就是导入这个Python模块,然后调用相应的方法。先大致看一下这个协议。

1.连接到服务器。

2.登录(根据需要)。

3.发出服务请求。

4.退出。

是不是有点熟悉?是的,这与FTP协议极其相似。唯一的区别是根据NNTP服务器配置的不同,登录这一步是可选的。

下面是一段Python伪代码。

from nntplib import NNTP

n = NNTP('your.nntp.server')

r,c,f,l,g = n.group('comp.lang.python')

n.quit()

一般来说,登录后需要调用group()方法来选择一个感兴趣的新闻组。该方法返回服务器的回复、文章的数量、第一篇和最后一篇文章的ID、新闻组的名称。有了这些信息后,就可以做一些其他操作,如从头到尾浏览文章、下载整个帖子(文章的标题和内容),或者发表一篇文章等。

在看真实的例子之前,先介绍一下nntplib.NNTP类的一些常用方法。

3.3.4 nntplib.NNTP类方法

与前一节列出ftplib.FTP 类的方法时一样,这里不会列出nntplib.NNTP的所有方法,只列出创建NNTP客户端程序时可能用得到的方法。

与表3-1所示的FTP对象一样,表3-2中没有提到其他NNTP对象的方法。为了避免混乱,这里只列出了可能用得到的。其余内容建议参考Python库手册。

表3-2 NNTP对象的方法 第3章 因特网客户端编程 - 图6

(续表) 第3章 因特网客户端编程 - 图7

3.3.5 交互式NNTP示例

这里是一个使用Python的NNTP库的交互式示例。它看上去与交互式的FTP示例差不多(出于隐私的原因,修改了其中的电子邮件地址)。

在调用表3-2中所列的group()方法来连接到一个组的时候,会得到一个长度为5的元组。

>>> from nntplib import NNTP

>>> n = NNTP('your.nntp.server')

>>> rsp, ct, fst, lst, grp = n.group('comp.lang.python')

>>> rsp, anum, mid, data = n.article('110457')

>>> for eachLine in data:

…   print eachLine

From: "Alex Martelli" <alex@…>

Subject: Re: Rounding Question

Date: Wed, 21 Feb 2001 17:05:36 +0100

"Remco Gerlich" <remco@…> wrote:

> Jacob Kaplan-Moss <jacob@…> wrote in comp.lang.python:

>> So I've got a number between 40 and 130 that I want to round up to

>> the nearest 10.That is:

>>

>>   40 —> 40, 41 —> 50, …, 49 —> 50, 50 —> 50, 51 —> 60

> Rounding like this is the same as adding 5 to the number and then

> rounding down.Rounding down is substracting the remainder if you were

> to divide by 10, for which we use the % operator in Python.

This will work if you use +9 in each case rather than +5 (note that he

doesn't really want rounding — he wants 41 to 'round' to 50, for ex).

Alex

>>> n.quit()

'205 closing connection - goodbye!'

>>>

3.3.6 客户端程序NNTP示例

示例3-2中的NNTP客户端示例中会尝试更复杂的内容。在之前的FTP客户端示例中下载的是最新的内容。与之类似,这里也下载 Python 语言新闻组 com.lang.python 里最新的一篇文章。

下载完成后,会显示文章的前20行,而且是前20行有意义的内容。有意义的内容是指那些非引用的文本(引用以“>”或“|”开头),也不是像这样的文本“In article <…>, soAndSo@some.domain wrote:”。

最后要智能处理空行。在文章中出现一个空行时,我们就显示一个空行,但如果有多个连续的空行,则只显示一个空行。只有含有真实数据的行才算在“前20行”之中。所以,最多可能显示39行输出,20行实际数据与19个空行交叉显示。

示例3-2 NNTP下载示例 (getFirstNNTP.py)

这个脚本下载并显示Python新闻组comp.lang.python中最新一篇文章的前20 个“有意义的”行。

第3章 因特网客户端编程 - 图8

第3章 因特网客户端编程 - 图9

如果脚本运行正常,可能会看到这样的输出。

$ getLatestNNTP.py

* Connected to host "your.nntp.server"

* Found newsgroup "comp.lang.python"

* Found last article (#471526):

From: "Gerard Flanagan" <grflanagan@…>

Subject: Re: Generate a sequence of random numbers that sum up to 1?

Date: Sat Apr 22 10:48:20 CEST 2006

* First (<= 20) meaningful lines:

def partition(N=5):

vals = sorted( random.random() for _ in range(2*N) )

vals = [0] + vals + [1]

for j in range(2*N+1):

yield vals[j:j+2]

deltas = [ x[1]-x[0] for x in partition() ]

print deltas

print sum(deltas)

[0.10271966686994982, 0.13826576491042208, 0.064146913555132801,

0.11906452454467387, 0.10501198456091299, 0.011732423830768779,

0.11785369256442912, 0.065927165520102249, 0.098351305878176198,

0.077786747076205365, 0.099139810689226726]

1.0

$

这个输出显示了新闻组帖子的原始内容,如下所示。

From: "Gerard Flanagan" <grflanagan@…>

Subject: Re: Generate a sequence of random numbers that sum up to 1?

Date: Sat Apr 22 10:48:20 CEST 2006

Groups: comp.lang.python

Gerard Flanagan wrote:

> Anthony Liu wrote:

> > I am at my wit's end.

> > I want to generate a certain number of random numbers.

> > This is easy, I can repeatedly do uniform(0, 1) for

> > example.

> > But, I want the random numbers just generated sum up

> > to 1 .

> > I am not sure how to do this.Any idea? Thanks.

> ———————————————————————————————

> import random

> def partition(start=0,stop=1,eps=5):

>   d = stop - start

>   vals = [ start + d random.random() for _ in range(2eps) ]

>   vals = [start] + vals + [stop]

>   vals.sort()

>   return vals

> P = partition()

> intervals = [ P[i:i+2] for i in range(len(P)-1) ]

> deltas = [ x[1] - x[0] for x in intervals ]

> print deltas

> print sum(deltas)

> ———————————————————————————————-

def partition(N=5):

vals = sorted( random.random() for _ in range(2*N) )

vals = [0] + vals + [1]

for j in range(2*N+1):

yield vals[j:j+2]

deltas = [ x[1]-x[0] for x in partition() ]

print deltas

print sum(deltas)

[0.10271966686994982, 0.13826576491042208, 0.064146913555132801,

0.11906452454467387, 0.10501198456091299, 0.011732423830768779,

0.11785369256442912, 0.065927165520102249, 0.098351305878176198,

0.077786747076205365, 0.099139810689226726]

1.0

当然,由于新文章会不断出现,因此输出内容始终会发生变化。只要服务器里一有文章更新,输出内容就会发生变化。

逐行解释

第1~9行

程序首先包含一些import语句并定义一些常量,与FTP客户端示例相似。

第11~40行

在第一部分,尝试连接到NNTP主机服务器,如果失败就退出(第13~4行)。第15行故意注释掉了,如果需要输入用户名和密码进行身份验证,可以启用这一行并修改第14行。接着尝试读取指定的新闻组。同样,如果新闻组不存在,或服务器没有保存这个新闻组,或需要身份验证,就退出(第26~40行)。

第42~55行

这一部分读取并显示一些头消息(第42~51行)。最有用的头消息包括作者、主题、日期。程序会读取这些数据并显示给用户。每次调用xhdr()方法时,都要给定想要提取消息头的文章的范围。因为这里只想获取一条消息,所以范围就是“X-X”,其中X是最新一条消息的号码。

xhdr()方法返回一个长度为 2 的元组,其中包含了服务器的响应(rsp)和指定范围的消息头的列表。因为只指定了一个消息(最新一条),所以只取列表的第一个元素(hdr[0])。数据元素是一个长度为2的元组,其中包含文章编号和数据字符串。由于已经知道了文章编号(在请求中给出了),因此只关心第二个元素,即数据字符串(hdr[0][1])。

最后一部分是下载文章的内容(第53~55行)。先调用body()方法,然后至多显示前20个有意义的行(在该部分开始定义的),最后从服务器注销,完成处理。

第57~80行

主要的处理任务由 displayFirst20()函数完成(第 57~80 行)。该函数接收文章的一些内容,并做一些预处理,如把计数器清 0,创建一个生成器表达式对文章内容的所有行做一些处理,然后“假装”刚碰到并显示了一行空行(第 59~61 行,稍后细说)。“Genexp”添加自Python 2.4,如果读者使用的是2.0~2.3版本,需要将这两行改为列表推导(实际上,读者不应该使用 2.4 之前的版本)。由于前导空格可能是Python 代码的一部分,因此在去掉字符串中的空格的时候,只删除字符串尾随的空格(rstrip())。

由于不想显示引用的文本和引用文本指示行;因此在第65~71行(也包含第64行)使用了一个大if语句。只有在当前行不是空行时,才做这个检查(第63行)。检查的时候,会把字符串转成小写,这样就能做到比较的时候不区分大小写(第64行)。

如果一行以“>”或“|”开头,说明这一般是一个引用。不过,将以“>>>”开头的行特殊处理,因为这有可能是交互命令行的提示,虽然这样可能有问题,会导致显示一条引用了三次的消息(比如一段文本到第4个回复的帖子时就被引用了3次)。(本章末尾有一个练习会处理这个问题)。另外,以“in article…”开头,以“writes:”或“wrote:”结尾,行末含有冒号的行,都是引用文本。使用continue语句跳过这些内容。

现在来处理空行。程序应该能智能处理并显示文章中的空行。如果有多个连续的空行,则只显示第一个,这样用户就不会看到许多空行,导致必须滚动才能看到有用的信息。同时也不能把空行计算到有意义的20行之中。所有这些都在第72~78行中实现。

第72行的if语句表示只有在上一行不为空,或者上一行为空但当前行不为空的时候才显示。也就是说,如果显示了当前行,就说明要么当前行不为空,要么当前行为空但上一行不为空。这是另一个比较有技巧的地方:如果遇到一个非空行,计数器加1,并将lastBlank标志设置为False,以表示这一行非空(第74~76行)。否则,如果遇到了空行,则把标志设为True。

现在回到第61行,先将lastBlank标志设为True,因为如果内容的第一行(不是前导数据或引用数据)是空行,则不会显示。需要显示的第一行是实际的数据。

最后,如果遇到了20个非空行就退出,丢弃其余内容(第79~80行)。否则,就应该已经遍历了所有内容,循环正常结束。

3.3.7 NNTP的其他内容

关于NNTP的更多内容,可与阅读NNTP协议定义/规范(RFC 977),参见http://tools.ietf.org/html/rfc977和http://www.networksorcery.com/enp/protocol/nntp.htm页面。其他相关的RFC还包括1036 和 2980。关于 Python 对 NNTP 的更多支持,可以从这里开始:http://docs.python.org/library/nntplib。

3.4 电子邮件

电子邮件既古老又现代。对于作者这些很早之前就开始使用因特网的人来说,电子邮件看上去都非常“古老”,更不用说与今日基于网页的在线聊天、即时聊天(IM)、数字电话(如VoIP[Voice over Internet Protocol])等更新、更快的通信方式相比了。下面将从宏观上介绍一下电子邮件是如何工作的。如果读者已经了解相关内容,只想学习用Python做电子邮件相关的开发,可以跳到下一节。

在介绍电子邮件的基础架构之前,读者是否真正了解电子邮件的确切定义呢?根据RFC 2822的定义,“(电子邮件)消息由头字段(统称消息标题)以及后面可选的正文组成”。对于一般用户来说,一说起电子邮件,无论是一封真的邮件,还是一封不请自来的商业广告(即垃圾邮件),都会想到邮件正文。不过 RFC 规定,邮件可以没有正文,但一定要有邮件标题,这一点要特别注意。

3.4.1 电子邮件系统组件和协议

不管读者是怎么认为的,实际上电子邮件诞生在现代因特网出现之前。电子邮件一开始用于在不同主机用户之间简单交换消息。注意,因为这些用户都使用同一台计算机,所以这里还没有涉及网络。在网络出现之后,用户就可以在不同的主机之间交换消息。当然,由于用户使用不同的计算机,计算机之间使用不同的协议,消息交换是一个很复杂的概念。直到20世纪80年代,因特网上收发电子邮件才有一个事实上的统一标准。

在深入细节之前,不禁想问,电子邮件是怎么工作的?一条消息是如何从发件人那里通过浩瀚的因特网到达收件人的?简单点来说,有一台发送计算机(发件人的消息从这里发送出去)和一台接收计算机(收件人的邮件服务器)。最好的解决方案是发送计算机知道如何连接到接收计算机,这样就可以直接把消息发送过去。但实际上一般没有这么顺利。

发送计算机需要找到某一台中间主机,而这台中间主机最终能到达最后的接收主机。接着这台中间主机需要找到一台离接收主机更近一些的主机。所以,在发送主机和接收主机之间,可能会有多台称为跳板的主机。如果仔细看看收到的电子邮件消息头标题,会看到一个“护照”标记,其中记录了这封邮件最终抵达之前,一路上都到过哪些地方。

为了更清楚地理解,先看看电子邮件系统的各个组件。最重要的组件是消息传输代理(MTA)。这是在邮件交换主机上运行的服务器进程,它负责邮件的路由、队列处理和发送工作。MTA 就是邮件从发送主机到接收主机所要经过的主机和“跳板”,所以也称为“消息传输”的“代理”。

要让所有这些工作起来,MTA 要知道两件事情:1)如何找到消息应该到达的下一台MTA;2)如何与另一台MTA通信。第一件事由域名服务(DNS)来查找目的域名的MX(Mail eXchange,邮件交换)来完成。查找到的可能不是最终收件人,而可能只是下一个能最终把消息送到目的地的主机。对于第二件事,MTA怎么把消息转给其他的MTA呢?

3.4.2 发送电子邮件

为了发送电子邮件,邮件客户端必须要连接到一个MTA,MTA靠某种协议进行通信。MTA之间通过消息传输系统(MTS)互相通信。只有两个MTA 都使用这个协议时,才能进行通信。在本节开始时就说过,由于以前存在很多不同的计算机系统,每个系统都使用不同的网络软件,因此这种通信很危险,具有不可预知性。更复杂的是,有的计算机使用互连的网络,而有的计算机使用调制解调器拨号,消息的发送时间也是不可预知的。事实上,作者曾经有一封邮件在发送9 个月后才收到!因特网的速度怎么会这么慢?这种复杂性导致了现代电子邮件的基础之一 ——简单邮件传输协议(Simple Mail Transfer Protocol,SMTP)的诞生。

1.SMTP、ESMTP、LMTP

SMTP原先由已故的Jonathan Postel(加州大学信息学院)创建,记录在RFC 821中,于1982年8月公布,其后有一些小修改。在1995年11月,通过RFC 1869,SMTP增加了一些扩展服务(即EXMTP),现在 STMP和ESMTP都合并到当前的RFC 5321中,于2008年10月公布。这里使用STMP同时表示SMTP和ESMTP。对于一般的应用,只要能登录服务器、发送邮件、退出即可。这些都很基础。

还有其他的协议,如LMTP(Local Mail Transfer Protocol,本地邮件传输协议),其基于SMTP和ESMTP,作为RFC 2033于1996年10月定义。SMTP需要有一个邮件队列,但这需要额外的存储和管理工作。而LMTP提供了更轻量级的系统,移除了对邮件队列的需求。但邮件需要立即发送(即不会入队)。LMTP服务器不暴露到外面直接与连接到因特网的邮件网关工作,以表示接收还是拒绝一条消息。而网关作为LMTP的队列。

2.MTA

一些实现SMTP的著名MTA包括以下几个。

开源MTA

Sendmail

Postfix

Exim

qmail

商业MTA

Microsoft Exchange

Lotus Notes Domino Mail Server

注意,虽然这些都实现了最小的SMTP协议需求,但其中大多数,尤其是一些商业MTA,都在服务器中加入了协议定义之外的特有功能。

SMTP是在因特网上的MTA之间消息交换的最常用MTSMTA。用SMTP把电子邮件从一台(MTA)主机传送到另一台(MTA)主机。发电子邮件时,必须要连接到一个外部SMTP服务器,此时邮件程序是一个SMTP客户端。而SMTP 服务器也因此成为消息的第一站。

3.4.3 Python和SMTP

是的,也有一个smtplib模块和一个需要实例化的smtplib.SMTP类。先回顾这个已经熟悉的过程。

1.连接到服务器。

2.登录(根据需要)。

3.发出服务请求。

4.退出。

像NNTP一样,登录是可选的,只有在服务器启用了SMTP身份验证(SMTP-AUTH)时才要登录。SMTP-AUTH在RFC 2554中定义。还是与NNTP一样,SMTP通信时只要一个端口,这里是端口号25。

下面是一些Python伪代码。

from smtplib import SMTP

n = SMTP('smtp.yourdomain.com')

.

n.quit()

在看真实的例子之前,先介绍一下smtplib.SMTP类的一些常用方法。

3.4.4 smtplib.SMTP类方法

除了smtplib.SMTP类之外,Python 2.6还引入了另外两个类,即SMTP_SSL和LMTP。后者实现了LMTP(如3.4.2节所述)。前者的作用类似SMTP,如3.4.2节所述,但通过加密的套接字通信,可以作为SMTP/TLS的替代品。STMP_SSL默认端口是465。

与之前一样,这里只列出创建SMTP 客户端应用程序所需要的方法。对大多数电子邮件发送程序来说,只需要两个方法:sendmail()和quit()。

sendmail()的所有参数都要遵循RFC 2822,即电子邮件地址必须要有正确的格式,消息正文要有正确的前导标题,正文必须由回车和换行符(\r\n)对分隔。

注意,实际的消息正文不是必需的。根据RFC 2822,“唯一需要的消息标题只有发送日期字段和发送地址字段”,即“Date:”和“From:”(MAIL FROM、RCPT TO、DATA)。

表3-3列出了一些常见的SMTP对象方法。还有一些方法没有提到,不过一般来说,其他方法在发送电子邮件时用不到。关于SMTP 对象的所有方法的更多信息,可以参见Python文档。

表3-3 SMTP对象常见的方法 第3章 因特网客户端编程 - 图10 ① 只用于SMTP-AUTH。

3.4.5 交互式SMTP示例

同样,这里介绍一个交互式示例。

>>> from smtplib import SMTP as smtp

>>> s = smtp('smtp.python.is.cool')

>>> s.set_debuglevel(1)

>>> s.sendmail('wesley@python.is.cool', ('wesley@python.is.cool',

'chun@python.is.cool'), ''' From: wesley@python.is.cool\r\nTo:

wesley@python.is.cool, chun@python.is.cool\r\nSubject: test

msg\r\n\r\nxxx\r\n.''')

send: 'ehlo myMac.local\r\n'

reply: '250-python.is.cool\r\n'

reply: '250-7BIT\r\n'

reply: '250-8BITMIME\r\n'

reply: '250-AUTH CRAM-MD5 LOGIN PLAIN\r\n'

reply: '250-DSN\r\n'

reply: '250-EXPN\r\n'

reply: '250-HELP\r\n'

reply: '250-NOOP\r\n'

reply: '250-PIPELINING\r\n'

reply: '250-SIZE 15728640\r\n'

reply: '250-STARTTLS\r\n'

reply: '250-VERS V05.00c++\r\n'

reply: '250 XMVP 2\r\n'

reply: retcode (250); Msg: python.is.cool

7BIT

8BITMIME

AUTH CRAM-MD5 LOGIN PLAIN

DSN

EXPN

HELP

NOOP

PIPELINING

SIZE 15728640

STARTTLS

VERS V05.00c++

XMVP 2

send: 'mail FROM:<wesley@python.is.cool> size=108\r\n'

reply: '250 ok\r\n'

reply: retcode (250); Msg: ok

send: 'rcpt TO:<wesley@python.is.cool>\r\n'

reply: '250 ok\r\n'

reply: retcode (250); Msg: ok

send: 'data\r\n'

reply: '354 ok\r\n'

reply: retcode (354); Msg: ok

data: (354, 'ok')

send: 'From: wesley@python.is.cool\r\nTo:

wesley@python.is.cool\r\nSubject: test msg\r\n\r\nxxx\r\n..\r\n.\r\n'

reply: '250 ok ; id=2005122623583701300or7hhe\r\n'

reply: retcode (250); Msg: ok ; id=2005122623583701300or7hhe

data: (250, 'ok ; id=2005122623583701300or7hhe')

{}

>>> s.quit()

send: 'quit\r\n'

reply: '221 python.is.cool\r\n'

reply: retcode (221); Msg: python.is.cool

3.4.6 SMTP的其他内容

关于SMTP的更多信息可以阅读SMTP协议定义/规范,即RFC 5321,参见http://tools.ietf.org/html/rfc2821。关于 Python对SMTP的更多支持,可以阅读http://docs.python.org/library/smtplib。

关于电子邮件,还有一个很重要的方面没有讨论,即如何正确设定因特网地址的格式和电子邮件消息。这些信息详细记录在最新的因特网消息格式规范(RFC 5322)中。可以访问http://tools.ietf.org/html/rfc5322来了解。

3.4.7 接收电子邮件

在以前,只有大学生、研究人员和工商企业的雇员会在因特网上用电子邮件通信。那时台式机还都是装有类UNIX操作系统的工作站。而家庭用户主要是在PC上拨号上网,并没有用到电子邮件。在20世纪90年代中期因特网大爆炸的时候,电子邮件才开始进入千家万户。

对于家族用户来说,在家里放一个工作站来运行SMTP是不现实的,因此必须要设计一种新的系统,能够周期性地把邮件下载到本地计算机,以供离线时使用。这样的系统就要有一套新的协议和新的应用程序来与邮件服务器通信。

这种在家用电脑中运行的应用程序叫邮件用户代理(Mail User Agent,MUA)。MUA从服务器上下载邮件,在这个过程中可能会自动删除它们(也可能不删除,留在服务器上,让用户手动删除)。不过MUA也必须要能发送邮件。也就是说,在发送邮件的时候,应用程序要能直接使用SMTP与MTA进行通信。在前面介绍SMTP的小节中已经看过这种发送邮件的客户端了。那下载邮件的客户端呢?

3.4.8 POP和IMAP

第一个用于下载邮件的协议称为邮局协议(Post Office Protocal,POP),记录在RFC 918中,于1984年10月公布,“邮局协议(POP)的目的是让用户的工作站可以访问邮箱服务器里的邮件,并在工作站中。通过简单邮件传输协议(SMTP)将邮件发送到邮件服务器”。POP协议的最新版本是第3版,也称为POP3。POP3在RFC 1939中定义,至今仍在广泛应用。

在 POP 出现几年之后有了一个与之竞争的协议,即因特网消息访问协议(Internet Message Access Protocol,IMAP)。IMAP还有其他名称,如“因特网邮件访问协议”、“交互式邮件访问协议”、“临时邮件访问协议”。第1版的IMAP是实验性质的,直到第2版才公布其RFC(于1988年7月发布的RFC 1064)。RFC 1064中指出,IMAP2受到了POP第2版(POP2)的启发。

IMAP旨在提供比POP更完整的解决方案,但它也因此比POP更复杂。例如,IMAP非常适合今天的需要,因为用户需要通过不同的设备使用电子邮件,如台式机、笔记本电脑、平板电脑、手机、视频游戏系统等。POP无法很好地应对多邮件客户端,尽管POP应用依然广泛,但大部分情况下已经被废弃了。注意,许多ISP当前只提供POP来接收(用SMTP发送)邮件。希望今后IMAP能得到更多应用。

当前广泛使用的版本是IMAP4rev1。实际上,Microsoft Exchange这个当今最主要的邮件服务器使用IMAP作为下载方式。在编写本书时,最新的IMAP4rev1协议草案是RFC 3501 (公布于2003年3月)。本书中的IMAP4同时表示IMAP4和IMAP4rev1协议。

要了解更多内容,建议查阅前面提到的RFC文档。图3-3显示了电子邮件这个复杂的系统。

现在进一步了解Python中对POP3和IMAP4的支持。

3.4.9 Python和POP3

与之前一样,这里导入poplib并实例化poplib.POP3 类。标准流程如下所示。

1.连接到服务器。

2.登录。

3.发出服务请求。

4.退出。

第3章 因特网客户端编程 - 图11 图3-3 因特网上的电子邮件发件人和收件人。客户端通过他们的MUA和相应的MTA来进行通信,下载和发送邮件。电子邮件从一个MTA“跳”到另一个MTA,直到到达目的地为止

Python伪代码如下。

from poplib import POP3

p = POP3('pop.python.is.cool')

p.user(…)

p.pass_(…)

p.quit()

在看真实的示例之前,必须要介绍一下poplib.POP3_SSL类(Python 2.4中添加的),该类需要提供一些凭证信息,然后通过加密连接传输邮件。同样,先来看一个交互式示例,介绍poplib.POP3类中的一些基本方法。

3.4.10 交互式POP3示例

下面是使用Python的poplib模块的交互式示例。其中第一次的时候故意输错密码,来显示在实际中服务器的报错消息。下面是完整的交互式输出内容。

>>> from poplib import POP3

>>> p = POP3('pop.python.is.cool')

>>> p.user('wesley')

'+OK'

>>> p.pass_("you'llNeverGuess")

Traceback (most recent call last):

File "<stdin>", line 1, in ?

File "/usr/local/lib/python2.4/poplib.py", line 202, in pass_

return self._shortcmd('PASS %s' % pswd)

File "/usr/local/lib/python2.4/poplib.py", line 165, in _shortcmd

return self._getresp()

File "/usr/local/lib/python2.4/poplib.py", line 141, in _getresp

raise error_proto(resp)

poplib.error_proto: -ERR directory status: BAD PASSWORD

>>> p.user('wesley')

'+OK'

>>> p.pass_('youllNeverGuess')

'+OK ready'

>>> p.stat()

(102, 2023455)

>>> rsp, msg, siz = p.retr(102)

>>> rsp, siz

('+OK', 480)

>>> for eachLine in msg:

…print eachLine

Date: Mon, 26 Dec 2005 23:58:38 +0000 (GMT)

Received: from c-42-32-25-43.smtp.python.is.cool

by python.is.cool (scmrch31) with ESMTP

id <2005122623583701300or7hhe>; Mon, 26 Dec 2005 23:58:37+0000

From: wesley@python.is.cool

To: wesley@python.is.cool

Subject: test msg

xxx

.

>>> p.quit()

'+OK python.is.cool'

3.4.11 poplib.POP3类方法

POP3类提供了许多方法用来下载和离线管理邮箱。最常用的方法列在表3-4中。

在登录时,user()方法不仅向服务器发送用户名,还会等待并显示服务器的响应,表示服务器正在等待输入该用户的密码。如果 pass_()方法验证失败,会引发一个 poplib.error_proto异常。如果成功,会得到一个以“+”号开头的应答消息,如“+OK ready”,然后锁定服务器上的这个邮箱,直到调用quit()方法为止。

调用 list()方法时,msg_list 的格式为:[‘msgnum msgsiz’,…],其中,msgnum和msgsiz分别是每个消息的编号和消息的大小。

还有一些方法这里没有列出。更多内容请参考Python库手册里poplib的文档。

表3-4 POP3对象的常用方法 第3章 因特网客户端编程 - 图12

3.4.12 客户端程序SMTP和POP3示例

示例3-3演示了如何使用SMTP和POP3创建一个既能接收和下载电子邮件也能上传和发送电子邮件的客户端。首先通过SMTP给自己(或其他测试账户)发一封电子邮件,等待一段时间(随便选了一个时间,如10秒钟),然后使用POP3下载这封电子邮件。下载下来的内容与发送的内容应该完全一样。如果程序无提示地结束,没有输出也没有异常,那就说明操作都成功了。

示例3-3 SMTP和POP3示例(myMail.py)

这个脚本(通过SMTP邮件服务器)发送一封测试电子邮件到目的地址,并马上(通过POP)把电子邮件从服务器上取回来。读者自己测试时,为了让程序能正常工作,需要修改服务器名称和电子邮件地址。

第3章 因特网客户端编程 - 图13

第3章 因特网客户端编程 - 图14

逐行解释

第1~8行

与本章前面的例子一样,程序一开始是一些import语句和常量定义。常量分别是发送邮件(SMTP)和接收邮件(POP3)的服务器。

第10~17行

这几行是消息内容的准备工作。对于这条用于测试的消息,寄件人和收件人是同一个用户。不要忘了,RFC 2822要求消息头和正文需要用空行隔开。

第19~23行

这一段代码连接到发送(SMTP)服务器来发送消息。这里再次用到了From和To的地址,这些地址要么是“真实”的收件人和寄件人的电子邮件地址,要么是投递寄件人 [2](envelope sender)和收件人。收件人参数应该是一个可迭代的对象。如果传递的是一个字符串,则它会转换成一个只有一个元素的列表。对于垃圾邮件,消息头中的地址和投递头中的地址是不一致的。

sendmail()的第三个参数是电子邮件消息本身。在这个函数返回后,就从SMTP服务器注销,并判断是否有错误发生过。接着等待一段时间,让服务器完成消息的发送与接收。

第25~32行

最后一部分用来下载刚刚发送的消息,并断言刚刚发送的和现在接收的消息完全相同。代码中先根据用户名和密码连接到POP3服务器。登录成功后,调用stat()方法得到可用消息列表。通过[0]符号选中第一条消息,然后调用retr()下载这条消息。

遇到空行则表示在此之前是邮件头部,之后是邮件正文。去掉消息头部分,比较原始消息正文和收到的消息正文。如果相同就不显示任何内容,程序正常退出,否则会出现断言失败。

由于错误的类型有很多种,这个脚本里没有进行错误检查,因此代码比较简洁(在本章末尾有一个练习就需要添加错误检查)。

现在读者对今日电子邮件的收发有了很全面的了解。如果想深入了解这一方面的开发内容,请参阅下一节中介绍的与电子邮件相关的Python模块,那些模块在电子邮件相关的程序开发方面有相当大的帮助。

3.4.13 Python和IMAP4

Python通过imaplib模块支持IMAP4。这与本章介绍的其他因特网协议非常相似。首先导入imaplib,实例化其中一个imaplib.IMAP4*类,标准流程与之前一样。

1.连接到服务器。

2.登录。

3.发出服务请求。

4.退出。

下面是对应的Python伪代码。

from imaplib import IMAP4

s= IMAP4('imap.python.is.cool')

s.login(…)

s.close()

s.logout()

这个模块定义了三个类,分别是 IMAP4、IMAP4_SSL、IMAP4_stream,这些类可以用来连接任何兼容IMAP4的服务器。就如同POP3_SSL对于POP,IMAP_SSL可以通过SSL加密的套接字连接IMAP4服务器。IMAP的另一个子类是IMAP4_stream,该类可以通过一个类似文件的对象接口与IMAP4服务器交互。后两个类在Python 2.3中添加。

现在来看看一个交互式例子,这个例子介绍了imaplib.IMAP4类中的基本方法。

3.4.14 交互式IMAP4示例

下面是一个使用Python的imaplib的交互式示例。

>>> s = IMAP4('imap.python.is.cool') # default port: 143

>>> s.login('wesley', 'youllneverguess')

('OK', ['LOGIN completed'])

>>> rsp, msgs = s.select('INBOX', True)

>>> rsp

'OK'

>>> msgs

['98']

>>> rsp, data = s.fetch(msgs[0], '(RFC822)')

>>> rsp

'OK'

>>> for line in data[0][1].splitlines()[:5]:

…print line

Received: from mail.google.com

by mx.python.is.cool (Internet Inbound) with ESMTP id 316ED380000ED

for <wesley@python.is.cool>; Fri, 11 Mar 2011 10:49:06 -0500 (EST) Received: by gyb11 with SMTP id 11so125539gyb.10

  for <wesley@python.is.cool>; Fri, 11 Mar 2011 07:49:03 -0800 (PST)

>>> s.close()

('OK', ['CLOSE completed'])

>>> s.logout()

('BYE', ['IMAP4rev1 Server logging out'])

3.4.15 imaplib.IMAP4类中的常用方法

前面提到过,IMAP协议比POP复杂,因此有很多方法这里没有列出。表3-5列出了一些基本方法,读者可能会在简单的电子邮件应用中用到这些方法。

表3-5 IMAP4对象的常见方法 第3章 因特网客户端编程 - 图15

下面是一些使用这些方法的示例。

NOP、NOOP或“no operation”。这些内容表示与服务器保持连接状态。

>>> s.noop()

('OK', ['NOOP completed'])

获取某条消息的相关信息。

>>> rsp, data = s.fetch('98', '(BODY)')

>>> data[0]

'98 (BODY ("TEXT" "PLAIN" ("CHARSET" "ISO-8859-1" "FORMAT" "flowed"

"DELSP" "yes") NIL NIL "7BIT" 1267 33))'

获取某条消息的头。

>>> rsp, data = s.fetch('98', '(BODY[HEADER])')

>>> data[0][1][:45]

'Received: from mail-gy.google.com (mail-gy.go')

获取所有已读消息的ID(也可以尝试使用“ALL”、“NEW”等)。

>>> s.search(None, 'SEEN')

('OK', ['1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 59 60 61 62

63 64 97'])

获取多条消息(使用冒号分隔,注意,右圆括号用来分隔结果)。

>>> rsp, data = s.fetch('98:100', '(BODY[TEXT])')

>>> data[0][1][:45]

'Welcome to Google Accounts.To activate your'

>>> data[2][1][:45]

'\r\n-b1_aeb1ac91493d87ea4f2aa7209f56f909\r\nCont'

>>> data[4][1][:45]

'This is a multi-part message in MIME format.'

>>> data[1], data[3], data[5]

(')', ')', ')')

3.5 实战

3.5.1 生成电子邮件

到目前为止,本章已经深入介绍了多种使用Python下载电子邮件消息的方法,甚至还讨论过如何创建简单的文本电子邮件消息,以及连接到SMTP服务器来发送邮件。但这其中没有介绍如何在Python中生成稍微复杂的消息。读者可能已经猜到,这里所说的稍微复杂的电子邮件消息是指不仅包含纯文本,还有附件、文本中的格式等。本节就介绍相关内容。

这种较长的消息由多个部分组成。比如消息中有纯本文的部分,可能还有对应的HTML部分,这部分针对使用Web浏览器作为邮件客户端的情形,除此之外还有一个或多个附件。邮件互换消息扩展(Mail Interchange Message Extension,MIME)格式就用来识别这些不同的部分。

Python 的 email 包很适合处理并管理整个电子邮件消息的 MIME 部分,这一节将使用email包和smtplib包。email包有多个组件,分别用来解析和生成电子邮件。本节首先介绍生成电子邮件,之后再简要介绍消息解析。

示例 3-4 中有两个创建电子邮件消息的示例,即make_mpa_msg()和 make_img_msg(),两者都创建了一条带有附件的电子邮件消息。前者创建并发送了一条多部分消息,后者创建并发送了一条电子邮件消息,其中含有一幅图片。示例代码后面是逐行解释。

示例3-4 生成电子邮件(email-examples.py)

这个Python 2脚本创建并发送了两种不同类型的电子邮件消息。

第3章 因特网客户端编程 - 图16

第3章 因特网客户端编程 - 图17

逐行解释

第1~7行

除了标准的起始行和docstring,还导入了MIMEImage、MIMEMultipart、MIMEText、SMTP类。

第9~18行

多部分选择消息通常包含两部分,一是以纯文本表示的邮件消息正文,以及等价的HTML格式。由邮件客户端来决定显示哪一部分。例如,基于 Web 的电子邮件系统会显示 HTML版本,而基于命令行的邮件阅读器只会显示纯文本版本。

为了创建这种类型的消息,需要使用 email.mime.multiple.MIMEMultipart 类,并传递alternative作为唯一的参数来实例化这个类。如果不传递这个参数,则前面的纯文本和HTML会分别作为消息中的附件,这种情况下,有些邮件系统会同时显示这两部分的内容。

这两部分都会用到 email.mime.text.MIMEText 类,因为这两部分内容都是纯文本。每个部分都要附加到邮件中,因为这两部分是在邮件创建之后才创建的。

第20~28行

make_img_msg()函数使用一个文件名作为参数。使用文件中的数据生成一个新的email.mime.image.MIMEImage实例。添加一个Content-Disposition头,接着将消息返回给用户。

第30~33行

sendMsg()的唯一目的是获取基本的电子邮件发送信息(发件人、收件人、消息正文),接着传送消息,然后返回给调用者。

要查看更详尽的输出内容,可以试试这个扩展:s.set_debuglevel(True),其中s是smtplib.SMTP服务器。最终,与前面一样,因为许多 SMTP 服务器需要登录,所以需要在这里登录(在登录之后,发送电子邮件消息之前)。

第35~48行

这是这段脚本的主要部分,它仅仅测试这两个函数。用这两个函数创建消息,然后添加From、To、Subject字段,然后将消息传送给这些收件人。当然,为了让应用能够工作,需要填充下面的字段:SENDER、RECIPS、SOME_IMG_FILE。

3.5.2 解析电子邮件

与从零生成一封电子邮件相比,解析电子邮件简单一些。一般要用到 email 包中的几个工具,如email.message_from_string()函数,以及message.walk()和message.get_payload()方法。下面是一个典型的模式。

def processMsg(entire_msg):

body = ''

msg = email.message_from_string(entire_msg)

if msg.is_multipart():

for part in msg.walk():

if part.get_content_type() == 'text/plain':

body = part.get_payload()

break

else:

body = msg.get_payload(decode=True)

else:

body = msg.get_payload(decode=True)

return body

这段代码很容易理解。下面是其中的主要函数解释。

email.message_from_string():用来解析消息。

msg.walk():遍历消息的附件。

part.get_content_type():获得正确MIME类型。

msg.get_payload():从消息正文中获取特定的部分。通常 decode标记会设为True,即邮件正文根据每个Content-Transfer-Encoding头解码。

3.5.3 基于Web的云电子邮件服务

到目前为止,本章介绍的相关协议的示例大部分情况下是针对理想状态,其中没有涉及安全或其他杂乱的问题。不过前面提到了一些服务器需要登录。

但在实际编码中,需要应对现实的威胁,防止实际维护的服务器成为黑客的目标,这些黑客会试图将服务器作为垃圾或钓鱼邮件的转发器,或用于其他恶意行为。这些系统(主要是邮件系统)会进行相应的封锁。本章前面的示例中,使用的是来自ISP的通用电子邮件服务。由于已经每个月付费使用了因特网服务,因此就“免费”获得了上传/发送和下载/接收功能。

现在来看看一些基于Web的公开电子邮件服务,如Yahoo! Mail和Google的Gmail服务。因为这些是SaaS(Software as a Serveice,软件即服务)类型的云服务,无须每月支付费用,所以看起来是完全免费的。但用户通常会看到一些投放的广告。若投放的广告越精确,则服务提供商就能获得越多的收益,以此填补提供这些服务的开销。

Gmail使用算法扫描电子邮件消息,对内容进行评价,然后通过优秀的机器学习算法来精准地投放广告。与一般的广告相比,这些精确的广告会更加吸引用户。这些广告一般是纯文本,位于电子邮件消息面板的右边。由于算法的作用,Google不仅为Web访问免费提供Gmail服务,还允许客户端服务使用POP3、IMAP4向外传送消息,以及使用SMTP发送电子邮件。

另一个方面,Yahoo!用图片的形式投放广告,这些图片会嵌入到Web应用中。由于Yahoo!广告投放得不精确,因此获得的收益也少。因此有些服务需要付费订阅(通过Yahoo! Mail Plus的形式),这样才能下载电子邮件。另一个原因可能是 Yahoo!不想用户很方便地就能移动他们的电子邮件。在编写本书时,Yahoo!当前无法通过SMTP发送电子邮件。在本节的后续部分会看到有关这两个电子邮件服务的示例代码。

3.5.4 最佳实践:安全、重构

这里还要花点时间讨论一些优秀的实践准则,如安全性和重构。有时最好的计划也会受挫于不同版本之间的差异,比如,新版本会对老版本进行改进并修复之前未曾发现的 bug。所以在实际中,所做的工作会比原先预想的要多。

在了解Google和Yahoo!两个邮件服务之前,先看看每组示例中会用到的一些样板代码。

from imaplib import IMAP4_SSL

from poplib import POP3_SSL

from smtplib import SMTP_SSL

from secret import * # where MAILBOX, PASSWD come from

who = …# xxx@yahoo/gmail.com where MAILBOX = xxx

from_ = who

to = [who]

headers = [

'From: %s' % from_,

'To: %s' % ', '.join(to),

'Subject: test SMTP send via 465/SSL',

]

body = [

'Hello',

'World!',

]

msg = '\r\n\r\n'.join(('\r\n'.join(headers), '\r\n'.join(body)))

首先,注意,现在已经不在温室里了,而是实际的开发环境中,因此需要加密Web上的连接。所以使用三个协议的SSL等价版本,因此原先的每个类名后面添加了“_SSL”。

其次,不能像前面那样在代码中使用纯文本保存登录名和密码。在实际中,将用户名和密码以纯文本形式嵌入到源码中是糟糕透顶的做法。这些信息应该要么从安全的数据库中获取,要么从编译后的字节码文件(.pyc或.pyo文件)获取,要么从公司内联网中的服务器或代理中获取。这个例子中假设这些信息位于secret.pyc文件中,其中的MAILBOX和PASSWD属性表示等价的私有信息。

最后一组变量仅仅表示实际的电子邮件消息,以及发件人和收件人(为了简化,这里发件人和收件人是同一个人)。构建电子邮件消息本身的方法比前一节介绍的稍微复杂一些,前面的邮件消息正文是单个字符串,需要填充相应的字段数据。

body = '''\

From: %(who)s

To: %(who)s

Subject: test msg

Hello World!

''' % {'who': who}

但这里选择使用列表替换前面的字符串。因为在实际中,电子邮件消息正文是由应用生成或控制的,而不是硬编码的字符串。在电子邮件头中使用字符串或许可行。但使用列表可以方便地向电子邮件中添加(或从中移除)某一行的内容。当邮件已经准备发送时,只需使用\r\n对调用str.join()就可以组装成正文(本章前面章节介绍过,\r\n是兼容RFC5322的SMTP的服务器使用的正式分隔符,其他有些服务器只接受换行符)。

对于消息正文的数据还做了另一点小的修改,邮件的收件人可能不止一个,所以to的变量类型也改成了列表。因此在创建最终的电子邮件头时需要使用 str.join()将收件人连接到一起。最后,查看在Yahoo! Mail和Gmail示例中会用到的一个特殊功能函数。这个函数仅仅获取入站电子邮件消息的Subject行。

def getSubject(msg, default='(no Subject line)'):

'''\

getSubject(msg) - 'msg' is an iterable, not a

delimited single string; this function iterates

over 'msg' look for Subject: line and returns

if found, else the default is returned if one isn't

found in the headers

'''

for line in msg:

if line.startswith('Subject:'):

return line.rstrip()

if not line:

return default

getSubject()函数很简单,它只查找邮件标题中的Subject行。如果发现了一个,该函数就立即返回。如果遇到空行,就表示邮件标题已结束,因此如果此时没有找到Subject行,则返回一个默认值。这个默认值是一个本局部变量,含有默认参数。用户可以传递自定义的默认字符串。出于性能方面的考虑,有些读者可能使用line[:8] == 'Subject'来避免调用str.startswith()方法。但line[:8]会调用str.getslice()。不过说实话,这种方法比str.startswith()快40%,如timeit测试所示。

>>> t = timeit.Timer('s[:8] == "Subject:"', 's="Subject: xxx"')

>>> t.timeit()

0.14157199859619141

>>> t.timeit()

0.1387479305267334

>>> t.timeit()

0.13623881340026855

>>>

>>> t = timeit.Timer('s.startswith("Subject:")', 's="Subject: xxx"')

>>> t.timeit()

0.23016810417175293

>>> t.timeit()

0.23104190826416016

>>> t.timeit()

0.24139499664306641

使用timeit是另一个好习惯,前面已经碰到过好多次了。如果有两个代码完成相同的工作,使用timeit可以知道哪一种效率更高。现在来看如何在实际代码中使用这些技术。

3.5.5 Yahoo! Mail

假设前面的样板代码已经执行过了,现在首先介绍Yahoo! Mail。这里的代码是示例3-3的扩展。其中还会通过STMP发送电子邮件,但通过POP和IMAP接收邮件。下面是原型脚本。

s = SMTP_SSL('smtp.mail.yahoo.com', 465)

s.login(MAILBOX, PASSWD)

s.sendmail(from_, to, msg)

s.quit()

print 'SSL: mail sent!'

s = POP3_SSL('pop.mail.yahoo.com', 995)

s.user(MAILBOX)

s.pass_(PASSWD)

rv, msg, sz = s.retr(s.stat()[0])

s.quit()

line = getSubject(msg)

print 'POP:', line

s = IMAP4_SSL('imap.n.mail.yahoo.com', 993)

s.login(MAILBOX, PASSWD)

rsp, msgs = s.select('INBOX', True)

rsp, data = s.fetch(msgs[0], '(RFC822)')

line = getSubject(StringIO(data[0][1]))

s.close()

s.logout()

print 'IMAP:', line

假设将所有这些内容都放在ymail.py文件中,然后通过下面的方式执行。

$ python ymail.py

SSL mail sent!

POP: Subject:Meet singles for dating, romance and more.

IMAP: Subject: test SMTP send via 465/SSL

在这个例子中,下载电子邮件需要有一个Yahoo! Mail Plus账号(而无论是否是付费用户,发送都是免费的)。但有些功能无法正常工作。首先,POP无法获取发送的邮件,但IMAP可以找到相应的邮件。一般来说,IMAP更加可靠。同时在前面的例子中,假设读者是付费用户,并使用Python 2.6.3及更新的版本。如果不满足这些要求,需要进行设置,不过设置起来也很方便。

如果不是Yahoo! Mail Plus付费用户,则不能下载电子邮件。非付费用户试图下载电子邮件时会出现下面的报错消息。

Traceback (most recent call last):

File "ymail.py", line 101, in <module>

s.pass_(PASSWD)

File "/Library/Frameworks/Python.framework/Versions/2.7/lib/

python2.7/poplib.py", line 189, in pass_

return self._shortcmd('PASS %s' % pswd)

File "/Library/Frameworks/Python.framework/Versions/2.7/lib/

python2.7/poplib.py", line 152, in _shortcmd

return self._getresp()

File "/Library/Frameworks/Python.framework/Versions/2.7/lib/

python2.7/poplib.py", line 128, in _getresp

raise error_proto(resp)

poplib.error_proto: -ERR [SYS/PERM] pop not allowed for user.

另外,STMP_SSL类添加自Python 2.6,但在2.6.3之前都有bug。所以为了编写正确使用SMTP和SSL的代码,需要2.6.3及其更新版本的Python。如果使用的Python版本早于2.6,则无法使用这个类;如果使用的Python版本在2.6.0~2.6.2之间,则会得到下面这样的错误。

Traceback (most recent call last):

File "ymail.py", line 61, in <module>

s.login(MAILBOX, PASSWD)

File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/

python2.6/smtplib.py", line 549, in login

self.ehlo_or_helo_if_needed()

File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/

python2.6/smtplib.py", line 509, in ehlo_or_helo_if_needed

if not (200 <= self.ehlo()[0] <= 299):

File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/

python2.6/smtplib.py", line 382, in ehlo

self.putcmd(self.ehlo_msg, name or self.local_hostname)

File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/

python2.6/smtplib.py", line 318, in putcmd

self.send(str)

File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/

python2.6/smtplib.py", line 310, in send

raise SMTPServerDisconnected('please run connect() first')

smtplib.SMTPServerDisconnected: please run connect() first

实际工作中总会遇到的一些问题,从来不会像教科书上那样完美。这些奇怪、无法预测的问题会让人抓狂。这里模拟出这些问题,希望不会打击到读者。

现在来整理一下输出。但更重要的是,添加实际环境中所需要的版本检查代码,习惯就好。示例3-5显示了最终版本的ymail.py。

示例3-5 Yahoo! Mail SMTP、POP、IMAP示例(ymail.py)

这段代码通过SMTP、POP、IMAP使用了Yahoo! Mail服务。

第3章 因特网客户端编程 - 图18

第3章 因特网客户端编程 - 图19

逐行解释

第1~8行

这些是标准起始行和导入行。

第10~15行

这里通过 platform.python_version()获取字符串形式的 Python 版本号。只有在使用 2.6.3及更新版本的Python时才导入smtplib属性;否则,将SMTP_SSL设置为None。

第17~21行

前面提到过,不要将用户名和密码这些私有信息写到源码文件中,而是将其放到其他地方,如编译过的字节码文件secret.pyc。这样一般用户就无法通过逆向工程获取MAILBOX和PASSWD数据。由于这只是个测试应用,因此在获取相关信息(第17 行)后,将邮件发件人和收件人变量设置为同一个人(第19~21行)。为什么发件人变量名为from_,而不 是from?

第23~32行

这一部分代码组成电子邮件消息的正文。第23~27行表示邮件标题(可以用一些代码方便地生成),第28~31行用于生成消息正文(同样可以用代码生成或放到可迭代变量中)。在末尾(第32行),用一行代码合并前面的所有内容(标题+正文),并使用正确的分隔符创建整个消息正文。

第34~43行

前面已经介绍过getSubject()函数,其唯一目的是在入站电子邮件标题中查找Subject行,如果没有找到Subject行,则使用默认字符串。设置默认值并不是必需的。

第45~57行

这是SMTP相关的代码。前面在第10~15行中,要么使用SMTP_SSL,要么将其设置为空。这里,如果获得SMTP_SSL类(第7行),则尝试连接到服务器,登录并发送邮件,最后退出(第48~53行)。否则,向用户提示需要2.6.3或更新版本的Python(第56~57行)。偶尔会由于不同的原因,如连接问题,导致从服务器断开。这种情况下一般重试,所以在第54~55行通知用户进行重试。

第59~70行

这一段是POP3相关的代码(第62~68行),前面已经介绍过相关内容。唯一区别在于添加了对非付费用户试图下载邮件的检查,所以需要在第69~70行捕捉poplib.error_proto异常。

第72~84行

这一段IMAP4代码也需要检查是否为付费用户。最后将功能汇总在一个try语句块中(第74~82行),并捕捉socket.error(第83~84行)。读者注意到这个微妙的地方了吗?在第79行使用了cStringIO.StringIO对象。因为IMAP返回单个庞大的字符串来表示电子邮件的消息。由于 getSubject()遍历其中的每一行,因此需要提供类似的东西来与其一起工作。于是通过StringIO为一个长字符串提供类似文件的接口。

这就是在实际当中使用Yahoo! Mail的方式。Gmail与之非常类似,只是所有访问都是“免费”的。另外,Gmail还可以使用标准的SMTP(需要用到TLS)。

3.5.6 Gmail

示例3-6介绍了Google的Gmail服务。除了通过SSL连接的SMTP之外,Gmail还提供使用传输层安全(Transport Layer Security,TLS)的SMTP,所以多了导入smtplib.SMTP类的语句,以及相关的代码。除此之外,其他内容(通过SSL连接的SMTP、POP、IMAP)与Yahoo! Mail的示例相同。因为这里下载电子邮件完全免费,所以无需针对非付费用户的异常处理程序。

示例3-6 Gmail的SMTP/TLS、SMTL/SSL、POP、IMAP示例(gmail.py)

这段代码通过SMTP、POP、IMAP使用了Google Gmail服务。

第3章 因特网客户端编程 - 图20

第3章 因特网客户端编程 - 图21

逐行解释

第1~8行

这些是常见的代码起始行和导入行,但添加了导入smtplib.SMTP的语句。示例中会使用这个类与TLS发送电子邮件消息。

第10~43行

这一部分与ymail.py中相似。其中一个区别是who变量是一个@gmail.com电子邮件地址(第19行)。还有一点是首先使用STMP/TLS,在Subject行中会看到这一点。同时没有导入smtplib.SMTPServerDisconnected异常,因为在这个测试中用不到。

第45~56行

这是通过TLS连接服务器的SMTP代码。从中可以看到,为了与服务器通信,老版本的Python需要更多的示例代码。同时端口号也与SMTP/SSL不同(第47行)。

第58~88行

剩下的代码与Yahoo! Mail中的几乎完全相同。前面提到过,这里移除了Gmail中用不到的一些错误检查。最后一个小差异是为了能使用SMTP/TLS和STMP/SSL发送消息,需要修改Subject行(第68行)。

希望读者通过这两个示例能够理解本章前面介绍的概念,并掌握应用实际开发中的一些知识,以及在实际中安全问题的重要性,还有不同Python版本之间的细微差别。虽然希望解决方案尽量只处理问题本身,但这与实际状况不相符。这里介绍的只是一些代表问题,实际的项目开发中需要考虑更多的问题。

3.6 相关模块

Python标准库对网络支持非常完善,特别是在因特网协议和客户端开发方面。下面列出一些相关模块,首先是电子邮件相关模块,随后是用于一般用途的因特网协议的模块。

3.6.1 电子邮件

Python自带了很多电子邮件相关的模块和包,可以用来构建应用程序。表3-6列出了一部分。

表3-6 电子邮件相关模块 第3章 因特网客户端编程 - 图22

3.6.2 其他因特网客户端协议

表3-7列出了其他因特网客户端协议方面的模块。

表3-7 因特网客户端协议相关模块 第3章 因特网客户端编程 - 图23

3.7 练习

FTP

3-1 简单FTP客户端。参考本章的FTP例子,写一个小的FTP客户端程序,能够去你喜欢的网站下载所使用的软件的最新版本。这个脚本应该每几个月就运行一次,以确保你在用的软件是“最新和最好的”。应该把FTP地址、登录名、密码信息放在一个表里,省得每次都要修改。

3-2 简单FTP客户端和模式匹配。在上一个练习的基础上创建一个新的FTP客户端程序。可以通过指定模式从远程主机上传和下载文件。比如,如果想把一些Python或PDF文件从一台计算机传到另一台计算机上,让用户输入“.py”或“doc.pdf”,程序会只传这些文件名匹配的文件。

3-3 智能FTP命令行客户端程序。创建一个与UNIX下/bin/ftp类似的命令行FTP应用程序,不过,这个FTP客户端要更好一些,能提供更有用的功能。可以参考http://ncftp.com的ncFTP。应用程序需要有以下功能:历史记录、书签(可以保存FTP 地址和登录信息)、下载进度显示等。可以用readline来记录历史命令,用curses来控制屏幕。

3-4 FTP和多线程。创建一个能使用Python线程库下载文件的FTP客户端程序。读者可以通过修改上一个练习的程序或者重写一个更简单的客户端来下载文件。要么在命令行参数里指定要下载的文件,要么做一个 GUI,在界面中让用户选择要下载的文件。选做题:支持模式,如*.exe;使用不同的线程来下载每个文件。

3-5 FTP和GUI。在练习3-3中编写的FTP客户端程序中加入GUI,让其成为一个完整的FTP应用程序。可以使用Python的任何GUI工具包。

3-6 子类化。从ftplib.FTP派生出一个FTP2类,在这个类中,不用像之前那4个retr() 和stor()方法中那样要给定“STOR filename”或“RETR filename”这样的命令。只要传文件名。要么重写已有的方法,要么在已有的方法名后加一个后缀2,如retrlines2()。

Python源码包中有一个Tools/scripts/ftpmirror.py脚本,这个脚本使用ftplib 模块,可以对整个FTP站点或FTP站点的一部分做镜像。它可以作为ftplib模块应用的扩展例子来使用。解答下面5个练习时,可以参考这个脚本。读者可以直接使用ftpmirror.py里的代码,也可以参考这个脚本自己重新写一个。

3-7 递归。ftpmirror.py脚本递归复制远程目录。遍写一个与ftpmirror.py相似的脚本,其默认行为是非递归的。只有在指定了“-r”选项的时候才递归地把文件子目录复制到本地文件系统中。

3-8 模式匹配。ftpmirror.py脚本支持“-s”选项,让用户指定模式(如“*.exe”),忽略掉匹配的文件。重新写一个更简单的 FTP 客户端程序或修改之前的程序,实现让用户指定通配符,程序只能下载匹配模式的文件。可以在之前练习的答案基础上实现。3-9 递归和模式匹配。写一个FTP客户端程序,把练习3-7和练习3-8的脚本集成在一起。

3-10 递归和ZIP文件。这个练习与练习3-7有些相似,只是不再直接把文件下载到本地文件系统中,而是升级现有的 FTP 客户端或创建一个新的客户端来下载远程文件,并将其压缩到一个ZIP(或TGZ、BZ2)文件中。使用“-z”选项让用户可以自动地备份一个FTP站点。

3-11 集成。实现一个最终的、功能齐全的FTP应用程序,包含练习3-7~练习3-10的所有功能。即支持“-r”“-s”和“-z”选项。

NNTP

3-12 NNTP介绍。修改示例3-2(getLatestNNTP.py),让其显示第一篇(而不是最后一篇)可用文章中有意义的内容。

3-13 代码改进。修正getLatestNNTP.py中就会输出3次引用行的问题,这是因为之前想输出Python交互解释的内容,但不应该显示3次引用的文本。用检查“>>>”后的代码是否为合法Python代码的方式来解决这个问题。如果合法,那就显示这一行数据;如果不合法,就认为是引用文本,不显示。选做题:再解决这样一个小问题,这里没有去掉前导空格,因为它可能是Python代码的缩进。如果真的是代码的缩进,就显示它;否则,认为它是一般的文本,先使用lstrip()方法处理后再显示。

3-14 查找文章。编写一个NNTP客户端程序,让用户能选择并登录感兴趣的新闻组。在登录成功后,提示用户输入一些关键字,使用这些关键字来查找文章的标题。把符合要求的文章列出来显示给用户。用户可以在列表中选择某一篇文章进行阅读,这时要能显示选定文章的内容。程序还要有简单的导航功能,如分页等。如果没有给出搜索关键字,则显示当前所有的文章。

3-15 搜索内容。修改上一练习的解决方案,让代码同时搜索主题和文章正文内容。允许对关键字进行“与”(AND)和“或”(OR)操作。也允许指定在标题和文章正文内容的“与”(AND)和“或”(OR)操作,即要处理以下一种情形:关键字要只在标题里出现、只在正文内容里出现,或者两者里面都要出现。

3-16 基于话题的新闻阅读工具。这不是说要写一个多话题的阅读工具,而是把相关的回帖组织到“话题”中。也就是说,把相关的文章放在一起,这与文章的发布时间没有关系。同一个话题中的文章按时间顺序排列。用户可以:

a)选择某一篇文章(正文)进行阅读,然后可以选择回到文章列表视图,顺序阅读当前话题的前一篇文章或后一篇文章。

b)允许回复话题,可以选择复制并引用之前的文章,用跟贴的方式回复整个新闻组。选做题:也允许用电子邮件回复给个人。

c)永久地删除话题,即后续的相关文章不会显示在文章列表中。要实现这个功能,需要使用列表记录需要删除的话题,这样就不会再次显示相关话题。若一个话题在几个月之后还没有人回复,就可以认为这个话题已经死了。

3-17 GUI新闻阅读工具。与上面的FTP练习差不多,选择一个Python GUI工具包来实现一个完整独立的GUI新闻阅读工具。

3-18 重构。与FTP的ftpmirror.py一样,NNTP也有一个示例脚本:Demo/scripts/newslist.py。运行这个脚本。这个脚本在很久之前编写的,读者可以做一些翻新工作。作为练习,要用Python新版本的一些特性和Python开发技巧来重构这个脚本,让这个脚本运行得更快。可以使用列表解析和生成器表达式,用更智能的字符串连接而不是调用不必要的函数等。

3-19 缓存。如其作者所说,newslist.py 的另一个问题是,“我应该把要忽略的空的新闻组列表保存下来,在每次运行的时候检查一下是否有新的文章,但我真的抽不出时间”。读者尝试实现这个功能。可以直接修改这个脚本,也可以修改练习3-18中的脚本。

电子邮件

3-20 标识符。POP3 调用 login()方法发送了登录名之后,使用 pass()方法发送密码。那为什么这个方法命名时要在后面加一个下划线,即“pass()”,而不是直接使用“pass()”?

3-21 POP和IMAP。编写一个应用程序,它使用poplib类(POP3或POP3_SSL)下载电子邮件,再使用imaplib完成相同的事情。读者可以参阅本章前面的代码。同时为什么需要将登录名和密码信息从源代码中移除?

下面的练习与本章示例3-3中的myMail.py应用程序有关。

3-22 电子邮件标题。在myMail.py的最后几行,比较了发送的消息正文与接收到的电子邮件消息正文。编写一段相似的代码,比较消息标题。提示:要忽略新加入的标题。

3-23 错误检查。添加对SMTP和POP3的错误检查。

3-24 SMTP和IMAP。加入IMAP的支持。选做题:支持两种邮件下载协议,让用户选择要使用哪一种协议。

3-25 撰写电子邮件。扩展练习3-24的程序,允许用户撰写和发送电子邮件。

3-26 电子邮件应用程序。再次扩展之前的电子邮件应用程序,在其中加入更有用的邮箱管理功能。应用程序要能读出当前所有电子邮件消息,并显示其Subject行。用户可以选择并查看某一封邮件。选做题:要能支持用外部应用程序查看附件。

3.27 GUI。向上一个练习的解决方案中添加一个GUI层,让它成为一个实际上完整的电子邮件应用程序。

3-28 垃圾邮件的特点。不请自来的垃圾邮件是当今的一大问题。所幸,针对这个问题有不少好的解决方案。这里不用另辟蹊径,而是想让读者了解一些处理垃圾邮件的特点。

a)“mbox”格式。在开始之前,先要将需要处理的电子邮件消息转为一个常见格式,比如 mbox 格式(读者也可以使用别的格式)。一旦已经有了一些 mbox 格式的消息,就把它们合并到一个文件中。提示:使用mailbox模块和email包。

b)标题。很多电子邮件的从邮件标题上就能看是否为垃圾邮件(可以用 email 包解析邮件标题,也可自行手动解析)。写一段代码来解决以下问题。

发送这条消息的电子邮件客户端软件是什么?(检查X-Mailer标题)

报文ID(Message-ID标题)的格式是否合法?

From、Received、Return-Path标题的域名是否匹配?域名和IP地址是否匹配?有没有X-Authentication-Warning标题?如果有,内容是什么?

c)信息服务器。一些服务器(如WHOIS、SenderBase.org等)可以根据IP地址或域名帮助找到电子邮件来自何方。找到一些这样的服务,写一些代码来得到来源地的国家、城市、网络所有者的名字、联系方法等。

d)关键字。垃圾邮件中有一些单词经常出现。之前一定见过,同时还包括这些单词的变形,比如用数字表示某个字母,首字母大写的随机字母组合等。把在垃圾邮件中经常出现的大量词汇放在一个列表中。将出现了这些词汇的邮件作为疑似垃圾邮件隔离。选做题:设计一种算法或加入一些关键字的变形来找出这些邮件。

e)钓鱼。这些垃圾邮件总是想把它们伪装成来自大银行或知名网站的合法电子邮件。其中包含某些链接,引诱用户输入自己私密的或敏感的信息,如登录名、密码、信用卡卡号等。这些骗子往往做得足以以假乱真。不过,他们还是免不了要让用户登录到与他们声称的并不相符的网站。这里,就可能会透露出很多信息,如看上去乱七八糟的域名,只用了IP地址,或32位整数形式而不是字节形式的IP地址等。编写一段代码来判断一封看上去像官方发送的电子邮件的真伪。

生成电子邮件

下面的这些练习需要用到email包来生成电子邮件,同时与email-examples.py中的代码有关。

3-29 多部分可选(Multipart Alternative)。多部分可选是什么意思?在前面简要了解了make_img_msg()函数,但该函数真正完成了什么?如果在实例化MIMEMultipart类时移除“alternative”,即email = MIMEMultipart(),则该函数的行为会有哪些变化?

3-30 Python 3。将 email-examples.py 代码移植到 Python 3 中(或创建一个能同时在Python 2.x和3.x中运行的代码)。

3-31 多个附件。3.5.1节介绍了make_img_msg()函数,它用来创建一封电子邮件,其中含有单幅图片。虽然这是一个良好的开始,但无法满足实际需求。这个练习需要读者创建一个名为attachImgs()/attach_images()的函数(读者可以使用其他名称),函数接受多个图片文件作为参数,将这些文件作为电子邮件的一个单独附件,并返回单个多部分消息对象。

3-32 健壮性。改进练习3-31中的attachImgs(),确保用户传入的是图像文件(如果不是图片则抛出异常)。也就是说,检查文件名来确保扩展名是.png、.jpg、.gif、.tif等。选做题:支持文件内省,即可以处理任何文件,包括错误和没有扩展名的文件,检查文件的真实类型。提示:访问http://en.wikipedia.org/wiki/File_format这个维基页面了解相关内容。

3-33 健壮性、网络。继续改进attachImgs()函数,除了添加本地文件之外,用户还可以使用URL添加在线图片,如http://docs.python.org/ _static/py.png。

3-34 电子表格。创建一个名为 attachSheets()的函数,用于向多部分电子邮件消息添加一个或多个电子表格文件。支持最常见的格式,如.csv、.xls、.xlsx、.ods、.uof/.uos等。可以以attachImgs()为原型,但不能使用email.mime.image.MIMEImage,而要使用 email.mime.base.MIMEBase,以及用 bigness 指定正确的 MIME 类型(如'application/vnd.ms-excel'。同时不要忘了修改Content-Disposition标题。

3-35 文档。与练习3-34类似,创建一个名为attachDocs()的新函数,向多部分电子邮件消息添加文档附件。支持常见的格式,如.doc、.docx、.odt、.rtf、.pdf、.txt、.uof/.uot等。

3-36 多附件类型。扩展练习3-35中支持的文件类型。创建一个名为attachFiles()的新函数,该函数可以接受任何类型的附件。读者可以随意将前几个练习中的代码复制到这个函数中。

其他内容

http://networksorcery.com/enp/topic/ipsuite.htm 这个链接列出了多个因特网协议,包括本章重点介绍的三个。而http://docspython.org/library/internet这个链接列出了Python支持的因特网协议。

3-37 开发另一个因特网客户端。本章介绍了4个使用Python开发因特网客户端的示例,选择另一个Python标准库模块支持的客户端协议,编写一个客户端应用。

3-38 *开发一个新的因特网客户端。这个练习难度很大,首先找到一个不常见或者正在开发且Python不支持的协议。编写代码让Python支持这个协议。要严肃对待,因为为Python添加对新协议的支持会作为PEP提交,相关模块代码会包含在未来发布的Python标准库中。