Python權威指南的10個項目(1~5)

引言
  我相信學習Python過的朋友,一定會喜歡上這門語言,簡單,庫多,易上手,學習成本低,但是如果是學習之後,不經常使用,或者工作中暫時用不到,那麼不久之後又會忘記,久而久之,就浪費了很多的時間再自己的“曾經”會的東西上。所以最好的方法就是實戰,通過真是的小型項目,去鞏固,理解,深入Python,同樣的久而久之就不會忘記。
  所以這裏小編帶大家編寫10個小型項目,去真正的實操Python,這10個小型項目是來自《Python權威指南》中後面10個章節的項目,有興趣的朋友可以自行閱讀。希望這篇文章能成爲給大家在Python的學習道路上的奠基石。
  建議大家是一邊看代碼,一邊學習,文章中會對代碼進行解釋:
這裏是項目的gitlab地址(全代碼):

https://gitlab.com/ZZY478086819/actualcombatproject

1. 項目1:自動添加標籤

  這個項目主要介紹如何使用Python傑出的文本處理功能,包括使用正則表達式將純文本文件轉換爲用 HTML或XML等語言標記的文件。

(1) 問題描述

  假設你要將一個文件用作網頁,而給你文件的人嫌麻煩,沒有 以HTML格式編寫它。你不想手工添加需要的所有標籤,想編寫一個程序來自動完成這項工作。大致而言,你的任務是對各種文本元素(如標題和突出的文本)進行分類,再清晰地標記它 們。就這裏的問題而言,你將給文本添加HTML標記,得到可作爲網頁的文檔,讓Web瀏覽器能 夠顯示它。然而,創建基本引擎後,完全可以添加其他類型的標記(如各種形式的XML和LATEX 編碼)。對文本文件進行分析後,你甚至可以執行其他的任務,如提取所有的標題以製作目錄。

(2) 代碼實現前準備

實現思路:
   - 輸入無需包含人工編碼或標籤
   - 程序需要能夠處理不同的文本塊(如標題、段落和列表項)以及內嵌文本(如突出的文 本和URL)。
   - 雖然這個實現添加的是HTML標籤,但應該很容易對其進行擴展,以支持其他標記語言
有用的工具:
   - 肯定需要讀寫文件,至少要從標準輸入
   - 可能需要迭代輸入行
   - 需要使用一些字符串方法
   - 可能用到一兩個生成器
   - 可能需要模塊re

(3) 簡單實現

分爲兩個步驟

  • 找出文本塊:要找出這些文本塊,一種簡單的方法是,收集空行前的所有行並將它們返回,然後重複這樣 的操作。不需要收集空行,因此不需要返回空文本塊(即多個空行)。另外,必須確保文件的最 後一行爲空行,否則無法確定最後一個文本塊到哪裏結束。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#生成器lines是個簡單的工具,在文件末尾添加一個空行
def lines(file):
    for line in file:
        yield line
    yield '\n'

# 生成器blocks實現了剛纔描述的方法。生成文本塊時,將其包含的所有行合併,
#並將兩端多餘的空白(如列表項縮進和換行符)刪除,得到一個表示文本塊的字符串。
def blocks(file):
    block=[]
    for line in lines(file):
        if line.strip():
            block.append(line)
        elif block:
            yield ''.join(block).strip()
            block=[]

if __name__=='__main__':
    file='../../file_data/test_input.txt'
    with open(file,'r+') as f :
        for line in blocks(f):
            print(line)
  • 添加一些標記:可按如下基本步驟進行:打印一些起始標記、對於每個文本塊,在段落標籤內打印它、打印一些結束標記。假設要將第一個文本塊放在一級標題標籤(h1)內,而不是段 落標籤內。另外,還需將用星號括起的文本改成突出文本(使用標籤em)。這樣程序將更有用一些。 由於已經編寫好了函數blocks。
import sys,re
#引用剛剛編寫的util模塊
from util import *

print('<html><head><title>zzy-python</title><body>')
title = True
file='../../file_data/test_input.txt'
#for block in blocks(sys.stdin) 這裏可以使用標準的輸入,小編爲了方便運行,就本地讀取
with open(file) as f:
    for block in blocks(f):
        re.sub(r'\*(.+?\*)',r'<em>\1</em>',block)
        if title:
            print('<h1>')
            print(block)
            print('</h1>')
            title=False
        else:
            print('<p>')
            print(block)
            print('</p>')
print('</body></html>')

  到這簡單的實現就完成了但是如果要擴展這個原型,該如何辦呢?可在for循環中添加檢查,以確定文本塊是否是標題、列表項等。爲此,需要添加其他的正則表達式,代碼可能很快變得很亂。更重要的是,要讓程序輸出其他格式的代碼(而不是HTML)很難,但是這個項目的目標之一就是能夠輕鬆地添加其他輸出格式。

(4) 完整實現

  爲了提高可擴展性,需提高程序的模塊化程度(將功能放在 獨立的組件中)。要提高模塊化程度,方法之一是採用面向對象設計。這裏我們需要尋找一些抽象,讓程序在變得複雜時也易於管理。下面先來列出一些潛在的組件:
   解析器:添加一個讀取文本並管理其他類的對象。
   規則:對於每種文本塊,都制定一條相應的規則。這些規則能夠檢測不同類型的文本塊 並相應地設置其格式。
   過濾器:使用正則表達式來處理內嵌元素。
   處理程序:供解析器用來生成輸出。每個處理程序都生成不同的標記。
那麼接下來,小編就對這幾個組件,進行詳細介紹:

① 處理程序
  對於每種文本塊,它都提供兩個處理方法:一個用於添加起始標籤,另一個用於添加結束標籤。例如它可能包含用於處理段落的方法start_paragraph和end_paragraph。生成HTML代碼時,可像 下面這樣實現這些方法:

class HTMLRenderer: 
    def start_paragraph(self):
        print('') 
    def end_paragraph(self):
         print('')

對於其他類型的文本塊,添加不同的開始和結束標籤,對於形如連接,**包圍的內容,需要特殊處理,例:

def sub_emphasis(self, match): 
    return '{}'.format(match.group(1))

當然對於簡單的文本內容,我們只需要:

def feed(self, data): 
    print(data)

最後,我們可以創建一個處理程序的父類,負責處理一些管 理性細節。例如:不通過全名調用方法(如start_paragraph---start(selef,name)---調用 ’start_’+ name方法)等等。
② 規則
  處理程序的可擴展性和靈活性都非常高了,該將注意力轉向解析(對文本進行解讀) 了。爲此,我們將規則定義爲獨立的對象,而不像初次實現中那樣使用一條包含各種條件和操作 的大型if語句。規則是供主程序(解析器)使用的。主程序必須根據給定的文本塊選擇合適的規則來對其進 行必要的轉換。換而言之,規則必須具備如下功能。
   - 知道自己適用於那種文本塊(條件)。
   - 對文本塊進行轉換(操作)。
  因此每個規則對象都必須包含兩個方法:condition和action:
方法condition只需要一個參數:待處理的文本塊。它返回一個布爾值,指出當前規則是否 適用於處理指定的文本塊。方法action也將當前文本塊作爲參數,但爲了影響輸出,它還必須能夠訪問處理器對象。

#我們以標題規則爲例:
def condition(self, block):
#如果文本塊符合標題的定義,就返回True;否則返回False。
 def action(self, block, handler):
/**調用諸如handler.start('headline')、handler.feed(block)和handler.end('headline')等方法。
我們不想嘗試其他規則,因此返回True,以結束對當前文本塊的處理。*/

  當然這裏還可以定義一個rule的父類,比如action,condition方法可以在不同的規則中有自己的實現。

③ 過濾器
  由於Handler類包含方法sub,每個過濾器都可用一個正則表達 式和一個名稱(如emphasis或url)來表示。
④ 解析器
  接下來就是應用的核心,Parser類。它使用一個處理程序以及一系列規則和過濾器 將純文本文件轉換爲帶標記的文件(這裏是HTML文件)。
其中包括了:完成準 備工作的構造函數、添加規則的方法、添加過濾器的方法以及對文件進行解析的方法。
⑤ 創建規則和過濾器
  至此,萬事俱備,只欠東風——還沒有創建具體的規則和過濾器。目前絕大部分工作都是在讓規則和過濾器與處理程序一樣靈活。通過使用一組複雜的規則,可處理複雜的文檔,但我們將保持儘可能簡單。只創建分別用於處理題目、其他標題和列表項的規則。應將相連的列表項視爲一個列表,因此還將創建一個處理 整個列表的列表規則。最後,可創建一個默認規則,用於處理段落,即其他規則未處理的所有文本塊。各個不同的複雜文檔的規則已經在代碼塊中解釋。
  最後我們通過正則表達式,添加過濾器,分別找出:出要突出的內容、URL和Email 地址。(https://gitlab.com/ZZY478086819/actualcombatproject
至此我們將以上的內容通過代碼實現,具體代碼小編已經上傳至github上,具體的編寫步驟爲:
處理程序(handlers.py) → 規則(rules.py)→主程序(markup.py)

2. 項目2:繪製圖表

  這個項目主要介紹:用Python創建圖表。具體地說,你將創建一個PDF文件,其中包含的圖表對 從文本文件讀取的數據進行了可視化。雖然常規的電子表格軟件都提供這樣的功能,但Python提 供了更強大的功能。
  PDF介紹:它指的 是可移植的文檔格式(portable document format)。PDF是Adobe開發的一種格式,可表示任何包 含圖形和文本的文檔。不同於Microsoft Word等文檔,PDF文件是不可編輯的,但有適用於大多 數平臺的免費閱讀器軟件。另外,無論在哪種平臺上使用什麼閱讀器來查看,顯示的PDF文件都 相同;而HTML格式則不是這樣的,它要求平臺安裝指定的字體,還必須將圖片作爲獨立的文件 進行傳輸。

(1) 問題描述

  根據不同的文本內容,生成相應的建PDF格式(和其他格式)的圖形和文檔。這個項目主要將根據有關太陽黑子的數據 (來自美國國家海洋和大氣管理局的空間天氣預測中心)創建一個折線圖。創建的程序必須具備如下功能:
   - 從網上下載數據文件
   - 對數據文件進行解析,並提取感興趣的內容
   - 根據這些數據創建PDF圖形

(2) 準備工作

   - 圖形生成包:ReportLab(import reportlab)
   - 測試數據:http://www.swpc.noaa.gov中下載

(3) 簡單實現

  ReportLab由很多部分組成,讓你能夠以多種方式生成輸出。就生成PDF而言,最基本的模塊 是pdfgen,其中的Canvas類包含多個低級繪圖方法。例如,要在名爲c的Canvas上繪製直線,可調 用方法c.line。
  這裏展示一個實例:它在一個100點×100點的PDF圖形中央繪製字符串"Hello, world!"。

from reportlab.graphics.shapes import Drawing,String
from reportlab.graphics import renderPDF

#創建一個指定尺寸的Drawing對象
d=Drawing(100,100)

#再創建具有指定屬性的圖形元素(這裏是一個String對象)
s=String(50,50,'Hello World',textAnchor='middle')
#將圖形元素添加到Drawing對象中
d.add(s)
#以PDF格式渲染Drawing對象,並將結果保存到文件中
renderPDF.drawToFile(d,'hello.pdf','A simple PDF file')

Python權威指南的10個項目(1~5)

(4) 繪製折折線

  爲繪製太陽黑子數據折線圖,需要繪製一些直線。實際上,你需要繪製多條相連的直線。ReportLab提供了一個專門用於完成這種工作的類——PolyLine。
要繪製折線圖,必須爲數據集中的每列數據繪製一條折線。
①這裏先創建出一個太陽黑子圖形程序的第一個原型:

from reportlab.lib import colors
from reportlab.graphics.shapes import *
from reportlab.graphics import renderPDF

# Year Month Predicted High Low
data=[
    (2007, 8, 113.2, 114.2, 112.2),
    (2007, 9, 112.8, 115.8, 109.8),
    (2007, 10, 111.0, 116.0, 106.0),
    (2007, 11, 109.8, 116.8, 102.8),
    (2007, 12, 107.3, 115.3, 99.3),
    (2008, 1, 105.2, 114.2, 96.2),
    (2008, 2, 104.1, 114.1, 94.1),
    (2008, 3, 99.9, 110.9, 88.9),
    (2008, 4, 94.8, 106.8, 82.8),
    (2008, 5, 91.2, 104.2, 78.2),
]
#創建一個指定尺寸的Drawing對象
drawing=Drawing(200,150)

pred=[row[2]-40 for row in data]
high = [row[3]-40 for row in data]
low = [row[4]-40 for row in data]
times=[200*((row[0]+row[1]/12.0)-2007)-110 for row in data]

drawing.add(PolyLine(list(zip(times,pred)), strokeColor=colors.blue))
drawing.add(PolyLine(list(zip(times,high)), strokeColor=colors.blue))
drawing.add(PolyLine(list(zip(times,low)), strokeColor=colors.blue))
drawing.add(String(65,115,'Sunspots',fontSize=18,fillColor=colors.red))
renderPDF.drawToFile(drawing,'report1.pdf','Sunspots')

Python權威指南的10個項目(1~5)
②最終版
這裏爲了方便我們直接讀取本地的文件,測試文件已經放入項目中:Predict.txt
Python權威指南的10個項目(1~5)
具體的項目代碼粘貼在小編的github中!

3. 項目3:萬能的XML

  這個項目的目標是,根據描述各種網頁和目錄的單個XML文件生成完整的網站。
實現目標:

  • 整個網站由單個XML文件描述,該文件包含有關各個網頁和目錄的信息
  • 程序應根據需要創建目錄和網頁
  • 應能夠輕鬆地修改整個網站的設計並根據新的設計重新生成所有網頁

    (1) 問題描述

      在這個項目中,要解決的通用問題是解析(讀取並處理)XML文件。小編之前接到的一個任務就是解析XML提取其中相應的字段,不過使用的java的dome4j解析的XML,雖然過程不復雜,但是我們看看Python有什麼獨到之處。

    (2) 準備工作

      - 使用的SAX解析器去解析XML(from xml.sax import make_parser)
      - 要編寫處理XML文件的程序,必須先設計要使用的XML格式(包含哪些屬性?各個標籤都用來做什麼),相當於XML文件的元數據信息
      這裏有些朋友可能對XML格式不是很瞭解,這裏小編做一個介紹:

    <website>
    <directory>
        <ul>
        </ ul >
    </directory>
    <directory>
        <page  name="index" title="Home Page">
    </directory>
    <h1>title<h1>
    </website>

      這裏的website是一個根標籤,整個XML報告中只有一個。
      director、h1、page、ul則屬於website中的標籤,可能有多個,也可能嵌套。
      name="index" 表示標籤中的屬性的name 和value
      這裏我們只有瞭解一個XML報告中的每個標籤的含義,才能做對應的解析,提取有用的信息。

    (3) 簡單實現

      說了這麼多我們先簡單實現一個解析XML,這裏提供一個文件website.xml。
    (具體文件小編會粘貼到自己的項目中)
    Python權威指南的10個項目(1~5)
    這裏我們通過解析website.xml,創建一個HTML頁面,執行如下任務:
       - 在每個page元素的開頭,打開一個給定名稱的新文件,並在其中寫入合適的HTML首部(包 括指定的標題)。
       - 在每個page元素的末尾,將合適的HTML尾部寫入文件,再將文件關閉。
       - 在page元素內部,遍歷所有的標籤和字符而不修改它們(將其原樣寫入文件)。
       - 在page元素外部,忽略所有的標籤(如website和directory)。

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    from xml.sax.handler import ContentHandler
    from xml.sax import parse
    '''
    這個模塊主要完成:
    簡單的解析這個XML,提取有用信息,重新格式化爲HTML格式,
    最終根據不同page寫入不同的HTML文件中
    '''
    class PageMaker(ContentHandler):
    #跟蹤是否在標籤內部
    passthrough = False
    #標籤的開始
    def startElement(self,name,attrs):
        if name=='page':
            self.passthrough=True
            self.out= open(attrs['name'] + '.html', 'w')  #創建輸出到的HTML文件的名稱
            self.out.write('<html><head>\n')
            #name="index" title="Home Page"
            #attrs['title']提取標籤中屬性的key-value
            self.out.write('<title>{}</title>\n'.format(attrs['title']))
            self.out.write('</head><body>\n')
        elif self.passthrough:  #如果標籤下有嵌套的子標籤
            self.out.write('<' + name)
            for key,val in attrs.items(): #獲取所有屬性
                self.out.write(' {}="{}"'.format(key, val))
            self.out.write('>')
    
    #標籤的結束
    def endElement(self, name):
        if name=='page':
            self.passthrough = False
            self.out.write('\n</body></html>\n')
            self.out.close()
        elif self.passthrough:
            self.out.write('</{}>'.format(name))
    
    #標籤中的內容比如:<h1>123</h1> --- > 123
    def characters(self, content):
        if self.passthrough:self.out.write(content)
    file_path='../../../file_data/website.xml'
    #解析
    parse(file_path,PageMaker())

    解析完成之後在當前目錄下:
    Python權威指南的10個項目(1~5)
    出現這幾個文件,就是解析出來的HTML。
    不知道大家有沒有發現以上代碼的不足之處:
       - 這裏我們在startElement和endElement使用了if判斷語句,這裏我們只處理了一個page標籤,如果要處理的標籤很多,那麼這個if將很長很長
       - HTML代碼時硬編碼
       - 我們查看標籤的時候由一個director標籤,這裏是將不同的page放入不同的目錄中,而以上的代碼最終生成的HTML都在同一個目錄下,這裏我們再次實現時將會改進

(4) 最終版

  這裏由於小編將代碼的各個功能進行了解耦,分不同的功能模塊進行開發,這裏小編將詳細介紹每個步驟具體實現什麼功能,當然最終的代碼小編也會上傳到github中供大家參考。
  鑑於SAX機制低級而簡單,編寫一個混合類來處理管理性細節通常很有幫助。這些管理性細 節包括收集字符數據,管理布爾狀態變量(如passthrough),將事件分派給自定義事件處理程序, 等等。就這個項目而言,狀態和數據處理非常簡單,因此這裏將專注於事件分派。
① 分派器混合類
  與其在標準通用事件處理程序(如startElement)中編寫長長的if語句,不如只編寫自定義 的具體事件處理程序(如startPage)並讓它們自動被調用。你可在一個混合類中實現這種功能, 再通過繼承這個混合類和ContentHandler來創建一個子類。
程序實現的功能:
   - startElement被調用時,如果參數name爲'foo',它應嘗試查找事件處理程序startFoo,並 使用提供給它的屬性調用這個處理程序
   - 同樣,endElement被調用時,如果參數name爲'foo',它應嘗試調用endFoo
   - 如果沒有找到相應的處理程序,這些方法應調用方法defaultStart或defaultEnd。如果沒 有這些默認處理程序,就什麼都不做
簡單案例:

class Dispatcher:
  def startElement(self, name, attrs): 
self.dispatch('start', name, attrs) 
def endElement(self, name): 
self.dispatch('end', name)
def dispatch(self, prefix, name, attrs=None): 
mname = prefix + name.capitalize() #將字符串的第一個字母變成大寫,其他字母變小寫
dname = 'default' + prefix.capitalize() 
method = getattr(self, mname, None) 
if callable(method): args = () 
else: method = getattr(self, dname, None) 
args = name, 
if prefix == 'start': args += attrs,
  if callable(method): method(*args)

②將首部和尾部寫入文件的方法以及默認處理程序
  我們將編寫專門用於將首部和尾部寫入文件的方法,而不在事件處 理程序中直接調用self.out.write。這樣就可通過繼承來輕鬆地重寫這些方法。
簡單案例:

def writeHeader(self, title):
 self.out.write("<html>\n <head>\n <title>")
 self.out.write(title)
 self.out.write("</title>\n </head>\n <body>\n")
def writeFooter(self):
 self.out.write("\n </body>\n</html>\n")

③ 支持目錄
  爲創建必要的目錄,需要使用函數os.makedirs,它在指定的路徑中創建必要的目錄。例如, os.makedirs('foo/bar/baz')在當前目錄下創建目錄foo,再在目錄foo下創建目錄bar,然後在目 錄bar下創建目錄baz。如果目錄foo已經存在,將只創建目錄bar和baz。同樣,如果目錄bar也已經 存在,將只創建目錄baz。然而,如果目錄baz也已經存在,通常將引發異常。爲避免出現這種情 況,我們將關鍵字參數exist_ok設置爲True。另一個很有用的函數是os.path.join,它使用正確 的分隔符(例如,在UNIX中爲/)將多條路徑合而爲一。
例:

def ensureDirectory(self):
 path = os.path.join(*self.directory)
 os.makedirs(path, exist_ok=True)

④ 事件的處理
  這裏需要4個事件處理程序,其中2個用於處理目錄,另外2個用於 處理頁面。目錄處理程序只使用了列表directory和方法ensureDirectory。頁面處理程序使用了方法writeHeader和writeFooter。另外,它們還設置了變量passthrough (以便將XHTML代碼直接寫入文件),而且打開和關閉與頁面相關的文件。

(5) 結果展示

Python權威指南的10個項目(1~5)
通過解析website.xml,得到以上的目錄已經html文件。具體的代碼在項目中,可以自行下載查看!

4. 項目4:新聞彙總

  本項目要編寫的程序是一個信息收集代理,能夠替你收集信息(具體地說是新聞)並生成新聞 彙總。在這個項目中,需要做的並 僅僅使用urllib下載文件,還將使用另一個網絡庫,即nntplib,它使用起來要難些。另外,還需重構程序以支持不同的新聞源和目的地,進而在中間層使用主引擎將前端和後端分開。
  最終項目實現的目標:
  - 可輕鬆地添加新聞源(乃至不同類型的新聞源) 能夠從衆多不同的新聞源收集新聞
  - 能夠以衆多不同的格式將生成的新聞彙編分發到衆多不同的目的地
  - 能夠輕鬆地添加新的目的地(乃至不同類型的目的地)

(1) 知識點擴展

  NNTP是一種標準網絡協議,用於管理在Usenet討論組中發佈的消息。NNTP服務器組成了一 個統一管理新聞組的全局網絡,通過NNTP客戶端(也稱爲新聞閱讀器)可發佈和閱讀消息。NNTP 服務器組成的主網絡稱爲Usenet,創建於1980年(但NNTP協議到1985年纔開始使用)。相比於最 新的Web潮流,這算是一種很古老的技術了,但從某種程度上說,互聯網的很大一部分都基於這 樣的古老技術。

(2) 工作準備

  • Nntplib類庫(from nntplib import NNTP)

(3) 初次實現

  最先開發出來一個簡單的版本:是從NNTP服務器上的新聞組下載 最新的消息,使用print直接將結果打印到標準輸出。

'''
一個簡單的新聞收集代理
'''

from nntplib import NNTP
#服務器域名
servername='news.gmane.org'
#指定新聞組設置爲當前新聞組,並返回一些有關該新聞組的信息
group='gmane.comp.python.committers'
#創建server客戶端對象
server=NNTP(servername)
#指定要獲取多少篇文章
howmany=10
#返回的值爲通用的服務器響應、新聞組包含的消息數、第一條和最後一條消息的編號以及新聞組的名稱
resp, count, first, last, name = server.group(group)
start = last-howmany+1

resp,overviews=server.over((start,last))

#從overview中提取主題,並使用ID從服務器獲取消息正文
for id,over in overviews:
    subject=over['subject']
    resp,info=server.body(id)
    print(subject)
    print('-'*len(subject))
    for line in info.lines:
        #消息正文行是以字節的方式返回的,但爲簡單起見,我們直接使用編碼Latin-1
        print(line.decode('latin1'))
    print()

#關閉連接
server.quit()

(4) 最終版

  這次我們將對代碼稍作重構以修復這種問題。你將把各部分代碼放在類和方法中,以提高程序的結構化程 度和抽象程度,這樣就可用其他類替換有些部分。
  統計一下我們大概需要哪些類::信息、 代理、新聞、彙總、網絡、新聞源、目的地、前端、後端和主引擎。這個名詞清單表明,需要下 面這些主要的類:NewsAgent、NewsItem、Source和Destination。
  各種新聞源構成了前端,目的地構成了後端,而新聞代理位於中間層。這裏我們對每個類進行詳細的說明:
① NewsItem
它只表示一段數據,其中包括標題和正文。

class NewsItem:
    def __init__(self, title, body):
        self.title = title
        self.body = body

② NewsAgent
  準確地確定要從新聞源和新聞目的地獲取什麼,先來編寫代理本身是個不錯的主意。代理 必須維護兩個列表:源列表和目的地列表。添加源和目的地的工作可通過方法addSource和 addDestination來完成。然後就是將新聞從源分發到目的地的方法。
③ Destination
   - 生成的文本爲HTML。
   - 將文本寫入文件而不是標準輸出中。
   - 除新聞列表外,還創建了一個目錄。
④ Source
   - 代碼封裝在方法getItems中。原來的變量servername和group現在是構造函數的參數。另 外,變量howmany也變成了構造函數的參數。
   - 調用了decode_header,它負責處理報頭字段(如subject)使用的特殊編碼。
   - 不是直接打印每條新聞,而是生成NewsItem對象(讓getItems變成了生成器)。
   總的來說就是:通過NewsItem將從網頁上獲取的新聞的內容和標題存放起來,這裏我們設置兩個數據源:一個是NNTP中獲取的新聞,一個是從urlopen從web網站中獲取的新聞,然後設置了兩個數據的目的地:一個是控制檯輸出,一個是寫入HTML文件中。通過NewsAgent對象,將數據源和目的地加入到列表中,然後在其distribute方法中,把從數據源獲取的數據發送給目的地。最後通過一個run方法,將這些步驟串聯起來,這樣就實現了一個簡單的從不同的渠道中獲取新聞,轉發的不同的渠道去。

5. 項目5:虛擬茶話會

   在這個項目中,將做些正式的網絡編程工作:編寫一個聊天服務器,讓人們能夠通過 網絡實時地聊天。只使用標準庫中的異步網絡 編程模塊(asyncore和asynchat)。

(1) 問題描述

大概的項目需求如下:

  • 服務器必須能夠接受不同用戶的多個連接。
  • 它必須允許用戶並行地操作。
  • 它必須能夠解讀命令,如say或logout。
  • 它必須易於擴展。
    其中的網絡連接和程序的異步特徵需要使用特殊工具來實現。

    (2) 工作準備

       - 需要用到的新工具:標準庫模塊asyncore及其相關的模塊asynchat
       - 框架asyncore讓你能夠處理多個同時連接的用戶
       - 計算機的IP和port:本項目中使用本機的IP和5005端口

(3) 初步實現

  我們來將程序稍做分解。需要創建兩個主要的類:一個表示聊天服務器,另一個表示聊天會 話(連接的用戶)。
① ChatServer 類

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from asyncore import dispatcher
import socket,asyncore

'''
一個能夠接受連接的服務器
'''

PORT=5005
NAME = 'TestChat'

'''
爲創建簡單的ChatServer類,可繼承模塊asyncore中的dispatcher類。dispatcher類基本上是
一個套接字對象,但還提供了一些事件處理功能。
'''
class ChatServer(dispatcher):
    '''
    一個接受連接並創建會話的類。它還負責向這些會話廣播
    '''
    def __init__(self,port):
        dispatcher.__init__(self)
        #調用了create_socket,並通過傳入兩個參數指定了要創建的套接字類型,通常都使用這裏使用的類型
        self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
        '''
            調用了set_reuse_addr,讓你能夠重用原來的地址(具體地說是端口號),
            即便未妥善關閉服務器亦如此。不會出現端口被佔用情況
        '''
        self.set_reuse_addr()
        '''
            bind的調用將服務器關聯到特定的地址(主機名和端口)。 
            空字符串表示:localhost,或者說當前機器的所有接口
        '''
        self.bind('',port)
        #listen的調用讓服務器監聽連接;它還將在隊列中等待的最大連接數指定爲5。
        self.listen(5)
    def handle_accept(self):
        '''
        重寫事件處理方法handle_accept,讓它在服務器接受客戶端連接時做些事情
        '''
        #調用self.accept,以允許客戶端連接。
        #返回一個連接(客戶端對應的套接字)和一個地址(有關發起連接的機器的信息)。
        conn,addr=self.accept()
        #addr[0]是客戶端的IP地址
        print('Connection attempt from',addr[0])
if __name__=='__main__':
    s=ChatServer(PORT)
    try:
        #啓動服務器的監聽循環
        asyncore.loop()
    except KeyboardInterrupt:
        pass

② ChatSession 類
  這是一個新的版本,這裏我們使用asynchat,我們設置一個會話,每一次有一個連接對象時,就將這個連接對象加入會話中,好處是:每個連接都會創建一個新的dispatcher對象。

'''
包含ChatSession類的服務器程序
'''

from asyncore import dispatcher
from asynchat import async_chat
import socket,asyncore

PORT=5005

class ChatSession(async_chat):
    def __init__(self,socket):
        async_chat.__init__(self,socket)
        #設置結束符,
        self.set_terminator("\r\n")
        self.data=[]

    #從套接字讀取一些文本
    def collect_incoming_data(self, data):
        self.data.append(data)

    #讀取到結束符時將調用found_terminator
    def found_terminator(self):
        line=''.join(self.data)
        self.data=[]
        #使用line做些事情……
        print(line)

class ChatServer(dispatcher):
    def __init__(self,port):
        dispatcher.__init__()
        self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind("",port)
        self.listen(5)
       #ChatServer存儲了一個會話列表
        self.sessions=[]
    #接受一個新請求,就會創建一個新的ChatSession對象,並將其附加到會話列表末尾
    def handle_accept(self):
        conn,addr=self.accept()
        self.sessions.append(ChatSession(conn))

if __name__=='__main__':
    s=ChatServer(PORT)
    try:
        asyncore.loop()
    except KeyboardInterrupt:
        print()

③ 整合
  要讓原型成爲簡單而功能完整的聊天服務器,還需添加一項主要功能:將用戶所說的內容(他 們輸入的每一行)廣播給其他用戶。要實現這種功能,可在服務器中使用一個簡單的for循環來 遍歷會話列表,並將內容行寫入每個會話。要將數據寫入async_chat對象,可使用方法push。
  這種廣播行爲也帶來了一個問題:客戶端斷開連接後,你必須確保將其從會話列表中刪除。 爲此,可重寫事件處理方法handle_close。

from asyncore import dispatcher
from asynchat import async_chat
import socket,asyncore

PORT = 5005
NAME = 'TestChat'

class ChatSession(async_chat):
    """
    一個負責處理服務器和單個用戶間連接的類
    """
    def __init__(self,server,sock):
        #標準的設置任務
        async_chat.__init__(self,sock)
        self.server=server
        self.set_terminator("\r\n")
        self.data=[]
        #問候用戶:
        self.push(("Welcome to %s \r\n" % self.server.name).encode())

    def collect_incoming_data(self, data):
        self.data.append(data.decode())

    def found_terminator(self):
        """
       如果遇到結束符,就意味着讀取了一整行,
       因此將這行內容廣播給每個人
        """
        line=''.join(self.data)
        self.data=[]
        self.server.broadcast(line)
    #客戶端斷開之後,將會話從列表中刪除
    def handle_close(self):
        async_chat.handle_close(self)
        self.server.disconnect(self)

class ChatServer(dispatcher):
    """
     一個接受連接並創建會話的類。它還負責向這些會話廣播
    """
    def __init__(self,port,name):
        dispatcher.__init__(self) #這一行一定要加
        self.name = name
        #標準的設置任務:
        self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind(('',port))
        self.listen(5)

        self.sessions=[]

    def disconnect(self,session):
        self.sessions.remove(session)

    def broadcast(self,line):
        for session in self.sessions:
            session.push((line+"\r\n").encode())

    def handle_accept(self):
        conn,addr=self.accept()
        self.sessions.append(ChatSession(self,conn))

if __name__ == '__main__':
    s=ChatServer(PORT,NAME)
    try:
        asyncore.loop()
    except KeyboardInterrupt:
        print

(4) 最終版本

  第一個版本雖然是個管用的聊天服務器,但其功能很有限,最明顯的缺陷是沒法知道每句話 都是誰說的。另外,它也不能解釋命令(如say或logout),而最初的規範要求提供這樣的功能。 有鑑於此,需要添加對身份(每個用戶都有唯一的名字)和命令解釋的支持,同時必須讓每個會 話的行爲都依賴於其所處的狀態(剛連接、已登錄等)。添加這些功能時,必須確保程序是易於擴展的。
① 基本命令解釋功能
  這裏我們可以定義一些簡單的命令,比如say、login 等等,即如果發送:say Hello, world!
將調用do_say('Hello, world!'),這個功能如何實現呢,這裏寫一段僞代碼:

#基本的命令解釋功能,例如:say Hello, world!
class CommandHandler:
    '''
        類似於標準庫中cmd.Cmd的簡單命令處理程序
    '''
    #參數不正確
    def unknown(self,session,cmd):
        session.push('Unknown command: {}s\r\n'.format(cmd).encode())
    #根據命令,匹配方法,調用
    def handler(self,session,line):
        if not line.strip():return
        parts=line.split(' ',1)
        cmd=parts[0]
        try:
            line=parts[1].strip()
        except IndexError:
            line=''
        meth = getattr(self, 'do_' + cmd, None)
        try:
            meth(session,line)
        except TypeError:
            self.unknown(session,cmd)
    def do_say(self,session,line):
        session.push(line.encode())

② 聊天室
  每個聊天室都是一個包含特定命令的CommandHandler。另外,它還應 記錄聊天室內當前有哪些用戶(會話)。除基本方法add和remove外,它還包含方法broadcast,這個方法對聊天室內的所有用戶(會 話)調用push。這個類還以方法do_logout的方式定義了一個命令——logout。這個方法引發異常 EndSession,而這種異常將在較高的層級(found_terminator中)處理。
僞代碼:

class EndSession(Exception):pass
class Room(CommandHandler):
    """
    可包含一個或多個用戶(會話)的通用環境。
    它負責基本的命令處理和廣播
    """
    def __init__(self,server):
        self.server=server
        self.sessions=[]
    def add(self,session):
        self.sessions.append(session)
    def remove(self,session):
        self.sessions.remove(session)

    def broadcast(self,line):
        for session in self.sessions:
            session.push(line.encode())
    def do_logout(self,session,line):
        raise EndSession

③ 登錄和退出聊天室
  除表示常規聊天室(這個項目中只有一個這樣的聊天室)之外,Room的子類還可表示其他狀 態,這正是你創建Room類的意圖所在。例如,用戶剛連接到服務器時,將進入專用的LoginRoom (其中沒有其他用戶)。LoginRoom在用戶進入時打印一條歡迎消息(這是在方法add中實現的)。 它還重寫了方法unknown,使其讓用戶登錄。這個類只支持一個命令,即命令login,這個命令檢 查用戶名是否是可接受的(不是空字符串,且未被其他用戶使用)。
  LogoutRoom要簡單得多,它唯一的職責是將用戶的名字從服務器中刪除(服務器包含存儲會 話的字典users)。如果用戶名不存在(因爲用戶從未登錄),將忽略因此而引發的KeyError異常。
④ 主聊天室
  主聊天室也重寫了方法add和remove。在方法add中,它廣播一條消息,指出有用戶進入,同 時將用戶的名字添加到服務器中的字典users中。方法remove廣播一條消息,指出有用戶離開。
除了這些方法以外,主聊天室還實現了:
  - 命令say(由方法do_say實現)廣播一行內容,並在開頭指出這行內容是哪位用戶說的。
  - 命令look(由方法do_look實現)告訴用戶聊天室內當前有哪些用戶。
  - 命令who(由方法do_who實現)告訴用戶當前有哪些用戶登錄了。在這個簡單的服務器中, 命令look和who的作用相同,但如果你對其進行擴展,使其包含多個聊天室,這兩個命令 的作用將有所區別。
最終實現
  - ChatSession新增了方法enter,用於進入新的聊天室。
  - ChatSession的構造函數使用了LoginRoom。
  -方法handle_close使用了LogoutRoom。
  - ChatServer的構造函數新增了字典屬性users和ChatRoom屬性main_room。

(5) 結果展示

  好吧,小編也是根據指南一步一步的將代碼實現了,但是不知道爲啥就是跑不成功,然後就從網上搜了搜如何解決,雖然也查到了相關的案例,神奇的事情發生,我copy多個某某大神的代碼,居然運行不了,而且報出同樣的錯誤,本來想解決一下,造福大家,但是小編能力有限,實在不知道如何下手,這裏小編把錯誤展示出來,有牛X的大神看見了幫小編分析解決一下唄!
Python權威指南的10個項目(1~5)
  但是 但是,雖然程序沒運行出來,但是至少學到了一些東西,總不能只知道代碼錯了,不知道代碼就行實現了啥,對不對,那不是欺騙了各位讀友嘛,所以小編這裏把上面代碼的整個實現過程畫了一個圖分享給大家:
Python權威指南的10個項目(1~5)

這個是Python權威指南的前5個項目,雖然後面了沒有實現效果圖,但是代碼和解釋是相當充分的,後續的5個項目均有呈現的效果和完整的代碼,大家放心小編在寫代碼時也踩了不少的坑,有些問題小編會以小案例的形式在測試代碼中體現:

代碼地址:https://gitlab.com/ZZY478086819/actualcombatproject

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章