引言:
我相信學習Python過的朋友,必定會喜歡上這門語言,簡單,庫多,易上手,學習成本低,可是若是是學習以後,不常用,或者工做中暫時用不到,那麼不久以後又會忘記,長此以往,就浪費了不少的時間再本身的「曾經」會的東西上。因此最好的方法就是實戰,經過真是的小型項目,去鞏固,理解,深刻Python,一樣的長此以往就不會忘記。
因此這裏小編帶你們編寫10個小型項目,去真正的實操Python,這10個小型項目是來自《Python權威指南》中後面10個章節的項目,有興趣的朋友能夠自行閱讀。但願這篇文章能成爲給你們在Python的學習道路上的奠定石。
建議你們是一邊看代碼,一邊學習,文章中會對代碼進行解釋:
這裏是項目的gitlab地址(全代碼):html
這個項目主要介紹如何使用Python傑出的文本處理功能,包括使用正則表達式將純文本文件轉換爲用 HTML或XML等語言標記的文件。前端
假設你要將一個文件用做網頁,而給你文件的人嫌麻煩,沒有 以HTML格式編寫它。你不想手工添加須要的全部標籤,想編寫一個程序來自動完成這項工做。大體而言,你的任務是對各類文本元素(如標題和突出的文本)進行分類,再清晰地標記它 們。就這裏的問題而言,你將給文本添加HTML標記,獲得可做爲網頁的文檔,讓Web瀏覽器能 夠顯示它。然而,建立基本引擎後,徹底能夠添加其餘類型的標記(如各類形式的XML和LATEX 編碼)。對文本文件進行分析後,你甚至能夠執行其餘的任務,如提取全部的標題以製做目錄。java
實現思路:
- 輸入無需包含人工編碼或標籤
- 程序須要可以處理不一樣的文本塊(如標題、段落和列表項)以及內嵌文本(如突出的文 本和URL)。
- 雖然這個實現添加的是HTML標籤,但應該很容易對其進行擴展,以支持其餘標記語言
有用的工具:
- 確定須要讀寫文件,至少要從標準輸入
- 可能須要迭代輸入行
- 須要使用一些字符串方法
- 可能用到一兩個生成器
- 可能須要模塊repython
分爲兩個步驟:git
#!/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)
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)很難,可是這個項目的目標之一就是可以輕鬆地添加其餘輸出格式。github
爲了提升可擴展性,需提升程序的模塊化程度(將功能放在 獨立的組件中)。要提升模塊化程度,方法之一是採用面向對象設計。這裏咱們須要尋找一些抽象,讓程序在變得複雜時也易於管理。下面先來列出一些潛在的組件:
解析器:添加一個讀取文本並管理其餘類的對象。
規則:對於每種文本塊,都制定一條相應的規則。這些規則可以檢測不一樣類型的文本塊 並相應地設置其格式。
過濾器:使用正則表達式來處理內嵌元素。
處理程序:供解析器用來生成輸出。每一個處理程序都生成不一樣的標記。
那麼接下來,小編就對這幾個組件,進行詳細介紹:web
① 處理程序
對於每種文本塊,它都提供兩個處理方法:一個用於添加起始標籤,另外一個用於添加結束標籤。例如它可能包含用於處理段落的方法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)
這個項目主要介紹:用Python建立圖表。具體地說,你將建立一個PDF文件,其中包含的圖表對 從文本文件讀取的數據進行了可視化。雖然常規的電子表格軟件都提供這樣的功能,但Python提 供了更強大的功能。
PDF介紹:它指的 是可移植的文檔格式(portable document format)。PDF是Adobe開發的一種格式,可表示任何包 含圖形和文本的文檔。不一樣於Microsoft Word等文檔,PDF文件是不可編輯的,但有適用於大多 數平臺的免費閱讀器軟件。另外,不管在哪一種平臺上使用什麼閱讀器來查看,顯示的PDF文件都 相同;而HTML格式則不是這樣的,它要求平臺安裝指定的字體,還必須將圖片做爲獨立的文件 進行傳輸。
根據不一樣的文本內容,生成相應的建PDF格式(和其餘格式)的圖形和文檔。這個項目主要將根據有關太陽黑子的數據 (來自美國國家海洋和大氣管理局的空間天氣預測中心)建立一個折線圖。建立的程序必須具有以下功能:
- 從網上下載數據文件
- 對數據文件進行解析,並提取感興趣的內容
- 根據這些數據建立PDF圖形
- 圖形生成包:ReportLab(import reportlab)
- 測試數據:http://www.swpc.noaa.gov中下載
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')
爲繪製太陽黑子數據折線圖,須要繪製一些直線。實際上,你須要繪製多條相連的直線。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')
②最終版
這裏爲了方便咱們直接讀取本地的文件,測試文件已經放入項目中:Predict.txt
具體的項目代碼粘貼在小編的github中!
這個項目的目標是,根據描述各類網頁和目錄的單個XML文件生成完整的網站。
實現目標:
應可以輕鬆地修改整個網站的設計並根據新的設計從新生成全部網頁
在這個項目中,要解決的通用問題是解析(讀取並處理)XML文件。小編以前接到的一個任務就是解析XML提取其中相應的字段,不過使用的java的dome4j解析的XML,雖然過程不復雜,可是咱們看看Python有什麼獨到之處。
- 使用的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、h一、page、ul則屬於website中的標籤,可能有多個,也可能嵌套。
name="index" 表示標籤中的屬性的name 和value
這裏咱們只有瞭解一個XML報告中的每一個標籤的含義,才能作對應的解析,提取有用的信息。
說了這麼多咱們先簡單實現一個解析XML,這裏提供一個文件website.xml。
(具體文件小編會粘貼到本身的項目中)
這裏咱們經過解析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())
解析完成以後在當前目錄下:
出現這幾個文件,就是解析出來的HTML。
不知道你們有沒有發現以上代碼的不足之處:
- 這裏咱們在startElement和endElement使用了if判斷語句,這裏咱們只處理了一個page標籤,若是要處理的標籤不少,那麼這個if將很長很長
- HTML代碼時硬編碼
- 咱們查看標籤的時候由一個director標籤,這裏是將不一樣的page放入不一樣的目錄中,而以上的代碼最終生成的HTML都在同一個目錄下,這裏咱們再次實現時將會改進
這裏因爲小編將代碼的各個功能進行了解耦,分不一樣的功能模塊進行開發,這裏小編將詳細介紹每一個步驟具體實現什麼功能,固然最終的代碼小編也會上傳到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代碼直接寫入文件),並且打開和關閉與頁面相關的文件。
經過解析website.xml,獲得以上的目錄已經html文件。具體的代碼在項目中,能夠自行下載查看!
本項目要編寫的程序是一個信息收集代理,可以替你收集信息(具體地說是新聞)並生成新聞 彙總。在這個項目中,須要作的並 僅僅使用urllib下載文件,還將使用另外一個網絡庫,即nntplib,它使用起來要難些。另外,還需重構程序以支持不一樣的新聞源和目的地,進而在中間層使用主引擎將前端和後端分開。
最終項目實現的目標:
- 可輕鬆地添加新聞源(乃至不一樣類型的新聞源) 可以從衆多不一樣的新聞源收集新聞
- 可以以衆多不一樣的格式將生成的新聞彙編分發到衆多不一樣的目的地
- 可以輕鬆地添加新的目的地(乃至不一樣類型的目的地)
NNTP是一種標準網絡協議,用於管理在Usenet討論組中發佈的消息。NNTP服務器組成了一 個統一管理新聞組的全局網絡,經過NNTP客戶端(也稱爲新聞閱讀器)可發佈和閱讀消息。NNTP 服務器組成的主網絡稱爲Usenet,建立於1980年(但NNTP協議到1985年纔開始使用)。相比於最 新的Web潮流,這算是一種很古老的技術了,但從某種程度上說,互聯網的很大一部分都基於這 樣的古老技術。
最早開發出來一個簡單的版本:是從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()
此次咱們將對代碼稍做重構以修復這種問題。你將把各部分代碼放在類和方法中,以提升程序的結構化程 度和抽象程度,這樣就可用其餘類替換有些部分。
統計一下咱們大概須要哪些類::信息、 代理、新聞、彙總、網絡、新聞源、目的地、前端、後端和主引擎。這個名詞清單代表,須要下 面這些主要的類: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方法,將這些步驟串聯起來,這樣就實現了一個簡單的從不一樣的渠道中獲取新聞,轉發的不一樣的渠道去。
在這個項目中,將作些正式的網絡編程工做:編寫一個聊天服務器,讓人們可以經過 網絡實時地聊天。只使用標準庫中的異步網絡 編程模塊(asyncore和asynchat)。
大概的項目需求以下:
- 須要用到的新工具:標準庫模塊asyncore及其相關的模塊asynchat
- 框架asyncore讓你可以處理多個同時鏈接的用戶
- 計算機的IP和port:本項目中使用本機的IP和5005端口
咱們來將程序稍作分解。須要建立兩個主要的類:一個表示聊天服務器,另外一個表示聊天會 話(鏈接的用戶)。
① 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
第一個版本雖然是個管用的聊天服務器,但其功能頗有限,最明顯的缺陷是無法知道每句話 都是誰說的。另外,它也不能解釋命令(如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。
好吧,小編也是根據指南一步一步的將代碼實現了,可是不知道爲啥就是跑不成功,而後就從網上搜了搜如何解決,雖然也查到了相關的案例,神奇的事情發生,我copy多個某某大神的代碼,竟然運行不了,並且報出一樣的錯誤,原本想解決一下,造福你們,可是小編能力有限,實在不知道如何下手,這裏小編把錯誤展現出來,有牛X的大神看見了幫小編分析解決一下唄!
可是 可是,雖然程序沒運行出來,可是至少學到了一些東西,總不能只知道代碼錯了,不知道代碼就行實現了啥,對不對,那不是欺騙了各位讀友嘛,因此小編這裏把上面代碼的整個實現過程畫了一個圖分享給你們:
這個是Python權威指南的前5個項目,雖而後面了沒有實現效果圖,可是代碼和解釋是至關充分的,後續的5個項目均有呈現的效果和完整的代碼,你們放心小編在寫代碼時也踩了很多的坑,有些問題小編會以小案例的形式在測試代碼中體現: