Python权威指南的10个项目(1~5

2023-01-31 04:01:34 项目 指南 权威

引言
  我相信学习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):

  当然这里还可以定义一个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

相关文章