python浓缩(17)网络客户端编程

本章主题

  • 引言

  • 文件传输

  • 文件传输协议(FTP)

  • 网络新闻、Usenet, 和新闻组

  • 网络新闻传输协议(NNTP)

  • 电子邮件

  • 简单邮件传输协议(SMTP)

  • 邮局协议 3(POP3)

  • 相关模块

前面的章节已经大致了解了那些使用套接字的低级别的网络通讯协议。这种网络互连是当今互联网中大部分客户端/服务器协议的核心。互联网中的网络协议包括文件传输(FTP, SCP 等),阅读Usenet 新闻组(NNTP),e-mail 发送(SMTP),从服务器上下载e-mail(POP3, IMAP)等等。这些协议的工作方式与之前在套接字编程中介绍的客户端/服务器的例子很像。唯一的不同在于,使用TCP/IP 等低级别的协议,基于此创建了新的,更具体的协议来实现刚刚描述的服务。

17.1 什么是因特网客户端?

在着手研究这些协议之前,“因特网客户端到底是什么”?把因特网简化成一个数据交换中心,数据交换的参与者是一个服务提供者和一个服务的使用者。有的人把它称为“生产者-消费者”(虽然这个词一般只用在讲解操作系统相关信息时)。服务器就是生产者,它提供服务,一般只有一个服务器(进程或主机等),和多个消费者,就像之前看的客户端/服务器模型那样。虽然不再使用底级别的套接字来创建因特网客户端,但模型是完全相同的。

我们将详细了解三个因特网协议——FTP, NNTP 和POP3,并写出它们的客户端程序。通过这些程序,你会发现这些协议的API 是多么的相似——由于保持接口的一致性有很大的好处,所以,这些相似性在设计之初就kao虑到了——更重要的是,你还能学会如何写出这些协议与其它协议实用的客户端程序来。虽然我们只着重说了这三个协议。在看完这些协议后,你就能有足够的能力写出任何因特网协议的客户端程序了。

17.2 文件传输

17.2.1 文件传输因特网协议

因特网中最流行的事情就是文件的交换。文件交换无处不在。有很多协议可以供因特网上传输文件使用。最流行的有文件传输协议(FTP),Unix-to-Unix 复制协议(UUCP),以及网页的超文本传输协议(HTTP)。另外,还有(Unix 下的)远程文件复制指令rcp(以及更安全,更灵活的scp 和rsync)。

迄今为止,HTTP,FTP 和scp/rsync 还是非常流行的。HTTP 主要用于网页文件的下载和访问Web服务上。一般不要求用户输入登录的用户名密码就可以访问服务器上的文件和服务。HTTP 文件传输请求主要是用于获取网页(文件下载)。

相对的,scp 和rsync 要求用户登录到服务器,否则不能上传或下载文件。至于FTP,跟scp/rsync一样,可以上传或下载文件,还采用了Unix 的多用户的概念,用户一定要输入有效的用户名和密码才能使用。不过,FTP 也允许匿名登录。

17.2.2 文件传输协议(FTP)

文件传输协议主要用于匿名下载公共文件。也可用于在两台电脑之间传输文件,尤其是在使用Unix 系统做为文件存储系统,使用其它机器来工作的情况。早在网络流行之前,FTP 就是在因特网上文件传输,软件和源代码下载的主要手段之一。

FTP 要求输入用户名和密码才能访问远程的FTP 服务器,也允许以匿名用户登录。不过,管理员要先设置FTP 服务器允许匿名用户登录。匿名用户的用户名是“anonymous”,密码一般是用户的e-mail 地址。与特定的用户拥有特定的帐户不同,这有点像是把FTP 公开出来让大家访问。匿名用户通过FTP 协议可使用的命令与一般的用户相比来说限制更多。

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

150756_x9ua_724288.png

1. 客户端连接远程的FTP 服务器

2. 客户端输入用户名和密码(或“anonymous”和e-mail 地址)

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

4. 客户端登出远程FTP 服务器,结束通讯

这只是很泛的一个流程。有时,由于网络两边电脑的崩溃或是网络的问题,会导致整个事务在完成之前被中断。一般,在客户端超过15 分钟(900 秒)不活动之后,连接就会被关闭。

在底层上,FTP 只使用TCP(见前面网络编程相关章节)它不使用UDP。而且,FTP 是客户端/服务器编程中很“与众不同”的例子。客户端和服务器都使用两个套接字来通讯:一个是控制和命令端口(21 号端口),另一个是数据端口(有时是20 号端口)。

图17-1 因特网上的FTP 客户端和服务器。客户端和服务器使用指令和控制端口发送FTP 协议,而数据通过数据端口传输。

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

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

17.2.3 Python 和FTP

怎么用Python 写FTP 客户端程序呢?我们之前已经提到过一些了。现在还要再加上相应的Python 模块导入和调用的操作。再来回顾一下流程:

1. 连接到服务器

2. 登录

3. 发出服务请求 (有可能有返回信息)

4. 退出

在使用Python 的FTP 支持时,需要做的就是导入ftplib 模块,并实例化一个ftplib.FTP类对象。所有的FTP 操作(如登录,传输文件和登出等)都要使用这个对象来完成。下面是一段Python的伪代码:

from ftplib import FTP
f = FTP('ftp.python.org')
f.login('anonymous', 'guess@who.org')
...
f.quit()

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

17.2.4 ftplib.FTP 类方法

在表17.1 中列出了最常用的方法,这个表并不全面——想查看所有的方法,请参阅模块源代码——但这里列出的方法组成了在Python 中FTP 客户端编程的“API”。也就是说,你不一定要使用其它的方法,因为它们或者是辅助函数,或者是管理函数,或者是被API 调用的。

表17.1 FTP 对象的方法

151523_48q6_724288.png

151523_p7Ut_724288.png

在一般的FTP 通讯中,要使用到的指令有login(), cwd(), dir(), pwd(), stor*(), retr*()和quit()。有一些没有列出的FTP 对象方法也是很有用的。请参阅Python 的文档以得到更多关于FTP 对象的信息:

http://python.org/docs/current/lib/ftp-objects.html

17.2.5 交互式FTP 示例

Python 中使用FTP 非常简单,甚至可以不用写脚本,直接在交互式解释器中实时地看到交互与输出。下面这个例子是在几nian前,python.org 还支持ftp 服务的时候做的:

17.2.6 客户端FTP 程序举例

之前说过,可以不写脚本,在交互环境中使用FTP。下面还是要写一段脚本,假设要从Mozilla 网站下载最新的Bugzilla 的代码。试着写一个应用程序,不过,也可以交互式地运行这段代码。程序使用FTP 库来下载文件,也做了一些错误检测。

不过,程序并不完全自动。要自己决定什么时候要去下载。如果你在使用类Unix 系统,你可以设定一个“cron”任务来自动下载。另一个问题是,如果文件的文件名或目录名改了的话,程序就不能正常工作了。

例17.1 FTP 下载示例 (getLatestFTP.py)这个程序用于下载网站中最新版本的文件。可以修改这个程序让它下载你喜欢的程序。

import ftplib
import os
import socket

HOST='ftp.mozilla.org'
DIRN = 'pub/mozilla.org/webtools'
FILE = 'bugzilla-LATEST.tar.gz'

def main():
    try:
        f = ftplib.FTP(HOST)
    except (socket.error, socket.gaierror) as e :
        print ('ERROR: cannot reach "%s"' % HOST)
        return
    print ('*** Connected to host "%s"' % HOST)

    try:
        f.login()
    except (ftplib.error_perm):
        print ("ERROR: cannot login anonymously")
        f.quit()
        return
    print ('*** Logged in as "anonymous"')


    try:
        f.cwd(DIRN)
    except ftplib.error_perm:
        print ('ERROR: cannot CD to "%s"' % DIRN)
        f.quit()
        return
    print ('*** Changed to "%s" folder' % DIRN)

    try:
        # 应该把文件对象保存到一个变量中, 如变量loc , 然后把loc.write 传给ftp.retrbinary()方法
        f.retrbinary('RETR %s' % FILE, open(FILE, 'wb').write)
    except ftplib.error_perm:
        print ('ERROR: cannot read file "%s"' % FILE)
        # 如果由于某些原因我们无法保存这个文件,那要把存在的空文件给删掉,以防搞乱文件系统
        os.unlink(FILE)
    else:
        # 我们使用了try-except-else 子句,而不是写两遍关闭FTP连接然后返回的代码
        print ('*** Downloaded "%s" to CWD' % FILE)
        f.quit()
        return

if __name__ == '__main__':
    main()

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

$ 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
$

我们传了一个回调函数给retrbinary(),它在每接收到一块二进制数据的时候都会被调用。这个函数就是我们创建的本地文件对应文件对象的write 方法。在传输结束的时候,Python解释器会自动关闭这个文件对象,而不会丢失数据。虽然这样方便,但最好还是不要这样做,做为一个程序员,要尽量做到在资源不再被使用的时候就直接释放,而不是依赖其它代码来做释放操作。

17.2.7 FTP 的其它方面

Python 同时支持主动和被动模式。Python2.1 及以后版本中,被动模式支持默认是打开的。以下是一些典型的FTP 客户端类型:

  • 命令行客户端程序:可以使用一些FTP 文件传输工具如/bin/ftp 或NcFTP,它们允许用户在命令行交互式的参与到FTP 通讯中来。

  • GUI 客户端程序:与命令行客户端程序相似,只是它是一个GUI 程序。如WsFTP 和Fetch 等。

  • 网页浏览器:在使用HTTP 之外,大多数网页浏览器(也是一个客户端)可以进行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 文件传输的程序。由于程序用于特殊目的,一般这种程序都不允许用户与服务器接触。

这四种客户端类型都可以用Python 来写。上面,我们用ftplib 来创建了一个自己的定制程序,你也可以自己做一个命令行的应用程序。在命令行的基础上,你可以使用一些界面工具包,如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 的信息:

ftp://ftp.isi.edu/in-notes/rfc959.txt以及网页

http://www.networksorcery.com/enp/protocol/ftp.htm。其它相关的RFC 有2228,2389,2428,2577,2640 和4217。想了解更多Python 对FTP 的支持,可以从这里开始:

http://python.org/docs/current/lib/module-ftplib.html

17.3 网络新闻

17.3.1 Usenet 与新闻组

Usenet 新闻系统是一个全球存档的“电子公告板”。各种主题的新闻组一应俱全。新闻组可以是面向全球泛泛而谈,也可以是只面向某个地理区域。

整个系统是一个由大量计算机组成的一个庞大的全球网络,计算机之间共享Usenet 上的帖子.如果某一个用户发了一个帖子到本地的Usenet 计算机上,这个帖子会被传播到其它相连的计算机上,并再由这些计算机传到与它们相连的计算机上,直到这个帖子传播到了全世界,每个人都收到这个帖子为止.

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

17.3.2 网络新闻传输协议(NNTP)

供用户在新闻组中下载或发表帖子的方法叫网络新闻传输协议(NNTP)。

224144_6qZ3_724288.png

作为客户端/服务器架构的另一个例子,NNTP 与FTP 的操作方式很像且简单得多。FTP 需要不同的端口来做登录,数据传输和控制,NNTP 只使用一个标准端口119 来做通讯。你给服务器一个请求,它做相应的反馈,见图17-2。

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

17.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 类的一些常用的方法。

17.3.4 nntplib.NNTP 类方法

跟前一节列出ftplib.FTP 类的方法时一样,我们不会列出nntplib.NNTP 的所有方法,只列出你创建NNTP 客户端程序时可能用得着的方法。

表17.2 NNTP 对象的方法

224321_xn2G_724288.png

224321_CCst_724288.png

跟上一节的FTP 对象表一样,还有一些NNTP 对象的方法没有提及。为了避免混乱,我们只列出了你可能用得到的。其余的,再次建议你参kaoPython 手册。

17.3.5 交互式NNTP 举例

接下来是一个如何使用Python 中NNTP 库的交互式的例子。它看上去跟交互式的FTP 的例子差不多。(出于保密的原因,e-mail 地址都做了修改)。

在调用表17.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!'
>>>
17.3.6 客户端程序NNTP 举例

在NNTP 客户端例子中,来点更复杂的。在之前的FTP 客户端例子中,是下载最新的文件,这一次,我们要下载Python 语言新闻组com.lang.python 里的最后一篇文章。

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

最后,智能的处理空行。文章中出现了一行空行,就显示一行空行,如果有多行连续的空行,只显示一行空行。只有有数据的行才算在“前20 行”之中。所以,最多可能显示39 行输出,20 行实际数据间隔了19 行空行。

如果脚本的运行正常的话,我们可能会看到这样的输出:

$ 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
$

例17.2 NNTP 下载示例 (getFirstNNTP.py)

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

import nntplib
import socket

HOST = 'your.nntp.server'
GRNM = 'comp.lang.python'
USER = 'wesley'
PASS = "you'llNeverGuess"

def main():
    try:
        n = nntplib.NNTP(HOST)#, user=USER, password=PASS)
    except socket.gaierror as e:
        print ('ERROR: cannot reach host "%s"' % HOST)
        print (' ("%s")' % eval(str(e))[1])
        return
    except nntplib.NNTPPermanentError as e:
        print ('ERROR: access denied on "%s"' % HOST)
        print (' ("%s")' % str(e))
        return
    print ('*** Connected to host "%s"' % HOST)

    try:
        rsp, ct, fst, lst, grp = n.group(GRNM)
        except nntplib.NNTPTemporaryError, e:
            print 'ERROR: cannot load group "%s"' % GRNM
            print ' ("%s")' % str(e)
            print ' Server may require authentication'
            print ' Uncomment/edit login line above'
            n.quit()
            return
        except nntplib.NNTPTemporaryError, e:
            print 'ERROR: group "%s" unavailable' % GRNM
            print ' ("%s")' % str(e)
            n.quit()
            return
        print '*** Found newsgroup "%s"' % GRNM

    # 头信息包括作者,主题和日期。这些数据会被读取并显示给用户
    # 在每一次调用xhdr()方法时,都要给定想要提取信息头的文章的范围。我们只想取一条信息,所以范围就是“X-X”,其中,X 是最后一条信息的号码。
    # xhdr()方法返回一个长度为2 的元组,包含了服务器的返回信息(rsp)和我们指定范围的信息头的列表。由于我们只指定了一个消息(最后一个),我们只取列表的第一个元素(hdr[0])。
    # 数据元素是一个长度为2 的元组,包含文章号和数据字符串。由于我们已经知道了文章号(我们在请求中给出了),我们只关心第二个元素,数据字符串(hdr[0][1])。
    # 最后一部分是下载文章的内容。先调用body()方法,然后显示前20 个有意义的行,最后登出服务器,完成执行。
    rng = '%s-%s' % (lst, lst)
    rsp, frm = n.xhdr('from', rng)
    rsp, sub = n.xhdr('subject', rng)
    rsp, dat = n.xhdr('date', rng)
    print '''*** Found last article (#%s):
From: %s
Subject: %s
Date: %s'''% (lst, frm[0][1], sub[0][1], dat[0][1])
    rsp, anum, mid, data = n.body(lst)
    displayFirst20(data)
    n.quit()


def displayFirst20(data):
    print '*** First (<= 20) meaningful lines:\n'
    count = 0
    lines = (line.rstrip() for line in data)
    lastBlank = True
    for line in lines:
        if line:
            lower = line.lower()
            if (lower.startswith('>') and not \
                lower.startswith('>>>')) or \
                lower.startswith('|') or \
                lower.startswith('in article') or \
                lower.endswith('writes:') or \
                lower.endswith('wrote:'):
                continue
            if not lastBlank or (lastBlank and line):
                print ' %s' % line
                if line:
                    count += 1
                    lastBlank = False
                else:
                    lastBlank = True
                    if count == 20:
                        break

if __name__ == '__main__':
    main()

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

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(2*eps) ]
> 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

主要的处理任务由displayFirst20()函数完成(57-80 行)。它接受文章的所有行做为参数,并做一些预处理,如把计数器清0,创建一个生成器表达式对文章内容的所有行做一些处理,然后“假装”我们刚碰到并显示了一行空行(59-61 行,稍后细说)。由于前导空格可能是Python 代码的一部分,所以在我们去掉字符串中的空格的时候,只删除字符串右边的空格(rstrip())。

我们要做的是,我们不要显示引用的文本和引用文本指示行。这就是65-71 行(也包含64 行)的那个大if 语句所要做的事。如果这一行不是空行的时候,才做这个检查(63 行)。检查的时候,会把字符串转成小写,这样就能做到比较的时候大小写无关(64 行)。

如果一行以“>”或“|”开头,说明这一般是一个引用。不过,我们认为“>>>”是一个例外,因为这有可能是交互命令行的提示,虽然这样可能有问题,因为它也可能是一段被引用了三次的消息(1 段文本到第4 个回复的帖子时被引用了3 次)却被显示了。

现在来处理空行。我们想让程序聪明一些,它应该能显示文章中的空行,但对空行的处理要做到智能。如果有多个连续的空行,则只显示第一个,这样用户不用看那么多行信息,导致有用的信息却在屏幕之外。我们也不能把空行计算到20 行有意义的行之中。所有这些要求都在72-78 行内实现。

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

现在回到第61 行,我们设lastBlank 标志为True,是因为,如果内容的第一行实际数据(不是前导数据或是引用数据)是一个空行,我们不会显示它。因为我们想要看第一行实际数据!

最后,如果我们已经显示了20 行非空行,则退出,放弃其余的行(79-80 行)。否则,我们应该已经遍历了所有行,循环也正常结束了。

17.3.7 NNTP 的其它方面

从NNTP 协议定义/规范(RFC 977)中,你可以得到更多关于NNTP 的信息:

ftp://ftp.isi.edu/in-notes/rfc977.txt以及网页

http://www.networksorcery.com/enp/protocol/nntp.htm。其它相关的RFC 有1036,2980。

想了解更多Python 对NNTP 的支持,可以从这里开始:

http://python.org/docs/current/lib/module-nntplib.html

17.4 电子邮件

本节介绍e-mail 如何工作的,看e-mail 的底层的结构之前,e-mail 的确切定义到底是什么?根据RFC2822,“消息由头域(合起来叫消息头)以及后面可选的消息体组成”。一般用户说起e-mail 就会想到它的内容,不管它是一封真的邮件还是垃圾邮件,都应该有内容。RFC 规定,邮件体是可选的,只有邮件头是必要的。

17.4.1 E-mail 系统组件和协议

电子邮件(e-mail)开始用于mainframe 的用户之间简单的交换信息。由于他们使用同一台电脑,所以未涉及到网络。当网络成为现实的时候,用户就可以在不同的主机之间交换信息。由于用户使用着不同的电脑,电脑之间使用着不同的协议,信息交换成了一个很复杂的概念。直到20 世纪80 nian代,因特网上用e-mail 进行信息交换才有了一个事实上的统一的标准。

在深入细节之前,e-mail 是怎么工作的?一条消息是如何从发件人那通过因特网到达收件人的?有一台发送电脑,和一台目的电脑(收件人的信件服务器)。最好的解决方案是发送电脑知道如何连接到接收电脑,它就可以直接把消息发送过去。实际上并不这么顺利。

发送电脑要查询到某一台中间主机,这台中间主机能到达最后的收件主机。然后这台中间主机要找一台离目的主机更近一些的主机。所以,在发送主机和目的主机之间会有多台叫做“跳板”的主机。如果你仔细看看你收到的e-mail 的邮件头,会看到一个“passport”标记,其中记录了邮件寄给你这一路上都到过了哪些地方。

先看看e-mail 系统的各个组件。最主要的组件是消息传输代理(MTA)。这是一个在邮件交换主机上运行的一个服务器程序,它负责邮件的路由,队列和发送工作。它们就是邮件从源主机到目的主机所要经过的跳板。所以也被称为是“信息传输”的“代理”。

MTA 要知道两件事情:1) 如何找到消息应该去的下一台MTA 2) 如何与另一台MTA 通讯。第一件事由域名服务(DNS)来查找目的域名的MX(邮件交换Mail eXchange)来完成。这对于最后的收件人是不必要的,但对其它的跳板来说,则是必要的。对于第二件事,MTA怎么把消息转给其它的MTA 呢?

17.4.2 发送E-mail

要发送e-mail,你的邮件客户端一定要连接到一个MTA,它们靠某种协议进行通讯。MTA 之间通讯所使用的协议叫消息传输系统(MTS)。只有两个MTA 都使用这个协议时,才能进行通讯。由于以前存在很多不同的计算机系统,每个系统都使用不同的网络软件,这种通讯很危险,具有不可预知性。更复杂的是,有的电脑使用互连的网络,而有的电脑使用调制解调器拨号,消息的发送时间也是不可预知的。出于对这些复杂度的kao虑,现代e-mail 的基础之一,简单邮件传输协议(SMTP)出现了。

SMTP

一些已经实现了SMTP的著名MTA 包括:

  • 开源MTA

  • Sendmail

  • Postfix

  • Exim

  • qmail (免费发布,但不开源)

商业MTA

  • Microsoft Exchange

  • Lotus Notes Domino Mail Server

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

SMTP 是在因特网上MTA 之间用于消息交换的最常用的MTS。它被MTA 用来把e-mail 从一台主机传送到另一台主机。在你发e-mail 的时候,你必须要连接到一个外部的SMTP 服务器,这时,你的邮件程序是一个SMTP 客户端。你的SMTP 服务器也因此成为了你的消息的第一个跳板。

17.4.3 Python 和SMTP

也存在一个smtplib 模块和一个smtplib.SMTP 类要实例化。再来看看过程吧:

1. 连接到服务器

2. 登录(如果需要的话)

3. 发出服务请求

4. 退出

登录是可选的,只有在服务器打开了SMTP 认证(SMTP-AUTH)时才要登录。SMTP 通讯时,只要一个端口25。下面是一些Python 的伪代码:

from smtplib import SMTP
n = SMTP('smtp.yourdomain.com')
...
n.quit()

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

17.4.4 smtplib.SMTP 类方法

不会列出所有的方法,只列出创建SMTP客户端程序所需要的方法。只有两个方法是必须的:sendmail()和quit()。sendmail()的所有参数都要遵循RFC 2822,即e-mail 地址必须要有正确的格式,消息体要有正确的前导头,前导头后面是两个回车和换行(\r\n)对。

注意,实际的消息体不是必要的。“唯一要求的头信息只有发送日期和发送地址”,即“Date:”和“From:”:(MAIL FROM, RCPT TO, DATA)还有一些方法没有被提到,一般来说,它们不是发送e-mail 所必须的。请参kaoPython文档以获取SMTP 对象的所有方法的信息。

181039_Cd2w_724288.png

17.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
17.4.6 SMTP 的其它方面

从SMTP 协议定义/规范(RFC 2821)中,你可以得到更多关于SMTP 的信息:

ftp://ftp.isi.edu/in-notes/rfc2821.txt以及网页

http://www.networksorcery.com/enp/protocol/smtp.htm

想了解更多Python 对SMTP 的支持,可以从这里开始:

http://python.org/docs/current/lib/module-smtplib.html

我们还没有讨论的e-mail 的一个很重要的方面是怎么正确的设定因特网地址的格式和e-mail消息。这些信息详细记录在因特网信息格式RFC 2822 中。可以在ftp://ftp.isi.edu/in-notes/rfc2822.txt下载。

17.4.7 接收E-mail

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

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

17.4.8 POP 和IMAP

用于下载邮件的第一个协议叫邮局协议,“邮局协议(POP)的目的是让用户的工作站可以访问邮箱服务器里的邮件。邮件要能从工作站通过简单邮件传输协议(SMTP)发送到邮件服务器”。POP 最新版本是第3 版,也叫POP3。POP3 至今为止仍在被广泛地使用。

POP 之后,出现了另一个叫交互式邮件访问协议(IMAP)。现在被使用的IMAP 版本是IMAP4rev1,它也被广泛地使用。事实上,当今世界上占有邮件服务器大多数市场的Microsoft Exchange 就使用IMAP 作为其下载机制。IMAP 的目的是要提供一个更全面的解决方案。不过,它比POP 更复杂。对IMAP 感兴趣的用户查看上述RFC 文档。图17-3 展示的复杂系统就是我们所认为的简单的e-mail。

图17-3 因特网上的E-Mail 发件人和收件人。客户端通过他们的MUA 和相应的MTA 进行通讯,来下载和发送邮件。E-Mail 从一个MTA“跳”到另一个MTA,直到到达目的地为止。

182452_a0UK_724288.png

17.4.9 Python 和POP3

导入poplib,实例化poplib.POP3 类。标准的做法如下:

1. 连接到服务器

2. 登录

3. 发出服务请求

4. 退出

Python 的伪代码如下:

from poplib import POP3
p = POP3('pop.python.is.cool')
p.user(...)
p.pass_(...)
...
p.quit()

先看一个交互式的例子以及介绍一下poplib.POP3 类的一些基本的方法。

17.4.10 交互式POP3 举例

下面是使用Python poplib 模块的交互式的例子:

>>> from poplib import POP3
>>> p = POP3('pop.python.is.cool')
>>> p.user('techNstuff4U')
'+OK'
>>> p.pass_('notMyPasswd')
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('techNstuff4U')
'+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'
17.4.10 poplib.POP3 类方法

POP3 类有无数的方法来帮助你下载和离线管理你的邮箱。最常用的列在表17.4 中。

182617_jrE3_724288.png

在登录时,user()方法不仅向服务器发送了用户名,也要等待服务器正在等待用户密码的返回信息。如果pass_()方法认证失败,会引发一个poplib.error_proto 的异常。成功会得到一个以'+'号开头的返回信息,然后服务器上的该邮箱就被锁定了,直到调用了quit()方法为止。

调用list()方法时,msg_list 的格式为:[‘msgnum msgsiz’,…],其中,msgnum 和msgsiz分别是每个消息的编号和消息的大小。想要了解更多信息,请参kaoPython 手册里poplib 的文档。

17.4.12 客户端程序SMTP 和POP3 举例

下面演示了使用SMTP 和POP3 来创建一个既能接收和下载e-mail 也能上传和发送e-mail 的客户端。我们将要先用SMTP 发一封e-mail 给自己(或其它测试帐户),等待一段时间—使用POP3 下载这封e-mail,下载下来的内容跟发送的内容应该是完全一样的。如果程序悄无声息地结束,没有输出也没有异常,那就说明我们的操作都成功了。

例17.3 SMTP 和POP3 示例 (myMail.py),这个脚本(通过SMTP 邮件服务器)发送一封测试e-mail 到目的地址,并马上(通过POP)把e-mail 从服务器上收回来。要让程序能正常工作,你需要修改服务器的名字和e-mail 的地址。

#!/usr/bin/env python
from smtplib import SMTP
from poplib import POP3
from time import sleep

# 发送邮件和接收邮件的服务器
SMTPSVR = 'smtp.python.is.cool'
POP3SVR = 'pop.python.is.cool'

# 消息头和消息体按照一定的格式放在一起组成一个可以发送的消息
origHdrs = ['From: wesley@python.is.cool', 'To: wesley@python.is.cool', 'Subject: test msg']
origBody = ['xxx', 'yyy', 'zzz']
origMsg = '\r\n\r\n'.join(['\r\n'.join(origHdrs), '\r\n'.join(origBody)])

# 连接到发送(SMTP)服务器
sendSvr = SMTP(SMTPSVR)
# 收件人参数应该是一个可迭代的对象,如果传的是一个字符串,就会被转成一个只有一个元素的列表
# 垃圾邮件中,消息头和信封头总是不一致的
errs = sendSvr.sendmail('wesley@python.is.cool', ('wesley@python.is.cool',), origMsg)
sendSvr.quit()
assert len(errs) == 0, errs

# 等待服务器完成消息的发送与接收
sleep(10) # wait for mail to be delivered

recvSvr = POP3(POP3SVR)
recvSvr.user('wesley')
recvSvr.pass_('youllNeverGuess')
# 调用stat()方法得到有效的消息的列表。我们先选第一条消息([0]),然后调用retr()下载这个消息
rsp, msg, siz = recvSvr.retr(recvSvr.stat()[0])
# 空行来分隔头和信息,去掉头部分,比较原始信息体和收到的信息体
# strip headers and compare to orig msg
sep = msg.index('')
recvBody = msg[sep+1:]
assert origBody == recvBody # assert identical

由于错误的类型太多,我们在这个脚本里不做错误检查,这样的好处是你可以直接看到出现了什么错误。在本章末尾有一个习题就是做错误检查的。现在,你对如何发送和接收e-mail 有了一个很全面的了解。如果你想深入了解这一方面的编程,请参阅下一章里介绍的e-mail 相关的模块,它们在程序开发方面有相当大的帮助。

17.5 相关模块

Python 最好的一个方面就是它在标准库中提供了相当的全面的网络支持。尤其在因特网协议和客户端开发方面的支持更为全面。下面列出了一些相关模块,首先是电子邮件相关的,随后是一般用途的因特网协议相关的。

17.5.1 E-mail

Python 自带了很多e-mail 模块和包可以帮助你创建应用程序。表17.5 中列出了一部分。

182721_wQe8_724288.png

17.5.2 其他网络协议

表17.6 因特网协议相关的模块

182742_CYsB_724288.png

转载于:https://my.oschina.net/cqlcql/blog/665097

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值