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

本章主题html

  • 引言python

  • 文件传输c++

  • 文件传输协议(FTP)程序员

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

  • 网络新闻传输协议(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 展现了这个协议,其工做流程以下:

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 对象的方法

在通常的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)。

做为客户端/服务器架构的另外一个例子,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 对象的方法

跟上一节的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 对象的全部方法的信息。

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,直到到达目的地为止。

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 中。

在登陆时,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 中列出了一部分。

17.5.2 其余网络协议

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

相关文章
相关标签/搜索