引言:
?我相信學習Python過的朋友,一定會喜歡上這門語言,簡單,庫多,易上手,學習成本低,但是如果是學習之后,不經(jīng)常使用,或者工作中暫時用不到,那么不久之后又會忘記,久而久之,就浪費了很多的時間再自己的“曾經(jīng)”會的東西上。所以最好的方法就是實戰(zhàn),通過真是的小型項目,去鞏固,理解,深入Python,同樣的久而久之就不會忘記。
?所以這里小編帶大家編寫10個小型項目,去真正的實操Python,這10個小型項目是來自《Python權威指南》中后面10個章節(jié)的項目,有興趣的朋友可以自行閱讀。希望這篇文章能成為給大家在Python的學習道路上的奠基石。
?建議大家是一邊看代碼,一邊學習,文章中會對代碼進行解釋:
這里是項目的gitlab地址(全代碼):
創(chuàng)新互聯(lián)是專業(yè)的昭化網(wǎng)站建設公司,昭化接單;提供成都網(wǎng)站建設、網(wǎng)站建設,網(wǎng)頁設計,網(wǎng)站設計,建網(wǎng)站,PHP網(wǎng)站建設等專業(yè)做網(wǎng)站服務;采用PHP框架,可快速的進行昭化網(wǎng)站開發(fā)網(wǎng)頁制作和功能擴展;專業(yè)做搜索引擎喜愛的網(wǎng)站,專業(yè)的做網(wǎng)站團隊,希望更多企業(yè)前來合作!
??這個項目主要介紹如何使用Python杰出的文本處理功能,包括使用正則表達式將純文本文件轉換為用 HTML或XML等語言標記的文件。
??假設你要將一個文件用作網(wǎng)頁,而給你文件的人嫌麻煩,沒有 以HTML格式編寫它。你不想手工添加需要的所有標簽,想編寫一個程序來自動完成這項工作。大致而言,你的任務是對各種文本元素(如標題和突出的文本)進行分類,再清晰地標記它 們。就這里的問題而言,你將給文本添加HTML標記,得到可作為網(wǎng)頁的文檔,讓Web瀏覽器能 夠顯示它。然而,創(chuàng)建基本引擎后,完全可以添加其他類型的標記(如各種形式的XML和LATEX 編碼)。對文本文件進行分析后,你甚至可以執(zhí)行其他的任務,如提取所有的標題以制作目錄。
實現(xiàn)思路:
? - 輸入無需包含人工編碼或標簽
? - 程序需要能夠處理不同的文本塊(如標題、段落和列表項)以及內嵌文本(如突出的文 本和URL)。
? - 雖然這個實現(xiàn)添加的是HTML標簽,但應該很容易對其進行擴展,以支持其他標記語言
有用的工具:
? - 肯定需要讀寫文件,至少要從標準輸入
? - 可能需要迭代輸入行
? - 需要使用一些字符串方法
? - 可能用到一兩個生成器
? - 可能需要模塊re
分為兩個步驟:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#生成器lines是個簡單的工具,在文件末尾添加一個空行
def lines(file):
for line in file:
yield line
yield '\n'
# 生成器blocks實現(xiàn)了剛才描述的方法。生成文本塊時,將其包含的所有行合并,
#并將兩端多余的空白(如列表項縮進和換行符)刪除,得到一個表示文本塊的字符串。
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('zzy-python ')
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'\1',block)
if title:
print('')
print(block)
print('
')
title=False
else:
print('')
print(block)
print('
')
print('')
??到這簡單的實現(xiàn)就完成了但是如果要擴展這個原型,該如何辦呢?可在for循環(huán)中添加檢查,以確定文本塊是否是標題、列表項等。為此,需要添加其他的正則表達式,代碼可能很快變得很亂。更重要的是,要讓程序輸出其他格式的代碼(而不是HTML)很難,但是這個項目的目標之一就是能夠輕松地添加其他輸出格式。
??為了提高可擴展性,需提高程序的模塊化程度(將功能放在 獨立的組件中)。要提高模塊化程度,方法之一是采用面向對象設計。這里我們需要尋找一些抽象,讓程序在變得復雜時也易于管理。下面先來列出一些潛在的組件:
? 解析器:添加一個讀取文本并管理其他類的對象。
? 規(guī)則:對于每種文本塊,都制定一條相應的規(guī)則。這些規(guī)則能夠檢測不同類型的文本塊 并相應地設置其格式。
? 過濾器:使用正則表達式來處理內嵌元素。
? 處理程序:供解析器用來生成輸出。每個處理程序都生成不同的標記。
那么接下來,小編就對這幾個組件,進行詳細介紹:
① 處理程序
?對于每種文本塊,它都提供兩個處理方法:一個用于添加起始標簽,另一個用于添加結束標簽。例如它可能包含用于處理段落的方法start_paragraph和end_paragraph。生成HTML代碼時,可像 下面這樣實現(xiàn)這些方法:
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)
最后,我們可以創(chuàng)建一個處理程序的父類,負責處理一些管 理性細節(jié)。例如:不通過全名調用方法(如start_paragraph---start(selef,name)---調用 ’start_’+ name方法)等等。
② 規(guī)則
?處理程序的可擴展性和靈活性都非常高了,該將注意力轉向解析(對文本進行解讀) 了。為此,我們將規(guī)則定義為獨立的對象,而不像初次實現(xiàn)中那樣使用一條包含各種條件和操作 的大型if語句。規(guī)則是供主程序(解析器)使用的。主程序必須根據(jù)給定的文本塊選擇合適的規(guī)則來對其進 行必要的轉換。換而言之,規(guī)則必須具備如下功能。
? - 知道自己適用于那種文本塊(條件)。
? - 對文本塊進行轉換(操作)。
?因此每個規(guī)則對象都必須包含兩個方法:condition和action:
方法condition只需要一個參數(shù):待處理的文本塊。它返回一個布爾值,指出當前規(guī)則是否 適用于處理指定的文本塊。方法action也將當前文本塊作為參數(shù),但為了影響輸出,它還必須能夠訪問處理器對象。
#我們以標題規(guī)則為例:
def condition(self, block):
#如果文本塊符合標題的定義,就返回True;否則返回False。
def action(self, block, handler):
/**調用諸如handler.start('headline')、handler.feed(block)和handler.end('headline')等方法。
我們不想嘗試其他規(guī)則,因此返回True,以結束對當前文本塊的處理。*/
??當然這里還可以定義一個rule的父類,比如action,condition方法可以在不同的規(guī)則中有自己的實現(xiàn)。
③ 過濾器
?由于Handler類包含方法sub,每個過濾器都可用一個正則表達 式和一個名稱(如emphasis或url)來表示。
④ 解析器
?接下來就是應用的核心,Parser類。它使用一個處理程序以及一系列規(guī)則和過濾器 將純文本文件轉換為帶標記的文件(這里是HTML文件)。
其中包括了:完成準 備工作的構造函數(shù)、添加規(guī)則的方法、添加過濾器的方法以及對文件進行解析的方法。
⑤ 創(chuàng)建規(guī)則和過濾器
?至此,萬事俱備,只欠東風——還沒有創(chuàng)建具體的規(guī)則和過濾器。目前絕大部分工作都是在讓規(guī)則和過濾器與處理程序一樣靈活。通過使用一組復雜的規(guī)則,可處理復雜的文檔,但我們將保持盡可能簡單。只創(chuàng)建分別用于處理題目、其他標題和列表項的規(guī)則。應將相連的列表項視為一個列表,因此還將創(chuàng)建一個處理 整個列表的列表規(guī)則。最后,可創(chuàng)建一個默認規(guī)則,用于處理段落,即其他規(guī)則未處理的所有文本塊。各個不同的復雜文檔的規(guī)則已經(jīng)在代碼塊中解釋。
?最后我們通過正則表達式,添加過濾器,分別找出:出要突出的內容、URL和Email 地址。(https://gitlab.com/ZZY478086819/actualcombatproject)
至此我們將以上的內容通過代碼實現(xiàn),具體代碼小編已經(jīng)上傳至github上,具體的編寫步驟為:
處理程序(handlers.py) → 規(guī)則(rules.py)→主程序(markup.py)
這個項目主要介紹:用Python創(chuàng)建圖表。具體地說,你將創(chuàng)建一個PDF文件,其中包含的圖表對 從文本文件讀取的數(shù)據(jù)進行了可視化。雖然常規(guī)的電子表格軟件都提供這樣的功能,但Python提 供了更強大的功能。
PDF介紹:它指的 是可移植的文檔格式(portable document format)。PDF是Adobe開發(fā)的一種格式,可表示任何包 含圖形和文本的文檔。不同于Microsoft Word等文檔,PDF文件是不可編輯的,但有適用于大多 數(shù)平臺的免費閱讀器軟件。另外,無論在哪種平臺上使用什么閱讀器來查看,顯示的PDF文件都 相同;而HTML格式則不是這樣的,它要求平臺安裝指定的字體,還必須將圖片作為獨立的文件 進行傳輸。
根據(jù)不同的文本內容,生成相應的建PDF格式(和其他格式)的圖形和文檔。這個項目主要將根據(jù)有關太陽黑子的數(shù)據(jù) (來自美國國家海洋和大氣管理局的空間天氣預測中心)創(chuàng)建一個折線圖。創(chuàng)建的程序必須具備如下功能:
- 從網(wǎng)上下載數(shù)據(jù)文件
- 對數(shù)據(jù)文件進行解析,并提取感興趣的內容
- 根據(jù)這些數(shù)據(jù)創(chuàng)建PDF圖形
- 圖形生成包:ReportLab(import reportlab)
- 測試數(shù)據(jù):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
#創(chuàng)建一個指定尺寸的Drawing對象
d=Drawing(100,100)
#再創(chuàng)建具有指定屬性的圖形元素(這里是一個String對象)
s=String(50,50,'Hello World',textAnchor='middle')
#將圖形元素添加到Drawing對象中
d.add(s)
#以PDF格式渲染Drawing對象,并將結果保存到文件中
renderPDF.drawToFile(d,'hello.pdf','A simple PDF file')
為繪制太陽黑子數(shù)據(jù)折線圖,需要繪制一些直線。實際上,你需要繪制多條相連的直線。ReportLab提供了一個專門用于完成這種工作的類——PolyLine。
要繪制折線圖,必須為數(shù)據(jù)集中的每列數(shù)據(jù)繪制一條折線。
①這里先創(chuàng)建出一個太陽黑子圖形程序的第一個原型:
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),
]
#創(chuàng)建一個指定尺寸的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')
②最終版
這里為了方便我們直接讀取本地的文件,測試文件已經(jīng)放入項目中:Predict.txt
具體的項目代碼粘貼在小編的github中!
這個項目的目標是,根據(jù)描述各種網(wǎng)頁和目錄的單個XML文件生成完整的網(wǎng)站。
實現(xiàn)目標:
應能夠輕松地修改整個網(wǎng)站的設計并根據(jù)新的設計重新生成所有網(wǎng)頁
在這個項目中,要解決的通用問題是解析(讀取并處理)XML文件。小編之前接到的一個任務就是解析XML提取其中相應的字段,不過使用的java的dome4j解析的XML,雖然過程不復雜,但是我們看看Python有什么獨到之處。
- 使用的SAX解析器去解析XML(from xml.sax import make_parser)
- 要編寫處理XML文件的程序,必須先設計要使用的XML格式(包含哪些屬性?各個標簽都用來做什么),相當于XML文件的元數(shù)據(jù)信息
這里有些朋友可能對XML格式不是很了解,這里小編做一個介紹:
ul >
title
這里的website是一個根標簽,整個XML報告中只有一個。
director、h2、page、ul則屬于website中的標簽,可能有多個,也可能嵌套。
name="index"表示標簽中的屬性的name 和value
這里我們只有了解一個XML報告中的每個標簽的含義,才能做對應的解析,提取有用的信息。
說了這么多我們先簡單實現(xiàn)一個解析XML,這里提供一個文件website.xml。
(具體文件小編會粘貼到自己的項目中)
這里我們通過解析website.xml,創(chuàng)建一個HTML頁面,執(zhí)行如下任務:
- 在每個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格式,
最終根據(jù)不同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') #創(chuàng)建輸出到的HTML文件的名稱
self.out.write('\n')
#name="index" title="Home Page"
#attrs['title']提取標簽中屬性的key-value
self.out.write('{} \n'.format(attrs['title']))
self.out.write('\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\n')
self.out.close()
elif self.passthrough:
self.out.write('{}>'.format(name))
#標簽中的內容比如:123
--- > 123
def characters(self, content):
if self.passthrough:self.out.write(content)
file_path='../../../file_data/website.xml'
#解析
parse(file_path,PageMaker())
解析完成之后在當前目錄下:
出現(xiàn)這幾個文件,就是解析出來的HTML。
不知道大家有沒有發(fā)現(xiàn)以上代碼的不足之處:
- 這里我們在startElement和endElement使用了if判斷語句,這里我們只處理了一個page標簽,如果要處理的標簽很多,那么這個if將很長很長
- HTML代碼時硬編碼
- 我們查看標簽的時候由一個director標簽,這里是將不同的page放入不同的目錄中,而以上的代碼最終生成的HTML都在同一個目錄下,這里我們再次實現(xiàn)時將會改進
這里由于小編將代碼的各個功能進行了解耦,分不同的功能模塊進行開發(fā),這里小編將詳細介紹每個步驟具體實現(xiàn)什么功能,當然最終的代碼小編也會上傳到github中供大家參考。
鑒于SAX機制低級而簡單,編寫一個混合類來處理管理性細節(jié)通常很有幫助。這些管理性細 節(jié)包括收集字符數(shù)據(jù),管理布爾狀態(tài)變量(如passthrough),將事件分派給自定義事件處理程序, 等等。就這個項目而言,狀態(tài)和數(shù)據(jù)處理非常簡單,因此這里將專注于事件分派。
① 分派器混合類
與其在標準通用事件處理程序(如startElement)中編寫長長的if語句,不如只編寫自定義 的具體事件處理程序(如startPage)并讓它們自動被調用。你可在一個混合類中實現(xiàn)這種功能, 再通過繼承這個混合類和ContentHandler來創(chuàng)建一個子類。
程序實現(xiàn)的功能:
- startElement被調用時,如果參數(shù)name為'foo',它應嘗試查找事件處理程序startFoo,并 使用提供給它的屬性調用這個處理程序
- 同樣,endElement被調用時,如果參數(shù)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("\n \n ")
self.out.write(title)
self.out.write(" \n \n \n")
def writeFooter(self):
self.out.write("\n \n\n")
③ 支持目錄
為創(chuàng)建必要的目錄,需要使用函數(shù)os.makedirs,它在指定的路徑中創(chuàng)建必要的目錄。例如, os.makedirs('foo/bar/baz')在當前目錄下創(chuàng)建目錄foo,再在目錄foo下創(chuàng)建目錄bar,然后在目 錄bar下創(chuàng)建目錄baz。如果目錄foo已經(jīng)存在,將只創(chuàng)建目錄bar和baz。同樣,如果目錄bar也已經(jīng) 存在,將只創(chuàng)建目錄baz。然而,如果目錄baz也已經(jīng)存在,通常將引發(fā)異常。為避免出現(xiàn)這種情 況,我們將關鍵字參數(shù)exist_ok設置為True。另一個很有用的函數(shù)是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,得到以上的目錄已經(jīng)html文件。具體的代碼在項目中,可以自行下載查看!
本項目要編寫的程序是一個信息收集代理,能夠替你收集信息(具體地說是新聞)并生成新聞 匯總。在這個項目中,需要做的并 僅僅使用urllib下載文件,還將使用另一個網(wǎng)絡庫,即nntplib,它使用起來要難些。另外,還需重構程序以支持不同的新聞源和目的地,進而在中間層使用主引擎將前端和后端分開。
最終項目實現(xiàn)的目標:
- 可輕松地添加新聞源(乃至不同類型的新聞源) 能夠從眾多不同的新聞源收集新聞
- 能夠以眾多不同的格式將生成的新聞匯編分發(fā)到眾多不同的目的地
- 能夠輕松地添加新的目的地(乃至不同類型的目的地)
NNTP是一種標準網(wǎng)絡協(xié)議,用于管理在Usenet討論組中發(fā)布的消息。NNTP服務器組成了一 個統(tǒng)一管理新聞組的全局網(wǎng)絡,通過NNTP客戶端(也稱為新聞閱讀器)可發(fā)布和閱讀消息。NNTP 服務器組成的主網(wǎng)絡稱為Usenet,創(chuàng)建于1980年(但NNTP協(xié)議到1985年才開始使用)。相比于最 新的Web潮流,這算是一種很古老的技術了,但從某種程度上說,互聯(lián)網(wǎng)的很大一部分都基于這 樣的古老技術。
最先開發(fā)出來一個簡單的版本:是從NNTP服務器上的新聞組下載 最新的消息,使用print直接將結果打印到標準輸出。
'''
一個簡單的新聞收集代理
'''
from nntplib import NNTP
#服務器域名
servername='news.gmane.org'
#指定新聞組設置為當前新聞組,并返回一些有關該新聞組的信息
group='gmane.comp.python.committers'
#創(chuàng)建server客戶端對象
server=NNTP(servername)
#指定要獲取多少篇文章
howmany=10
#返回的值為通用的服務器響應、新聞組包含的消息數(shù)、第一條和最后一條消息的編號以及新聞組的名稱
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:
#消息正文行是以字節(jié)的方式返回的,但為簡單起見,我們直接使用編碼Latin-1
print(line.decode('latin1'))
print()
#關閉連接
server.quit()
這次我們將對代碼稍作重構以修復這種問題。你將把各部分代碼放在類和方法中,以提高程序的結構化程 度和抽象程度,這樣就可用其他類替換有些部分。
統(tǒng)計一下我們大概需要哪些類::信息、 代理、新聞、匯總、網(wǎng)絡、新聞源、目的地、前端、后端和主引擎。這個名詞清單表明,需要下 面這些主要的類:NewsAgent、NewsItem、Source和Destination。
各種新聞源構成了前端,目的地構成了后端,而新聞代理位于中間層。這里我們對每個類進行詳細的說明:
① NewsItem
它只表示一段數(shù)據(jù),其中包括標題和正文。
class NewsItem:
def __init__(self, title, body):
self.title = title
self.body = body
② NewsAgent
準確地確定要從新聞源和新聞目的地獲取什么,先來編寫代理本身是個不錯的主意。代理 必須維護兩個列表:源列表和目的地列表。添加源和目的地的工作可通過方法addSource和 addDestination來完成。然后就是將新聞從源分發(fā)到目的地的方法。
③ Destination
- 生成的文本為HTML。
- 將文本寫入文件而不是標準輸出中。
- 除新聞列表外,還創(chuàng)建了一個目錄。
④ Source
- 代碼封裝在方法getItems中。原來的變量servername和group現(xiàn)在是構造函數(shù)的參數(shù)。另 外,變量howmany也變成了構造函數(shù)的參數(shù)。
- 調用了decode_header,它負責處理報頭字段(如subject)使用的特殊編碼。
- 不是直接打印每條新聞,而是生成NewsItem對象(讓getItems變成了生成器)。
總的來說就是:通過NewsItem將從網(wǎng)頁上獲取的新聞的內容和標題存放起來,這里我們設置兩個數(shù)據(jù)源:一個是NNTP中獲取的新聞,一個是從urlopen從web網(wǎng)站中獲取的新聞,然后設置了兩個數(shù)據(jù)的目的地:一個是控制臺輸出,一個是寫入HTML文件中。通過NewsAgent對象,將數(shù)據(jù)源和目的地加入到列表中,然后在其distribute方法中,把從數(shù)據(jù)源獲取的數(shù)據(jù)發(fā)送給目的地。最后通過一個run方法,將這些步驟串聯(lián)起來,這樣就實現(xiàn)了一個簡單的從不同的渠道中獲取新聞,轉發(fā)的不同的渠道去。
在這個項目中,將做些正式的網(wǎng)絡編程工作:編寫一個聊天服務器,讓人們能夠通過 網(wǎng)絡實時地聊天。只使用標準庫中的異步網(wǎng)絡 編程模塊(asyncore和asynchat)。
大概的項目需求如下:
- 需要用到的新工具:標準庫模塊asyncore及其相關的模塊asynchat
- 框架asyncore讓你能夠處理多個同時連接的用戶
- 計算機的IP和port:本項目中使用本機的IP和5005端口
我們來將程序稍做分解。需要創(chuàng)建兩個主要的類:一個表示聊天服務器,另一個表示聊天會 話(連接的用戶)。
① ChatServer 類
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from asyncore import dispatcher
import socket,asyncore
'''
一個能夠接受連接的服務器
'''
PORT=5005
NAME = 'TestChat'
'''
為創(chuàng)建簡單的ChatServer類,可繼承模塊asyncore中的dispatcher類。dispatcher類基本上是
一個套接字對象,但還提供了一些事件處理功能。
'''
class ChatServer(dispatcher):
'''
一個接受連接并創(chuàng)建會話的類。它還負責向這些會話廣播
'''
def __init__(self,port):
dispatcher.__init__(self)
#調用了create_socket,并通過傳入兩個參數(shù)指定了要創(chuàng)建的套接字類型,通常都使用這里使用的類型
self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
'''
調用了set_reuse_addr,讓你能夠重用原來的地址(具體地說是端口號),
即便未妥善關閉服務器亦如此。不會出現(xiàn)端口被占用情況
'''
self.set_reuse_addr()
'''
bind的調用將服務器關聯(lián)到特定的地址(主機名和端口)。
空字符串表示:localhost,或者說當前機器的所有接口
'''
self.bind('',port)
#listen的調用讓服務器監(jiān)聽連接;它還將在隊列中等待的最大連接數(shù)指定為5。
self.listen(5)
def handle_accept(self):
'''
重寫事件處理方法handle_accept,讓它在服務器接受客戶端連接時做些事情
'''
#調用self.accept,以允許客戶端連接。
#返回一個連接(客戶端對應的套接字)和一個地址(有關發(fā)起連接的機器的信息)。
conn,addr=self.accept()
#addr[0]是客戶端的IP地址
print('Connection attempt from',addr[0])
if __name__=='__main__':
s=ChatServer(PORT)
try:
#啟動服務器的監(jiān)聽循環(huán)
asyncore.loop()
except KeyboardInterrupt:
pass
② ChatSession 類
這是一個新的版本,這里我們使用asynchat,我們設置一個會話,每一次有一個連接對象時,就將這個連接對象加入會話中,好處是:每個連接都會創(chuàng)建一個新的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=[]
#接受一個新請求,就會創(chuàng)建一個新的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()
③ 整合
要讓原型成為簡單而功能完整的聊天服務器,還需添加一項主要功能:將用戶所說的內容(他 們輸入的每一行)廣播給其他用戶。要實現(xiàn)這種功能,可在服務器中使用一個簡單的for循環(huán)來 遍歷會話列表,并將內容行寫入每個會話。要將數(shù)據(jù)寫入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):
"""
一個接受連接并創(chuàng)建會話的類。它還負責向這些會話廣播
"""
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),而最初的規(guī)范要求提供這樣的功能。 有鑒于此,需要添加對身份(每個用戶都有唯一的名字)和命令解釋的支持,同時必須讓每個會 話的行為都依賴于其所處的狀態(tài)(剛連接、已登錄等)。添加這些功能時,必須確保程序是易于擴展的。
① 基本命令解釋功能
這里我們可以定義一些簡單的命令,比如say、login 等等,即如果發(fā)送:say Hello, world!
將調用do_say('Hello, world!'),這個功能如何實現(xiàn)呢,這里寫一段偽代碼:
#基本的命令解釋功能,例如:say Hello, world!
class CommandHandler:
'''
類似于標準庫中cmd.Cmd的簡單命令處理程序
'''
#參數(shù)不正確
def unknown(self,session,cmd):
session.push('Unknown command: {}s\r\n'.format(cmd).encode())
#根據(jù)命令,匹配方法,調用
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。這個方法引發(fā)異常 EndSession,而這種異常將在較高的層級(found_terminator中)處理。
偽代碼:
class EndSession(Exception):pass
class Room(CommandHandler):
"""
可包含一個或多個用戶(會話)的通用環(huán)境。
它負責基本的命令處理和廣播
"""
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
③ 登錄和退出聊天室
除表示常規(guī)聊天室(這個項目中只有一個這樣的聊天室)之外,Room的子類還可表示其他狀 態(tài),這正是你創(chuàng)建Room類的意圖所在。例如,用戶剛連接到服務器時,將進入專用的LoginRoom (其中沒有其他用戶)。LoginRoom在用戶進入時打印一條歡迎消息(這是在方法add中實現(xiàn)的)。 它還重寫了方法unknown,使其讓用戶登錄。這個類只支持一個命令,即命令login,這個命令檢 查用戶名是否是可接受的(不是空字符串,且未被其他用戶使用)。
LogoutRoom要簡單得多,它唯一的職責是將用戶的名字從服務器中刪除(服務器包含存儲會 話的字典users)。如果用戶名不存在(因為用戶從未登錄),將忽略因此而引發(fā)的KeyError異常。
④ 主聊天室
主聊天室也重寫了方法add和remove。在方法add中,它廣播一條消息,指出有用戶進入,同 時將用戶的名字添加到服務器中的字典users中。方法remove廣播一條消息,指出有用戶離開。
除了這些方法以外,主聊天室還實現(xiàn)了:
- 命令say(由方法do_say實現(xiàn))廣播一行內容,并在開頭指出這行內容是哪位用戶說的。
- 命令look(由方法do_look實現(xiàn))告訴用戶聊天室內當前有哪些用戶。
- 命令who(由方法do_who實現(xiàn))告訴用戶當前有哪些用戶登錄了。在這個簡單的服務器中, 命令look和who的作用相同,但如果你對其進行擴展,使其包含多個聊天室,這兩個命令 的作用將有所區(qū)別。
最終實現(xiàn):
- ChatSession新增了方法enter,用于進入新的聊天室。
- ChatSession的構造函數(shù)使用了LoginRoom。
-方法handle_close使用了LogoutRoom。
- ChatServer的構造函數(shù)新增了字典屬性users和ChatRoom屬性main_room。
好吧,小編也是根據(jù)指南一步一步的將代碼實現(xiàn)了,但是不知道為啥就是跑不成功,然后就從網(wǎng)上搜了搜如何解決,雖然也查到了相關的案例,神奇的事情發(fā)生,我copy多個某某大神的代碼,居然運行不了,而且報出同樣的錯誤,本來想解決一下,造福大家,但是小編能力有限,實在不知道如何下手,這里小編把錯誤展示出來,有牛X的大神看見了幫小編分析解決一下唄!
但是 但是,雖然程序沒運行出來,但是至少學到了一些東西,總不能只知道代碼錯了,不知道代碼就行實現(xiàn)了啥,對不對,那不是欺騙了各位讀友嘛,所以小編這里把上面代碼的整個實現(xiàn)過程畫了一個圖分享給大家:
這個是Python權威指南的前5個項目,雖然后面了沒有實現(xiàn)效果圖,但是代碼和解釋是相當充分的,后續(xù)的5個項目均有呈現(xiàn)的效果和完整的代碼,大家放心小編在寫代碼時也踩了不少的坑,有些問題小編會以小案例的形式在測試代碼中體現(xiàn):