您好,登錄后才能下訂單哦!
引言:
??我相信學習Python過的朋友,一定會喜歡上這門語言,簡單,庫多,易上手,學習成本低,但是如果是學習之后,不經常使用,或者工作中暫時用不到,那么不久之后又會忘記,久而久之,就浪費了很多的時間再自己的“曾經”會的東西上。所以最好的方法就是實戰,通過真是的小型項目,去鞏固,理解,深入Python,同樣的久而久之就不會忘記。
??所以這里小編帶大家編寫10個小型項目,去真正的實操Python,這10個小型項目是來自《Python權威指南》中后面10個章節的項目,有興趣的朋友可以自行閱讀。希望這篇文章能成為給大家在Python的學習道路上的奠基石。
??建議大家是一邊看代碼,一邊學習,文章中會對代碼進行解釋:
這里是項目的gitlab地址(全代碼):
??這個項目主要介紹如何使用Python杰出的文本處理功能,包括使用正則表達式將純文本文件轉換為用 HTML或XML等語言標記的文件。
??假設你要將一個文件用作網頁,而給你文件的人嫌麻煩,沒有 以HTML格式編寫它。你不想手工添加需要的所有標簽,想編寫一個程序來自動完成這項工作。大致而言,你的任務是對各種文本元素(如標題和突出的文本)進行分類,再清晰地標記它 們。就這里的問題而言,你將給文本添加HTML標記,得到可作為網頁的文檔,讓Web瀏覽器能 夠顯示它。然而,創建基本引擎后,完全可以添加其他類型的標記(如各種形式的XML和LATEX 編碼)。對文本文件進行分析后,你甚至可以執行其他的任務,如提取所有的標題以制作目錄。
實現思路:
?? - 輸入無需包含人工編碼或標簽
?? - 程序需要能夠處理不同的文本塊(如標題、段落和列表項)以及內嵌文本(如突出的文 本和URL)。
?? - 雖然這個實現添加的是HTML標簽,但應該很容易對其進行擴展,以支持其他標記語言
有用的工具:
?? - 肯定需要讀寫文件,至少要從標準輸入
?? - 可能需要迭代輸入行
?? - 需要使用一些字符串方法
?? - 可能用到一兩個生成器
?? - 可能需要模塊re
分為兩個步驟:
#!/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('<h2>')
print(block)
print('</h2>')
title=False
else:
print('<p>')
print(block)
print('</p>')
print('</body></html>')
??到這簡單的實現就完成了但是如果要擴展這個原型,該如何辦呢?可在for循環中添加檢查,以確定文本塊是否是標題、列表項等。為此,需要添加其他的正則表達式,代碼可能很快變得很亂。更重要的是,要讓程序輸出其他格式的代碼(而不是HTML)很難,但是這個項目的目標之一就是能夠輕松地添加其他輸出格式。
??為了提高可擴展性,需提高程序的模塊化程度(將功能放在 獨立的組件中)。要提高模塊化程度,方法之一是采用面向對象設計。這里我們需要尋找一些抽象,讓程序在變得復雜時也易于管理。下面先來列出一些潛在的組件:
?? 解析器:添加一個讀取文本并管理其他類的對象。
?? 規則:對于每種文本塊,都制定一條相應的規則。這些規則能夠檢測不同類型的文本塊 并相應地設置其格式。
?? 過濾器:使用正則表達式來處理內嵌元素。
?? 處理程序:供解析器用來生成輸出。每個處理程序都生成不同的標記。
那么接下來,小編就對這幾個組件,進行詳細介紹:
① 處理程序
??對于每種文本塊,它都提供兩個處理方法:一個用于添加起始標簽,另一個用于添加結束標簽。例如它可能包含用于處理段落的方法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>
<h2>title<h2>
</website>
這里的website是一個根標簽,整個XML報告中只有一個。
director、h2、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))
#標簽中的內容比如:<h2>123</h2> --- > 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個項目均有呈現的效果和完整的代碼,大家放心小編在寫代碼時也踩了不少的坑,有些問題小編會以小案例的形式在測試代碼中體現:
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。