小樓一夜聽春語 xingbb
关注数: 5 粉丝数: 922 发帖数: 1,385 关注贴吧数: 10
大盘预测2020-06-20 先总结上周的分析: “大盘仍会上涨,并且在短期内(下周一、二)可能迎来较大涨幅,第一目标位2930,第二目标位2960,第三目标位2985。但是,行情也会在出现较大涨幅后快速回落(北向资金同时出逃),形成一次大的调整。所以,操作上建议保持仓位不动,随着大盘上涨逐渐减少仓位或清仓。” 大盘在周一受欧美股市影响回调,之后在周二到达第一目标位2930,经过周三与周四的震荡,最终在周五到达第二目标位2960,并且继续向上,在2975处遭遇较大阻力后回落。 周五的走势弥补了3月12日产生的缺口,缺口上方压力不可忽视。 从数据分析来看,指数本周从L线底部位置一直上攻站稳T线,表现出稳步上行的趋势,创出阶段新高。连续的上涨的动力已经减弱,需要回调整理,以便蓄势继续上攻。大盘涨跌提示图也出现了绿色的下跌信号。主力净量趋势图呈现向好的趋势,短期仍未比较平稳的上行趋势。最后,活跃指数趋势图中,买卖活跃指数均呈下降趋势,意味着短期多空双方持谨慎态度,市场中观望情绪较高。指数当前位置较高,面临压力较大,在观望情绪中,向下回调概率较大。综上所述,大盘在下周的表现,就比较清晰了。 周一面临小幅回调,之后周二与周三会重新向上尝试突破2975压力位,并有可能到达2985的最终目标位,当到达2985附近时,不要忽略的是3000点整数关卡的巨大压力以及假期前避险情绪所导致的指数回落。所以,当指数靠近2985时,操作上建议减仓或清仓。 另外,说明一下,我们的预测是基于大盘的资金流向数据,并不包含对政策、经济以及外围市场表现的预期,所以一些突发性的事件可能导致预测出现偏差,还需要各位投资者多关注时事新闻,综合判断。
Django2:Web项目开发入门笔记-完整汇总贴 这个笔记的系列也在这里连续共享完毕。 现在做一下整理,方便大家参考。 当然,在学习Django2之前需要先有Python3的基础,大家可以先了解:http://tieba.baidu.com/p/5704362301 01.Django2简介与安装 http://tieba.baidu.com/p/5585730094 02.Web框架简介(MVC/MTV) http://tieba.baidu.com/p/5589118856 03.创建Web应用/URL分发/视图函数 http://tieba.baidu.com/p/5592613262 04.URL参数获取与视图处理 http://tieba.baidu.com/p/5595681242 05.创建模板与视图调用 http://tieba.baidu.com/p/5609011116 06.内置标签与过滤器/循环/条件判断 http://tieba.baidu.com/p/5610024202 07.模型编写/数据迁移/添加数据 http://tieba.baidu.com/p/5611660280 08.查询数据与排序/查询的匹配规则 http://tieba.baidu.com/p/5611702250 09.JavaScript/JQuery/Ajax/数据操作 http://tieba.baidu.com/p/5612670376 10.JQuery循环 http://tieba.baidu.com/p/5618574359 11.JQuery/QuerySet转JSON/JSON解析 http://tieba.baidu.com/p/5620734746 12.Django后台/使用中文/添加数据 http://tieba.baidu.com/p/5620739430 13.Ubuntu系统中使用Apache2部署Django2项目/Ubuntu图形界面/远程登录/安装软件包 http://tieba.baidu.com/p/5620742775 14.创建配置文件/使用虚拟环境 http://tieba.baidu.com/p/5624898249 15.Windows系统中使用IIS部署Django2项目 http://tieba.baidu.com/p/5624900793 16.Ubuntu系统中使用Nginx部署Django2项目 http://tieba.baidu.com/p/5644593443 17.Django的表单 http://tieba.baidu.com/p/5644598795 18.多种表单字段/多种方式与模板整合 http://tieba.baidu.com/p/5644601537 19.发送电子邮件 http://tieba.baidu.com/p/5663136315 20.CentOS系统中使用Nginx部署Django2项目 http://tieba.baidu.com/p/5663139299 21.通过模型创建表单 http://tieba.baidu.com/p/5663143245 22.通过模型创建表单集 http://tieba.baidu.com/p/5663145500 23.URL Name/通用视图 http://tieba.baidu.com/p/5703808996 24.上下文处理器/中间件 http://tieba.baidu.com/p/5703842016 25.Cookie/Session http://tieba.baidu.com/p/5704536924
Django2:Web项目开发入门笔记(25) 这一篇教程,我们一起来了解Cookie与Session。 Cookie是由Web服务器保存在用户浏览器上的小文本文件,它可以包含有关用户的信息。 当Web服务器创建了Cookies 后,只要在其有效期内(有效期可以由开发人员设定),当用户访问同一个Web服务器时,浏览器首先要检查本地的Cookies,并将其原样发送给 Web 服务器。 这种状态信息称作“Persistent Client State HTTP Cookie”,简称为 Cookies。 以上是引用百度百科中对“Cookie”的描述。 对于“Cookie”的使用,每个人都应该体验过。 例如,我们成功登录百度网站,如果不主动退出,即便是隔一天再打开浏览器进入百度网站,依然能够保持登录状态。 这就是“Cookie”在起作用,它通过将一些信息保存在客户端,解决HTTP的会话保持问题。 提示:HTTP是无状态的,所以无法保持会话状态。保持会话状态是指将同一用户多次进行HTTP连接所发出的不同请求形成关联。 在我们登录百度网站时,服务器创建了“Cookie”文件发送给浏览器进行保存。 当我们再次打开浏览器访问百度网站时,浏览器会先从本地保存的“Cookie”文件中查找与百度网站相匹配的“Cookie”文件,如果存在并有效则回送给服务器,服务器进行相应的验证之后,根据验证结果返回相应的状态到浏览器。 接下来,我们就使用“Cookie”完成一个保持登录状态功能。 1、创建一个简单的模板,添加一个表单。 表单中包含两个状态下的元素。 已登录状态包含一句问候语和一个退出链接。 未登录状态包含一个文本输入框和一个按钮。 示例代码: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <form action="/" method="post"> {% csrf_token %} {% if username %} 您好,{{ username }}! <a href="exit/">退出</a> {% else %} <input type="text" name="username"> <input type="submit" value="登录"> {% endif %} </form> </body> </html> 通过这个模板,我们能够看出,页面呈现什么样的状态,取决于{{ username }}这个变量中是否存在数据内容。 2、添加URL分发配置 示例代码: path('', siteviews.index), path('exit/',siteviews.exit), 3、定义视图函数 在视图函数中,我们要对“Cookie”进行设置与读取的操作。 设置“Cookie”比较常用的参数包括: key:键 value:值 max_age:多少秒后过期 expires:什么时间过期 path:指定哪些URL可以访问当前“Cookie”文件(默认为“/”,表示所有) domain:指定哪些域名可以访问当前“Cookie”文件(默认为“None”,表示当前域名) secure:默认值为“False”(通过HTTPS传输时需设置为“True”) httponly:默认值为“False”(如果设置为“True”,则只能通过HTTP传输,JS无法获取和修改) 示例代码: from django.shortcuts import render, HttpResponseRedirect def index(request): if request.COOKIES.get('username', None): # 读取Cookie信息 return render(request, 'index.html', {'username': request.COOKIES['username']}) # 读取Cookie信息 if request.method == 'POST': username=request.POST['username'] res =render(request,'index.html',{'username':username}) res.set_cookie('username',username,30) # 设置Cookie信息 return res else: return render(request,'index.html') def exit(request): res = HttpResponseRedirect('/') res.delete_cookie('username') # 删除Cookie信息 return res 完成上述代码后,大家运行开发服务器,在打开的页面中输入一个用户名,点击登录按钮,就能够切换为登录后的状态。 并且,在30秒内刷新当前页面,会持续保持登录状态。 而超过30秒之后,又会恢复未登录状态。 大家可以通过浏览器的开发者工具(F12),查看“Cookie”文件的内容。在上图中,暴露了一个严重的问题,就是“Cookie”的内容是直接可见的。 所以,切记不要在“Cookie”中存储重要或者敏感的用户信息。 这样直接暴露的“Cookie”信息,非常容易被攻击者盗取并篡改。 但是,我们又需要通过“Cookie”持续保持会话状态。 有一种解决方法是“加盐”和“解盐”,也就是对“Cookie”信息设置时进行加密,读取时进行解密,以防止攻击者使用篡改过的信息欺骗服务器。 我们对之前的视图函数进行修改。 示例代码: def index(request): if request.COOKIES.get('username', None): # return render(request, 'index.html', {'username': request.COOKIES['username']}) return render(request, 'index.html', {'username': request.get_signed_cookie('username', salt='用于加密的字符串')}) if request.method == 'POST': username = request.POST['username'] res = render(request, 'index.html', {'username': username}) # res.set_cookie('username',username,30) res.set_signed_cookie('username', username, salt='用于加密的字符串') return res else: return render(request, 'index.html') 上方代码中,注释的内容为修改前的语句,红色内容为修改后的语句。 当这样修改之后,在浏览器中查看到的“Cookie”文件信息,就包含了一段安全密钥。当我们访问网站时,浏览器会将包含了密钥的“Cookie”信息回送到服务器,服务器对密钥进行解密验证。 这种解决方法虽然提高了安全性,但是用户的信息仍然是暴露的。 目前,最好的解决方式是通过“Cookie”结合“Session”来保持会话状态。 在“Cookie”文件中,只保存一个名为“sessionid”的随机字符串。 而与“sessionid”对应的用户信息全部保存在服务器端。 Django中提供了5种类型的“Session”: 数据库(默认) 缓存 文件 缓存+数据库 加密cookie 这里只为大家介绍默认的基于数据库的“Session”。 使用这种类型的“Session”,需要先完成数据库的设置,并通过“makemigrations”和“migrate”命令完成相关数据表的创建, 然后,就能够在项目中使用“Session”了。 示例代码: def index(request): if request.session.get('username', None): return render(request, 'index.html', {'username': request.session['username']}) if request.method == 'POST': username = request.POST['username'] request.session['username'] = username return render(request, 'index.html', {'username': username}) else: return render(request, 'index.html') def exit(request): del request.session['username'] return HttpResponseRedirect('/') 通过上方代码,我们就完成和之前代码同样的功能。 此时,在浏览器中关于“Cookie”的信息,就不再有用户的信息,只有“sessionid”。在上图中,大家能够看到,默认“Session”的有效时间为14天,如果需要修改,我们可以在“settings.py”中进行修改。 示例代码: SESSION_ENGINE = 'django.contrib.sessions.backends.db' # Session的引擎(默认) SESSION_COOKIE_NAME = 'mysession' # Session的Cookie的Key SESSION_COOKIE_PATH = "/" # Session的Cookie的可访问路径(默认) SESSION_COOKIE_DOMAIN = None # Session的cookie保存的域名(默认) SESSION_COOKIE_SECURE = False # 是否HTTPS传输Cookie(默认) SESSION_COOKIE_HTTPONLY = True # 是否Session的Cookie只支持HTTP传输(默认) SESSION_COOKIE_AGE = 604800 # Session的Cookie的有效时长(秒)(默认1209600) SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 是否关闭浏览器使得Session过期(默认) SESSION_SAVE_EVERY_REQUEST = False # 是否每次请求都保存Session(默认) 修改之后,在浏览器中能够看到信息的变化。本节练习源代码:【点此下载】
Django2:Web项目开发入门笔记(24) 这一篇教程,我们一起来了解Django的上下文处理器(Context Processor)和中间件(Middleware)。 对于这两块内容,我们最好先了解概念和原理,才能比较好的去应用它们。 一、上下文处理器(Context Processor) 上下文处理器也称作上下文渲染器。 我个人认为处理比渲染更容易理解,而且英文中“Processor”的意思是处理,“Render”的意思是渲染。 但是称为上下文渲染器也有道理,渲染更接近于使用场景。 Django中的Context Processor主要是应用于模板,完成页面的绘制的一些处理,也就是所说的页面渲染。 还有,上下文又是什么? 这个概念,感觉很难懂。 从语文的角度来说,上下文是语境; 同一句话,在不同的语境中,会有不同的意思。 例如,小明考试成绩100分,爸爸说“干得漂亮”,这是表扬的意思。 而小明考试成绩0分,爸爸说“干得漂亮”,这是讽刺的意思。 所以,同样的一句话,我们需要和上文联系到一起才能明白真正的意思。 从计算机编程角度来说,上下文是环境。 也就是说,同一段处理程序对于不同的环境,反馈出不同的处理结果。 借用网上的一个例子:用户访问站点的时候,站点的所有页面上都要能够显示这个用户自己的IP地址。 这样的功能,我们需要从请求(request)中获取到访问用户的IP地址,然后呈现到页面中。 也许大家能够想到,我们可以在每个页面对应的视图函数中进行这个处理,但是未免太过麻烦。 最好的方式是将处理过程定义一次,就能够在每个页面中使用。 此时,我们可以通过自定义一个上下文处理器帮助我们完成。 面对不同来源的用户通过同一个处理器完成页面上不同IP地址的渲染。 1、创建上下文处理器 在项目文件夹(“wsgi.py”所在的文件夹)新建一个文件“context_processor.py”。 在这个文件中添加获取用户IP地址的代码。 示例代码: def getuserip(request): ip = request.META['REMOTE_ADDR'] if ip == '127.0.0.1': return {'user_type': '本机用户'} return {'user_type': '外部用户(%s)' % ip} 注意:上下文处理器必须返回一个字典。 2、将上下文处理器添加到模板设置 打开文件“settings.py”,在模板设置中添加处理器中的函数。 TEMPLATES = [ { ...省略部分代码... 'OPTIONS': { 'context_processors': [ ...省略部分代码... 'MyProject.context_processor.getuserip' ], }, }, ] 上方代码中,红色部分为新增代码。 3、在页面模板中添加标记 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> 当前访问用户:{{ user_type }} </body> </html> 完成上述代码后,Django调用模板时会先通过上下文处理器进行处理,并将处理后返回的数据字典传入模板,通过模板标记获取数据。 此时,我们就可以打开开发服务器,进行访问测试了。 注意:如果需要允许外部访问,需要监听所有IP地址,服务器启动命令为:runserver 0.0.0.0:端口号。 本机访问结果:外部访问结果:(例如手机)二、中间件(Middleware) 中间件,我们需要搞清楚它在什么中间,起到什么作用? 根据官方文档的说法,中间件是请求(request)与响应(response)之间的钩子(hooks)框架。 …又多了个钩子。 个人理解钩子实际上就是挂在主程序上的监控程序,当主程序运行中满足了钩子的触发条件时,执行钩子的处理程序。在官方文档中,给出的中间件示例非常简单直观,并且包含了两种写法。 1、以函数的形式定义中间件。 示例代码: def simple_middleware(get_response): # One-time configuration and initialization. def middleware(request): # Code to be executed for each request before # the view (and later middleware) are called. response = get_response(request) # Code to be executed for each request/response after # the view is called. return response return middleware 2、以类的形式定义中间件 class SimpleMiddleware: def __init__(self, get_response): self.get_response = get_response # One-time configuration and initialization. def __call__(self, request): # Code to be executed for each request before # the view (and later middleware) are called. response = self.get_response(request) # Code to be executed for each request/response after # the view is called. return response 那么,中间件怎么使用?能起到什么样的作用呢? 依然通过举例说明。 例如,我们发布的项目只允许本机和指定ip访问,不允许外部其他地址的设备访问,就可以通过中间件来实现。 首先,创建一个中间件文件“middleware.py”,并定义访问限制的中间件。 这个文件可以创建一个新的Package进行存放,例如:MW。 示例代码: from django.http import HttpResponseForbidden class AaccessRestrictionMiddleware: def __init__(self, get_response): self.get_response = get_response self.wite_ip = ['127.0.0.1', '192.168.31.18'] # 初始化ip地址白名单 def __call__(self, request): ip = request.META['REMOTE_ADDR'] # 获取访问用户的ip if ip not in self.wite_ip: # 如果ip不在白名单中 return HttpResponseForbidden('您被禁止访问!') # 返回响应 response = self.get_response(request) return response 然后,我们打开文件“settings.py”,在中间件设置中添加中间件的类。 示例代码: MIDDLEWARE = [ 'MW.middleware.AaccessRestrictionMiddleware', ...省略其他中间件... ] 通过以上操作,我们就可以限制只有IP白名单中的用户才能够访问我们的项目。 中间件比较像游戏的外挂程序,游戏主程序的执行过程中一旦满足触发外挂的情形出现,外挂就会执行相应的处理。但是,外挂并不会改变主程序,当我们不需要外挂的时候,只需要移除外挂程序,主程序依然能够正常的执行。 本节练习源代码:【点此下载】
Django2:Web项目开发入门笔记(23) 这一篇教程,我们一起来了解一些Django的URL Name和通用视图的内容。 一、URL Name 我们在做URL分发时,通过“path()”方法将一个路径匹配一个视图函数。 示例代码: path('details<int:id>', shopviews.getgoodsdetails), 这样的路径,当我们访问类似“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fdetails1&urlrefer=13553c4d45d9a63767d49e366f660f1d”这样的网址时,就能够调用相应的视图进行处理。 不过,假如我们在完成了项目之后,想把这样的路径改为“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fdetails%2F1&urlrefer=b3ca700fee23e2e943916a920460c2f1”的格式,那就意味着所有使用了原有格式的路径都需要进行修改,这样的修改不能保证完全不会出现遗漏。 为了避免这种不安全的修改,我们在项目编写时,就可以主动去处理可能出现的问题。 在“path()”方法的参数中,可以再提供一个“name”参数的值,为“URL”设定一个名称。 然后,模板中使用“URL”时,不再写具体的路径,而是通过模板标记{% url 'URL名称' 更多参数%}自动产生相应的路径。 例如一个商品列表页面,点击某一商品时,能够进入该商品的详情页。 1、定义模型类并创建数据库表 大家可以参考《Django2:Web项目开发入门笔记(8)》创建商品的模型类和数据库表。 2、定义视图函数 示例代码: def getgoodslist(request): goodslist = GoodsInfo.objects.all() return render(request, 'goods_list.html', {'goodslist': goodslist}) def getgoodsdetails(request, id): goodsdetails = GoodsInfo.objects.filter(id=id) return render(request, 'goods_details.html', {'goodsdetails': goodsdetails}) 3、配置URL分发 示例代码: from MyShop import views as shopviews path('', shopviews.getgoodslist), path('details<int:id>', shopviews.getgoodsdetails, name='details'), 4、创建模板 示例代码:(goods_list.html) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>商品列表</title> </head> <body> <h3>商品列表</h3> {% for goods in goodslist %} <p><a href="{% url 'details' goods.id %}">{{ goods.id }}.{{ goods.goods_name }}</a></p> {% endfor %} </body> </html> 在上方代码中,模板标记“{% url ‘details’ goods.id %}”中的“‘details’”对应的就是URL分发配置中的URL“details<int:id>” 参数“goods.id”对应的就是“<int:id>”。 示例代码:(goods_details.html) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>商品详情</title> </head> <body> {% for info in goodsdetails %} 名称:{{ info.goods_name }} 数量:{{ info.goods_number }} 价格:{{ info.goods_price }} {% endfor %} </body> </html> 完成上方代码中后,大家可以启动开发服务器,进行测试。 打开商品列表的页面后,点击商品名称就能够打开对应的商品详情页面。 此时,浏览器地址栏中的地址类似于“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fdetails1&urlrefer=13553c4d45d9a63767d49e366f660f1d”。 然后,大家可以把URL分发配置中的URL进行修改。 示例代码: path('details/<int:id>', shopviews.getgoodsdetails, name='details'), 修改之后,刷新商品列表,并点击商品名称打开详情页面。 此时,浏览器地址栏中的地址类似于“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fdetails%2F1&urlrefer=b3ca700fee23e2e943916a920460c2f1”。 二、通用视图 1、TemplateView 按照我们之前所学内容,如果想让一个模板的内容呈现为页面,我们需要自定义一个视图函数。 哪怕只是一个没有数据加载的静态页面,也需要这么去做。 例如,我们有一个“hello.html”的模板。 示例代码: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>欢迎</title> </head> <body> 你好!欢迎访问小楼帅哥的个人网站! </body> </html> 这个模板内容与数据无关,呈现为页面之后,不会再有任何变化。 像这样的页面,实际上我们无需定义视图函数,可以直接使用Django给我们提供的通用模板视图类“TemplateView”。 在配置URL分发时,我们需要导入这个类,然后在“path()”方法中调用这个类的“as_view()”方法,并指定“as_view()”方法的“template_name”参数值为模板名称。 示例代码: from django.views.generic.base import TemplateView path('hello/', TemplateView.as_view(template_name='hello.html')), 这样结束之后,我们就可以启动开发服务器,访问“hello/”这个路径所打开的页面了。 2、ListView “ListView”能够方便的帮助我们实现列表视图。 在‘views.py’文件中,我们不再定义视图函数,而是定义视图类继承“ListView”类。 示例代码: from MyShop.models import GoodsInfo from django.views.generic.list import ListView class GoodsListView(ListView): model = GoodsInfo template_name = 'goods_list2.html' 在视图类中,我们只需要指定数据来源的模型类以及相应的模板文件。 然后,在URL分发配置中,我们添加新的配置。 示例代码: path('list2/', shopviews.GoodsListView.as_view(),name='goods_list2'), 通过自定义视图类的“as_view()”方法,就能够将模型所有数据与模板进行整合。 最后,在视图中我们遍历数据对象,完成数据内容的填充。 示例代码:(goods_list2.html的关键代码) <h3>商品列表</h3> {% for goods in object_list %} <p><a href="{% url 'details' goods.id %}">{{ goods.id }}.{{ goods.goods_name }}</a></p> {% endfor %} 这里大家要注意,数据对象的名称为“object_list”,不再是自定义的名称。 上述操作是将全部商品数据在页面上呈现,如果我们只想呈现出符合某种条件的数据,该怎么做呢? 例如,我们只显示所有商品数据的前5条。 我们可以在自定义视图类中,重写“get_queryset()”方法。 示例代码: # views.py class GoodsList5View(ListView): template_name = 'goods_list3.html' def get_queryset(self): context = GoodsInfo.objects.all()[0:5] return context #urls.py path('list3/', shopviews.GoodsList5View.as_view(), name='goods_list3'), #goods_list3.html <h3>商品列表</h3> {% for goods in object_list %} <p><a href="{% url 'details' goods.id %}">{{ goods.id }}.{{ goods.goods_name }}</a></p> {% endfor %} 3、DetailView “DetailView”是通用详情视图。 我们可以通过继承“DetailView”类,完成商品详情的视图类。 然后,通过配置URL分发和创建相应的模板完成查看商品详情的功能。 示例代码: #views.py class GoodsDetailView(DetailView): model = GoodsInfo template_name = 'goods_details2.html' #urls.py path('goods_details/<pk>', shopviews.GoodsDetailView.as_view(), name='goods_details'), #goods_details2.html <body> 名称:{{ object.goods_name }} 数量:{{ object.goods_number }} 价格:{{ object.goods_price }} 管理员:{{ manager }} </body> 注意上方代码中,URL分发配置中的URL参数要使用“<pk>”,也就是“Primary Key”(主键)的意思。 如果不是通过主键查询可以写成<slug:参数名称>。 在《Django2:Web项目开发入门笔记(4)》中曾经提到,slug类型是由字母、数字、横线以及下划线其中一种或多种组成的字符串。 我们除了能够查询到某个商品的详情,还可以在模型数据的基础上增加一些内容。 例如,把每个商品详情的末尾加上“管理员:小楼” 。 示例代码: #views.py class GoodsDetailView(DetailView): model = GoodsInfo template_name = 'goods_details2.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['manager'] = '小楼' return context # goods_details2.html <body> 名称:{{ object.goods_name }} 数量:{{ object.goods_number }} 价格:{{ object.goods_price }} 管理员:{{ manager }} </body> 以上代码中,红色部分为新增内容。 4、RedirectView “RedirectView”是重定向视图。 例如,当用户访问“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fdjango2&urlrefer=4f8a5d1d4ef894bddcc86c256e806f09”时,页面跳转到“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fwww.opython.com%2Fdjango2&urlrefer=8148ee3109883da346e6c9308069998e”。 示例代码: from django.views.generic.base import RedirectView path('django2/',RedirectView.as_view(url='http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fwww.opython.com%2Fdjango2%2F&urlrefer=f4ffdd7aed7e4adf4db84b0b29e4b08a'),name='django2'), 这个视图我们也可以通过创建子类重写某些内容,实现更多的重定向功能。 最后,关于通用视图还有很多内容,大家可以参考官方文档相关内容: Use generic views: Less code is better Built-in class-based views API 本节练习源代码:【点此下载】
Django2:Web项目开发入门笔记(20) 这一篇教程,我们一起来了解如何在CentOS系统中将Django2的Web项目部署到Nginx服务器。 CentOS系统虽然和Ubuntu系统都是Linux系统,但是环境搭建和部署过程还是有一些区别。 整个流程分为几个部分: 安装图形桌面与远程登录 安装Python3.6及相关库文件 安装Django2 安装uWSGI 安装Nginx 配置Nginx 使用ini文件启动uWSGI服务器 使用supervisor管理uWSGI服务器 接下来,我们就逐一完成这些步骤。 一、安装图形界面 在此之前大家应该先完成CentOS系统的最小化安装。 然后,安装我们需要的图形界面和远程登录功能。 因为远程登录需要图形界面支持,所以从顺序上先进行图形界面的安装,再安装远程登录。 (一)使用Gnome桌面 GNOME桌面比较耗费系统资源,在主机上我们更多是通过命令行进行操作,所以,我比较倾向于使用面向低性能硬件的Xfce桌面。 如果选择使用Xfce桌面,大家可以略过此步骤,直接浏览第(二)部分。 1、安装软件源 执行命令: yum install epel* -y 2、更新软件包 执行命令: yum -y upgrade 3、安装桌面支持 执行命令: yum groupinstall "X Window System" "GNOME Desktop" -y 或者: yum -y groupinstall "Server with GUI" 4、安装xrdp和vnc 执行命令: yum install tigervnc-server xrdp -y 5、启动xrdp服务,并且设置为开机启动 执行命令: systemctl start xrdp (二)使用Xfce桌面 1、安装软件源 执行命令: yum install epel* -y 2、更新软件包 执行命令: yum -y upgrade 3、安装桌面管理器 执行命令: yum install lightdm -y 4、安装桌面 执行命令: yum groupinstall xfce -y 5、安装远程服务 执行命令: yum install tigervnc-server xrdp -y 6、禁用GDM桌面管理器 执行命令: systemctl disable gdm 7、启用LightDM桌面管理器 执行命令: systemctl enable lightdm 8、配置Xfce为默认桌面 执行命令: vim ~/.Xclients 在打开的文件中,写入以下内容: #!/bin/bash XFCE="$(which xfce4-session 2>/dev/null)" exec "$XFCE" 然后,执行命令,增加执行权限: chmod +x ~/.Xclients 9、启动或重启远程连接服务 执行命令: systemctl start xrdp 或者: systemctl restart xrdp 10、设置远程连接为开机启动 执行命令: systemctl enable xrdp 二、安装Python3.6 CentOS系统自带的是Python2.7.5,可以通过输入“python”命令打开。 我们安装了Python3.6之后,需要使用命令“python3”启动Python3.6的Shell。 1、安装相关库文件 执行命令: yum -y install zlib* yum -y install gcc yum -y install gcc-c++ yum -y install openssl yum -y install openssl-devel yum -y install sqlite yum -y install sqlite-devel yum -y install readline readline-devel 2、安装Python3.6与相关库文件 (一)安装Python3.6 首先,创建一个用于保存下载文件的文件夹,并赋予权限。 执行命令: mkdir /home/centos/Downloads cd /home/centos/Downloads chmod 777 /home/centos/Downloads 然后,下载Python的源码安装包。 下载地址:http://tieba.baidu.com/mo/q/checkurl?url=https%3A%2F%2Fwww.python.org%2Fdownloads%2Fsource%2F&urlrefer=8e5b913ca6d09ad88cafc54a8617e746 如果是通过Windows远程登录,可以直接复制系统中下载好的Python源码安装包,粘贴到CentOS系统的文件夹中。 当然,也可以在CentOS系统中通过“wget”命令进行下载。 执行命令: wget http://tieba.baidu.com/mo/q/checkurl?url=https%3A%2F%2Fwww.python.org%2Fftp%2Fpython%2F3.6.5%2FPython-3.6.5.tgz&urlrefer=47c0e34c85c0a1335d7031ebd3ebe5be 最后,解压缩软件安装包,进入解压缩后的目录进行安装。 执行命令: tar xfz Python-3.6.5.tgz cd Python-3.6.5 ./configure --enable-shared --with-ssl=openssl make && make install 安装完毕后,启动Python3.6。 执行命令: python3 此时,可能会出现错误。 python3: error while loading shared libraries: libpython3.6m.so.1.0: cannot open shared object file: No such file or directory 产生错误的原因是:配置文件添加了参数“ --enable-shared ”,Python3.6运行时没有加载到文件“libpython3.6m.so.1.0 ”。 实际上我们在执行“make”命令时,已经编译了这个文件,解决问题的方法就是把编译好的文件复制到特定的目录中。 执行命令: cd /home/centos/Downloads/Python-3.6.5 cp libpython3.6m.so.1.0 /usr/local/lib64/ cp libpython3.6m.so.1.0 /usr/lib/ cp libpython3.6m.so.1.0 /usr/lib64/ (二)安装相关库 我们需要安装 “python36-devel”。 这个库没有在系统默认源中,所以无法直接通过“yum”命令进行安装。 我们需要先添加一个安装源工具,通过下载rpm文件进行安装。 但是,直接下载rpm文件进行安装有可能会出现错误。 Warning: user mockbuild does not exist. using root 所以,我们需要先安装一个依赖库。 执行命令: yum install mock -y useradd -s /sbin/nologin mockbuild 然后,下载“rpmforge”的安装文件。 可以到“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Frepoforge.org%2Fuse%2F&urlrefer=8871b81d6bf6468b3c0f48184e373ec3”进行下载,或者通过“wget”命令进行下载。 执行命令: cd /home/centos/Downloads/ wget http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Frepository.it4i.cz%2Fmirrors%2Frepoforge%2Fredhat%2Fel7%2Fen%2Fx86_64%2Frpmforge%2FRPMS%2Frpmforge-release-0.5.3-1.el7.rf.x86_64.rpm&urlrefer=266ed01b991aa9e7711b69743cd82261下载完成后进行安装。 执行命令: rpm -ivh rpmforge-release-0.5.3-1.el7.rf.x86_64.rpm 此时,可能会出现“NOKEY”的错误,是因为“yum”安装了旧版本的“GPG keys”造成的,解决办法是导入“gpg”。 执行命令: rpm --import /etc/pki/rpm-gpg/RPM* 接下来,就可以安装 “python36-devel”了。 可以先通过命令,搜索python3-devel的可用版本。 执行命令: yum search python3 | grep devel 在搜索结果中可以看到一些关于Python3.X的文件名称,其中有一个为“Python36”。 执行命令: yum -y install python36-devel 这样就完成了“python36-devel”的安装。 三、安装Django2 Python3.6默认安装后,需要使用命令“pip3”安装第三方库。 先安装Django的依赖库“pytz”。 执行命令: pip3 install pytz 然后,安装Django。 执行命令: pip3 install django 或者,下载Django的安装包,放入“Downloads”文件夹后进行安装。 执行命令: cd /home/centos/Downloads/ tar xfz Django-2.0.3.tar.gz cd Django-2.0.3 python3 setup.py install 最后,测试一下Python3.6、Django以及Sqlite3是否能够正常使用。 执行命令: python3 >>>import django >>>import sqlite3 四、安装uWSGI 注意:不要用“yum install uwsgi”进行安装,这样装完会关联系统中的Python2.7,并且系统可能会自带uwsgi,自带uwsgi的启动项在“/usr/sbin/”中,而我们通过“pip3”命令安装uwsgi的启动项在“/usr/local/bin/”目录中。 执行命令: pip3 install uwsgi 如果怕搞混,我们可以卸载系统自带的uwsgi,然后将启动项复制到“/usr/sbin/”目录中。 执行命令: yum remove uwsgi cp -f /usr/local/bin/uwsgi /usr/sbin/ 或者,我们可以将自己安装的uwsgi启动项复制到“/usr/sbin/”目录中时,改名为“uwsgi3”。 执行命令: cp -f /usr/local/bin/uwsgi /usr/sbin/uwsgi3 接下来,我们测试一下uwsgi是否能够正常工作。 创建一个测试文件“mytest.py”。 执行命令: vi /var/www/mytest.py 写入内容: # !/usr/bin/env python3 # 也可以写成“#!/usr/bin/python3.6”。 def application(env, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return [b'UWSGI Test...'] 代码输入完毕,按“ESC”键并键入“:wq”回车,保存测试文件。 然后,系统中如果没有自带浏览器的话,可以安装火狐浏览器。 执行命令: yum install firefox -y 最后,进行测试。 执行命令: cd /var/www/ uwsgi --http :8888 --wsgi-file mytest.py 此时,通过本机浏览器访问“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888&urlrefer=9252966fafd5ec27b6e8eaf8cc625442”或者“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Flocalhost%3A8888&urlrefer=115cb5f640cde1dc95693c68f4b68508”进行测试,如果页面中显示“UWSGI Test…”字样,则说明测试成功,uwsgi可以正常工作了。 五、安装Nginx 首先,安装Nginx的依赖库“pcre”。 执行命令: yum install -y pcre pcre-devel 然后,安装Nginx。 执行命令: yum install -y nginx* 安装完成后,就可以通过命令控制Nginx了。 启动命令:/usr/sbin/nginx 停止命令:/usr/sbin/nginx -s stop 退出命令:/usr/sbin/nginx -s quit 重载命令:/usr/sbin/nginx -s reload 查询进程:ps aux|grep nginx 另外,我们还可以设置Nginx为开机自启动。 执行命令: vi /etc/rc.local 在打开的文件中,添加一行内容。 /usr/local/nginx/sbin/nginx 六、配置Nginx 创建配置文件,并写入内容。 执行命令: vi /etc/nginx/conf.d/MyWeb.conf 写入内容: server { listen 80; server_name http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fwww.qqtbb.com%3B&urlrefer=6791f0a3c3ee8cb6c9a983df7eb84403 charset utf-8; client_max_body_size 5M; location /media { alias /var/www/MyWeb/media; } location /static { alias /var/www/MyWeb/static; } location / { uwsgi_pass 127.0.0.1:8888; include /etc/nginx/uwsgi_params; } } 提示:配置内容的详细说明,可以参考《Django2:Web项目开发入门笔记(16)》。 内容输入完毕(使用时请先清除注释),按“ESC”键并键入“:wq”回车保存,并让服务器重载配置。 执行命令: nginx -s reload 此时,可能发生错误。 nginx: [error] open() “/usr/local/var/run/nginx.pid” failed (2: No such file or directory) 解决方法是找到“nginx.conf”的文件夹目录,然后运行“nginx”命令。 例如,“nginx.conf”文件在“/etc/nginx/”目录中。 执行命令: nginx -c /etc/nginx/nginx.conf nginx -s reload 如果发生80端口被占用的情况,可以先查询占用端口的进程,通过“kill”命令关闭进程。 执行命令: lsof -i :80 kill -9 [进程ID] nginx -c /etc/nginx/nginx.conf 七、使用ini文件启动uWSGI服务器 首先,使用ini文件启动uWSGI服务器,需要安装依赖库。 执行命令: yum install uwsgi-plugin-python3 然后,在项目文件夹中创建ini文件。 例如,在“/var/www/MyWeb”中存放Web项目文件。 执行命令: vi /var/www/MyWeb/uwsgi.ini 在新建的文件中输入内容。 [uwsgi] socket = 127.0.0.1:8888 # 因为要接收来自Nginx的Socket,此处必须和Nginx的设置保持一致。 chdir = /var/www/MyWeb/ wsgi-file = MyWeb/wsgi.py # 完整路径是“/var/www/MyWeb/MyWeb/wsgi.py” processes = 3 # 注意,此处启用了多进程,之后使用supervisor管理uWSGI时,需要增加配置项。 threads = 5 chmod-socket = 664 chown-socket = www-data pidfile= /var/www/MyWeb/MyWeb.pid vacuum = true 提示:配置内容的详细说明,可以参考《Django2:Web项目开发入门笔记(16)》。 内容输入完毕(使用时请先清除注释),按“ESC”键并键入“:wq”回车保存,然后就可以通过配置文件启动uWSGI服务器了。 启动命令: uwsgi3 --ini /var/www/MyWeb/uwsgi.ini 停止命令: uwsgi3 --stop /var/www/MyWeb/MyWeb.pid 重载配置: uwsgi3 --reload uwsgi.ini 再次强调:启动时注意Python版本是否Python3.6,并且不要使用系统自带的uwsgi。 此时,通过域名就能够访问我们的Web项目了。 八、使用supervisor管理uWSGI服务器 supervisor可以在程序意外关闭时自动重新启动,使用它管理uWSGI服务器非常不错。 不过,supervisor只支持Python2,我们需要通过CentOS自带的Python2.7进行安装。 首先,安装“pip”工具。 执行命令: cd /usr/lib/python2.7/site-packages/ easy_install pip 然后,安装“supervisor”。 pip install supervisor 接下来,进行配置。 执行命令: echo_supervisord_conf > /etc/supervisord.conf vi /etc/supervisord.conf 在打开的文件末尾添加内容。 注意:语句前面不要有空格。 [program:MyWeb] command=uwsgi --ini /var/www/MyWeb/uwsgi.ini directory=/var/www/MyWeb/ startsecs=10 stopwaitsecs=10 stopasgroup=true killasgroup=true autostart=true autorestart=true 提示:配置内容的详细说明,可以参考《Django2:Web项目开发入门笔记(16)》。 这里特别需要注意的是,如果“uwsgi.ini”文件中开启了多进程,一定要加上下面两句。 stopasgroup = true # 用于停止进程组,即停止所有通过“uwsgi.ini”配置启动的进程。 killasgroup = true # 用于关闭进程组,即关闭所有通过“uwsgi.ini”配置启动的进程。 如果不添加这两句,supervisorctl命令停止或关闭进程时,只会关闭其中1个进程,从而导致再次启动或重启uWSGI失败,出现类似“MyWeb: ERROR (spawn error)”的错误。 这是因为残留的孤儿进程,阻止了新的同类进程的开启。 当我们完成配置文件的修改之后,必须重新加载配置文件,才能使其生效。 执行命令: supervisorctl reload 最后,启动supervisord和uWSGI。 执行命令: /usr/bin/supervisord -c /etc/supervisord.conf 或者: supervisorctl -c /etc/supervisord.conf start MyWeb 此时,可能会发生错误。 Error: Another program is already listening on a port that one of our HTTP servers is configured to use. Shut this program down first before starting supervisord. 这是因为supervisord已经启动过,我们需要先查询已开启的程序,将其关闭。 执行命令: find / -name supervisor.sock unlink /被链接文件的所在路径/supervisor.sock 另外,通过“supervisorctl”命令,可以方便的管理项目。 启动项目命令:supervisorctl start [配置文件中的项目名称] 重启项目命令:supervisorctl restart [配置文件中的项目名称] 停止项目命令:supervisorctl stop [配置文件中的项目名称] 控制所有项目:supervisorctl <start/restart/stop> all
Django2:Web项目开发入门笔记(16) 这一篇教程,我们一起来了解如何将Django2的Web项目部署到Nginx服务器。 首先,大家可以参考《Django2:Web项目开发入门笔记(13)》先完成以下准备工作。 安装Ubuntu系统并更新apt-get 安装Python3.6与依赖 安装Django2.0.3 安装Sqlite3与依赖 安装OpenSSL等相关依赖 也就是说,除了Apache2和mod_wsgi之外全部都需要安装。 完成上述内容的安装之后,再将项目文件放入/var/www目录中,我们就可以开始Nginx的安装了。 一、安装Nginx和依赖 执行命令: sudo apt-get install nginx sudo apt-get install libpcre3 libpcre3-dev 二、安装uwsgi 执行命令: cd /usr/local/lib/python3.6/site-packages/ sudo python3.6 pip install uwsgi 三、测试uwsgi 先在/var/www目录中创建一个测试文件。 执行命令: sudo chmod 777 /var/www cd /var/www sudo vi mytest.py 在VI编辑器中,我们输入测试文件的代码。 示例代码: def application(env, start_response): start_response('200 OK', [('Content-Type','text/html')]) return [b'UWSGI Test...'] 代码输入完毕,按“ESC”键并键入“:wq”回车,保存测试文件后,进行测试。 执行命令: sudo uwsgi --http :8888 --wsgi-file mytest.py 通过本机浏览器访问“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888&urlrefer=9252966fafd5ec27b6e8eaf8cc625442”或者“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Flocalhost%3A8888&urlrefer=115cb5f640cde1dc95693c68f4b68508”进行测试,如果页面中显示“UWSGI Test…”字样,则说明测试成功,uwsgi可以正常工作了。 如果,重复测试时,提示端口被占用,可以先将已启动的进程关闭。 先通过名称查询相关进程,然后通过“kill”命令将进程关闭。 执行命令: sudo ps -ef|grep uwsgi sudo kill -9 [端口号] 也可以使用“killall”命令通过名称关闭全部相关进程。 sudo kill -9 uwsgi 如果命令无效,可以先获取“root”权限,再进行尝试。 四、配置Nginx 首先,我们为项目添加配置文件。 执行命令: sudo vi /etc/nginx/sites-available/MyWeb.conf 在VI编辑器中写入配置文件内容。 server { listen 80; # 监听端口 server_name http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fwww.qqtbb.com%3B+&urlrefer=b237aa2d3635b3207ecd8e43ea01ecb6 # 服务器名(多个域名用逗号分隔) charset utf-8; # 服务器字符集 client_max_body_size 5M; # 上传文件最大限制 location /media { # 媒体文件位置 alias /var/www/MyWeb/media; } location /static { # 静态文件位置 alias /var/www/MyWeb/static; } location / { # 为uwsgi协议提供支持(实现与uWSGI服务器通信) uwsgi_pass 127.0.0.1:8888; #为uWSGI服务器设置监听地址(套接字或sock文件) # uwsgi_pass unix:///var/www/MyWeb/MyWeb.sock; # 使用sock文件 include /etc/nginx/uwsgi_params; # 为uwsgi请求增加参数。 } } 内容输入完毕(使用时请先清除注释),按“ESC”键并键入“:wq”回车保存,然后将配置文件的快捷方式添加到/etc/nginx/sites-enabled/目录中,激活配置文件。 执行命令: sudo ln -s /etc/nginx/sites-available/MyWeb.conf /etc/nginx/sites-enabled/MyWeb.conf 然后,重启Nginx服务器或者让服务器重新加载配置文件。 重载配置命令: sudo service nginx reload 重启服务命令: sudo service nginx restart 如果重载配置文件或者重启服务器发生错误,需要检查配置文件是否有效。 语法检查命令: sudo service nginx configtest 注意,语法检查只是检查是否配置文件中语法全部正确,并不能排除配置项是否存在问题。 接下来,我们启动uWSGI服务器测试一下项目是否能够正常运行。 执行命令: sudo uwsgi --http :8888 --chdir /var/www/MyWeb --module MyWeb.wsgi 通过本机浏览器访问“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888&urlrefer=9252966fafd5ec27b6e8eaf8cc625442”或者“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Flocalhost%3A8888&urlrefer=115cb5f640cde1dc95693c68f4b68508”进行测试,如果项目页面访问正常,则说明配置文件 五、使用配置文件启动uWSGI服务器 uWSGI服务器也可以通过配置文件进行启动,不但避免使用过长的命令,而且能增加更多的配置。 执行命令: cd /var/www/MyWeb sudo vi uwsgi.ini 配置文件内容: [uwsgi] socket = 127.0.0.1:8888 # 监听地址(套接字或sock文件) # socket=/var/www/MyWeb/MyWeb.sock # 使用sock文件 chdir = /var/www/MyWeb/ # 项目根目录 wsgi-file = MyWeb/wsgi.py # wsgi文件路径 processes = 3 # 开启的工作进程数 threads = 5 # 每个工作进程的线程数 chmod-socket = 664 # 客户端访问MyWeb.sock文件的权限 chown-socket = www-data # 客户端请求的所有者 pidfile= /var/www/MyWeb/MyWeb.pid # 保存进程文件的路径 vacuum = true # 服务器退出时自动删除sock文件和pid文件 内容输入完毕(使用时请先清除注释),按“ESC”键并键入“:wq”回车保存,然后就可以通过配置文件启动uWSGI服务器了。 启动命令: sudo uwsgi --ini uwsgi.ini 停止命令: sudo uwsgi --stop uwsgi.pid 重载配置: uwsgi --reload uwsgi.ini 六、使用Supervisor管理uWSGI Supervisor是一个进程管理工具,能够方便我们对uWSGI服务器进行管理,并能够在uWSGI服务器意外关闭时,重新启动uWSGI服务器。 不过,目前Supervisor没有Python3的版本,我们需要使用Ubuntu系统自带的Python2进行安装。 如果没有安装pip,则需要先安装pip。 执行命令: sudo apt-get install python-pip 接下来,安装Supervisor。 执行命令: sudo pip install supervisor 安装完毕后,创建配置文件。 执行命令: sudo echo_supervisord_conf > /etc/supervisord.conf 打开配置文件,并添加配置。 执行命令: sudo vi /etc/supervisord.conf 配置内容: [program:MyWeb] # 这里的“MyWeb”是启动/停止/重启项目时使用的名称 command=uwsgi --ini /var/www/MyWeb/uwsgi.ini # 启动uWSGI服务器的命令 directory=/var/www/MyWeb/ # 项目根目录 startsecs=10 # 进程持续运行多长时间认为启动成功 stopwaitsecs=10 # 向进程发出关闭信号后等待系统返回“SIGCHILD”信号的时间 autostart=true # uWSGI随Supervisord启动 autorestart=true # uWSGI进程意外关闭后自动重启 内容输入完毕(使用时请先清除注释),按“ESC”键并键入“:wq”回车保存。 尝试启动Supervisor。 执行命令: sudo supervisord -c /etc/supervisord.conf 此时,极有可能发生错误:unix:///tmp/supervisor.sock no such file。 这是因为配置文件中,默认将“supervisor.sock”文件存入/tmp/目录所导致的。 再次打开配置文件并修改以下内容: /tmp/supervisor.sock 修改为/var/run/supervisor.sock (有两处需要修改) /tmp/supervisord.log 修改为/var/log/supervisor.log /tmp/supervisord.pid 修改为/var/run/supervisor.pid 接下来,在修改后的目录中创建“supervisor.sock”文件,并给予读写权限。 执行命令: sudo touch /var/run/supervisor.sock sudo chmod 777 /var/run/supervisor.sock 再次启动Supervisor,又会发生错误:Unlinking stale socket /var/run/supervisor.sock 这次的错误是因为缺少依赖。 执行命令: sudo apt-get install aptitude sudo aptitude install python-meld3 安装完依赖,继续尝试启动Supervisor,此时还可能产生错误:Error: Another program is already listening on a port that one of our HTTP servers is configured to use. Shut this program down first before starting supervisord. 这是因为已经有“supervisor.sock”文件被链接(上一次启动Supervisor造成的)。 我们需要先查找哪个“supervisor.sock”文件被链接了。 执行命令: sudo find / -name supervisor.sock 结果中会显示链接的文件,例如:/run/supervisor.sock 然后,通过“unlink”命令取消查询到的链接。 执行命令: sudo unlink /被链接文件的所在路径/supervisor.sock 到这里,我们应该就能够正常启动Supervisor对uWSGI进行管理了。 启动项目命令:sudo supervisorctl -c /etc/supervisord.conf start [配置文件中的项目名称] 重启项目命令:sudo supervisorctl -c /etc/supervisord.conf restart [配置文件中的项目名称] 停止项目命令:sudo supervisorctl -c /etc/supervisord.conf stop [配置文件中的项目名称] 控制所有项目:sudo supervisorctl -c /etc/supervisord.conf <start/restart/stop> all
Django2:Web项目开发入门笔记(7) 这一篇教程,我们一起来学习Django2中模型的使用。提示:为了更顺利的学习新的内容,建议大家在PyCharm中重新创建项目和应用。 在之前的教程中,我们已经知道模型(Model)是和数据库相关的。 Django支持多种数据库,包括默认的SQLite3以及MySQL和PostgreSQL等数据库。 使用哪一种数据库需要在项目的settings.py中进行进行配置。 如果使用默认的SQLite3数据库,这个配置是Django创建项目时已经添加好的。 示例代码: DATABASES = { # 数据库配置 'default': { # 默认数据库 'ENGINE': 'django.db.backends.sqlite3', # 数据库引擎设置 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # 数据库的路径与名称 } } 注意检查一下项目文件夹中是否包含“db.sqlite3”这个数据库文件。 如果没有的话,可以通过PyCharm进行添加。 在项目文件夹上点击鼠标右键,菜单中选择新建(New)-数据源(Data Source),此时会弹出一个窗口。 在这个窗口中,我们可以选择数据库文件的存放路径(Path),直接填写名称即是存放在当前项目文件夹中。 数据库驱动(Driver)选择sqlite(Xerial),然后点击确定(OK)按钮。在新打开的窗口中,我们可以点击测试链接(Test Connection)测试一下数据库链接是否正常。点击确定(OK)按钮后,数据库文件就创建好了。 接下来,我们编写模型文件(models.py)的代码。 这里值得一提的是,在更换数据库时,“models.py”中的代码不用做任何更改,只需要在“settings.py”文件中更改数据库的配置。 我们来看一下创建数据库表的流程,就知道为什么这么说了。 创建数据表的流程一共3步: 模型文件(models.py)中编写数据表所对应的类; 通过“manage.py”的“makemigrations”命令创建迁移项; 通过“manage.py”的“migrate”命令创进行迁移。 也就是说,我们在模型文件(models.py)中创建了数据表对应的类,剩下的工作都通过“manage.py”的命令,让Django自动去执行。 一、编写模型文件(models.py) 假设我们创建一个商品信息表。 模型文件(models.py)中,需要创建类以及类的特性(变量),实际上类名对应的是数据库的表名,而特性(变量)对应的是数据表的列(字段)。 示例代码: class GoodsInfo(models.Model): goods_name = models.CharField(max_length=30) goods_number = models.IntegerField() goods_price = models.FloatField() 在上方代码中,我们创建了一些不同类型的字段的对象,包括字符串类型(CharField)、整数类型(IntegerField)和浮点数类型(FloatField)。 字段类型有很多种,例如还有邮箱类型、URL类型等等,大家可以参考官方文档进行使用。 http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2Fwww.opython.com%2Ffiles%2Fdocs%2Fdjango2%2Fref%2Fmodels%2Ffields.html&urlrefer=e8666c9178a79c337cf62932fa007067 并且,大家能够看到,我们在创建字段对象时还可以输入参数,这些参数在上方文档中也有详尽的描述。 比较常见的参数有几种: max_length:该字段最大字符数量限制。 null :如果为True,则该字段默认为null值。 blank :如果为True,则该字段可以为空值。 default :该字段的默认值。 primary_key :该字段设置为主键。 unique:该字段的值必须是当前数据列中唯一的。 choices:该字段的值为二元数组或列表,例如:((1,2),(a,b))。 当我们编写完模型文件(models.py),接下来就可以进行数据迁移,将模型文件(models.py)中的数据迁移到数据库。 二、创建迁移项与执行迁移操作 在创建迁移项之前,我们需要先在全局配置文件(settings.py)中,将当前应用添加到“INSTALLED_APPS”列表。 INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'MySite.apps.MysiteConfig', # 添加此项 # 'MySite' # 也可以直接添加应用名称 ] 然后,在命令行终端中,通过命令创建迁移项。 python manage.py makemigrations 或者,我们在PyCharm的工具(Tools)中,运行“manage.py”的任务(Run manage.py Task),然后输入命令创建迁移项。 创建迁移项完毕后,会在项目文件夹下的“migrations”文件夹中出现迁移项文件,类似“0001_initial.py”。 接下来,我们再通过“migrate”命令进行数据迁移操作。 python manage.py migrate 或者,在“manage.py”的任务终端中,执行“migrate”命令。 这个命令会将模型文件(models.py)中的数据同步到数据库,完成数据表的创建。 执行的命令与结果:当我们完成以上步骤,在数据库中就会出现一个名为“MySite_goods”(应用名称_小写类名)的数据表。 不过,在创建数据表的时候,我们可能会遇到这两种情况: “models.py”模块中的类遗漏了某个字段或者某个字段觉得没有用了; “models.py”模块中,类的某个字段写错了类型或者参数。 当发生这样的情况,创建的数据表就不符合要求,而不能正常使用,我们必须修改表的结构,或者更改字段的设置。 那么,如何解决呢? 在Django1.7版本之前,我们需要借助第三方库“South”进行表结构或者字段的修改操作。 而在Django1.7版本开始,“South”已经集成在“Django.core”中。 我们只需要修改“models.py”模块中类的内容,然后再次运行“makemigrations”和“migrate”命令就能够完成修改。 不过要注意的是,如果更改表结构时,是为数据表添加新的字段,在执行“makemigrations”命令时,需要选择一个选项,并输入该字段的默认值。 如果不想在运行命令时这么麻烦,不妨在“models.py”模块中添加新的字段时,直接写入默认值。 例如商品销量字段: goods_sales = models.IntegerField(default=0) 当我们完成数据表的创建,我们可以尝试进行数据的添加与读取。 大家还记得之前我们学习的SQL语句吗? 相信有很多人都已经记不清了吧! 别担心,我就是想吓你们一跳,喔嚯嚯嚯嚯嚯嚯嚯嚯嚯! 使用Django可以不用SQL语句,就能够实现数据库的增删改查。 不管执行什么数据操作,我们都需要先导入模型文件(models.py)中的类。 在命令行终端我们导入Goods类。 >>>from MySite.models import Goods 然后,通过类进行数据操作。 一、添加数据 添加数据有两种方式,我们分别演示。 第一种:通过create()方法添加。 >>>Goods.objects.create(goods_name='铅笔',goods_number='10',goods_price='1.5') <Goods: Goods object (1)> 第二种:通过save()方法添加。 >>>g=Goods(goods_name='橡皮',goods_number='20',goods_price='0.5') g.save() 或者使用另外一种写入参数的方式。 >>>g=Goods() >>>g.goods_name='直尺' >>>g.goods_number=15 >>>g.goods_price=2.4 >>>g.save() 二、查询数据 查询数据我们可以使用下列语句。 >>>Goods.objects.all() <QuerySet [<Goods: Goods object (1)>, <Goods: Goods object (2)>, <Goods: Goods object (3)>]> 通过all()方法,能够获取数据库中所有的数据对象。 那么,能不能看到数据的值呢? 通过values()方法,就能够看到数据库中所有数据的值。 >>>Goods.objects.values() <QuerySet [{'id': 1, 'goods_name': '铅笔', 'goods_number': 10, 'goods_price': 1.5}, {'id': 2, 'goods_name': '橡皮', 'goods_number': 20, 'goods_price': 0.5}, {'id': 3, 'goods_name': '直尺', 'goods_number': 15, 'goods_price': 2.4}]> 关于数据库的操作,我们到此先告一段落,在下一篇教程中,我们结合URL分发、视图和模板,在浏览器的页面中进行一些对数据的操作。
Django2:Web项目开发入门笔记(6) 这一篇教程,我们继续了解Django中模板的使用。 主要内容如下: 内置标签和过滤器; 模板中使用循环; 模板中添加条件判断。 一、内置标签和过滤器 Django中有很多的内置标签,例如之前已经使用的block和extends。 关于内置标签,大家可以通过官方文档进行了解。【点此查看】 在官方文档的页面中,除了内置模板标签,还有过滤器。 过滤器实际上就是一些函数,帮助我们进行数据的处理。 我们可以通过管道符“|”使用过滤器,格式为“{{ 变量|过滤器1|过滤器2 }}”。 我们来做个练习,试用几个过滤器。 首先,我们在视图中先添加一个视图函数。 在视图函数中,我们添加两项数据和页面内容进行整合。 示例代码: def fiter_test(request): return render(request, 'filter.html', {'letters': 'abc', 'number': 1}) 然后,我们添加URL分发配置。 示例代码: path('filter/', siteviews.fiter_test), 最后,我们创建一个模板文件’filter.html’,在模板文件中我们使用过滤器对数据进行处理。 示例代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>过滤器</title> </head> <body> {{ letters|length }}<br> {{ letters|upper }}<br> {{ letters|join:',' }}<br> {{ number|add:1 }}<br> {{ number|add:-1 }}<br> </body> </html> 在模板代码中,我们通过过滤器对字符串‘letters’分别进行了长度获取、转为大写以及逗号分隔的操作;对数字“number”分别进行了加1和减1的操作。 页面显示结果:二、模板中使用循环 在一个模板页面中添加单个数据可以通过“{{ 变量名 }}”的方式来实现。 如果需要添加多个数据,则可以通过字典来实现。 例如,视图中向模板页面传入的数据为字典(例如:{‘person’:{‘name’:’小楼’,’sex’:’男’}}),在模板中则可以通过“{{ person.name }}”和“{{ person.sex}}”获取到相应的数据。 但是,如果页面中我们需要呈现一个列表(例如新闻列表),这时会有大量的数据需要处理,该如何实现呢? 另外,如果列表中有个别的项需要处理(例如第一项为红色字体),如何处理呢? 这些需求的实现,需要在模板中使用循环和条件判断。 如果有新闻列表内容的话,我们需要能够在页面中呈现出来。这就需要在视图函数中获取一个新闻列表的数据内容,并在模板中循环加载出每一个列表项。 因为,我们还没有接触过模型(Model),暂时还不能和数据库进行关联,在这里我们先手动写出一个新闻列表的内容数据。 1、定义视图函数 在之前编写好的视图函数中,我们写出一个新闻列表内容,然后整合到模板定义的页面内容中。 此处只以经济新闻为例。 示例代码: def news_list(request, news_type): news_dict = {'economic': '经济', 'sport': '体育'} news_titles = [] if news_type == 'economic': news_titles = [('12/5', '作者成为全国首富。'), ('12/4', '作者成为全省首富。'), ('12/3', '作者成为全市首富。'), ('12/2', '作者成为镇里首富。'), ('12/1', '作者成为村里首富。')] return render(request, 'news_list.html', {'news_type': news_dict[news_type], 'news_titles': news_titles}) 2、模板中添加循环 示例代码: {% extends 'base.html' %} {% block title %}{{ news_type }}新闻{% endblock %} {% block content %} {{ news_type }}新闻列表: <ul> {% for date,title in news_titles %} <li>{{ title }}({{ date }})</li> {% endfor %} </ul> {% endblock %} 注意,模板中的循环是由“{% for 变量 in 可迭代对象 %}”和循环的内容以及“{% endfor %}”组成。 当我们完成上方代码之后,访问经济新闻列表页面,就能看到想要的效果了。 三、模板中添加条件判断 通过添加循环的代码,我们也能够看出,在模板中添加逻辑代码其实非常简单,只要了解添加的规则和能够使用的功能就可以了。 接下来,我们就尝试将新闻列表第一项的用红色字体显示。 示例代码: {% extends 'base.html' %} {% block title %}{{ news_type }}新闻{% endblock %} {% block content %} {{ news_type }}新闻列表: <ul> {% for date,title in news_titles %} {% if forloop.first %} <li><font color="red">{{ title }}</font> ({{ date }})</li> {% endif %} <li>{{ title }}({{ date }})</li> {% endfor %} </ul> {% endblock %} 上方代码中,绿色部分是新增的代码。 通过条件判断,如果是循环第一项“forloop.first”,将“{{ title }}”的内容用“font”标签变为红色字体。 “forloop.first”是循环的一个变量,像这样的变量还有其他几个。 在PyCharm中,我们输入“forloop.”之后,就能够看到这些变量。当我们写完“if”的语句块之后,不要忘了在最后添加“{% endif %}”。 不管是“for”还是“if”,在模板中他们都是标签,需要有开始标签和结束标签。 以上是我们对列表内容的一些处理。 不过,有时列表中没有内容的话,我们也需要进行处理。 在之前的示例中,不管是体育新闻还是经济新闻,我们都用的同一模板。 体育新闻,我们并没有任何列表数据,这时需要给用户一个提示。 那么,如何判断数据内容是空的,并给出相应提示呢? 示例代码: {% extends 'base.html' %} {% block title %}{{ news_type }}新闻{% endblock %} {% block content %} {{ news_type }}新闻列表: <ul> {% for date,title in news_titles %} {% if forloop.first %} <li><font color="red">{{ title }}</font> ({{ date }})</li> {% endif %} <li>{{ title }}({{ date }})</li> {% empty %} 抱歉:暂时没有新闻内容! {% endfor %} </ul> {% endblock %} 上方代码中,绿色部分是新增内容。 通过在for循环中添加“{% empty %}”,就能够对数据内容为空进行判断,从而给出相应提示。 当我们完成本篇教程的所有代码,再次启动开发服务器,访问经济新闻列表和体育新闻列表,就能够看到我们想要的结果了。 经济新闻页面:体育新闻页面:本节练习源代码:【点此下载】
Django2:Web项目开发入门笔记(5) 前面的教程我们对Django2中的URL分发以及视图进行了初步的了解,这一篇教程我们接触模板的使用。既然我们是创建Web项目,就少不了HTML代码。 每个呈现给用户的页面,都是由HTML代码所组成。 所以,对于HTML的相关知识,大家需要有一定的了解。 网上有很多关于HTML的教程,在此不做推荐。 另外一点,既然我们学习Web项目的开发,大家可以观察一下各类网站,页面是有分类的。 例如,一个新闻网站,会有不同种类新闻的列表页,还有各种新闻的内容页。 我们在制作网站的时候,肯定不会把每个HTML页面都写一遍,因为同类页面中的内容,往往只是有部分差异,大部分都是相同的。 例如,新闻列表页。 每一类新闻的列表页,只是列表内容不同,页面的其它部分往往是一样的。 那么,我们能不能把新闻列表页面只做一次,让里面的列表内容是可以动态加载的呢? 比如用户点击体育新闻就加载体育新闻的列表,点击经济新闻就加载经济新闻的列表。 当然是可以的,这就是模板的用途。 具体来说,模板可以是同一类页面的模板,也可以是不同页面中相同内容的模板。 也就是说,我们可以把同类页面只编写一次HTML代码,就可以通过动态载入数据呈现出不同的页面内容;也可以把多个页面中都存在的部分内容(例如页面导航和底部信息)只编写一次HTML代码,通过模板的嵌套加载到不同的页面中。 一、模板的使用 我们只需要创建包含HTML代码的模板文件,放置到模板文件夹(templates)中,然后在全局设置文件(settings.py)中添加相关配置,即可在视图中调用模板,并且通过视图函数可以将不同的数据与调用模板进行整合,形成不同的页面。 1、创建模板文件 在PyCharm项目文件列表的模板文件夹(templates)上点击鼠标右键,选择新建(New)一个HTML文件(HTML File)。 虽然,模板文件的后缀名称不要求必须是“.html”,但是在PyCharm中创建HTML文件会方便我们编写HTML代码(自动提示和代码补全)。 例如,我们创建一个名为“index.html”的文件,作为站点首页的模板,并写入HTML代码。 示例代码: <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <h4>这是我的第一个模板页面。</h4> </body> </html> 2、添加视图函数 打开视图模块“views.py”,添加视图函数调用模板。 示例代码: from django.shortcuts import render # 创建应用时自动导入的模块 def index(request): # 定义站点首页视图函数 return render(request, 'index.html') # 调用模板返回页面内容 3、配置URL分发 打开模块“urls.py”,在配置列表中添加新的配置。 示例代码: from django.contrib import admin from django.urls import path from MySite import views as siteviews # 导入视图模块并创建别名 urlpatterns = [ path('', siteviews.index), # 配置处理访问网站根目录的视图 path('admin/', admin.site.urls), ] 完成上面的步骤后,启动开发服务器,通过浏览器访问当前项目,就能够看到页面内容了。二、模板的嵌套 在很多页面都有的重复内容,我们可以做成模板,然后嵌套到页面中使用。 例如,我们新建两个HTML文件,一个作为顶部导航,一个作为底部信息,让index页面的模板中包含这两个模板的内容。 访问首页时效果如下图。先来编写顶部导航内容的模板。 示例代码:(nav.html) <h3>这里是顶部导航模板内容</h3> <hr> 然后,编写底部信息内容的模板。 示例代码:(footer.html) <hr> <h3>这里是底部信息模板内容</h3> 最后,修改首页模板。 示例代码:(index.html) <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> {% include 'nav.html' %} <h4>这是我的第一个模板页面。</h4> {% include 'footer.html' %} </body> </html> 大家能够看到,通过“{% include ‘模板名称’ %}”就能够把其它模板嵌套到当前模板内。 三、模板的复用 实际上通过嵌套,我们就实现了部分内容(顶部导航和底部信息)的复用。 不过,我们在开发Web项目的过程中,会有很多页面都包含重复的内容。 我们在每一类页面的模板中都进行重复内容的嵌套也是很麻烦。 所以,我们可以先做一个基本的模板,然后各类页面的模板复用这个基本的模板,只把页面中不同的部分进行重写。 1、创建基本模板 示例代码:(base.html) <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %}默认{% endblock %} - 我的网站</title> </head> <body> {% include 'nav.html' %} {% block content %}这里是网站内容!{% endblock %} {% include 'footer.html' %} </body> </html> 在上方代码中,大家能够看到我们通过“{% block 名称 %}内容{% endblock %}”定义了一些内容,这些内容就能够在复用这个模板内容的模板中重写。 2、复用基本模板 接下来,我们在首页页面模板中复用上方的模板。 示例代码:(index.html) {% extends 'base.html' %} 当完成上方代码中,大家通过浏览器访问当前项目,能够看到基本模板的内容。但是,这不是我们想要的结果。 网站的标题和页面的内容都需要修改。 3、重写模板内容 我们在首页页面模板中继续添加内容,对复用的基本模板内容进行重写。 示例代码:(index.html) {% extends 'base.html' %} {% block title %}首页{% endblock %} {% block content %} <h4>这是我的第一个模板页面。</h4> {% endblock %} 通过上方代码,大家不难看出,“{% block 名称 %}内容{% endblock %}”能够对复用模板内容中相同名称的部分进行重写。 当完成上述内容,我们再次通过浏览器访问项目,就能够看到和之前一样的首页页面内容。 四、模板与数据整合 模板用于定义页面内容的展现,如果想动态的显示不同的内容,还需要与数据进行整合。 例如,访问不同的新闻列表页面,但是都是用相同的模板,模板中页面标题和新闻列表名称应该是可以改变的,而这些可变的数据应该由视图函数添加到模板定义的页面内容中。 1、创建模板 示例代码:(news_list.html) {% extends 'base.html' %} {% block title %}{{ news_type }} }}新闻{% endblock %} {% block content %} {{ news_type }}新闻列表: {% endblock %} 上方代码中“{{ 变量名称 }}”能够获取视图函数中存入的变量数据。 2、定义视图函数 示例代码: def news_list(request, news_type): # 定义新闻列表视图 news_dict = {'economic': '经济', 'sport': '体育'} # 创建参数字典 return render(request, 'news_list.html', {'news_type': news_dict[news_type]}) # 整合数据并返回页面内容 3、设置URL分发 示例代码: path('news_list/<str:news_type>', siteviews.news_list), 通过以上步骤,我们就完成了想要的功能。 访问经济新闻的连接:http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fnews_list%2Feconomic&urlrefer=18961aa06ee83737ba20721180541148访问体育新闻列表的连接:http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fnews_list%2Fsport&urlrefer=745e7c35eac8e024ff8c1cc14b3a1ef0本节练习源代码:【点此下载】
Django2:Web项目开发入门笔记(3) 使用PyCharm创建Django的Web项目非常方便,在之前的教程中我们能够看到PyCharm已经帮我们创建了一些内容。我们在列表中点击项目的包“MyWeb”,然后运行,就能够启动开发服务器。此时,我们打开浏览器,输入地址“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8000%2F&urlrefer=779a9cdb22d05541a3641264451d2039”,就能够看到Django安装成功的提示页面。不过,这仅仅是Django的页面内容,如果想用Django开发一个真正可以访问的Web应用,我们需要在项目中创建一个新的应用。 创建应用可以通过命令行进行创建。 例如,创建一个名称为“MySite”的应用,命令为: python manage.py startapp MySite 先别急! 既然我们使用了PyCharm这个开发环境,创建应用还有另外一种方式。 在工具(Tools)菜单中,选择运行任务(Run manage.py Task)。此时会启动命令行窗口。 在命令行窗口中,我们输入“startapp MySite”,回车之后即可完成应用的创建。“manage.py”文件包含很多命令,不仅仅可以创建应用,还能对服务器、数据库以及会话等等进行相关操作。 如果想了解这些命令,可以直接运行“manage.py”文件,就能够看到相关的帮助信息。 Available subcommands: [auth] changepassword createsuperuser [contenttypes] remove_stale_contenttypes [django] check compilemessages createcachetable dbshell diffsettings dumpdata flush inspectdb loaddata makemessages makemigrations migrate sendtestemail shell showmigrations sqlflush sqlmigrate sqlsequencereset squashmigrations startapp startproject test testserver [sessions] clearsessions [staticfiles] collectstatic findstatic runserver 例如刚才启动开发服务器的操作,我们也可以通过“manage.py”文件来完成。 在命令行窗口,我们输入“runserver 端口号”就能够启动开发服务器。当我们完成应用的创建,此时在项目文件列表中又会增加一些新的内容。到这里,我们终于看到了MTV框架中的模型(Models)和视图(Views)。 接下来,我们基于已经生成的内容,尝试着做一个首页。 内容不用很复杂,只需要一句话就可以。从这个练习开始,我们逐步了解Django的使用。 1、新增视图函数。 视图函数用于返回响应内容,也就是用户看到的页面。 在“views.py”文件中添加新的代码(带注释部分),定义index(request)函数,参数request是必需的。 from django.shortcuts import render # 暂时没有作用 from django.http import HttpResponse # 从http模块中导入HttpResponse类 # Create your views here. def index(request): # 定义站点首页视图函数 return HttpResponse('啊!~~这是我的第一次!') # 返回响应内容对象 完成上方代码后,当调用index函数时,就能够将一个页面内容的对象返回给用户。 2、配置网址分发。 在“urls.py”文件中添加新的代码(带注释部分),将访问网站根目录的url交由视图中的index函数进行处理。 from django.contrib import admin from django.urls import path from MySite import views as siteviews # 从项目的包中导入视图模块 urlpatterns = [ path('', siteviews.index), # 来自服务器的请求为网站根目录时,由视图中的index函数进行处理。 path('admin/', admin.site.urls), ] 这样,当访问“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2FIP&urlrefer=839dc2c086013440ef075419cc59133f地址:端口号”时,会通过“urls.py”文件进行分发,调用视图中的index函数,得到返回的响应内容对象。 当我们完成以上两步,就可以启动开发服务器,设置端口号为“8888”,并且通过“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2F&urlrefer=2cdf995f277156b225ea9f6bf374e8cb”访问了。 除此以外,大家还可以通过“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A8888%2Fadmin%2F&urlrefer=58fc2ae9e6ef419447775164914b8cd6”进行访问,是不是看到了Django自带的管理后台? 之所以能够打开后台页面,就是由上方代码中“path(‘admin/’, admin.site.urls),”这一句进行分发的。吧 另外还要补充一点:如果项目中同时有多个应用的话,应该在每个应用的包中单独创建“urls.py”模块,然后在项目的“urls.py”模块中包含应用的URL分发配置。 示例代码: from django.contrib import admin from django.urls import path, include urlpatterns = [ path('应用的包名/', include('应用的包名.urls')), path('admin/', admin.site.urls), ] 这样就能够在用户访问某一个应用时,调用该应用的URL分发配置。 好了,这篇教程我们先学习到这里,谢谢大家的支持与鼓励!
Django2:Web项目开发入门笔记(2) 在开始使用Django2之前,我们需要先了解一些关于Web框架的相关知识,这对我们了解Django的工作原理以及能够顺利的学习Django有很大的帮助。 首先,我们要了解的是设计模式和框架的区别。 设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述(引自百度百科)。 上面对设计模式这个定义,看上去很难理解。 所以,我举一个常见的例子。 就拿盖房子(某种环境)来说,都要建造地基、墙体和屋顶(反复出现的问题),我们需要先打好地基,砌好墙体,再装上屋顶(解决问题的方案)。 这就是盖房子的设计模式。 而框架则是按照设计模式完成的半成品,在框架的基础上可以进行二次开发。 还是拿盖房子来说,框架就好像根据上面盖房子的设计模式所建造出来的只有地基、四面墙体和屋顶的毛坯房,如何增加新的墙体与装修都可以由房主自己再进行处理。 在编程领域,通常会听到MVC框架。 实际上,MVC框架是基于 MVC这种创建 Web 应用程序的设计模式所开发出来的框架。 那么,MVC设计模式是什么样的呢? 先来看这个英文缩写每一个字母所代表的内容: M:Model(模型) V: View(视图) C:Controller (控制器) Model(模型)是应用程序中用于处理应用程序数据逻辑的部分,通常模型对象负责在数据库中存取数据。 View(视图)是应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。 Controller(控制器)是应用程序中处理用户交互的部分,通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。 大家可以通过下面这张图,了解MVC模式的组成结构与关系。关于MVC设计模式以及基于MVC的框架,大家可以自行查找相关资料进行深入的了解。 这里之所以提及这个设计模式,是因为我们所使用的Django这个框架的设计模式与之非常相似。 Django是标准的基于MTV设计模式的框架。 这里的MTV和我们所熟知的音乐电视( Music Television )没有任何关系。 这个英文缩写每一个字母所代表的内容,如下所示: M:Model(模型) T:Templates(模板) V: Views(视图) 大家看完上面的英文缩写所代表的内容,会发现多了一个Template(模板)却少了一个Controller (控制器)。 实际上并没有缺少Controller (控制器),Django框架本身承担了控制器的角色。 Django处理请求的顺序如下: 1、通过Web服务器网关接口(WSGI:Web Server Gateway Interface)对Web服务器的socket请求进行处理; 提示:WSGI是Python应用程序或框架和Web服务器之间的一种接口。 2、Django框架(控制器)控制用户输入,进行URL匹配,通过映射列表将请求发送到合适的视图; 3、视图(Views)向模型(Model)和模板(Templates)发送或获取数据; 4、模型(Model)对数据库进行存取数据; 5、模板(Templates)用于将内容和展现分离,通过模板描述数据如何展现; 6、视图(Views)将模板和数据整合,形成最终页面; 7、Django框架(控制器)返回页面展示给用户。 大家可以通过下面这张图,了解Django处理请求的过程。简单来说,Django是将每个URL的页面请求分发给不同的View(视图)进行处理,View(视图)再调用相应的Model(模型)进行数据的保存或读取,并且View(视图)也会调用相应Template(模板),将Model(模型)读取到的数据与Template(模板)进行整合,形成最终页面后返回给控制器,展示给用户。 通过对上面所述内容所的了解,我们再回到PyCharm中看一下,Django项目自动生成的内容,就会基本明白其中一些内容的用途。还有一些内容(例如manage.py)在之后的教程中,我们再进一步进行了解。
Django2:Web项目开发入门笔记(1) 在学习了Python入门教程之后,我们应该选择一个方向进行深入。 从这篇教程开始,我们一起学习Python的Web项目开发。 基于Python的Web开发框架有很多种,在这里不一一赘述。 Django是这些框架中比较成熟并拥有庞大用户群体的一种。 与一些轻量级框架(例如Flask)不同,Django有丰富的内建功能,提供一站式解决方案,便于初学者快速实现一个Web项目。 当然,并不是说轻量级框架不如Django,轻量级框架更适合灵活的定制化开发,但同时也意味着很多功能要由开发人员自己来实现。 这就好比想做一道菜。 轻量级框架就像蔬菜店,只提供基本的食材,这就要求顾客具有一定的烹饪知识和技巧,能够自己加工搭配食材,完成烹饪过程。 不过,这样的好处在于顾客能够随心所欲的进行搭配制作,做出各式各样的菜品。 而Django这样的框架则像一个半成品供应店,食材都已经加工好,并且按配方搭配好,顾客只需要按照一定流程,就能够完成菜品的制作。这对于烹饪知识和技巧有限或者希望快速做出一道菜的顾客来说是不错的选择。 但是,半成品的缺点在于限制了顾客的自由度,不利于顾客的自由发挥。 所以,作为一个入门级的Python开发人员,选择Django应该是个不错的决定。 这里需要注意的是,在之前的学习中,我们一直使用的是PyCharm的社区版,如果要进行基于Django的Web项目开发,我们需要使用PyCharm的专业版。 PyCharm专业版,才能够直接创建Django的项目。 PyCharm专业版下载地址:http://tieba.baidu.com/mo/q/checkurl?url=https%3A%2F%2Fwww.jetbrains.com%2Fzh%2Fpycharm%2Fdownload%2Fdownload-thanks.html&urlrefer=6bd155287d420c36f78fc1a4adc86cdd 注意:PyCharm专业版只提供一定时间的试用,大家可以考虑购买,或者通过一些其他方式完成软件的激活,本站不提供相关的解决方案。 当我们完成PyCharm专业版的安装之后,将软件打开。 如果之前已经做过项目的话,此时可能会直接打开之前的项目。 我们需要在文件(File)菜单中,选择关闭项目(Close Project)的选项,关闭当前项目。 关闭项目之后,会弹出创建项目的窗口,我们点击创建新项目(Create New Project)的选项,进入创建新项目的窗口。 在创建新项目的窗口中,左侧列表选择Django,右侧选择一个本地已创建好的文件夹,然后点击创建(Create)按钮,就会开始Django项目的创建。在Django项目的创建过程中,如果我们还没有安装Django的话,PyCharm会自动帮助我们进行安装。注意:PyCharm会自动帮助我们安装Django2.0版本,这个版本不支持Python2。 如果想自己安装Django的话,也可以使用pip命令进行安装。 pip install Django 当我们完成Django项目的创建,这个时候在PyCharm左侧的项目文件列表中会自动创建一些内容。这些内容就是我们基于Django开发Web项目的关键内容。 至于这些内容如何使用,我们在之后的教程中逐步进行了解。 这里需要知道的是venv这文件夹中的内容。 这个文件夹内容的出现,是因为在创建Django项目时,同时创建了一个独立的Python运行环境。 在创建Django项目的窗口,Python解释器设置中有一个默认选项,使用Virtualenv新建运行环境(New environment using Virtualenv)。实际上,如果我们只开发一个应用的话,可以选择下方的现有翻译器(Existing interpreter)。 但是,如果同时开发多个应用的话,每个应用又需要独立的运行环境,就需要选择创建新的虚拟环境。 例如,系统中只安装了Python2.7,安装第三方库的时候都会安装到同一目录。 但是同时需要开发两个项目,一个使用Django1.7,另一个使用Django1.9,这就产生了冲突。 所以,这个时候我们需要为不同的项目创建独立的运行环境,就需要用到Virtualenv。 实际上virtualenv也是一个模块,通过pip命令进行安装,然后通过这个模块手动创建虚拟环境。 关于virtualenv模块的使用,有兴趣的同学可以自行搜索一下相关资料进行了解,在此不做赘述。
《Python3萌新入门笔记--练习项目》完整汇总帖 Python3萌新练习项目,一共包含10个项目,现进行汇总,以方便大家参考。 这些项目是《Python3萌新入门笔记》的配套练习。 《Python3萌新入门笔记》完整汇总贴http://tieba.baidu.com/p/5575316171 练习项目列表: 练习项目01:转换文档为HTML(上)http://tieba.baidu.com/p/5567250570 练习项目02:转换文档为HTML(下)http://tieba.baidu.com/p/5568638384 练习项目03:绘制生成PDF折线图(上)http://tieba.baidu.com/p/5571514288 练习项目04:绘制生成PDF折线图(下)http://tieba.baidu.com/p/5573190935 练习项目05:解析XML(上)http://tieba.baidu.com/p/5573199239 练习项目06:解析XML(下)http://tieba.baidu.com/p/5575117155 练习项目07:新闻采集(上)http://tieba.baidu.com/p/5576783323 练习项目08:新闻采集(下)http://tieba.baidu.com/p/5576756632 练习项目09:在线聊天室(上)http://tieba.baidu.com/p/5576761370 练习项目10:在线聊天室(下)http://tieba.baidu.com/p/5578017493 练习项目11:在线编辑文件(上)http://tieba.baidu.com/p/5578027862 练习项目12:在线编辑文件(下)http://tieba.baidu.com/p/5579578328 练习项目13:简单的BBS功能(上)http://tieba.baidu.com/p/5579542284 练习项目14:简单的BBS功能(下)http://tieba.baidu.com/p/5579572668 练习项目15:P2P在线文件共享(一)http://tieba.baidu.com/p/5581359434 练习项目16:P2P在线文件共享(二)http://tieba.baidu.com/p/5581387638 练习项目17:P2P在线文件共享(三)http://tieba.baidu.com/p/5581391564 练习项目18:P2P在线文件共享(四)http://tieba.baidu.com/p/5581392792 练习项目19:使用python制作游戏(上)http://tieba.baidu.com/p/5581395766 练习项目20:使用python制作游戏(中)http://tieba.baidu.com/p/5581398515 练习项目21:使用python制作游戏(下)http://tieba.baidu.com/p/5581402389 如果大家喜欢这些内容,还请多多支持我! 其中部分文章内容度娘不给发,见下图。
练习项目20:使用python制作游戏(中) 上一篇教程,我们通过实现一个敌机由上至下的循环过程,初步了解了pygame这个模块的使用。 接下来,我们进行第二阶段,真正的实现一个通过鼠标控制的小游戏。 因为内容量相对前面的教程来说比较大,我们把第二阶段再分成两个部分来实现。 第一部分:实现配置文件和游戏对象模块; 第二部分:实现游戏主程序部分。 一、配置文件(config.ini) 在配置文件中,我们需要作如下配置: 游戏图片:图片的名称 游戏窗口:宽、高、背景色(红\蓝\绿)、边缘填充、是否全屏、字体尺寸 游戏速度:落下速度(敌方)、移动速度(己方)、速度增量(敌方) 游戏关卡:敌机数量 飞机实体:顶部、侧边 说明:飞机实体设置,是用于对己方飞机rect特性尺寸的调整,详见Plane类的代码。 配置文件内容: [Image] plane = images/plane.png enemy = images/enemy.png welcome = images/welcome.png [Screen] width = 360 height = 640 red = 255 blue = 255 green = 255 margin = 5 full_screen = 0 font_size = 24 [Speed] drop_speed = 1 move_speed = 2 speed_increase = 1 [Level] pre_level = 5 [Plane_padding] top = 15 side = 15 二、游戏对象模块(objects.py) 1、导入模块并解析配置文件 在这个模块的开始,我们需要先导入需要使用的其它模块,并且对配置文件进行解析,方便在游戏对象类中使用配置数据。 示例代码: import configparser # 导入配置文件解析模块 import pygame from random import randrange file = 'config.ini' # 配置文件路径 config = configparser.ConfigParser() # 创建解析器对象 config.read(file, encoding='UTF-8') # 读取并解析配置文件 2、创建抽象类 在游戏对象模块中,我们只需要创建Enemy(敌方飞机)和Plane(己方飞机)两个游戏对象类。 不过,在这两个类中,会有很多相同的代码。 我们可以把这些代码抽取出来,单独写成一个类。 示例代码: class GameSprite(pygame.sprite.Sprite): # 定义游戏对象的抽象类 def __init__(self, image): # 定义构造方法(需要添加图片参数) pygame.sprite.Sprite.__init__(self) # 重载超类的构造方法 self.image = pygame.image.load(image).convert_alpha() # 创建image特性存储Surface对象 self.rect = self.image.get_rect() # 通过image创建rect特性 screen = pygame.display.get_surface() # 获取屏幕外观 shrink = -config.getint('Screen', 'margin') # 获取配置文件数值 self.area = screen.get_rect().inflate(shrink, shrink) # 获取屏幕外观填充后的区域(飞机的活动区域) 3、创建敌方飞机对象类 敌方飞机我们要考虑以下内容,包括: 加载后的初始位置; 每次移出屏幕下方后重置的位置; 每一帧刷新后位置(受速度影响); 每一帧刷新时,判断敌方飞机是否从完全移出屏幕下边缘。 示例代码: class Enemy(GameSprite): # 定义敌方飞机对象类 def __init__(self, speed): # 定义构造方法(需要添加速度参数) GameSprite.__init__(self, config.get('Image', 'enemy')) # 重载超类构造方法(参数中传入图片) self.speed = speed # 为敌方飞机设置速度特性 self.reset() # 重置敌方飞机位置 def reset(self): # 定义重置位置的方法 x = randrange(self.area.left, self.area.right) # 从活动范围中获取x轴的随机位置 self.rect.midbottom = x, 0 # 设置游戏对象底部中心位置 def update(self): # 定义刷新位置的方法 self.rect.top += self.speed # 设置每帧向下移动的距离 self.landed = self.rect.top >= self.area.bottom # 获取敌机是否已从屏幕下方完全移出 4、创建己方飞机对象类 己方飞机我们要考虑以下内容,包括: 垂直方向的初始位置; 水平方向可移动范围; 每一帧刷新后的位置(受鼠标指针位置和屏幕可活动区域影响); 触碰到敌方飞机的处理。 示例代码: class Plane(GameSprite): # 定义己方飞机对象类 def __init__(self): # 定义构造方法 GameSprite.__init__(self, config.get('Image', 'plane')) # 重载超类构造方法(参数中传入图片) self.rect.bottom = self.area.bottom # 设置游戏对象垂直方向底部初始位置在屏幕底部 self.pad_top = config.getint('Plane_padding', 'top') # 获取配置文件中游戏对象顶部填充数值 self.pad_side = config.getint('Plane_padding', 'side') # 获取配置文件中游戏对象两侧填充数值 def update(self): # 定义刷新位置的方法 self.rect.centerx = pygame.mouse.get_pos()[0] # 设置游戏对象水平中心位置为鼠标指针的x轴位置 self.rect = self.rect.clamp(self.area) # 设置游戏对象水平方向可活动区域为屏幕外观填充后的区域 def touches(self, other): # 设置触碰到其它游戏对象的方法 bounds = self.rect.inflate(-self.pad_side, -self.pad_top) # 获取游戏对象填充后的边界范围(顶部与两侧) bounds.bottom = self.rect.bottom # 设置边界范围的底部为游戏对象的底部 return bounds.colliderect(other.rect) # 返回游戏对象边界是否触碰到其它游戏对象 在完成以上内容之后,我们就可以着手主程序模块的编写了。 在编写之前,我们先预先了解一下整个程序的结构。我们已经完成了整个程序左侧的部分,在下一篇教程中,我们一起完成右侧的部分。 在这里,我们先对右侧部分内容做一些整体上的理解,这样在编写代码的时候才能有一个清晰的逻辑。 1、State类。 在游戏中有两个状态,我们来看一下状态的类型和产生,分别是: 暂停状态:进入游戏、进入关卡以及游戏结束时。 运行状态:游戏进行时。 2、Level类。 这个类是运行状态,主要是对关卡的处理,包括控制游戏的速度、创建游戏对象、判断游戏结束或者进入下一关以及关卡屏幕的显示方案等。 3、Paused类。 在游戏中会有多个暂停状态,包括: 打开游戏时; 显示信息时; 游戏过关时; 游戏结束时。 每一个暂停的状态,都可以点击继续,都要显示背景颜色、图片以及文字。 所以,我们在Paused类中将多个暂停状态共有的内容进行抽象处理。 4、Paused类的子类。 Paused类的子类包含以下几个: Infol类:游戏信息 StartUp类:打开游戏 LevelCleared类:游戏过关 GameOver类:游戏结束 这些子类只需要定义显示的图片以及文字内容,或者其它独有的功能。 5、Game类。 进行游戏初始化以及定义游戏运行的方法。 综上所述,在游戏主程序模块中,代码的结构如下所示。 示例代码:(planegame.py) import pygame, sys from pygame.locals import * import objects # 导入游戏对象模块 import configparser file = 'config.ini' config = configparser.ConfigParser() config.read(file, encoding='UTF-8') class State: """泛型游戏状态类,可以处理事件和屏幕显示。""" def handle(self, event): """处理退出事件。""" pass def paused_display(self, screen): """暂停时显示。""" pass def display(self, screen): """关卡中显示。""" pass class Level(State): """游戏等级,用于计算共有多少敌机落下,移动游戏对象以及其他与游戏相关的逻辑任务。""" def __init__(self, number=1): """初始化关卡的等级和游戏对象。""" pass def update(self, game): """关卡运行,并进行游戏结束或通过关卡的处理。""" pass def display(self, screen): """关卡运行时的屏幕显示。""" pass class Paused(State): """暂停游戏的状态,按任意键或点击鼠标退出暂停状态。""" def handle(self, event): """处理按任意键继续游戏。""" pass def update(self, game): """按任意键时进入下一个游戏状态。""" pass def paused_display(self, screen): """暂停时显示。""" pass class Info(Paused): """游戏信息。""" pass class StartUp(Paused): """进入游戏。""" pass class LevelCleared(Paused): """游戏过关。""" def __init__(self, number): """过关信息。""" pass def next_state(self): """创建下一关卡。""" pass class GameOver(Paused): """游戏结束。""" pass class Game: """负责主事件循环的游戏对象,完成在不同状态间切换的任务。""" def __init__(self): """初始化。""" pass def run(self): """运行游戏。""" pass if __name__ == '__main__': game = Game() game.run() 本节练习源代码:【点此下载】
练习项目19:使用python制作游戏(上) 这个练习项目来自《Python基础教程(第2版)》,案例原名为“DIY街机游戏”。 原文中做的是一个天上掉秤砣的游戏,玩家需要控制屏幕底部的香蕉左右移动,躲避掉下来的秤砣。 在这篇教程中,我们使用的素材是两个飞机。 也就是说,玩家要控制屏幕底部的飞机,躲避其他从屏幕上方飞来的敌机。这个练习项目,我们需要使用到pygame模块。 安装命令:py -m pip install pygame –user 安装完成之后,可以运行下方命令进行测试,测试程序是一个带有图像与声音小游戏。 测试命令:py -m pygame.examples.aliens 在完成模块的安装之后,我们就可以开始进行项目练习了。 在这篇教程中,我们的练习目标是完成一个敌机在屏幕中向下移动,并且当敌机从屏幕下方消失时,再次让敌机从顶部随机位置出现。 一、导入模块 sys:用于退出游戏程序 pygame:用于游戏的功能 pygame.locals:游戏中需要用到的一些常量 random:用于敌机水平方向随机出现的位置 这里特别说明以下pygame.locals这个模块,它的里面包含所有定义好的常量,例如:FULLSCREEN(全屏)和KEYDOWN(按键按下)。 常量和变量一样,用于保存某些值,区别在于变量的值会发生改变,而常量的值是固定的。 在Python中一般表示常量的名称是全部大写字母的单词。 示例代码: import sys, pygame, time # 导入需要的模块 from pygame.locals import * # 导入所有常量 from random import randrange # 导入随机范围的方法 二、创建游戏对象 在游戏中的每一个飞机都是一个游戏对象(子图形),要通过素材图片进行创建。 在游戏对象中要包含对图片的处理以及重置和更新的方法。 在这个游戏中,重置方法负责将游戏对象移动回初始位置,更新方法则控制游戏对象的移动方向。 一个游戏对象类的创建,需要继承pygame.sprite模块中的Sprite类。 通过图片创建游戏对象时,需要用载入的图片(Surface对象)调用get_rect()方法,这个方法会返回一个Rect对象(rectangle:矩形,理解为一个默认与图片尺寸相同的不可见矩形。),这个对象包含很多特性,例如:left、top、width和height属性,也就是位置与宽高。每个游戏对象的类都要包含image和rect两个特性,image(Surface对象)决定游戏对象显示的内容,rect(Rect对象)决定游戏对象的位置、尺寸。 所以,我们可以通过设置rect对象的特性改变游戏对象的位置。 示例代码: class Enemy(pygame.sprite.Sprite): # 通过图片创建游戏对象类 def __init__(self): self.image = enemy_image pygame.sprite.Sprite.__init__(self) # 重载超类的构造方法 self.rect = self.image.get_rect() # 从图片创建游戏对象的rect特性 self.reset() # 游戏对象重置位置 def reset(self): # 定义游戏对象重置位置的方法 self.rect.top = -self.rect.height # 设置游戏对象y轴初始随机位置 self.rect.centerx = randrange(screen_size[0]) # 设置游戏对象x轴的随机位置 def update(self): # 重写超类中更新游戏对象位置的方法 self.rect.top += 1 # 每次位置向下移动1像素 if self.rect.top > screen_size[1]: # 如果超出屏幕下边界 self.reset() # 重置位置 注意:在上方代码中的变量enemy_image和screen_size会在之后的主程序代码中定义。 二、主程序 主程序包含以下功能: 初始化:包括屏幕尺寸、显示模式以及鼠标指针的设定; 载入图像:读取本地的图像文件 游戏对象集:创建游戏对象集将所有游戏对象添加(当前就1个) 屏幕初始化:定义屏幕的颜色,创建屏幕外观并填充颜色后,弹出显示屏幕。 清除游戏对象的方法:对游戏对象经过的区域进行清理;因为游戏运行时是在不停的绘图,要将之前的图擦除,再绘制新图。 循环:监控退出事件以及进行游戏对象的处理 大家可以通过代码中的注释,仔细理解整个实现过程。 示例代码: # 初始化 pygame.init() screen_size = 360, 640 # 设置游戏屏幕尺寸 pygame.display.set_mode(screen_size, FULLSCREEN) # 设置显示模式(FULLSCREEN:全屏) pygame.mouse.set_visible(0) # 隐藏鼠标指针 # 载入游戏对象的图像 enemy_image = pygame.image.load('enemy.png').convert_alpha() # 载入图片 # 创建游戏对象集并添加游戏对象 sprites = pygame.sprite.RenderUpdates() # 创建游戏对象集 sprites.add(Enemy()) # 添加游戏对象 # 设置屏幕外观 bgcolor = (255, 255, 255) # 游戏屏幕背景颜色 screen = pygame.display.get_surface() # 创建屏幕外观 screen.fill(bgcolor) # 屏幕填充背景颜色 pygame.display.flip() # 弹出屏幕 # 清除游戏对象 def clear_callback(surf, rect): # 定义清除游戏对象的方法 surf.fill(bgcolor, rect) # 屏幕外观中的游戏对象区域填充背景颜色 # 监控退出事件 while True: for event in pygame.event.get(): # 获取事件 if event.type == QUIT: # 如果事件类型是退出 sys.exit() # 退出程序 if event.type == KEYDOWN and event.key == K_ESCAPE: # 如果事件类型为按键并且按键为ESC sys.exit() # 退出程序 # 实现游戏对象的不停移动 pygame.time.Clock().tick(100) # 设置每秒帧数 sprites.clear(screen, clear_callback) # 清除游戏对象经过的区域 sprites.update() # 更新所有游戏对象 updates = sprites.draw(screen) # 屏幕中绘制所有游戏对象 pygame.display.update(updates) # 更新所有游戏对象的显示 完成以上代码之后,我们运行程序,就能够看到一个由上至下移动的敌机。 本节练习源代码:【点此下载】
练习项目18:P2P在线文件共享(四) 这一篇教程,我们完成P2P在线文件共享项目的最后一部分。 先看一下我们的最终实现目标。如上图所示,在下载器中我们需要添加一个文件列表,当下载完毕,文件列表刷新,显示已下载的文件。 并且,在本篇教程新增了一个双击列表中的文件名称打开文件的功能。 我们先来汇总一下要加入的所有功能、控件以及需要的模块。 一、类和方法 需要为Node新增子类,在子类中添加获取文件列表的方法,在这个方法中要调用os模块中的listdir()方法。 添加双击列表项打开文件的方法,在这个方法中需要os模块中的abspath()方法获取绝对路径,并通过system()方法打开文件。 添加更新文件列表的方法。 二、控件 添加列表控件,并绑定双击事件的处理方法。 了解了要添加的内容,我们就可以编写代码了。 示例代码中的注释部分是新增内容。 示例代码: from os import listdir # 导入获取目录列表的方法 from pclient import random_string from pserver import Node, UNHANDLED from xmlrpc.client import ServerProxy, Fault from threading import Thread from time import sleep from os.path import abspath # 导入获取绝对路径的方法 import wx import sys import os # 导入os模块 HEAD_START = 0.1 SECRET_LENGTH = 10 class ListableNode(Node): # 定义Node子类 def file_list(self): # 定义获取文件列表的方法 return listdir(self.dirname) # 返回文件列表 class Client(wx.App): def __init__(self, url_file, dir_name, url): self.dir_name = dir_name self.secret = random_string(SECRET_LENGTH) node = ListableNode(url, dir_name, self.secret) # 实例化ListableNode节点对象 thread = Thread(target=node._start) thread.setDaemon(1) thread.start() sleep(HEAD_START) self.server = ServerProxy(url) for line in open(url_file): self.server.hello(line.strip()) super(Client, self).__init__() # 这一句放在最后,否则Client对象的服务器代理尚未创建,无法正常运行。 def OnInit(self): window = wx.Frame(None, title='文件下载器', size=(400, 200)) # 修改窗口高度 background = wx.Panel(window) self.user_input = user_input = wx.TextCtrl(background) submit = wx.Button(background, label='下载', size=(80, 25)) submit.Bind(wx.EVT_BUTTON, self.fetchHandler) hbox = wx.BoxSizer() hbox.Add(user_input, proportion=1, flag=wx.ALL | wx.EXPAND, border=10) hbox.Add(submit, flag=wx.TOP | wx.BOTTOM | wx.RIGHT, border=10) self.files = files = wx.ListBox(background) # 添加列表控件 files.Bind(wx.EVT_LISTBOX_DCLICK, self.dclickItemHandler) # 绑定列表项点击事件处理方法 self.update_list() # 更新文件列表 vbox = wx.BoxSizer(wx.VERTICAL) vbox.Add(hbox, proportion=0, flag=wx.EXPAND) # 添加列表控件到垂直容器 vbox.Add(files, proportion=1, flag=wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, border=10) background.SetSizer(vbox) window.Show() return True def update_list(self): # 定义更新文件列表的方法 self.files.Set(self.server.file_list()) # 设置列表控件内容为文件列表 def dclickItemHandler(self, event): # 定义列表项点击事件的处理方法 file_path = abspath('%s/%s' % (self.dir_name, self.files.GetStringSelection())) # 获取文件绝对路径 os.system(file_path) # 打开文件 def fetchHandler(self, event): filename = self.user_input.GetValue() try: self.server.fetch(filename, self.secret) self.update_list() # 下载后更新文件列表 except Fault as f: if f.faultCode != UNHANDLED: raise print('没有找到文件:', filename) def main(): url_file, dir_name, url = sys.argv[1:] client = Client(url_file, dir_name, url) client.MainLoop() if __name__ == '__main__': main() 到这里P2P在线文件共享的练习项目就全部完成了。 大家可以在测试文件夹中添加多个测试文件,然后进行下载测试。 本节练习源代码:【点此下载】
练习项目17:P2P在线文件共享(三) 这一篇教程,我们在之前已编写模块的基础上,通过wxPython实现GUI图形界面的客户端。一、导入模块 我们需要导入之前server模块和client模块中实现的一些类和方法,另外还需要导入wx模块。 示例代码: from pclient import random_string # 导入获取随机密钥的方法 from pserver import Node, UNHANDLED # 导入节点类和请求状态变量 from xmlrpc.client import ServerProxy, Fault from threading import Thread from time import sleep import wx # 导入wxPython模块 import sys 二、创建变量 变量与CMD客户端是一样的。 示例代码: HEAD_START = 0.1 SECRET_LENGTH = 10 三、定义客户端类(Client) 在这个类中,我们需要完成以下三个方面: 服务器、服务器代理以及GUI界面初始化。 定义GUI图形界面。 定义下载事件的处理方法。 示例代码: class Client(wx.App): # 定义客户端类 def __init__(self, url_file, dir_name, url): # 定义构造方法 pass def OnInit(self): # 重写超类初始化界面的方法 pass def fetchHandler(self, event): # 定义下载事件的处理方法 pass 接下来,就逐一完成这些内容。 1、定义构造方法。 在构造方法中主要是启动GUI图形界面、启动服务器以及创建服务器代理对象。 这里需要注意的是,通过超类对象启动GUI界面。 示例代码: class Client(wx.App): # 定义客户端类 def __init__(self, url_file, dir_name, url): # 定义构造方法 super(Client, self).__init__() # 将Clint类的对象转换为超类的对象,能够运行OnInit()方法。 self.secret = random_string(SECRET_LENGTH) node = Node(url, dir_name, self.secret) thread = Thread(target=node._start) thread.setDaemon(1) thread.start() sleep(HEAD_START) self.server = ServerProxy(url) for line in open(url_file): self.server.hello(line.strip()) 2、重写初始化GUI界面的方法。 这一部分内容大家可以参考之前我们讲过的wxPython部分。 示例代码: def OnInit(self): # 重写超类初始化界面的方法 window = wx.Frame(None, title='文件下载器', size=(400, 85)) # 创建程序主窗口 background = wx.Panel(window) # 创建功能面板 self.user_input = user_input = wx.TextCtrl(background) # 添加文本框控件 submit = wx.Button(background, label='下载', size=(80, 25)) # 添加下载按钮控件 submit.Bind(wx.EVT_BUTTON, self.fetchHandler) # 绑定下载事件处理方法 hbox = wx.BoxSizer() # 创建尺寸器 hbox.Add(user_input, proportion=1, flag=wx.ALL | wx.EXPAND, border=10) # 水平容器中添加文本框 hbox.Add(submit, flag=wx.TOP | wx.BOTTOM | wx.RIGHT, border=10) # 水平容器中添加下载按钮 vbox = wx.BoxSizer(wx.VERTICAL) # 创建垂直容器 vbox.Add(hbox, proportion=0, flag=wx.EXPAND) # 将水平容器添加到垂直容器 background.SetSizer(vbox) # 将垂直容器添加到尺寸器 window.Show() # 显示程序窗口 return True 3、定义下载事件的处理方法。 点击下载按钮时的事件由这个方法进行处理,主要是调用服务器代理对象的fetch()方法。 示例代码: def fetchHandler(self, event): # 定义下载事件的处理方法 filename = self.user_input.GetValue() try: self.server.fetch(filename, self.secret) except Fault as f: if f.faultCode != UNHANDLED: raise print('没有找到文件:', filename) 四、定义与启动主程序 主程序依然通过命令行启动。 示例代码: def main(): # 定义主程序函数 url_file, dir_name, url = sys.argv[1:] client = Client(url_file, dir_name, url) client.MainLoop() if __name__ == '__main__': main() 到这里我们就完成了一个简单的GUI客户端的编写。 同样通过命令行启动多个客户端,然后尝试通过文件名下载文件。 本节练习源代码:【点此下载】
练习项目15:P2P在线文件共享(一) 这个练习项目来自《Python基础教程(第2版)》,案例原名为“使用XML-RPC进行文件共享”。 原文是基于Pyhton2.7,其中使用的一些模块在Python3中已经发生改变,这里使用Python3完成这个练习项目 。 练习过程分为两个阶段: 实现基本文件共享功能 实现基于CMD客户端界面的文件分享功能 在开始练习之前,我们先了解一下P2P(Peer to Peer)的基本原理。 P2P原为网络通信技术名词,意思是“对等网络”。 在了解对等网络之前,我们先来看一下网络连接模式中另外一种形式的网络,即客户端/服务器网络(Client/Server)。在客户端/服务器网络中,服务器是网络的核心,而客户端是网络的基础,客户端依靠服务器获得所需要的网络资源,而服务器为客户端提供网络必须的资源。 为了更清楚的理解,我用一张简单的图来表示。而对等网络则是另外一种形式,在对等网络上,各台计算机有相同的功能,无主从之分,一台计算机都是既可作为服务器,设定共享资源供网络中其他计算机所使用,也可以作为客户端获取其他计算机上的共享资源。大家常用的BT下载,就是对等网络的一种具体实现。 当我们使用BT下载软件时,我们的计算机既是服务器又是客户端,不但能够下载自己需要的资源,同时也在上传他人需要的资源(即为其他计算机提供资源共享服务)。 在对等网络中,每一台计算机都是一个节点,当一个节点进行资源下载的时候,是如何工作的呢?如上图所示,当节点Peer1进行资源下载时,会先通过广播功能向所有已知节点发出请求,当某个节点(例如Peer2)收到请求,立即对请求进行处理,先对本地资源进行查询。如果找到相应的资源,则回复Peer1节点;如果没有找到可以提供的资源,则通过广播功能向自己的所有已知节点转发Peer1的请求。以此类推。 不过,大家能够想到,这样一层一层的进行请求,几乎是没有尽头的。说明:上图中数字表示访问链长度。 所以,一般会对这个访问链的长度加以限制,例如,只能进行6次广播,如果查询不到资源即终止。 综上所述,每个节点都能够应该具备下图中的功能。了解了P2P的概念以及文件共享的原理,接下来,我们先尝试创建一个简单的服务器和客户端。 这里需要的是xmlrpc模块。 示例代码:(服务器) from xmlrpc.server import SimpleXMLRPCServer server = SimpleXMLRPCServer(('', 6666)) # 创建服务器对象 def twice(x): # 定义供客户端调用的函数 return 2 * x server.register_function(twice) # 注册开放给客户端的函数到服务器对象 server.serve_forever() # 运行服务器 示例代码:(客户端) from xmlrpc.client import ServerProxy server = ServerProxy('http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F219.142.209.7%3A6666&urlrefer=9772e09d62a7ec44e329a9fb2d9447ef') # 连接服务器,创建服务器代理对象。 print(server.twice(6)) # 调用服务器提供的函数,显示输出结果为:12 先运行服务器,再运行客户端,我们能够看到显示输出的结果。 了解了服务器的创建与访问,接下来我们就完成一个功能相对完整的服务器,并模拟客户端的一些访问请求。 首先,先编写服务器代码。 一、导入模块 实现上述服务器功能,需要用到多个模块。 每个模块的用途请参考代码中的注释。 示例代码: from xmlrpc.server import SimpleXMLRPCServer # 用于创建服务器 from xmlrpc.client import ServerProxy # 用于向其它节点发出请求 from urllib.parse import urlparse # 用于URL解析 from os.path import join, isfile # 用于路径处理和文件查询 二、定义常量 常量和变量一样,用于保存值。 区别在于,变量的值会在程序中发生改变,而常量的值是固定的。 在Python中常量的命名通常是全大写字母的单词。 这里的常量用于访问链长度限制和表示查询状态等。 示例代码: MAX_HISTORY_LENGTH = 6 # 访问链最大长度 OK = 1 # 查询状态:正常 FAIL = 2 # 查询状态:无效 EMPTY = '' # 空数据 三、定义获取端口号的函数 当客户端需要下载资源时,实际上需要先对当前节点服务器的共享资源进行查询,所以,我们在创建服务器时,需要在参数中传入当前节点的URL以供查询,并且要根据这个URL中所提供的端口号,创建服务器对象。 所以,我们需要一个函数,能够获取URL中端口号。 示例代码: def get_port(url): # 定义获取端口号的函数 result = urlparse(url)[1] # 解析并获取URL中的[域名:端口号] port = result.split(':')[-1] # 获取以":"进行分割后的最后一组 return int(port) # 转换为整数后返回 三、定义节点类(Node) 节点的类中包含一个节点的所有功能。 示例代码: class Node: def __init__(self, url, dir_name, secret): self.url = url self.dirname = dir_name self.secret = secret self.known = set() def _start(self): # 定义启动服务器的内部方法 pass def _handle(self, filename): # 定义处理请求的内部方法 pass def _broadcast(self, filename, history): # 定义广播的内部方法 pass def query(self, filename, history=[]): # 定义接受请求的方法 pass def hello(self, other): # 定义向添加其它节点到已知节点的方法 pass def fetch(self, filename, secrt): # 定义下载的方法 pass 在上方代码中,类的构造函数不但创建了类的变量保存传入的参数,并且创建了一个已知节点的集合(利用了集合可以去重的特点)。 另外,大家要注意内部方法的名称都是单下划线“_”开头,表示受保护的方法,仅限在模块中的内部调用。 还记得双下划线”__”开头的方法吗?表示是类的私有方法,仅限类中可以调用。 其实,不管单下划线还是双下划线开头的方法,如果你非要在外部调用,也是拦不住的……(前面的教程中提到过) 四、定义启动服务器的方法 在创建服务器对象时,我们将Node类的实例注册到服务器对象,这样就不需要为每个方法进行注册。 示例代码: def _start(self): # 定义启动服务器的方法 server = SimpleXMLRPCServer(('', get_port(self.url)), logRequests=False) server.register_instance(self) # 注册类的实例到服务器对象 server.serve_forever() 五、定义处理请求的方法 在这个方法中,我们需要通过请求的文件名称和目录路径组成文件路径,通过文件路径检查文件是否存在。 如果文件不存在返回无效的状态和空数据,否则返回正常的状态和读取的文件数据。 示例代码: def _handle(self, filename): # 定义处理请求的方法 file_path = join(self.dirname, filename) # 获取请求路径 if not isfile(file_path): # 如果路径不是一个文件 return FAIL, EMPTY # 返回无效状态和空数据 return OK, open(file_path).read() # 返回正常状态和读取的文件数据 六、定义广播请求的方法 广播请求时,需要遍历已知节点,如果节点被访问过,则继续向下一节点发出请求。 如果被请求的节点发生异常,说明该节点失效,将其从已知节点中移除。 如果被请求的节点有效,返回正常的状态和数据。 如果所有已知节点都未能请求到需要的资源,返回无效的状态和空数据。 示例代码: def _broadcast(self, filename, history): # 定义广播的方法 for other in self.known.copy(): # 遍历已知节点的列表 if other in history: # 如果已知节点存在于历史记录 continue # 继续下一个已知节点信息 try: server = ServerProxy(other) # 访问非历史记录中的已知节点 state, data = server.query(filename, history) # 向已知节点发出请求 if state == OK: # 如果状态为正常 return OK, data # 返回有效状态和数据 except OSError: self.known.remove(other) # 如果发生异常从已知节点列表中移除节点 return FAIL, EMPTY # 返回无效状态和空数据 七、定义接收请求的方法 当服务器接收到请求之后,交由内部处理程序进行处理,查询当前节点的资源状态并读取数据。 如果获取到正常状态,返回状态和数据;否则,向所有已知节点广播请求。 这里要注意,在广播请求之前,要把当前节点的URL存放在历史记录列表中,这样能够避免对当前节点的重复请求,并形成访问链;并且,每一层接收请求处理过后,如果没有获取到资源,也都要将当前节点的URL在再次广播请求前存入历史记录列表。 当访问链长度(即历史记录数量)大于等于限定长度时,要返回无效的状态和空数据,不再广播请求。 示例代码: def query(self, filename, history=[]): # 定义接收请求的方法 state, data = self._handle(filename) # 获取处理请求的结果 if state == OK: # 如果是正常状态 return state, data # 返回状态和数据 else: # 否则 history.append(self.url) # 历史记录添加已请求过的节点 if len(history) >= MAX_HISTORY_LENGTH: # 如果历史请求超过6次 return FAIL, EMPTY # 返回无效状态和空数据 return self._broadcast(filename, history) # 返回广播结果 八、定义向添加其它节点到已知节点的方法 这个方法比较简单,只需要将其他节点的URL添加到已知节点。 示例代码: 不过要注意,服务器中的每个方法都必须有返回值,否则,会发生错误。错误提示为:“annot marshal None unless allow_none is enabled”,意思是不能返回None值,除非参数allow_none(允许为空)为启用。这个参数allow_none是指ServerProxy类进行实例化时的参数之一。 def hello(self, other): # 定义向添加其它节点到已知节点的方法 self.known.add(other) # 添加其它节点到已知节点 return OK # 返回值是必须的 九、定义下载的方法 为了避免通过未经许可的渠道获取资源,我们需要在实例化节点时设定密钥,并在下载节点资源时验证密钥。 当密钥验证成功,我们通过接收请求的方法进行请求处理,获取到资源状态和读取的数据。 当资源状态正常时,进行文件的创建,将读取到的数据写入到文件中。 示例代码: def fetch(self, filename, secrt): # 定义下载的方法 if secrt != self.secret: # 如果密钥不匹配 return FAIL, EMPTY # 返回无效状态和空数据 state, data = self.query(filename) # 处理请求获取文件状态与与数据 if state == OK: # 如果返回正常的状态 with open(join(self.dirname, filename), 'w') as file: # 写入模式打开文件 file.write(data) # 将获取到的数据写入文件 return OK # 返回值是必须的 else: return FAIL # 返回值是必须的 十、启动服务器 最后,我们编写启动服务器的代码。 示例代码: if __name__ == '__main__': url = 'http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A6666&urlrefer=42d5ed7f0e88e11593914ba81d02d830' directory = 'NodeFiles01' secret = '123456' node = Node(url, directory, secret) node._start() 接下来,编写客户端代码。 一、发出请求 在客户端中发出请求,我们需要准备请求的文件名称和正确的密钥。 然后,通过ServerProxy类创建服务器代理对象,调用接收请求的方法。 示例代码: from xmlrpc.client import ServerProxy filename = 'file.txt' # 请求的资源文件名称 url1 = 'http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A7777&urlrefer=f24d487918ae3d181dc9441bd7b7ab6b' # 请求的服务器URL peer1 = ServerProxy(url1) # 创建服务器代理对象 print(peer1.query(filename)) # 调用服务器的接收请求方法 url2 = 'http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A6666&urlrefer=42d5ed7f0e88e11593914ba81d02d830' peer2 = ServerProxy(url2) print(peer2.query(filename)) 进行这一步测试时,大家需要先启动多个服务器。 如果是本机测试,这些服务器要有不同的端口、目录名称以及密钥,并且在部分目录中放入被请求的文件。 例如: Node(‘http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A6666&urlrefer=42d5ed7f0e88e11593914ba81d02d830’, ‘NodeFiles01’, ‘123456’) # 目录中有文件“file.txt” Node(‘http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%3A7777&urlrefer=f24d487918ae3d181dc9441bd7b7ab6b’, ‘NodeFiles02’, ‘654321’) 当我们运行上方客户端代码时,我们会看到结果: [2, ”] [1, ‘这是一个用于下载测试的文件!’] 二、添加节点到已知节点 我们将存在被请求文件的节点URL添加到已知节点。 示例代码: peer1.hello(url2) print(peer1.query(filename)) 运行上方代码,显示结果为: [2, ”] [1, ‘这是一个用于下载测试的文件!’] [1, ‘这是一个用于下载测试的文件!’] 三、下载文件 下载文件只需要添加一句代码,对用服务器代理对象的fetch()方法。 因为在上一段代码中,我们已经将存有请求文件的节点添加到了peer1的已知节点中,所以peer1节点能够完成文件下载。 示例代码: peer1.fetch(filename, '123456') # 下载文件 本节练习源代码:【点此下载】
练习项目12:在线编辑文件(下) 这一篇教程,我们来完成在线文件编辑这个练习项目的第二阶段。 我们在上一篇教程中,已经了解了CGI的使用,如果大家有兴趣的话,可以尝试实现一个在线留言的功能。 类似下图所示:(就因为图里那几个字,被系统删帖N次...)注意:这并不是在线聊天的功能,页面不经过刷新是看不到最新留言的。 上方的留言功能允许用户输入昵称,默认显示游客。当留言成功时,新的留言在最上方出现。 此案例源代码在本节资料中获取,下载地址在本文末尾。 接下来,我们回归正题。 第二阶段的内容包括三个文件:index.html、edit.py和save.py。 从文件名上大家就能够理解这三个文件各自的功能。 “index.html”这个文件,是我们要打开的首页。 实际上这么命名是有讲究的,因为在访问Web站点时,如果没有指定访问哪个页面(例如:http://你的外网地址:8888/),会自动打开站点设置的默认页面,通常情况下默认页面就是“index.html”这个文件。在IIS信息服务管理器中,点击站点名称就能够在窗口中部看到默认页面的设置项。 而edit.py和save.py这两个文件一个是编辑功能的实现,一个是保存功能的实现。 下面,我先通过截图让大家了解要实现的目标。 index页面:edit页面:save页面:有了清晰的目标,才能更好地理解每一段示例代码。 1、创建“index.html”文件 在PyCharm中新建一个HTML文件。在创建好的文件中会自带一些代码。 这些代码中有一些需要修改,包括:语言类型、字符集和页面标题。 示例代码: <!DOCTYPE html><html lang="en"> <!"en"需要改为"zh_cn"><head> <meta charset="UTF-8"> <!"UTF-8"需要改为"gbk"> <title>Title</title> <!"Title"需要改为自定义的页面标题></head><body></body></html> 按照上方的注释修改完之后,我们需要添加一些表单标签的代码。 示例代码: <!DOCTYPE html><html lang="zh_cn"> <!修改后的代码><head> <meta charset="gbk"> <!修改后的代码> <title>在线文本编辑</title><!haha> <!修改后的代码></head><body><form action="edit.py" method="post"> <!新增加的代码> <b>请输入文件名称:</b><br/> <input type="text" name="filename"> <input type="submit" value="打开"></form></body></html> 新增加的代码是一对<form>标签和其中的内容。 在<form>标签中,我们需要指定属性“action”为“edit.py”,也就是说当这个表单提交时,由“edit.py”这个脚本进行处理。而后面一个属性“method”需要指定数据的提交的方式,默认为“post”,也可以使用“get”。 在两个<form>标签之间,有两个<input>标签,一个是文本框,一个是提交按钮。因为要获取文本框中输入的内容,所以文本框标签中要设置“name”属性,这个属性的值可以自定义,例如这里填入的是“filename”。 2、创建“html.py”文件 前面没有提到创建这个文件。 这个文件作用是什么呢? 我们在上一篇教程中了解到,在CGI脚本中,我们必须要添加指定的语句。 示例代码: print("Content-Type: text/html")print("") 或者 print("Content-Type: text/html\n") 上述代码是输出头部信息,如果不添加的话,页面会报错无法访问。 但是如果每一个CGI脚本文件都添加的话感觉很麻烦。 所以,我们可以单独创建一个“html.py”文件(必须用这个名称),把上述代码写入。 这样的话,CGI脚本中不添加上述代码也能够正常打开。 注意:此方法未查到相关文档依据,具体原因不明,是小楼无意中的一个发现。如有高手知晓,敬请留言解惑,谢谢! 3、创建“edit.py”文件 “edit.py”文件用于处理“index.html”页面提交的数据。 实际上,是要通过“index.html”页面中输入的文件名打开本地相应的文件。 那么,我们就需要在代码中,对“index.html”页面中文本框的输入进行判断,如果没有输入文件名或没有找到相应的文件都要给出提示,并给出一个能够点击返回的文本链接。 类似下图:示例代码: # -*- coding:gbk -*- import cgi,sys form = cgi.FieldStorage() filename = form.getvalue('filename') if not filename: print('<font color="red">请输入一个文件名称!</font>') print('<a href="\index.html" >返回</a>') sys.exit() try: text = open(filename).read() except FileNotFoundError: print('<font color="red">文件未找到,请输入正确的文件名称!</font>') print('<a href="\index.html" >返回</a>') sys.exit() 在上方代码中,要注意以下几点: 声明编码为“gbk”。 导入sys模块,通过sys.exit()方法结束当前语句下方代码的执行。 通过try/except语句,捕获文件不存在的异常(FileNotFoundError),进行相应处理。 通过<font>标签并设置“color”属性,创建红色文字提示。 通过<a>标签添加返回链接。 如果文件名正确输入,能够打开本地的某个文件,这时我们让CGI脚本将正确的HTML页面代码输出给浏览器。 示例代码: print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>编辑</title> </head> <body> <form action="save.py" method="post"> <input type="hidden" value="%s" name="filename"/> <b>内容编辑:</b>%s<br/> <in> <textarea rows="20" cols="60" name="content">%s</textarea><br/> <b>输入密码:</b> <input type="password" name="password"/> <input type="submit" value="保存文档"> </form> </body> </html> ''' % (filename,filename,text)) 在上方代码中,要注意以下3点: <form>标签包含的表单内容,要通过”save.py”脚本文件进行处理; 从上往下,第一个<input>标签的“type”属性值为“hidden”,是将这个标签隐藏,不在页面中显示;而它的“value”属性值为“%s”,写入的是打开的本地文件的名称。这个标签起到的作用是,将当前打开的文件名称传递给”save.py”脚本文件进行后续保存文件内容的处理。 “type”属性为“password”的<input>标签是一个密码类型的文本框,用于输入密码。 4、创建“save.py”文件 保存文件的处理比较简单,主要是一些验证的处理。 首先,验证文件名称、编辑内容和密码是否都正常被获取。 示例代码: import cgi, sys, hashlib form = cgi.FieldStorage() filename = form.getvalue('filename') password = form.getvalue('password') content = form.getvalue('content') if not (filename and password and content): # 如果有任何一项内容为空值 print('<font color="red">无效的输入!</font>') print('<a href="\edit.py" >返回</a>') sys.exit() 然后,还要验证输入的密码和预留密码是否一致。 示例代码: pwd = hashlib.md5() # 创建md5加密对象 pwd.update(password.encode()) # 添加加密内容 if pwd.hexdigest() != 'e10adc3949ba59abbe56e057f20f883e': # 将加密后的md5码与预置md5码对比 print('<font color="red">无效的密码!</font>') print('<a href="\edit.py" >返回</a>') sys.exit() 最后,如果以上验证都没有问题,保存文件,给出保存成功的提示。 示例代码: with open(filename, 'w') as file: file.write(content) print('<font color="green">文档已保存!</font>') print('<a href="\index.html" >返回</a>') 到这里,我们就完成了整个练习项目。 本节练习源代码:【点此下载】发不了网址...
练习项目14:简单的BBS功能(下) 这一篇教程,我们完成简单的BBS功能第二阶段。 首先,通过一些截图我们看一下,我们的任务目标。 最终的首页: 教程里这张图发出来就被删帖,可以↓↓↓↓消息查看页:消息编辑页:消息保存页:大家所看到的一共有4个页面,分别是: 首页:消息列表,可以点击消息标题查看消息,也可点击发布消息链接发布新消息; 消息查看页:查看消息内容,并且可以点击回复消息进行回复; 消息编辑页:可以发布新的消息,也可以回复其它消息。 消息保存页:对编辑的消息进行保存。 根据这四个页面的功能,我们的编写代码时也分为四个脚本文件。 一、首页(main.py) 这个页面,我们需要将消息列表显示出来。 并且,需要提供一个发布消息的文字链接,能够进入消息编辑页。 显示消息列表的内容,我们需要查询出数据库中所有的消息内容,并参照上一篇教程的实现,将消息内容有层级的显示在页面上。 示例代码: import psycopg2.extras conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>我的论坛</title> </head> <body> <h3>小楼帅哥的第一个BBS</h3> ''') sql = 'select * from messages' curs.execute(sql) messages = curs.fetchall() top_level = [] children = {} for message in messages: parrent_id = message['reply_to'] if parrent_id is None: top_level.append(message) else: children.setdefault(parrent_id, []).append(message) def format_show(message): # 创建递归函数 print('<h5><a href="view.py?id=%(id)i">%(sender)s:%(subject)s</a></h5>' % message) print('<font size="2">{}</font>'.format(message['text'])) try: kids = children[message['id']] except KeyError: pass else: print('<blockquote>') for kid in kids: format_show(kid) print('</blockquote>') for message in top_level: # 遍历顶级消息列表 format_show(message) # 调用递归函数 print(''' <hr/> <a href="edit.py"><font size="2">发布消息</font></a> </body> </html> ''') 在上方代码中,因为点击消息标题需要跳转到消息查看页,我们需要为消息标题加上链接,并且在链接中加入消息的id。 消息查看页的名称是“view.py”,所以链接为:view.py?id=%(id)i。 这是一个相对路径,它能够在与当前文件(main.py)同一目录下找到名为“view.py”的文件将其打开。 链接中的问号“?”,表示后方内容为参数变量和值,“=”前方的“id”是变量,后方的“%(id)i”是值。 “%(id)i”中的“%i”表示这里需要插入一个十进制的整数,“(id)”则能够从“message”字典中获取到。 通过这样的处理,我们在打开“view.py”文件的时候,就能够通过cgi模块获取消息id。 二、消息查看页(view.py) 这个页面中,我们需要根据从首页传过来的id,显示相应的消息内容。 示例代码: import cgi, sys, psycopg2.extras form = cgi.FieldStorage() id = form.getvalue('id') # 获取URL中的id参数 conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>查看消息</title> </head> <body> ''') try: id = int(id) # 将id转换为整数类型 except: # 如果转换异常 print('无效的消息id!') sys.exit() # 退出脚本执行 sql = 'select * from messages where id=%i' % id curs.execute(sql) rows = curs.fetchall() if not rows: # 如果没有查询结果 print('消息不存在!') sys.exit() # 退出脚本执行 row = rows[0] # 获取查询结果中的第一个字典 print(''' <p> <h5>%(subject)s</h5> <font size="2">%(sender)s</font></br> <pre>%(text)s</pre> </p> <hr/> <a href="main.py"><font size="2">返回首页</font></a>| <a href="edit.py?reply_to=%(id)s"><font size="2">回复消息</font></a> </body> </html> ''' % row) 在上方代码中,HTML末尾部分,需要加入两个文字链接,分别能够返回首页和回复消息。 返回首页的链接,可以在<a>标签的“href”属性中直接填入相对路径“main.py”。 回复消息的链接,需要能够打开“edit.py”并且将被回复消息的id通过URL中的参数“reply_to”进行传递。 三、消息编辑页(edit.py) 不管是新发布的消息,还是回复的消息,都能够在这个页面进行编辑。 区别在于,新发布的消息,无需做什么处理;而回复的消息要获取URL中传入的“reply_to”参数内容,根据被回复消息的id自动填入标题(见本文开头的截图),并且在发布消息的时候,还要将这个id传入“save.py”,也就是保存消息页。 示例代码: import sys, cgi, psycopg2.extras form = cgi.FieldStorage() reply_to = form.getvalue('reply_to') # 获取被回复消息的id subject = '' # 创建标题变量 print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>编辑消息</title> </head> <body> <form action="save.py" method="post"> ''') # 创建HTML基本代码并添加表单 if reply_to is not None: # 如果获取到被回复消息的id try: reply_to = int(reply_to) # 将id转换为整数类型 except: # 如果转换发生异常 print('无法回复该消息!') sys.exit() # 退出脚本执行 else: # 如果id可用 print(' <input type="hidden" value="%s" name="reply_to"/>' % reply_to) # 将被回复消息id写入表单元件值 conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) sql = 'select subject from messages where id=%i' % reply_to # 通过被回复消息id查询被回复消息的标题 curs.execute(sql) subject = curs.fetchone()[0] # 将查询到的标题存入变量 if not subject.startswith('回复:'): # 如果标题开头没有“回复:”字样 subject = '回复:' + subject # 为标题添加“回复:”字样 print(''' <b><font size="2">主题:</font></b><br/> <input type="text" value="%s" style="width:240px" name="subject"/><br/> <b><font size="2">作者:</font></b><br/> <input type="text" style="width:240px" name="sender"/><br/> <b><font size="2">编辑内容:</font></b><br/> <textarea rows="5" cols="50" style="width:240px" name="text"></textarea><br/> <input type="submit" value="发布"> </form> <hr/> <a href="main.py"><font size="2">返回首页</font></a> </body> </html> ''' % subject) 上方代码运行时,会显示一个消息编辑页面,点击提交按钮时,会将表单数据交由“save.py”进行处理。 四、消息保存页(save.py) 这个页面需要将获取的表单内容插入到数据库的数据表中。 如果是发布新消息,需要提供“subject”、“sender”和“text”三个字段内容。 而如果是回复消息,在插入数据表时,还需要提供“reply_to”字段。 那么,是不是回复消息,我们可以从表单中尝试获取“reply_to”的值,如果能够获取到id,就是回复消息,如果获取到的是None,则是新发布的消息。 另外,在这个页面中,我们还需要对字段中的单引号“’”进行处理,因为SQL语句中的字符串字段会带有单引号,如果字段值中的单引号不做处理,将会引发异常。如果在字段值中有单引号的话,我们需要用两个单引号来表示。 示例代码: import sys, cgi, psycopg2 def quote(string): # 定义处理单引号的函数 if string: # 如果不是空值或None值 return string.replace("'", "''") # 将单引号替换为两个单引号 else: return string form = cgi.FieldStorage() subject = quote(form.getvalue('subject')) # 获取字段值并进行单引号处理 sender = quote(form.getvalue('sender')) # 获取字段值并进行单引号处理 reply_to = form.getvalue('reply_to') text = quote(form.getvalue('text')) # 获取字段值并进行单引号处理 if reply_to is not None: # 如果有被回复消息的id try: reply_to = int(reply_to) # 将id转换为整数类型 except: # 如果转换异常 print(''' <font size="2" color="red">无法发布该消息!</font> <hr/> <a href="main.py"><font size="2">返回首页</font></a> ''') sys.exit() # 退出脚本执行 if not (subject and sender and text): # 如果有任何一个字段值为空值或者None值 print(''' <font size="2" color="red">请输入回复内容!</font> <hr/> <a href="edit.py?reply_to=%s"><font size="2">返回编辑</font></a> ''' % reply_to) else: # 如果是符合要求的输入 conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor() if reply_to is None: # 如果是回复消息 sql = "insert into messages(subject,sender,text) values('%s','%s','%s')" % (subject, sender, text) else: # 如果是新发布消息 sql = "insert into messages(subject,sender,reply_to,text) values('%s','%s','%i','%s')" % ( subject, sender, reply_to, text) curs.execute(sql) conn.commit() print(''' <font size="2" color="green">发布成功!</font> <hr/> <a href="main.py"><font size="2">返回首页</font></a> ''') 完成以上各部分代码之后,这个简单的BBS功能就全部完成了。 本节练习源代码:【点此下载】
练习项目14:简单的BBS功能(下) 这一篇教程,我们完成简单的BBS功能第二阶段。 首先,通过一些截图我们看一下,我们的任务目标。 最终的首页:消息查看页:消息编辑页:消息保存页:大家所看到的一共有4个页面,分别是: 首页:消息列表,可以点击消息标题查看消息,也可点击发布消息链接发布新消息; 消息查看页:查看消息内容,并且可以点击回复消息进行回复; 消息编辑页:可以发布新的消息,也可以回复其它消息。 消息保存页:对编辑的消息进行保存。 根据这四个页面的功能,我们的编写代码时也分为四个脚本文件。 一、首页(main.py) 这个页面,我们需要将消息列表显示出来。 并且,需要提供一个发布消息的文字链接,能够进入消息编辑页。 显示消息列表的内容,我们需要查询出数据库中所有的消息内容,并参照上一篇教程的实现,将消息内容有层级的显示在页面上。 示例代码: import psycopg2.extras conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>我的论坛</title> </head> <body> <h3>小楼帅哥的第一个BBS</h3> ''') sql = 'select * from messages' curs.execute(sql) messages = curs.fetchall() top_level = [] children = {} for message in messages: parrent_id = message['reply_to'] if parrent_id is None: top_level.append(message) else: children.setdefault(parrent_id, []).append(message) def format_show(message): # 创建递归函数 print('<h5><a href="view.py?id=%(id)i">%(sender)s:%(subject)s</a></h5>' % message) print('<font size="2">{}</font>'.format(message['text'])) try: kids = children[message['id']] except KeyError: pass else: print('<blockquote>') for kid in kids: format_show(kid) print('</blockquote>') for message in top_level: # 遍历顶级消息列表 format_show(message) # 调用递归函数 print(''' <hr/> <a href="edit.py"><font size="2">发布消息</font></a> </body> </html> ''') 在上方代码中,因为点击消息标题需要跳转到消息查看页,我们需要为消息标题加上链接,并且在链接中加入消息的id。 消息查看页的名称是“view.py”,所以链接为:view.py?id=%(id)i。 这是一个相对路径,它能够在与当前文件(main.py)同一目录下找到名为“view.py”的文件将其打开。 链接中的问号“?”,表示后方内容为参数变量和值,“=”前方的“id”是变量,后方的“%(id)i”是值。 “%(id)i”中的“%i”表示这里需要插入一个十进制的整数,“(id)”则能够从“message”字典中获取到。 通过这样的处理,我们在打开“view.py”文件的时候,就能够通过cgi模块获取消息id。 二、消息查看页(view.py) 这个页面中,我们需要根据从首页传过来的id,显示相应的消息内容。 示例代码: import cgi, sys, psycopg2.extras form = cgi.FieldStorage() id = form.getvalue('id') # 获取URL中的id参数 conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>查看消息</title> </head> <body> ''') try: id = int(id) # 将id转换为整数类型 except: # 如果转换异常 print('无效的消息id!') sys.exit() # 退出脚本执行 sql = 'select * from messages where id=%i' % id curs.execute(sql) rows = curs.fetchall() if not rows: # 如果没有查询结果 print('消息不存在!') sys.exit() # 退出脚本执行 row = rows[0] # 获取查询结果中的第一个字典 print(''' <p> <h5>%(subject)s</h5> <font size="2">%(sender)s</font></br> <pre>%(text)s</pre> </p> <hr/> <a href="main.py"><font size="2">返回首页</font></a>| <a href="edit.py?reply_to=%(id)s"><font size="2">回复消息</font></a> </body> </html> ''' % row) 在上方代码中,HTML末尾部分,需要加入两个文字链接,分别能够返回首页和回复消息。 返回首页的链接,可以在<a>标签的“href”属性中直接填入相对路径“main.py”。 回复消息的链接,需要能够打开“edit.py”并且将被回复消息的id通过URL中的参数“reply_to”进行传递。 三、消息编辑页(edit.py) 不管是新发布的消息,还是回复的消息,都能够在这个页面进行编辑。 区别在于,新发布的消息,无需做什么处理;而回复的消息要获取URL中传入的“reply_to”参数内容,根据被回复消息的id自动填入标题(见本文开头的截图),并且在发布消息的时候,还要将这个id传入“save.py”,也就是保存消息页。 示例代码: import sys, cgi, psycopg2.extras form = cgi.FieldStorage() reply_to = form.getvalue('reply_to') # 获取被回复消息的id subject = '' # 创建标题变量 print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>编辑消息</title> </head> <body> <form action="save.py" method="post"> ''') # 创建HTML基本代码并添加表单 if reply_to is not None: # 如果获取到被回复消息的id try: reply_to = int(reply_to) # 将id转换为整数类型 except: # 如果转换发生异常 print('无法回复该消息!') sys.exit() # 退出脚本执行 else: # 如果id可用 print(' <input type="hidden" value="%s" name="reply_to"/>' % reply_to) # 将被回复消息id写入表单元件值 conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) sql = 'select subject from messages where id=%i' % reply_to # 通过被回复消息id查询被回复消息的标题 curs.execute(sql) subject = curs.fetchone()[0] # 将查询到的标题存入变量 if not subject.startswith('回复:'): # 如果标题开头没有“回复:”字样 subject = '回复:' + subject # 为标题添加“回复:”字样 print(''' <b><font size="2">主题:</font></b><br/> <input type="text" value="%s" style="width:240px" name="subject"/><br/> <b><font size="2">作者:</font></b><br/> <input type="text" style="width:240px" name="sender"/><br/> <b><font size="2">编辑内容:</font></b><br/> <textarea rows="5" cols="50" style="width:240px" name="text"></textarea><br/> <input type="submit" value="发布"> </form> <hr/> <a href="main.py"><font size="2">返回首页</font></a> </body> </html> ''' % subject) 上方代码运行时,会显示一个消息编辑页面,点击提交按钮时,会将表单数据交由“save.py”进行处理。 四、消息保存页(save.py) 这个页面需要将获取的表单内容插入到数据库的数据表中。 如果是发布新消息,需要提供“subject”、“sender”和“text”三个字段内容。 而如果是回复消息,在插入数据表时,还需要提供“reply_to”字段。 那么,是不是回复消息,我们可以从表单中尝试获取“reply_to”的值,如果能够获取到id,就是回复消息,如果获取到的是None,则是新发布的消息。 另外,在这个页面中,我们还需要对字段中的单引号“’”进行处理,因为SQL语句中的字符串字段会带有单引号,如果字段值中的单引号不做处理,将会引发异常。如果在字段值中有单引号的话,我们需要用两个单引号来表示。 示例代码: import sys, cgi, psycopg2 def quote(string): # 定义处理单引号的函数 if string: # 如果不是空值或None值 return string.replace("'", "''") # 将单引号替换为两个单引号 else: return string form = cgi.FieldStorage() subject = quote(form.getvalue('subject')) # 获取字段值并进行单引号处理 sender = quote(form.getvalue('sender')) # 获取字段值并进行单引号处理 reply_to = form.getvalue('reply_to') text = quote(form.getvalue('text')) # 获取字段值并进行单引号处理 if reply_to is not None: # 如果有被回复消息的id try: reply_to = int(reply_to) # 将id转换为整数类型 except: # 如果转换异常 print(''' <font size="2" color="red">无法发布该消息!</font> <hr/> <a href="main.py"><font size="2">返回首页</font></a> ''') sys.exit() # 退出脚本执行 if not (subject and sender and text): # 如果有任何一个字段值为空值或者None值 print(''' <font size="2" color="red">请输入回复内容!</font> <hr/> <a href="edit.py?reply_to=%s"><font size="2">返回编辑</font></a> ''' % reply_to) else: # 如果是符合要求的输入 conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor() if reply_to is None: # 如果是回复消息 sql = "insert into messages(subject,sender,text) values('%s','%s','%s')" % (subject, sender, text) else: # 如果是新发布消息 sql = "insert into messages(subject,sender,reply_to,text) values('%s','%s','%i','%s')" % ( subject, sender, reply_to, text) curs.execute(sql) conn.commit() print(''' <font size="2" color="green">发布成功!</font> <hr/> <a href="main.py"><font size="2">返回首页</font></a> ''') 完成以上各部分代码之后,这个简单的BBS功能就全部完成了。 本节练习源代码:【点此下载】
练习项目13:简单的BBS功能(上) 这个练习项目来自《Python基础教程(第2版)》,案例原名为“自定义电子公告板”。 练习包括两个阶段: 第一阶段:实现与PostgreSQL的连接与数据操作,并将数据呈现为网页。 第二阶段:实现消息列表、查看消息、编辑消息、回复消息和保存信息等功能。 本篇教程我们完成第一阶段。 一、创建数据库与数据表 大家可以参考《PostgreSQL数据库简易安装教程》完成数据库以及表的创建。 除了《PostgreSQL数据库简易安装教程》中创建数据库与数据表的SQL语句,这里再给大家列出一些当前项目准备过程中可能需要用到的语句。 此处以数据库“myproject_db”和数据表“messages”以及用户“opython”简单举例。 注意:红色部分加粗部分均为可变内容,并且,SQL Shell中命令结束加分号“;”才能够执行。 <- 删除数据库 -> drop database myproject_db; <- 添加超级用户 -> create user opython with superuser password ‘123456‘; <- 修改密码 -> alter user opython with password ‘654321‘; <- 删除用户名 -> drop user opython ; <- 删除数据表 -> drop table messages; <- 添加数据行 -> insert into messages(列名1,列名2,…) values (值1,值2,…); <- 查询数据行 -> select * from messages; select subject,text from messages where id=1; 在进行下一步之前,如果还没有创建数据表“messages”的话,可以通过如下语句进行创建。 示例代码: <- 创建数据表 -> create table messages( id serial primary key, subject text not null, sender text not null, reply_to integer references messages, text text not null ); 这里,我对每一列的名称做一下解释: id:消息序号,serial表示自动增长的数字,primary key表示这一列是不可重复的主键。 subject:消息标题,text表示文本类型,not null表示不可为空值。 sender:消息发送人。 reply_to:被回复消息序号,integer表示整数类型,references messages表示引用messages表的主键,也就是说被回复消息的序号必须是messages表中主键序号之一,即只有已存在表中的消息才能被回复。 text:消息内容。 二、尝试与数据库进行连接 与数据库进行连接,我们需要使用到psycopg2模块。 通过psycopg2模块中的connect()方法与数据库进行连接。 注意,connect()方法中参数的提供有两种不同的写法。 示例代码: import psycopg2 conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') # 连接数据库 # 另一种写法:conn=psycopg2.connect('user=opython password=opython dbname=myproject_db ') curs = conn.cursor() # 获取游标对象 sql = 'select * from messages' curs.execute(sql) # 执行SQL语句 print(curs.fetchall()) # 显示输出所有查询结果 当我们运行代码,我们能够看到由数据库中所有数据行元组所组成的列表。 当然,如果数据库中没有任何数据,只会显示一个空列表。 到这里,我们已经能够成功的进行数据库的访问。 三、尝试向数据库中添加数据 我们可以通过input()函数模拟获取用户输入,然后根据输入中是否有被回复消息的序号来决定如何插入新的数据。 这个功能实现起来很简单,请大家参考示例代码进行理解。 示例代码: import psycopg2 subject = input('标题:') sender = input('发送人:') reply_to = input('回复给:') text = input('内容:') if reply_to: # 如果是回复已有消息 sql = "insert into messages(subject,sender,reply_to,text) values('%s','%s',%s,'%s')" % ( subject, sender, reply_to, text) else: # 否则是发布新的消息 sql = "insert into messages(subject,sender,text) values('%s','%s','%s')" % (subject, sender, text) conn = psycopg2.connect(database='myproject_db', user='postgres', password='opython') curs = conn.cursor() curs.execute(sql) conn.commit() # 提交数据操作 sql = 'select * from messages' curs.execute(sql) print(curs.fetchall()) 运行上方代码之后,我们就能够向数据库中添加新的内容了。 四、查询数据并输出到HTML 输出的网页内容如下图: 我们可以在PyCharm中先新建一个HTML文件,编写页面代码。 示例代码:(HTML) <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>我的论坛</title> </head> <body> <h3>小楼帅哥的第一个BBS</h3> <b><font size="2">id.发送人:消息标题</font></b><br/> <font size="2">消息内容</font> <blockquote> <b><font size="2">id.回复人:回复标题</font></b><br/> <font size="2">回复内容</font> </blockquote> </body> </html> 接下来,我们可以新建一个CGI或py脚本,将这些HTML代码复制到脚本代码中使用。 示例代码: import psycopg2.extras conn = psycopg2.connect(database='myproject_db', user='opython', password='opython') curs = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) # 参数是以字典形式获取查询结果 print(''' <!DOCTYPE html> <html lang="zh_cn"> <head> <meta charset="gbk"> <title>我的论坛</title> </head> <body> <h3>小楼帅哥的第一个BBS</h3> ''') sql = 'select * from messages' curs.execute(sql) messages = curs.fetchall() top_level = [] # 保存父级消息的列表 children = {} # 保存子级消息的字典 for message in messages: # 遍历返回的消息列表 parrent_id = message['reply_to'] # 获取消息元组中的reply_to列 if parrent_id is None: # 如果不是回复的消息 top_level.append(message) # 添加到父级消息列表 else: children.setdefault(parrent_id, []).append(message) # 否则以被回复消息的序号为键添加回复消息到值的列表 def format_show(message): print('<h5>{}.{}:{}</h5>'.format(message['id'], message['sender'], message['subject'])) # 输出父级消息内容 print('<font size="2">{}</font>'.format(message['text'])) try: kids = children[message['id']] # 根据父级消息id从子级消息字典中查找所有子级消息 except KeyError: pass else: print('<blockquote>') for kid in kids: # 遍历找到的消息列表 format_show(kid) # 递归进行下一级消息的处理 print('</blockquote>') print('<p>') for message in top_level: # 遍历父级消息 format_show(message) # 调用递归函数进行多级遍历 print(''' </p> </body> </html> ''') 在上方代码中,重点内容是所有消息的输出使用到了递归函数。 基本原理是:读取一条顶级消息时,调用递归函数,将顶级消息输出到页面,然后以顶级消息的id为键,获取到子级消息字典中对应的值的列表。然后,再对子级消息进行遍历,将子级消息作为父级消息再次调用递归函数。 到这里,我们就完成了第一阶段的练习。 本节练习源代码:【点此下载】
练习项目11:在线编辑文件(上) 这个练习项目来自《Python基础教程(第2版)》,案例原名为“使用CGI进行远程编辑”。 练习包括两个阶段: 第一阶段:实现在线对文件的编辑与保存的基本功能。 第二阶段:在线打开指定文件名的文件进行编辑与保存,并添加密码保护。 这一篇教程,我们先来完成第一个阶段的练习。 首先,我们看一下要实现的内容。如上图所示,我们要完成一个浏览器中能够打开的页面,在页面中的文本域控件中能够输入内容,点击提交按钮时,文本域中的内容被保存。 保存内容的文件需要在项目文件夹中创建,例如新建一个文本文档,并将文件名称更改为“form.dat”。 接下来,大家能够看到,在图中浏览器地址栏访问页面的URL是“IP地址:端口号/文件名”。 IP地址很明显不是局域网内的IP地址,而是本机在互联网上的IP地址。 这个IP地址,我们通过百度搜索关键字“IP”就能够查到。那么,端口号“8888”还有后面的文件名“form.py”是怎么回事呢? 这里先不要着急,我们需要先了解一些额外的信息。 既然是远程进行文件编辑,目的是可以通过互联网上的其他计算机能够通过上方的IP地址,进行文件的访问、编辑与保存。 我们的教程是基于Windows系统,如果想实现这样的功能,需要我们在本机开启互联网信息服务(IIS),也就是把本机变成一个Web服务器。 另外,我们还需要Web服务器支持CGI(通用网关接口)应用程序,这样才能通过CGI运行Python文件或者CGI文件。 开启IIS以及CGI应用程序支持的步骤如下: 1、在【控制面板】-【程序与功能】的窗口左侧,我们点击【打开或关闭Windows】功能的选项(需要等待一段时间)。 2、在打开的窗口中找到【Internet信息服务】,选中【Web管理工具】前方的复选框。3、点击【万维网服务】前面的“+”,在展开的选项中选中【常见HTTP功能】前方的复选框。3、在【万维网服务】的子选项中,继续找到【应用程序开发功能】,选中【CGI】前方的复选框。4、完成以上设置后,点击确定按钮(需要等待一段时间)。 通过以上几个步骤,我们就开启了我们所需要的功能。 但是,现在还不能正常使用。 我们还想要在开启的IIS功能中进行进一步的设置,配置我们的站点。 配置站点的具体步骤: 1、在【控制面板】窗口中双击【管理工具】,然后双击【Internet信息服务(IIS)管理器】。 补充:也可以、在桌面的计算机图标上点击鼠标右键,选择【管理】打开计算机管理窗口;双击窗口左侧【服务和应用程序】选项,在展开的选项中双击打开【Internet信息服务(IIS)管理器】。 2、在窗口左侧,展开列表我们能够看到【网站】这一项,将此项展开,里面有一个默认的站点【Default Web Site】,在这个站点名称上点击鼠标右键,选择【重命名】,将站点名称改为自己想要的名称(例如:MySite)。 3、在窗口右侧,点击【基本设置】,指定网站的根目录。这里指向的是项目文件夹。4、此时的站点还不能运行CGI或Python文件,我们需要为这些文件添加支持。 在窗口的中部双击打开【处理程序映射】,点击右侧【添加模块映射】选项,并依次填入如下内容: 文件路径,例如:填入“*.cgi”则支持运行所有的CGI脚本文件;填入“*.py”则支持运行所有的Python脚本文件。 模块,选择“CgiModule”。 可执行文件,填入“Python.exe”文件的路径,末尾必须加上“ %s %s”,并且注意空格。 配置名称,例如:MyCGI(千万别用“CGI”,否则你会被报错弄到爽死的!)。 填入内容之后,点击确定按钮,此时会弹出对话框,选择【是】,允许添加ISAPI扩展。5、完成上面一步之后,我们点击窗口左侧的计算机名称,在窗口中部找到【ISAPI和CGI限制】,将其打开。在打开的窗口中,应该包含了一项没有描述的内容。如果没有的话,可以点击右侧的【添加】,添加内容包括: ISAPI或CGI路径:和上一步的“python.exe”文件路径保持一致。 描述:任意输入即可,例如:CGI。 运行执行扩展路径:必须勾选说明:通过以上两步,我们就为站点指定了CGI或者Python的脚本文件由哪一个程序功能模块和应用程序执行。 6、因为是网络用户访问本机,我们需要为站点目录以及Python应用程序目录设置访问权限。 首先,我们查看一下要授权给哪一个用户组。 还是在刚才的设置窗口中,点击左侧的站点名称,然后在窗口中部双击打开【身份验证】。 在打开的界面上,我们能够看到【匿名身份验证】的状态为【已启用】。 点中这一项,再点击窗口右侧的编辑,我们能够看到【特定用户】所对应的用户组名称(例如:IUSER)。知道了访问站点的用户组名称,我们就要给这个用户组访问站点目录和Python应用程序的权限。 操作步骤如下(以项目文件夹为例): 在本地硬盘的项目文件夹上点击鼠标右键,打开【属性】,选择【安全】; 在【安全】的窗口中,点击【编辑】,点击【添加】,点击【高级】,点击右侧的【立即查找】; 在下方的搜索结果中,找到之前看到的用户组名称,双击这个名称,然后点击确定按钮。 在用户名或组的列表中,点中新添加的用户组,将权限设置为【完全控制】。注意:“Python.exe”文件所在的文件夹也要进行上述权限设置。 到此为止,Web服务器就能够正常工作了。 我们来做个测试。 在项目文件夹下创建两个文件,文件名称分别为“xxx.cgi”和“xxx.py”。 然后,在这两个文件中分别写入一些代码。 示例代码: print("Content-Type: text/html") print("") print("总算特么能正常使用了啊!") 通过“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%2Fxxx.cgi&urlrefer=5f762784c6eac1b8ea23fb0691095d80”和“http://tieba.baidu.com/mo/q/checkurl?url=http%3A%2F%2F127.0.0.1%2Fxxx.py&urlrefer=707b4ae3807c04b7981423dd1475d3e8”我们即可访问上述两个文件。说到这里,我们来了解一下IIS和CGI的概念(引用自百度百科)。 IIS(Internet Information Services),意为互联网信息服务,是由微软公司提供的基于运行Microsoft Windows的互联网基本服务。IIS是一种Web(网页)服务组件,其中包括Web服务器、FTP服务器、NNTP服务器和SMTP服务器,分别用于网页浏览、文件传输、新闻服务和邮件发送等方面。 CGI (Common Gateway Interface),意为通用网关接口。CGI应用程序能与浏览器进行交互,还可通过数据库API 与数据库服务器等外部数据源进行通信,从数据库服务器中获取数据。格式化为HTML文档后,发送给浏览器,也可以将从浏览器获得的数据放到数据库中。几乎所有服务器都支持CGI,可用任何语言编写CGI。 从概念上来看,IIS负责提供Web服务器功能,CGI则可以通过编写应用程序处理数据并输出HTML文档。 那么,我们所要做的就是编写CGI脚本或者Python脚本,将这些脚本放在服务器中处理用户的访问。 不过,现在我们有可能还不能够通过互联网上的其他计算机访问我们创建好的站点。 这是因为很多计算机都是通过路由器连接互联网。 在之前关于网络编程的教程中,我们了解了IP和端口的相关知识。 我们知道IP地址指向一台计算机,而端口指向计算机上的应用程序。 如果计算机直接连接互联网,通过IP地址和端口号访问我们创建好的站点。 但是,如果计算机是通过路由器连接互联网,IP地址指向的是路由器,路由器在接收到网络上其他计算机的访问请求时,无法知道应该由局域网内的哪一台计算机的哪一个端口来处理这个请求。 所以,为了能够让互联网上的其他计算机能够访问局域网内的某一台计算机,我们需要进行端口映射设置(也可能叫端口转发)。 因为不同的路由器设置界面不一样,这里不做截图举例,只做简单的说明。 1、通过本机浏览器访问192.168.0.1或者192.168.1.1打开路由器的设置页面。 注意:也可能是其它IP地址,具体见路由器的说明书。 2、在路由器设置界面中找到类似端口映射(或端口转发)的设置,一般在高级设置中。 3、添加映射(或转发)条目,将外部访问请求的端口(例如:8888)指向某个计算机(例如:192.168.0.222)的80端口。 也就是说,当有来自互联网其他计算机对8888这个端口的访问时,路由器将该请求转到局域网内IP地址为192.168.0.222的计算机的80端口上。 通过这样的设置,我们创建的站点就能够正常被互联网上的其他计算机访问了。 注意:IIS中创建的站点默认端口为80,如果需要修改,可以在创建站点的窗口右侧找到【绑定】的设置,修改里面的端口号数字。 当完成以上设置,我们可以尝试用本机的外网IP加上端口号来访问刚才创建的CGI和Python脚本。到此为止,所有的准备工作才算全部完成。 接下来,我们实现如何远程访问并编辑本地文件。 因为刚才已经做了相关配置,所以创建CGI脚本可以使用“.cgi”文件,也可以使用“.py”文件。 这里为了编写代码方便,在PyCharm中我们使用“.py”文件编写。 首先,我们需要使用cgi模块,主要是使用这个模块中的FieldStorage类。 因为,FieldStorage类能够获取到HTML页面中控件中的内容。 具体实现过程,大家还是通过示例代码中的注释来理解。 Python文件名称:form.py 保存内容文件名称:form.dat 示例代码: # -*- coding:gbk -*- import cgi form = cgi.FieldStorage() # 获取表单内容对象 file_text = open('form.dat').read() # 读取数据文件已有内容 text = form.getvalue('content', file_text) # 获取字段值存入变量,如果字段值为空值则将数据文件已有内容存入变量。 with open('form.dat', 'w') as file: # 打开数据文件 file.write(text) # 写入文本域控件中的内容 print("Content-Type: text/html") # 输出HTML头内容 print("") # 输出空行结束头部(如果前一句末尾没有加入“\n”,则必须添加此语句。) print(''' <html> <head> <!-- 声明字符集 --> <meta charset="gbk"> <title>在线编辑</title> </head> <body> <!-- 创建表单,指定处理程序和提交方式。 --> <form action="form.py" method="POST"> <!-- 留言文本域,值来自content变量。 --> <textarea rows="10" cols="40" name="content" >%s</textarea><br /> <!-- 提交按钮,提交表单数据。 --> <input value="提交" type="submit" /> </form> </body> </html> ''' % text) # 输出页面代码并将言内容写入文本域控件 在上方代码中,有以下几个关键点: 1、首行要声明编码为“gbk”,另外,在FieldStorage类名称上点击鼠标右键,选择去往(Go To)选项中的实现(Implementation)选项,在打开的cgi模块中,找到FieldStorage类的构造方法,将里面的参数encoding=’utf-8’改为encoding=’gbk’。否则,我们编写的程序不能支持中文。 2、代码中的print(“Content-Type: text/html”) 和print(“”) 这两句是必须的,这是声明输出的文件内容类型以及结束头部信息。 3、getvalue()方法的参数为两个,前面的参数是key,也就是HTML标签中的name值,通过这个key能够获取到HTML标签中的value值;后面的参数是默认值,也就是当没有找到指定的key或者通过key获取的值为空值时所返回的值。 4、HTML代码中的<Form>标签为表单标签,而“type”为“submit”的<input>标签为提交按钮,点击这个提交按钮时,页面中的内容会被FieldStorage类获取,然后页面重新加载,再次执行Python文件中的代码。 5、HTML中注释格式为:<!注释内容> 6、在PyCharm中编辑HTML内容,可以单独创建一个HMTL文件进行编写,PyCharm为我们提供了一些便利的功能。例如: 自动为我们写好一些必须的代码; 编写代码时给出输入提示; 可以通过点击编辑区右上方的浏览器图标,快速在浏览器中预览; 可以通过快捷键<Ctrl+Alt+L>让代码变得规范整齐。 当我们在浏览器中通过IP地址和端口号访问上方写好的Python文件,如果没有问题,能够看到如下HTML页面。当我们在页面上的编辑区输入一些文字并点击提交按钮时,输入的内容会保存到“form.dat”文件中,并且内容也会在浏览器中显示,不会随页面刷新而消失。 本节练习源代码:【点此下载】
练习项目10:在线聊天室(下) 这一篇教程,我们继续使用Python完成带有更多功能的料甜市。 因为功能比较多,这里我们先把功能归类,然后在此基础上编写代码。 分类示意图:(有图发不上来,哎!) 如上图所示,在新的功能中,我们要支持一些命令。 所以,需要一个对命令进行处理的类(CMDHandler)。 然后,房间实际上有三个,一个用于用户登入的房间(CheckInRoom),一个用于用户登出的房间(CheckOutRoom),还有就是进行聊天的房间(ChatRoom)。 房间都要包含一些功能,例如进入房间、离开房间、广播以及退出料甜市。 所以,我们抽象出一个房间类(Room),定义这些功能,再让具体的房间去继承。 除了以上所述内容,我们还要处理每一个来自客户端的连接与通信,这就需要创建聊天会话的类(ChatSession)和结束会话的类(EndSession)。 最后,还有启动服务创建与客户端连接的类(ChatServer)。 有了功能的具体划分之后,我们就依次来实现各个类。 1、导入模块 示例代码: from asyncore import dispatcher from asynchat import async_chat import asyncore 2、CMDHandler类 在前面的示意图中,我们看到了如下命令: login <用户名>:登录 logout:退出 say <发言内容>:发言 look:查看当前房间中的在线用户 who:查看当前服务器中的所有在线用户 注意:在当前案例中,我们只创建一个料甜市,所以房间中的用户和服务器中的用户是一样的,也就意味着look和who这两个命令的效果是相同的。 对应上述这些命令,我们需要编写相应的方法。 为了能够简单的调用命令所对应的方法,我们可以将方法名定义为: login:do_login() logout:do_logout() say:do_say() look:do_look() who:do_who() 如上所述,每个方法名都是前缀“do_”和命令m名称组成。 所以,当我们获取到来自客户端的数据,就要对数据进行分析,获取数据中包含的命令,并根据不同的命令调用不同的方法。 不过,我们还要想到,用户可能会有错误的输入,没有输入命令,或者输入了错误的命令,这种情况我们也需要进行处理。 接下来,大家可以通过示例代码中的注释了解整个处理过程。 示例代码: class CMDHandler: def unknown(self, session, cmd): # 定义未知命令的处理方法 session.push('不支持命令:{}\r\n请重新输入!\r\n'.format(cmd).encode('GBK')) # 向客户端推送错误提示 def handle(self, session, data): # 定义命令的处理方法 if data.strip(): # 判断去除空格后是否还有数据 parts = data.split(' ', 1) # 对数据以空格为分割符进行分割(最大分割次数为1次) cmd = parts[0] # 分割后的第1部分为命令 try: line = parts[1].strip() # 将分割后的第2部分去除空格保存到变量 except IndexError: # 如果捕获索引错误 line = '' # 设置变量为空值 method = getattr(self, 'do_' + cmd, None) # 获取指定名称的方法对象 try: method(session, line) # 调用获取到的方法对象 except TypeError: # 如果捕获类型错误(没有找到方法) self.unknown(session, cmd) # 调用未知命令的处理方法 3、Room类 这个类要定义房间的功能。 示例代码: class Room(CMDHandler): 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('GBK')) # 向每一个用户会话广播信息(注意编码) def do_logout(self, session, line): # 定义退出命令的处理方法 raise EndSession # 抛出结束会话的异常 这里要注意remove()方法和do_logout()方法的区别。 remove()方法是从一个房间离开的方法,例如离开CheckInRoom进入ChatRoom就会调用这个方法。 do_logout()方法是关闭客户端与服务器的连接对话的方法。 3、EndSession类 在上一段代码中我们看到了EndSession,它是我们定义的异常类,所以这个类要继承Exception类。 在这个类中,无需添加什么内容。 示例代码: class EndSession(Exception): pass 4、CheckInRoom类 为了避免和登录这个动作混淆,这里我使用了CheckInRoom这个名称。 这就好像我们去开房,要先在登记处的房间进行登记,然后才能进入入住的房间。 在这个类中,我们要实现登录的相关处理,包括: 用户进入房间时发送欢迎信息 未使用login命令登录时的处理 使用login命令登录时的的处理 并且,使用login命令登录时也有不同的情形需要处理: 未带有用户名 用户名已使用 正确输入 接下来,大家还是通过代码中的注释理解这些功能的实现。 示例代码: class CheckInRoom(Room): def add(self, session): # 重写超类的方法 Room.add(self, session) # 重载超类的方法 session.push('欢迎进入{}料甜市!\r\n'.format(self.server.name).encode('GBK')) # 推送欢迎信息 def unknown(self, session, cmd): # 重写位置命令的处理方法 session.push('请使用命令 <login 用户名> 进行登录!\r\n'.encode('GBK')) # 推送命令错误的提示 def do_login(self, session, line): # 定义登录命令的处理方法 name = line.strip() # 数据中命令后方的内容去重空格后作为用户名 if not name: # 如果没有内容 session.push('请输入用户名!\r\n'.encode('GBK')) # 推送提示 elif name in self.server.users: # 如果服务器的用户列表中已有这个用户名 session.push('用户名 <{}> 已被使用!\r\n'.format(name).encode('GBK')) # 推送提示 else: # 正确输入时 session.name = name # 将用户名保存到会话 session.enter(self.server.main_room) # 将会话进入到主聊天房间 在上方代码中,enter()方法我们还没有定义,它是一个会话进入某个房间的方法,需要在ChatSession类中定义这个方法。 5、ChatRoom类 这个是料甜市的类,主要实现功能如下: 一个会话进入当前房间的处理方法 一个会话离开当前房间的处理方法 处理say命令的方法 处理look命令的方法 处理who命令的方法 大家通过示例代码中的注释理解实现过程。 示例代码: class ChatRoom(Room): def add(self, session): self.broadcast('用户 <' + session.name + '> 进入料甜市!\r\n') # 广播用户进入房间的信息 Room.add(self, session) # 重载超类的方法 self.server.users[session.name] = session # 向服务器的用户列表添加会话的用户名 def remove(self, session): self.broadcast('用户 <' + session.name + '> 离开料甜市!\r\n') # 广播用户离开房间的信息 Room.remove(self, session) # 重载超类的方法 def do_say(self, session, line): # 定义say命令的处理方法 self.broadcast(session.name + ':' + line + '\r\n') # 广播用户的发言信息 def do_look(self, session, line): # 定义look命令的处理方法 session.push('当前房间在线用户\r\n'.encode('GBK')) session.push('----------------------\r\n'.encode('GBK')) for user in self.sessions: # 遍历所有会话 session.push('{}\r\n'.format(user.name).encode('GBK')) # 推送每个会话中的用户名信息 def do_who(self, session, line): # 定义who命令的处理方法 session.push('当前服务器在线用户\r\n'.encode('GBK')) session.push('----------------------\r\n'.encode('GBK')) for user in self.server.users: # 遍历服务器用户列表 session.push('{}\r\n'.format(user).encode('GBK')) # 推送服务器中所有的用户名信息 注意,上方代码中的“server.users”是服务器的用户列表,users需要在ChatServer类中定义。 6、CheckOutRoom类 还是以开房举例(虽然我很少干这个事),退房时,我们不能一走了之,也要到登记处的房间进行登记。 在这个类中,我们只需要定义进入登记房间的方法。 示例代码: class CheckOutRoom(Room): def add(self, session): # 重写进入房间的方法 try: del self.server.users[session.name] # 从服务器用户列表中移除当前会话的用户名 except KeyError: pass 7、ChatSession类 在上一篇教程中我们编写过这个类,现在,我们再给它添加一些新的功能。 在这个类中,我们要对会话进行处理,包括会话进入房间、会话中的数据处理以及会话关闭的处理。 新增部分,在示例代码中添加了注释,大家还是通过注释进行理解。 class ChatSession(async_chat): def __init__(self, server, sock): async_chat.__init__(self, sock) self.server = server # 保存传入服务器对象 self.set_terminator('\r\n'.encode()) self.data = [] self.name = None # 创建会话的用户名变量 self.enter(CheckInRoom(server)) # 将当前会话添加到登录的房间 def enter(self, room): # 定义进入房间的方法 try: current = self.room # 获取当前会话的房间 except AttributeError: # 如果当前会话没有在任何房间 pass else: # 如果当前会话没有在某个房间 current.remove(self) # 从当前会话所在的房间移除当前会话 self.room = room # 设置当前会话的房间为传入的房间 room.add(self) # 传入的房间添加当前会话 def collect_incoming_data(self, data): self.data.append(data.decode()) def found_terminator(self): line = ''.join(self.data) self.data = [] try: self.room.handle(self, line) # 处理客户端发来的数据 except EndSession: # 捕获结束会话异常 self.handle_close() # 调用关闭连接的处理方法 def handle_close(self): # 重写关闭连接的处理方法 async_chat.handle_close(self) # 重载超类的方法 self.enter(CheckOutRoom(self.server)) # 将当前会话进入退出登记的房间 8、ChatSever类 在上一篇教程中我们页编写过这个类,同样,我们在这里要添加和修改的内容。 具体添加的内容,大家看代码中的注释。 示例代码: class ChatSever(dispatcher): def __init__(self, name, port): dispatcher.__init__(self) self.create_socket() self.set_reuse_addr() self.bind(('', port)) self.listen(5) self.name = name # 保存传入的料甜市服务器名称 self.users = {} # 初始化服务器用户列表 self.main_room = ChatRoom(self) # 设定主聊天房间 def handle_accept(self): conn, addr = self.accept() ChatSession(self, conn) # 将服务器对象和连接对象传入会话对象 以上就是所有类的编写。 最后,我们就可以运行服务器和Telnet客户端进行测试了。 示例代码:(启动服务器) if __name__ == '__main__': name = '心动' port = 6666 server = ChatSever(name, port) try: asyncore.loop() except KeyboardInterrupt: print('服务器已关闭!') 本节练习源代码:【点此下载】
练习项目10:在线聊天室(下) 这一篇教程,我们继续使用Python完成带有更多功能的聊天室。 因为功能比较多,这里我们先把功能归类,然后在此基础上编写代码。 分类示意图:如上图所示,在新的功能中,我们要支持一些命令。 所以,需要一个对命令进行处理的类(CMDHandler)。 然后,房间实际上有三个,一个用于用户登入的房间(CheckInRoom),一个用于用户登出的房间(CheckOutRoom),还有就是进行聊天的房间(ChatRoom)。 房间都要包含一些功能,例如进入房间、离开房间、广播以及退出聊天室。 所以,我们抽象出一个房间类(Room),定义这些功能,再让具体的房间去继承。 除了以上所述内容,我们还要处理每一个来自客户端的连接与通信,这就需要创建聊天会话的类(ChatSession)和结束会话的类(EndSession)。 最后,还有启动服务创建与客户端连接的类(ChatServer)。 有了功能的具体划分之后,我们就依次来实现各个类。 1、导入模块 示例代码: from asyncore import dispatcher from asynchat import async_chat import asyncore 2、CMDHandler类 在前面的示意图中,我们看到了如下命令: login <用户名>:登录 logout:退出 say <发言内容>:发言 look:查看当前房间中的在线用户 who:查看当前服务器中的所有在线用户 注意:在当前案例中,我们只创建一个聊天室,所以房间中的用户和服务器中的用户是一样的,也就意味着look和who这两个命令的效果是相同的。 对应上述这些命令,我们需要编写相应的方法。 为了能够简单的调用命令所对应的方法,我们可以将方法名定义为: login:do_login() logout:do_logout() say:do_say() look:do_look() who:do_who() 如上所述,每个方法名都是前缀“do_”和命令m名称组成。 所以,当我们获取到来自客户端的数据,就要对数据进行分析,获取数据中包含的命令,并根据不同的命令调用不同的方法。 不过,我们还要想到,用户可能会有错误的输入,没有输入命令,或者输入了错误的命令,这种情况我们也需要进行处理。 接下来,大家可以通过示例代码中的注释了解整个处理过程。 示例代码: class CMDHandler: def unknown(self, session, cmd): # 定义未知命令的处理方法 session.push('不支持命令:{}\r\n请重新输入!\r\n'.format(cmd).encode('GBK')) # 向客户端推送错误提示 def handle(self, session, data): # 定义命令的处理方法 if data.strip(): # 判断去除空格后是否还有数据 parts = data.split(' ', 1) # 对数据以空格为分割符进行分割(最大分割次数为1次) cmd = parts[0] # 分割后的第1部分为命令 try: line = parts[1].strip() # 将分割后的第2部分去除空格保存到变量 except IndexError: # 如果捕获索引错误 line = '' # 设置变量为空值 method = getattr(self, 'do_' + cmd, None) # 获取指定名称的方法对象 try: method(session, line) # 调用获取到的方法对象 except TypeError: # 如果捕获类型错误(没有找到方法) self.unknown(session, cmd) # 调用未知命令的处理方法 3、Room类 这个类要定义房间的功能。 示例代码: class Room(CMDHandler): 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('GBK')) # 向每一个用户会话广播信息(注意编码) def do_logout(self, session, line): # 定义退出命令的处理方法 raise EndSession # 抛出结束会话的异常 这里要注意remove()方法和do_logout()方法的区别。 remove()方法是从一个房间离开的方法,例如离开CheckInRoom进入ChatRoom就会调用这个方法。 do_logout()方法是关闭客户端与服务器的连接对话的方法。 3、EndSession类 在上一段代码中我们看到了EndSession,它是我们定义的异常类,所以这个类要继承Exception类。 在这个类中,无需添加什么内容。 示例代码: class EndSession(Exception): pass 4、CheckInRoom类 为了避免和登录这个动作混淆,这里我使用了CheckInRoom这个名称。 这就好像我们去开房,要先在登记处的房间进行登记,然后才能进入入住的房间。 在这个类中,我们要实现登录的相关处理,包括: 用户进入房间时发送欢迎信息 未使用login命令登录时的处理 使用login命令登录时的的处理 并且,使用login命令登录时也有不同的情形需要处理: 未带有用户名 用户名已使用 正确输入 接下来,大家还是通过代码中的注释理解这些功能的实现。 示例代码: class CheckInRoom(Room): def add(self, session): # 重写超类的方法 Room.add(self, session) # 重载超类的方法 session.push('欢迎进入{}聊天室!\r\n'.format(self.server.name).encode('GBK')) # 推送欢迎信息 def unknown(self, session, cmd): # 重写位置命令的处理方法 session.push('请使用命令 <login 用户名> 进行登录!\r\n'.encode('GBK')) # 推送命令错误的提示 def do_login(self, session, line): # 定义登录命令的处理方法 name = line.strip() # 数据中命令后方的内容去重空格后作为用户名 if not name: # 如果没有内容 session.push('请输入用户名!\r\n'.encode('GBK')) # 推送提示 elif name in self.server.users: # 如果服务器的用户列表中已有这个用户名 session.push('用户名 <{}> 已被使用!\r\n'.format(name).encode('GBK')) # 推送提示 else: # 正确输入时 session.name = name # 将用户名保存到会话 session.enter(self.server.main_room) # 将会话进入到主聊天房间 在上方代码中,enter()方法我们还没有定义,它是一个会话进入某个房间的方法,需要在ChatSession类中定义这个方法。 5、ChatRoom类 这个是聊天室的类,主要实现功能如下: 一个会话进入当前房间的处理方法 一个会话离开当前房间的处理方法 处理say命令的方法 处理look命令的方法 处理who命令的方法 大家通过示例代码中的注释理解实现过程。 示例代码: class ChatRoom(Room): def add(self, session): self.broadcast('用户 <' + session.name + '> 进入聊天室!\r\n') # 广播用户进入房间的信息 Room.add(self, session) # 重载超类的方法 self.server.users[session.name] = session # 向服务器的用户列表添加会话的用户名 def remove(self, session): self.broadcast('用户 <' + session.name + '> 离开聊天室!\r\n') # 广播用户离开房间的信息 Room.remove(self, session) # 重载超类的方法 def do_say(self, session, line): # 定义say命令的处理方法 self.broadcast(session.name + ':' + line + '\r\n') # 广播用户的发言信息 def do_look(self, session, line): # 定义look命令的处理方法 session.push('当前房间在线用户\r\n'.encode('GBK')) session.push('----------------------\r\n'.encode('GBK')) for user in self.sessions: # 遍历所有会话 session.push('{}\r\n'.format(user.name).encode('GBK')) # 推送每个会话中的用户名信息 def do_who(self, session, line): # 定义who命令的处理方法 session.push('当前服务器在线用户\r\n'.encode('GBK')) session.push('----------------------\r\n'.encode('GBK')) for user in self.server.users: # 遍历服务器用户列表 session.push('{}\r\n'.format(user).encode('GBK')) # 推送服务器中所有的用户名信息 注意,上方代码中的“server.users”是服务器的用户列表,users需要在ChatServer类中定义。 6、CheckOutRoom类 还是以开房举例(虽然我很少干这个事),退房时,我们不能一走了之,也要到登记处的房间进行登记。 在这个类中,我们只需要定义进入登记房间的方法。 示例代码: class CheckOutRoom(Room): def add(self, session): # 重写进入房间的方法 try: del self.server.users[session.name] # 从服务器用户列表中移除当前会话的用户名 except KeyError: pass 7、ChatSession类 在上一篇教程中我们编写过这个类,现在,我们再给它添加一些新的功能。 在这个类中,我们要对会话进行处理,包括会话进入房间、会话中的数据处理以及会话关闭的处理。 新增部分,在示例代码中添加了注释,大家还是通过注释进行理解。 class ChatSession(async_chat): def __init__(self, server, sock): async_chat.__init__(self, sock) self.server = server # 保存传入服务器对象 self.set_terminator('\r\n'.encode()) self.data = [] self.name = None # 创建会话的用户名变量 self.enter(CheckInRoom(server)) # 将当前会话添加到登录的房间 def enter(self, room): # 定义进入房间的方法 try: current = self.room # 获取当前会话的房间 except AttributeError: # 如果当前会话没有在任何房间 pass else: # 如果当前会话没有在某个房间 current.remove(self) # 从当前会话所在的房间移除当前会话 self.room = room # 设置当前会话的房间为传入的房间 room.add(self) # 传入的房间添加当前会话 def collect_incoming_data(self, data): self.data.append(data.decode()) def found_terminator(self): line = ''.join(self.data) self.data = [] try: self.room.handle(self, line) # 处理客户端发来的数据 except EndSession: # 捕获结束会话异常 self.handle_close() # 调用关闭连接的处理方法 def handle_close(self): # 重写关闭连接的处理方法 async_chat.handle_close(self) # 重载超类的方法 self.enter(CheckOutRoom(self.server)) # 将当前会话进入退出登记的房间 8、ChatSever类 在上一篇教程中我们页编写过这个类,同样,我们在这里要添加和修改的内容。 具体添加的内容,大家看代码中的注释。 示例代码: class ChatSever(dispatcher): def __init__(self, name, port): dispatcher.__init__(self) self.create_socket() self.set_reuse_addr() self.bind(('', port)) self.listen(5) self.name = name # 保存传入的聊天室服务器名称 self.users = {} # 初始化服务器用户列表 self.main_room = ChatRoom(self) # 设定主聊天房间 def handle_accept(self): conn, addr = self.accept() ChatSession(self, conn) # 将服务器对象和连接对象传入会话对象 以上就是所有类的编写。 最后,我们就可以运行服务器和Telnet客户端进行测试了。 示例代码:(启动服务器) if __name__ == '__main__': name = '心动' port = 6666 server = ChatSever(name, port) try: asyncore.loop() except KeyboardInterrupt: print('服务器已关闭!') 本节练习源代码:【点此下载】
练习项目09:在线聊天室(上) 这个练习项目来自《Python基础教程(第2版)》,案例原名为“虚拟茶话会”。 其实,这个项目就是要实现一个简单的在线聊天室。 在完成这个项目之前,我们需要开启Windows系统的Telnet客户端。 在系统的【控制面板】-【程序和功能】的窗口中,点击左侧的【打开或关闭Windows功能】。 在弹出的窗口中,勾选【Telnet客户端】,然后点击确定按钮,等待系统设置完成。这个Telnet客户端用于模拟用户和我们编写的聊天室服务器进行通信。 这个项目练习分为两个两个阶段。 第一阶段:了解服务器的搭建,实现基本通讯功能; 第二阶段:实现用户登录、发言、查看在线用户、离开聊天室以及向所有登录用户推送系统信息和用户发言等功能。 我们先来完成第一阶段的目标。 在这里,我们需要使用Python的内置模块中的aysncore模块。 使用aysncore模块主要是为了实现多用户的同时连接。 在aysncore模块包含了一个dispatcher类,通过这个类能够创建套接字对象。 除此之外,我们之后会使用到这个类中的一些方法进行事件处理。 所以,在代码中,我们让服务器类继承自dispatcher类。 1、尝试搭建一个支持多用户连接的服务器。 实现这个服务器,代码比较简单,并且在之前的课程中我们也接触过相关内容。 大家通过代码中的注释,基本就能够理解。 示例代码:(服务器) import asyncore from asyncore import dispatcher class ChatServer(dispatcher): # 定义聊天服务器类 def __init__(self, port): # 重写构造方法 dispatcher.__init__(self) # 重载超类的构造方法 self.create_socket() # 创建套接字对象 self.set_reuse_addr() # 设置地址可重用 self.bind(('', port)) # 绑定本机地址与端口 self.listen(5) # 设置监听连接数 def handle_accept(self): # 重写处理客户端连接的方法 ssl, addr = self.accept() # 获取服务器端的SSL通道和远程客户端的地址 ssl.send('您已成功连接服务器!'.encode()) # 发送欢迎信息 print('连接来自:', addr[0], '端口:', addr[1]) # 显示输出连接的客户端信息 port = 6666 # 设置服务器端口号 server = ChatServer(port) # 实例化聊天服务器 try: asyncore.loop() # 运行异步循环 except KeyboardInterrupt: # 捕获键盘中断异常 print('服务器已被关闭!') 注意:代码中的set_reuse_addr()方法能够保证服务器未正常关闭时,再次开启服务器能够重用端口号。因为服务器异常关闭时,可能导致端口依然被占用,新启动的服务器无法使用该端口。 运行上方代码,开启服务器。 这个时候,我们就可以通过telnet命令和服务器进行连接。 telnet命令为:telnet 127.0.0.1 6666或者telnet localhost 6666 当然,我们也可以编写一个客户端进行连接。 示例代码:(客户端) import socket server = socket.socket() host = socket.gethostname() port = 6666 server.connect((host, port)) print(server.recv(1024).decode()) 上方的客户端代码运行之后,获取到欢迎信息就自动退出了。 如果是通过Windows系统中的命令行终端启动服务器的话,可以通过快捷键<Ctrl+C>进行关闭(按完之后可能要等一小会儿),这时except语句会捕获KeyboardInterrupt异常,在命令行窗口中显示输出文字信息“服务器已关闭!”。 2、再次实现聊天服务器,添加处理连接会话的功能。 这一次服务器的实现,我们需要使用asynchat模块。 asynchat模块完成了大部分对套接字的读写操作,我们接下来只需要重写模块中的collect_incoming_data()方法和found_terminator()方法。 大家先来看再次实现的服务器代码。 示例代码:(服务器) import asyncore from asyncore import dispatcher from asynchat import async_chat class ChatSession(async_chat): def __init__(self, sock): async_chat.__init__(self, sock) self.set_terminator('\r\n'.encode()) # 设置数据的终止符号 self.data = [] # 创建数据列表 self.push('欢迎进入聊天室!'.encode('GBK')) # 向单个客户端发送欢迎信息 def collect_incoming_data(self, data): # 重写处理客户端发来数据的方法 self.data.append(data.decode()) # 将客户端发来的数据添加到数据列表 def found_terminator(self): # 重写发现数据中终止符号时的处理方法 line = ''.join(self.data) # 将数据列表中的内容整合为一行 self.data = [] # 清空数据列表 print(line) # 显示客户端输出发来的内容 class ChatServer(dispatcher): def __init__(self, port): dispatcher.__init__(self) self.create_socket() self.set_reuse_addr() self.bind(('', port)) self.listen(5) self.sessions = [] def handle_accept(self): ssl, addr = self.accept() self.sessions.append(ChatSession(ssl)) # 将新的用户连接会话添加到会话列表 port = 6666 server = ChatServer(port) try: asyncore.loop() except KeyboardInterrupt: print('服务器已被关闭!') 在上方代码中,当服务器运行后,每一个来自客户端的连接,都会被作为ChatSession类的参数,实例化为一个会话对象。 在会话对象中,来自客户端的数据内容通过collect_incoming_data()方法进行读取、处理和暂存,并通过found_terminator()方法检测数据中是否包含设置的指定终止符号,当发现终止符号时,对所有的暂存数据进行处理(例如,将发言内容推送给聊天室中所有的在线用户)。 注意,代码中的push()方法能够像单个客户端发送数据内容,发送数据内容时注意进行编码,编码格式为“GBK”,因为不进行编码的话,当发送的内容包含中文时,通过telnet连接所收到的内容会变成乱码。 这里,为了便于测试,我们将客户端也进行更新。 同样要注意,接收内容时要进行解码,编码的格式和push()方法中的格式保持一致。 示例代码:(客户端) import socket server = socket.socket() host = socket.gethostname() port = 6666 server.connect((host, port)) print(server.recv(1024).decode('GBK')) # 注意解码以及编码格式 while True: name = input('请输入发言内容:') server.send('{}\r\n'.format(name).encode()) # 注意将输入内容加上终止符号 启动服务器,并运行客户端。 此时,客户端会收到来自服务器的欢迎信息。 当在客户端输入内容回车之后,服务端的运行窗口会显示来自客户端的内容。 大家可以启动多个客户端进行测试,每个客户端发出的内容都会显示在服务器的运行窗口中。 3、添加广播以及客户端断开连接的功能 在聊天室中,当一个用户发言时,其他用户都能够看到这条发言。 所以,我们需要在代码中添加广播功能,这也就是我们创建会话列表的原因。 将每一个来自客户端的连接保存为一个会话,添加到会话列表中,当广播内容时,遍历这个会话列表,将广播内容推送到每一个会话的客户端。 当然,当一个用户进入或离开聊天室,也就是打开或关闭会话连接时,我们需要将这个用户的连接会话从会话列表中添加或移除,并向其他会话广播该用户进入或离开的信息。(示例代码中只以离开为例,大家可以自行添加进入的广播代码。) 上面所说的这些功能,大家可以通过示例代码中的注释进行理解。 示例代码:(服务器) from asynchat import async_chat from asyncore import dispatcher import asyncore class ChatSession(async_chat): def __init__(self, server, sock, addr): async_chat.__init__(self, sock) self.server = server self.addr = addr self.set_terminator('\r\n'.encode()) self.data = [] self.push('欢迎进入{}聊天室!\r\n'.format(server.name).encode('GBK')) 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) # 从会话列表中移除当前会话 self.server.broadcast('{}离开聊天室!\r\n'.format(self.addr[0])) # 广播当前会话客户端离开信息 class ChatServer(dispatcher): def __init__(self, port, name): dispatcher.__init__(self) self.create_socket() self.bind(('', port)) self.listen(5) self.name = name # 设置服务器名称 self.sessions = [] def disconnect(self, session): # 定义客户端断开连接的方法 self.sessions.remove(session) # 从会话列表移除断开连接的会话 def broadcast(self, line): # 定义广播的方法 for session in self.sessions: # 遍历所有会话 session.push('{}\r\n'.format(line).encode('GBK')) # 向所有会话的客户端推送内容 def handle_accept(self): conn, addr = self.accept() self.sessions.append(ChatSession(self, conn, addr)) if __name__ == '__main__': port = 6666 name = 'Python' server = ChatServer(port, name) try: asyncore.loop() except KeyboardInterrupt: print('服务器已关闭!') 运行上方代码启动服务器。 因为需要在客户端显示服务器推送的信息,所以这里我们需要使用Telnet连接服务器。 打开多个命令行终端,每个都通过telnet命令连接服务器,这时每个终端都会显示来自服务器的欢迎信息。 当从任意一个终端输入内容并按下回车键发送,所有的终端中都会显示这条内容。 而且,当关闭任何一个命令行终端,其他的终端中都会显示服务器推送的用户离开信息。 到这里,这个练习项目的第一阶段我们就完成了。 本节练习源代码:【点此下载】
1 下一页