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 因特網協議相關的模塊

相關文章
相關標籤/搜索