Python第三方库开发应用实战

978-7-115-51495-0
作者: 张有菊
译者:
编辑: 张涛
分类: Python

图书目录:

详情

《Python第三方库开发应用实战》循序渐进地讲解了Python中常用第三方库的核心知识,并通过具体实例的实现过程演练了各个库的使用流程。全书共12章,分别讲解了Tornado框架、Django框架、Flask框架、数据库存储框架、数据库驱动框架、使用ORM操作数据库、特殊文本格式处理、图像处理、图形用户界面、数据可视化、第三方多媒体库、第三方网络开发库。 本书适用于已经了解了Python基础语法的读者,也适用于希望进一步提高自己Python开发水平的读者,还可以作为大专院校相关专业的师生用书和培训学校的教材。
  

如您想下载本书配套资源,请添加本书QQ读者群: 685431007。进群后,可在群公告获得本书配套资源的下载地址。    

图书摘要

版权信息

书名:Python第三方库开发应用实战

ISBN:978-7-115-51495-0

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

编  著 张有菊

责任编辑 张 涛

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书循序渐进地讲解了Python中常用第三方库的核心知识,并通过具体实例的实现过程演练了各个库的使用流程。全书共12章,分别讲解了Tornado框架、Django框架、Flask框架、数据库存储框架、数据库驱动框架、使用ORM操作数据库、特殊文本格式处理、图像处理、图形用户界面、数据可视化、第三方多媒体库、第三方网络开发库。

本书适用于已经了解了Python基础语法的读者,也适用于希望进一步提高自己Python开发水平的读者,还可以作为大专院校相关专业的师生用书和培训学校的教材。


Python的应用越来越广,这种语言的成功得益于第三方库的支持,第三方库能够帮助程序员迅速实现需要的功能,提高程序员的开发效率。为了让更多的读者了解Python中常用的框架和第三方库的使用方法,作者专门编写了本书。

本书精心挑选了12类常用的库进行讲解,正是这些功能强大的第三方库吸引了众多程序员纷纷加入到Python开发者行列中。本书主要内容如下。

第1章讲解了Tornado框架的使用,主要包括Tornado开发基础、表单和模板操作、数据库操作等。

第2章讲解Django框架的应用,包括搭建Django开发环境、使用Django后台系统开发一个博客系统、开发一个在线商城系统、使用Mezzanine库、使用Cartridge库、使用django-oscar 库等。

第3章讲解Flask框架,包括Flask开发基础、基于Flask开发Web程序、表单操作、使用数据库、收发电子邮件、开发图书借阅管理系统等。

第4章讲解数据库存储框架,包括pickleDB、TinyDB和ZODB,并通过开发个人日志系统,讲解这些数据库存储框架的应用。

第5章介绍数据库驱动框架,包括连接MySQL数据库、连接PostgreSQL数据库、连接SQLite3数据库、连接NoSQL数据库等。

第6章介绍如何使用ORM操作数据库,如使用mysqlclient连接数据库、使用Peewee连接数据库、使用Pony连接数据库、使用mongoengine连接MongoDB数据库等。

第7章介绍特殊文本格式处理的框架知识,如Tablib模块、Office模块/库、PDF模块/库等。

第8章介绍图像处理的框架,包括使用Pillow库、使用hmap库,以及使用pyBarcode库创建条形码、使用qrcode库创建二维码、使用face_recognition库实现人脸识别等。

第9章讲解和图形用户界面有关的库,包括PyQt库、pyglet库、toga库、wxPython库等。

第10章介绍与数据可视化有关的库,如Matplotlib库、pygal库、csvkit库、NumPy库等。

第11章介绍多媒体库,如使用audiolazy库处理数字信号、使用audioread库实现音频解码、使用eyeD3库处理音频,以及m3u8库、mutagen库、pydub库、tinytag库、moviepy库、scikit-video库的用法等。

第12章介绍网络开发库,包括HTML、XML、HTTP和URL的处理等。

本书内容涵盖了常用的Python第三方库,通过案例,循序渐进地讲解了这些库中函数的使用方法,帮助读者快速掌握和应用这些库。

本书采用理论加实例的讲解方式,通过实例展示知识点的应用,达到了学以致用的目的。

通过对本书的学习,读者可以构建自己的Python工具箱。借助该工具箱,读者能够使用Python 开发各种类型的应用程序。

本书适用于已经了解了Python基础语法的读者,也适用于希望进一步提高自己Python开发水平的读者,还可以作为大专院校相关专业的师生用书和培训学校的教材。

在编写过程中,本书得到了人民邮电出版社编辑的大力支持,正是各位编辑的高效工作,才使得本书能够顺利出版。另外,也十分感谢我的家人给予的巨大支持。本人水平毕竟有限,书中纰漏之处在所难免,诚请读者提出意见或建议,以便修订并使之更臻完善。编辑联系邮箱是zhangtao@ptpress.com.cn。

最后感谢您购买本书,希望本书能成为您编程路上的挚友,祝您阅读快乐!

作者


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书配套资源包括书中示例的源代码。

要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意,为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/ selfpublish/submission即可)。

如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


Tornado是FriendFeed使用的可扩展的非阻塞式Web服务器及其相关工具的开源版本。这个Web框架看起来有些像web.py或者Google的WebApp。不过为了能有效地利用非阻塞式服务器环境,这个Web框架还包含了一些相关的有用的工具和优化措施。本章将详细讲解使用Tornado框架开发Web应用程序的核心知识。

Tornado是Python中一种比较流行的、强大的、可扩展的Web非阻塞式开源服务器框架,也是一个异步的网络库,能够帮助开发者快速简单地编写出高速运行的Web应用程序。

Tornado基于Bret Taylor和其他人员为FriendFeed所开发的网络服务框架,在FriendFeed被Facebook收购后得以开源。Tornado在设计之初就考虑到了性能因素,旨在解决C10K问题。这样的设计使得它成为一个高性能的框架。此外,它还拥有用户验证、社交网络以及与外部服务(如数据库和网站API)进行异步交互的工具。

自2009年第一个版本发布以来,Tornado已经获得了很多社区的支持,并且在不同的场合得以应用。除了FriendFeed和Facebook外,还有很多公司在生产上转向Tornado,包括Quora、Turntable.fm、Bit.ly、Hipmunk及MyYearbook等公司。

Tornado框架的主要特点如下。

在现实应用中,通常将Tornado框架分为如下4个部分。

在Python程序中使用Tornado框架之前,首先需要搭建Tornado框架环境。Tornado框架可以通过pip或者easy_install命令进行安装。pip命令如下。

pip install tornado

easy_install命令如下。

easy_install tornado

在控制台中使用easy_install命令的安装界面如图1-1所示。

图1-1 使用“easy_install”命令安装Tornado框架

在Tornado框架中,是通过继承类tornado.Web.RequestHandler来编写Web服务器端程序的,并编写get()、post()业务方法,以实现对客户端指定URL的GET请求和POST请求的回应。然后启动框架中提供的服务器以等待客户端连接,处理相关数据并返回请求信息。下面的实例文件app.py演示了使用Python编写一个基本Tornado程序的过程。

源码路径:daima\1\1-2\app.py

import tornado.ioloop            #导入Tornado框架中的相关模块
import tornado.web               #导入Tornado框架中的相关模块
#定义子类MainHandler
class MainHandler(tornado.web.RequestHandler):
    def get(self):                #定义请求业务函数get()
      self.write("Hello, world") #输出文本

def make_app():                  #定义应用配置函数
   return tornado.web.Application([
       (r"/", MainHandler),      #定义URL映射列表
   ])

if __name__ == "__main__":
    app = make_app()             #调用配置函数
    app.listen(8888)             #设置监听服务器8888端口
    tornado.ioloop.IOLoop.current().start()    #启动服务器

在上述实例代码中,首先导入了Tornado框架的相关模块,然后自定义URL的响应业务方法(GET、POST等)。接下来,实例化Tornado模块中提供的Application类,并传URL映射列表及有关参数。最后启动服务器。在命令提示符下的对应子目录中执行:

python app.py

如果没有语法错误,服务器就已经启动并等待客户端连接了。在服务器运行以后,在浏览器地址栏中输入http://localhost:8888,就可以访问服务器,看到默认主页页面了。在浏览器中的执行效果如图1-2所示。

图1-2 执行效果

通过上述实例可以看出,使用Tornado框架编写的服务器端程序的结构是非常清晰的。其基本工作就是编写相关的业务处理类,并将它们和某一特定的URL映射起来,Tornado框架服务器收到对应的请求后进行调用。一般来说,对于比较简单的网站项目,可以把所有的代码放入同一个模块之中。但为了维护方便,可以按照功能将其划分到不同的模块中,其一般模块结构(目录结构)如下。

proj\
     manage.py                #服务器启动入口
     settings.py              #服务器配置文件
     url.py                   #服务器URL配置文件
     handler\
                login.py      #相关URL业务请求处理类
     db\                      #数据库操作模块目录
     static\                  #静态文件存放目录
                js\           #JavaScript文件存放目录
                css\          #CSS文件目录
                img\          #图片资源文件目录
     templates\               #网页模板文件目录

在Python程序中,客户端经常需要获取如下3类参数。

1.获取URL中的参数

在Tornado框架中,要获取URL中包含的参数,需要在URL定义中获取参数,并在对应的业务方法中给出相应的参数名进行获取。在Tornado框架的URL定义字符串中,使用正则表达式来匹配URL及URL中的参数,比如:

 (r"uid/([0-9]+)",UserHdl)

上述形式的URL字符串定义可以接受形如“uid/”后跟一位或多位数字的客户端URL请求。针对上面的URL定义,可以如下方式定义get()方法:

def get (self,uid):
    pass

此时,当发来匹配的URL请求时会截取与正则表达式匹配的部分,传递给get()方法,这样可以把数据传递给uid变量,以在方法get()中使用。

下面的实例文件can.py演示了在GET方法中获取URL参数的过程。

源码路径:daima\1\1-2\can.py

import tornado.ioloop                     #导入Tornado框架中的相关模块
import tornado.web                        #导入Tornado框架中的相关模块
class zi(tornado.web.RequestHandler):     #定义子类zi
    def get(self,uid):                    #获取URL参数
         self.write('你好,你的UID是%s!' % uid)    #显示UID,来源于下面的正则表达式

app = tornado.web.Application([    #使用正则表达式获取参数
     (r'/([0-9]+)',zi),
     ],debug=True)

if __name__ == '__main__':
     app.listen(8888)              #设置监听服务器的8888端口
     tornado.ioloop.IOLoop.instance().start() #启动服务器

在上述实例代码中,使用正则表达式定义了URL字符串,使用get()方法获取了URL参数中的UID。在浏览器中的执行效果如图1-3所示。

图1-3 执行效果

2.获取GET和POST中的参数

在Tornado框架中,要获取GET或POST中的请求参数,只需要调用从类RequestHandler中继承来的get_argument()方法即可。方法get_argument()的原型如下。

get_argument('name', default=",strip=False)

下面的实例文件po.py演示了获取POST参数的过程。

源码路径:daima\1\1-2\po.py

import tornado.ioloop         #导入Tornado框架中的相关模块
import tornado.web            #导入Tornado框架中的相关模块
html_txt = """                #初始化变量html_txt
<!DOCTYPE html>               #下面是一段普通的HTML代码
<html>
    <body>
        <h2>收到GET请求</h2>
        <form method='post'>
        <input type='text' name='name' placeholder='请输入你的姓名' />
        <input type='submit' value='发送POST请求' />
        </form>
    </body>
</html>
"""
class zi(tornado.web.RequestHandler):  #定义子类zi
     def get(self):                    #定义方法get()以处理get请求
        self.write(html_txt)           #作为页面内容进行处理
     def post(self):                   #定义方法post()以处理post请求
        name = self.get_argument('name',default='匿名',strip=True)   #获取上面表单中的姓名
        self.write("你的姓名是:%s" % name)       #显示姓名
app = tornado.web.Application([                #实例化Application对象
     (r'/get',zi),
     ],debug=True)

if __name__ == '__main__':
     app.listen(8888)                          #设置监听服务器的8888端口
     tornado.ioloop.IOLoop.instance().start()  #启动服务器

在上述实例代码中,当服务器收到GET请求时返回一个带有表单的页面内容;当用户填写自己的姓名并单击“发送POST请求”按钮时,将用户输入的姓名以POST参数形式发送到服务器端。最后在服务器端调用方法get_argument()来获取输出请求。在浏览器中输入“http://localhost:8888/get”后的初始执行效果如图1-4所示。

在表单中输入姓名“浪潮软件”,然后单击“发送POST请求”按钮后的执行效果如图1-5所示。

图1-4 初始执行效果

图1-5 输入信息并单击按钮后的执行效果

cookie(有时也用其复数形式cookies)指某些网站为了辨别用户身份、进行会话跟踪而存储在用户本地终端上的数据(通常经过加密)。在现实应用中,服务器可以利用cookie包含信息的任意性来筛选并经常性维护这些信息,以判断在HTTP传输中的状态。cookie最典型的应用是判定注册用户是否已经登录网站,用户可能会得到提示,是否在下一次进入此网站时保留用户信息以便简化登录手续,这些都是cookie的功用。另一个重要应用场合是“购物车”之类处理。用户可能会在一段时间内在同一家网站的不同页面中选择不同的商品,这些信息都会写入cookie,以便在最后付款时提取信息。

在Tornado框架中提供了直接作用于一般cookie和安全cookie的方法。安全的cookie指存储在客户端的cookie是经过加密的,客户端只能查看到加密后的数据。在Tornado框架中,使用cookie和安全cookie的常用方法原型如下。

下面的实例文件co.py演示了在不同页面中设置与获取cookie值的过程。

源码路径:daima\1\1-2\co.py

import tornado.ioloop          #导入Tornado框架中的相关模块
import tornado.web             #导入Tornado框架中的相关模块
import tornado.escape          #导入Tornado框架中的相关模块
#定义处理类aaaa,用于设置cookie的值
class aaaa(tornado.web.RequestHandler):
     def get(self):            #处理get请求
          #URL编码处理
          self.set_cookie('odn_cookie',tornado.escape.url_escape("未加密COOKIE串"))
          #设置普通cookie
          self.set_secure_cookie('scr_cookie',"加密SCURE_COOKIE串")
          #设置加密cookie
          self.write("<a href='/shcook'>查看设置的COOKIE</a>")
#定义处理类shcookHdl,用于获取cookie的值
class shcookHdl(tornado.web.RequestHandler):
     def get(self):        #处理get请求
         #获取普通cookie
         odn_cookie = tornado.escape.url_unescape(self.get_cookie('odn_cookie'))
         #进行URL解码
          scr_cookie = self.get_secure_cookie('scr_cookie').decode('utf-8')
         #获取加密cookie
          self.write("普通COOKIE:%s,<br />安全COOKIE:%s" % (odn_cookie,scr_cookie))

app = tornado.web.Application([
     (r'/sscook',aaaa),
     (r'/shcook',shcookHdl),
     ],cookie_secret='abcddddkdk##$$34323sdDsdfdsf#23')
if __name__ == '__main__':
     app.listen(8888)                           #设置监听服务器的8888端口
     tornado.ioloop.IOLoop.instance().start()   #启动服务器

在上述实例代码中定义了两个类,分别用于设置cookie的值和获取cookie的值。当在浏览器中输入“http://localhost:8888/sscook”时开始设置cookie,初始执行效果如图1-6所示。

当单击页面中的“查看设置的COOKIE”链接时,会访问“shcook”,显示出刚刚设置的cookie值。执行效果如图1-7所示。

图1-6 初始执行效果

图1-7 单击“查看设置的COOKIE”链接后的执行效果

所谓URL转向,是通过服务器的特殊设置,将访问当前域名的用户引导到用户指定的另一个URL页面。在Tornado框架中可以实现URL转向的功能,这需要借助如下两个方法实现URL转向功能。

使用类RedirectHandler的语法格式如下。

(r'/aaa', tornado.Web.RedirectHandler, dict (url='/abc'))

下面的实例文件zh.py演示了实现两种URL转向功能的过程。

源码路径:daima\1\1-2\zh.py

import tornado.ioloop                #导入Tornado框架中的相关模块
import tornado.web                   #导入Tornado框架中的相关模块
#定义类DistA,作为转向的目标URL请求处理程序
class DistA(tornado.web.RequestHandler):
     def get(self):                              #获取get请求
          self.write("被转向的目标页面!")          #显示一个字符串
#定义转向处理器类SrcA
class SrcA(tornado.web.RequestHandler):
     def get(self):                              #获取get请求
          self.redirect('/dist')                 #业务逻辑转向,指向一个URL

app = tornado.web.Application([
     (r'/dist',DistA),                           #指向DistA类
     (r'/src',SrcA),                             #指向SrcA类
     (r'/rdrt',tornado.web.RedirectHandler,{'url':'/src'})  #定义一个直接转向URL
     ])

if __name__ == '__main__':
     app.listen(8888)                            #设置监听服务器的8888端口
     tornado.ioloop.IOLoop.instance().start()    #启动服务器

在上述实例代码中定义了两个类,其中类DistA作为转向的目标URL请求处理程序,类SrcA是转向处理程序。当访问指向这个业务类时,会转向“/dist”网址。最后,在类Application中定义一个直接转向,只要访问“/rdrt”就会直接转向“/src”。在执行后如果试图访问“/rdrt”的URL,会转向“/src”,再最终转向“/dist”。也就是说,无论是访问“/rdrt”,还是访问“/src”,最终的执行效果都如图1-8所示。

图1-8 执行效果

大多数Web应用程序都有一组对所有用户一视同仁的文件,这些文件在应用程序运行时是不会发生变化的。这些可以是用于网站装饰的媒体文件(如图片),用于描述如何在屏幕上绘制网页的CSS,能够被浏览器下载和执行的JavaScript代码,不含动态内容的HTML页面等。这些不发生变化的文件称为静态资源文件。

Tornado框架支持在网站页面中直接使用静态的资源文件,如图片、JavaScript脚本、CSS等。当需要用到静态文件资源时,需要在Application类初始化时提供“static_path”参数。下面的实例文件tu.py演示了使用图片静态资源文件的过程。

源码路径:daima\1\1-2\tu.py

import tornado.ioloop               #导入Tornado框架中的相关模块
import tornado.web                  #导入Tornado框架中的相关模块
#定义类AAA,用于访问输出静态图片文件
class AAA(tornado.web.RequestHandler):
    def get(self):                  #获取get请求
        self.write("<img src='/static/ttt.jpg' />")   #使用一幅本地图片
app = tornado.web.Application([
     (r'/stt',AAA),                     #参数"/stt"的请求
     ],static_path='./static')          #调用本网站中的图片"static/ttt.jpg"
if __name__ == '__main__':
     app.listen(8888)                   #设置监听的服务器8888端口
     tornado.ioloop.IOLoop.instance().start()    #启动服务器

在上述实例代码中,通过参数“/stt”请求返回HTML代码中的一个img标签,并调用本网站中的图片“static/ttt.jpg”。在初始化类Application时提供了static_path参数,以指明静态资源的目录。最终的执行效果如图1-9所示。

图1-9 执行效果

在Tornado框架中可以灵活地使用表单和模板技术,通过使用这些技术可以实现动态Web功能。

在下面的实例文件001.py中,首先实现一个让用户填写注册信息的HTML表单,然后显示表单处理结果。

源码路径:daima\1\1-3\001.py

import os.path

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

from tornado.options import define, options
define("port", default=8001, help="运行在指定端口", type=int)

class IndexHandler(tornado.web.RequestHandler):
      def get(self):
           self.render('index.html')

class PoemPageHandler(tornado.web.RequestHandler):
      def post(self):
           noun1 = self.get_argument('noun1')
           noun2 = self.get_argument('noun2')
           verb = self.get_argument('verb')
           noun3 = self.get_argument('noun3')
           self.render('poem.html', roads=noun1, wood=noun2, made=verb,
                      difference=noun3)

if __name__ == '__main__':
     tornado.options.parse_command_line()
     app = tornado.web.Application(
          handlers=[(r'/', IndexHandler), (r'/poem', PoemPageHandler)],
          template_path=os.path.join(os.path.dirname(__file__), "templates")
     )
     http_server = tornado.httpserver.HTTPServer(app)
     http_server.listen(options.port)
     tornado.ioloop.IOLoop.instance().start()

为了突出Web程序界面的美观性,接下来我们将使用模板技术。框架Tornado自身提供了一个轻量级的模板模块tornado.template,用于快速并且灵活地实现模板功能。我们将模板文件保存在“templates”文件夹中,其中文件index.html作为注册表单。具体实现代码如下。

源码路径:daima\1\1-3\templates\index.html

<!DOCTYPE html>
<html>
     <head><title>会员登录</title></head>
     <body>
          <h1>输入注册信息.</h1>
          <form method="post" action="/poem">
          <p>用户名<br><input type="text" name="noun1"></p>
          <p>密码<br><input type="text" name="noun2"></p>
          <p>确认密码<br><input type="text" name="verb"></p>
          <p>性别<br><input type="text" name="noun3"></p>
          <input type="submit">
          </form>
     </body>
</html>

模板文件poem.html用于显示注册结果信息。具体实现代码如下。

源码路径:daima\1\1-3\templates\poem.html

<!DOCTYPE html>
<html>
     <head><title>注册结果</title></head>
     <body>
          <h1>下面是你的注册信息</h1>
          <p>用户名:{{roads}}<br>密码:{{wood}}<br>确认密码:{{made}}<br>性别:
             {{difference}}.</p>
     </body>
</html>

开始调试本实例。首先运行前面的Python文件001.py,然后在浏览器中输入http://localhost:8001/,接下来会显示注册表单,这是由模板文件index.html实现的。执行效果如图1-10所示。在表单中输入注册信息,并单击“提交查询内容”按钮后显示注册结果,这是由模板文件poem.html实现的。执行效果如图1-11所示。

图1-10 注册表单

图1-11 注册结果

在上面的实例文件001.py中,定义了RequestHandler子类,并把它们传给tornado.web.Application对象。通过如下代码向Application对象中的init()方法传递一个template_path参数。

template_path=os.path.join(os.path.dirname(__file__), "templates")

参数template_path的功能是告诉Tornado模板文件的具体位置,模板是一个允许你嵌入Python代码片段的HTML文件。通过上述代码告诉Python,在Tornado应用文件相同目录下的templates文件夹中寻找模板文件。当告诉Tornado在哪里可以找到模板文件后,就可以使用类RequestHandler中的render()函数告诉Tornado读入模板文件,插入其中的模板代码,并返回结果给浏览器。例如,在IndexHandler中通过如下代码告诉Tornado在文件夹“templates”下找到一个名为index.html的文件,读取其中的内容,并发送给浏览器。

self.render('index.html')

在框架Tornado中,为模板功能提供了如下内置函数。

在模板中可以使用一个自己编写的函数,这时只需要将函数名作为模板的参数传递即可,就像使用其他变量一样。例如:

>>> from tornado.template import Template
>>> def disemvowel(s):
...    return ''.join([x for x in s if x not in 'aeiou'])
...
>>> disemvowel("george")
'grg'
>>> print Template("my name is {{d('mortimer')}}").generate(d=disemvowel)
my name is mrtmr

再看下面的演示实例。首先在模板文件界面中提示用户输入两个文本:“源”文本和“替代”文本。单击“提交”按钮后会返回替代文本的一个副本,并将其中每个单词替换成源文本中首字母相同的某个单词。本实例包括4个文件:002.py(Tornado程序)、style.css(CSS文件)、index.html和munged.html(Tornado模板)。

(1)在Python文件002.py中定义了两个请求处理类——IndexHandler和MungedPageHandler。具体实现代码如下。

源码路径:daima\1\1-3\moban2\002.py

from tornado.options import define, options
define("port", default=8001, help="运行给定的端口", type=int)

class IndexHandler(tornado.web.RequestHandler):
      def get(self):
           self.render('index.html')

class MungedPageHandler(tornado.web.RequestHandler):
      def map_by_first_letter(self, text):
           mapped = dict()
           for line in text.split('\r\n'):
                  for word in [x for x in line.split(' ') if len(x) > 0]:
                       if word[0] not in mapped: mapped[word[0]] = []
                       mapped[word[0]].append(word)
           return mapped

     def post(self):
           source_text = self.get_argument('source')
           text_to_change = self.get_argument('change')
           source_map = self.map_by_first_letter(source_text)
           change_lines = text_to_change.split('\r\n')
           self.render('munged.html', source_map=source_map, change_lines=change_lines,
                       choice=random.choice)

if __name__ == '__main__':
      tornado.options.parse_command_line()
      app = tornado.web.Application(
           handlers=[(r'/', IndexHandler), (r'/poem', MungedPageHandler)],
           template_path=os.path.join(os.path.dirname(__file__), "templates"),
           static_path=os.path.join(os.path.dirname(__file__), "static"),
           debug=True
      )
      http_server = tornado.httpserver.HTTPServer(app)
      http_server.listen(options.port)
      tornado.ioloop.IOLoop.instance().start()
app = tornado.web.Application(
     handlers=[(r'/', IndexHandler), (r'/poem', MungedPageHandler)],
     template_path=os.path.join(os.path.dirname(__file__), "templates"),
     static_path=os.path.join(os.path.dirname(__file__), "static"),
     debug=True
)

这样设置了一个当前应用目录下名为static的子目录作为static_path的参数。现在应用将以读取static目录下的filename.ext来响应诸如/static/filename.ext的请求,并在响应的主体中返回。

(2)模板文件index.html用于显示主表单界面。具体实现代码如下。

源码路径:daima\1\1-3\moban2\templates\index.html

<!DOCTYPE html>
<html>
     <head>
          <link rel="stylesheet" href="{{ static_url("style.css") }}">
          <title>操作</title>
     </head>
     <body>
          <h1>替换操作</h1>
          <p>在下面输入两个文本,替换文本将用源文本中同一字母开头的单词替换单词。</p>
          <form method="post" action="/poem">
          <p>Source text<br>
               <textarea rows=4 cols=55 name="source"></textarea></p>
          <p>Text for replacement<br>
               <textarea rows=4 cols=55 name="change"></textarea></p>
          <input type="submit">
          </form>
     </body>
</html>

在Tornado模板中提供了static_url()函数来生成static目录下文件的URL。下面是在文件index.html中static_url调用的代码。

<link rel="stylesheet" href="{{ static_url("style.css") }}">

通过上述代码调用static_url生成了URL的值,并输出类似下面的代码。

<link rel="stylesheet" href="/static/style.css?v=ab12">

此处为什么使用static_url而不是在模板中使用硬编码方式呢?有如下两个原因。

(3)模板文件munged.html用于显示替换结果。具体实现代码如下。

源码路径:daima\1\1-3\moban2\templates\munged.html

<!DOCTYPE html>
<html>
     <head>
          <link rel="stylesheet" href="{{ static_url("style.css") }}">
          <title>结果</title>
     </head>
     <body>
          <h1>现在的文本是</h1>
          <p>
{% for line in change_lines %}
     {% for word in line.split(' ') %}
          {% if len(word) > 0 and word[0] in source_map %}
               <span class="replaced"
                          title="{{word}}">{{ choice(source_map[word[0]]) }}</span>
          {% else %}
               <span class="unchanged" title="unchanged">{{word}}</span>
          {% end %}
     {% end %}
               <br>
{% end %}
          </p>
     </body>
</html>

在上述代码中迭代替代文本中的每行,再迭代每行中的每个单词。如果当前单词的第一个字母是source_map字典的一个键,则使用random.choice函数从字典的值中随机选择一个单词并展示它。如果字典的键中没有这个字母,则展示源文本中的原始单词。每个单词包括一个span标签,其中的class属性指定这个单词是替换后的(class="replaced")还是原始的(class="unchanged")。(我们还将原始单词放到了span标签的title属性中,以便于用户在鼠标指针经过单词时可以查看哪些单词被替换。下面看执行效果。假设在表单中分别输入“from tornado.template import Template”和“print Template("my name is {{d('mortimer')}}").generate(d=disemvowel)”,如图1-12所示。

图1-12 输入表单

单击“提交查询内容”按钮后执行替换操作,并显示替换后的结果,如图1-13所示。

图1-13 替换后的结果

在下面的内容中,假设已经在计算机上安装了MongoDB,并通过PyMongo作为驱动来连接MongoDB。本节将介绍在Tornado框架中实现数据库操作的知识。

假设我们需要编写一个只从MongoDB读取数据的Web服务,然后编写一个可以读写数据的服务。例如,将要创建的应用是一个基于Web的简单字典,在发送一个指定单词的请求后返回这个单词的定义。一个典型的交互如下。

$ curl http://localhost:8000/oarlock
{definition: "连接到一个设备",
"word": "oarlock"}

这个Web服务将从MongoDB数据库中取得数据。具体来说,将根据word属性查询文档。在查看Web应用本身的源码之前,先从Python解释器向数据库中添加一些单词。例如,通过如下文件001.py,可以向MongoDB数据库中添加指定的单词。

源码路径:daima\1\1-4\001.py

import pymongo
conn = pymongo.MongoClient("localhost", 27017)
db = conn.example
db.words.insert({"word": "oarlock", "definition": "A device attached to a rowboat to hold the oars in
    place"})
db.words.insert({"word": "seminomadic", "definition": "Only partially nomadic"})
db.words.insert({"word": "perturb", "definition": "Bother, unsettle, modify"})

通过如下命令开启MongoDB服务:

mongod --dbpath "h:\data"

在上述命令中,“h:\data”是一个保存MongoDB数据库数据的目录,读者可以随意在本地计算机硬盘中创建,并且还可以自定义目录名字。然后运行文件001.py,执行后会向MongoDB数据库中添加指定的单词。

为了验证上述添加的单词,我们编写如下所示的文件definitions_readonly.py,在Tornado框架中实现对MongoDB数据库的访问。

源码路径:daima\1\1-4\definitions_readonly.py

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

import pymongo

from tornado.options import define, options
define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
      def __init__(self):
           handlers = [(r"/(\w+)", WordHandler)]
           conn = pymongo.MongoClient("localhost", 27017)
           self.db = conn["example"]
           tornado.web.Application.__init__(self, handlers, debug=True)

class WordHandler(tornado.web.RequestHandler):
      def get(self, word):
           coll = self.application.db.words
           word_doc = coll.find_one({"word": word})
           if word_doc:
                del word_doc["_id"]
                self.write(word_doc)
           else:
                self.set_status(404)
                self.write({"error": "word not found"})

if __name__ == "__main__":
     tornado.options.parse_command_line()
     http_server = tornado.httpserver.HTTPServer(Application())
     http_server.listen(options.port)
     tornado.ioloop.IOLoop.instance().start()

运行上述实例文件definitions_readonly.py,然后在浏览器中输入“http://localhost:8000/perturb”后会显示:

{"word": "perturb", "definition": "Bother, unsettle, modify"}

这说明在Tornado框架中实现了对MongoDB数据库数据的访问功能。如果在浏览器中请求一个数据库中没有添加的单词,会得到一个404错误以及一条错误消息。

{"error": "word not found"}

那么,这个程序是如何工作的呢?让我们看看这个程序的主线。开始,我们在程序的最上面导入了pymongo库。然后在TornadoApplication对象的init方法中实例化了一个pymongo连接对象。我们在Application对象中创建了一个db属性,指向MongoDB的example数据库。下面是相关的代码。

conn = pymongo.MongoClient ("localhost", 27017)
self.db = conn["example"]

一旦我们在Application对象中添加了db属性,就可以在任何RequestHandler对象中使用self.application.db访问它。其实这正是我们为了取出pymongo的words集合对象而在WordHandler的get方法中所做的事情。

def get(self, word):
     coll = self.application.db.words
     word_doc = coll.find_one({"word": word})
     if word_doc:
          del word_doc["_id"]
          self.write(word_doc)
     else:
          self.set_status(404)
          self.write({"error": "word not found"})

在我们将集合对象指定给变量coll后,我们使用用户在HTTP路径中请求的单词调用find_one方法。如果我们发现这个单词,则从字典中删除_id键(以便Python的JSON库可以将其序列化),然后将其传递给RequestHandler的write方法。write方法将会自动序列化字典为JSON格式。

如果find_one方法没有匹配任何对象,则返回None。这时将响应状态设置为404,并且写一个简短的JSON来提示用户这个单词在数据库中没有找到。

在上述实例中,虽然可以很简单地在字典中查询单词,但是在交互解释器中添加单词的过程会非常麻烦。其实我们完全可以使HTTP在请求网站服务时创建和修改单词:首先发出一个特定单词的POST请求,然后根据请求中给出的定义修改已经存在的定义。如果这个单词并不存在,则创建它。例如,通过如下过程创建一个新的单词“pants”。

http://localhost:8000/pants
{"definition": "a leg shirt", "word": "pants"}

下面的实例文件definitions_readwrite.py演示了实现一个可读写Web服务的过程。

源码路径:daima\1\1-4\definitions_readwrite.py

define("port", default=8000, help="run on the given port", type=int)

class Application(tornado.web.Application):
     def __init__(self):
          handlers = [(r"/(\w+)", WordHandler)]
          conn = pymongo.MongoClient("localhost", 27017)
          self.db = conn["definitions"]
          tornado.web.Application.__init__(self, handlers, debug=True)

class WordHandler(tornado.web.RequestHandler):
     def get(self, word):
          coll = self.application.db.words
          word_doc = coll.find_one({"word": word})
          if word_doc:
              del word_doc["_id"]
              self.write(word_doc)
          else:
              self.set_status(404)
     def post(self, word):
          definition = self.get_argument("definition")
          coll = self.application.db.words
          word_doc = coll.find_one({"word": word})
          if word_doc:
               word_doc['definition'] = definition
               coll.save(word_doc)
          else:
               word_doc = {'word': word, 'definition': definition}
               coll.insert(word_doc)
          del word_doc["_id"]
          self.write(word_doc)

if __name__ == "__main__":
     tornado.options.parse_command_line()
     http_server = tornado.httpserver.HTTPServer(Application())
     http_server.listen(options.port)
     tornado.ioloop.IOLoop.instance().start()

在上述代码中,使用get_argument()函数获取了POST请求中传递的definition参数,然后使用find_one()函数从数据库中加载给定单词的文档。如果发现这个单词的文档,将definition条目的值设置为从POST参数中取得的值,然后调用集合对象的save()函数将改变写到数据库中。如果没有发现文档则创建一个新的,并使用insert()函数将文档保存到数据库中。无论上述哪种情况,都要在数据库操作执行之后在响应中写文档(注意,首先要删掉_id属性)。

接下来,通过一个图书管理系统的实现过程,介绍在Tornado框架中使用MongoDB数据库实现动态Web的过程。

源码路径:daima\1\1-4\BookManger

(1)在MongoDB服务器中创建一个数据库和集合,并用图书内容进行填充。例如,下面的演示过程。

>>> import pymongo
>>> conn = pymongo.MongoClient ()
>>> db = conn["bookstore"]
>>> db.books.insert({
...    "title":"Python开发从入门到精通",
...    "subtitle": "Python",
...    "image":"123.gif",
...    "author": "浪潮",
...    "date_added":20171231,
...    "date_released": "August 2007",
...    "isbn":"978-7-596-52932-1",
...    "description":"<p>[...]</p>"
... })
ObjectId('4eb6f1a6136fc42171000000')
>>> db.books.insert({
...    "title":"PHP从入门到精通",
...    "subtitle": "Web服务",
...    "image":"345.gif",
...    "author": "学习PHP",
...    "date_added":20171231,
...    "date_released": "May 2007",
...    "isbn":"978-7-534-52926-0",
...    "description":"<p>[...]>/p>"
... })
ObjectId('4eb6f1cb136fc42171000001')

(2)编写Python程序文件burts_books_db.py。首先在程序中添加一个db属性来连接MongoDB服务器,然后使用连接的find()函数从数据库中取得图书文档的列表,并在渲染recommended.html时将这个列表传递给RecommendedHandler的get方法。文件burts_books_db.py的具体实现代码如下。

#!/usr/bin/env python
import os.path

import tornado.auth
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
from tornado.options import define, options

import pymongo

define("port", default=8001, help="请运行在给定的端口", type=int)

class Application(tornado.web.Application):
      def __init__(self):
            handlers = [
                 (r"/", MainHandler),
                 (r"/recommended/", RecommendedHandler),
            ]
            settings = dict(
                 template_path=os.path.join(os.path.dirname(__file__), "templates"),
                 static_path=os.path.join(os.path.dirname(__file__), "static"),
                 ui_modules={"Book": BookModule},
                 debug=True,
                 )
            conn = pymongo.MongoClient("localhost", 27017)
            self.db = conn["bookstore"]
            tornado.web.Application.__init__(self, handlers, **settings)


class MainHandler(tornado.web.RequestHandler):
      def get(self):

            self.render(
                   "index.html",
                   page_title = "图书管理| 主页",
                   header_text = "欢迎使用图书管理系统!",
            )

class RecommendedHandler(tornado.web.RequestHandler):
      def get(self):
            coll = self.application.db.books
            books = coll.find()
            self.render(
                   "recommended.html",
                   page_title = "图书系统 | 图书信息",
                   header_text = "图书信息",
                   books = books
            )

class BookModule(tornado.web.UIModule):
      def render(self, book):
            return self.render_string(
                  "modules/book.html", 
                  book=book,
            )

      def css_files(self):
            return "css/recommended.css"

      def javascript_files(self):
            return "js/recommended.js"


def main():
     tornado.options.parse_command_line()
     http_server = tornado.httpserver.HTTPServer(Application())
     http_server.listen(options.port)
     tornado.ioloop.IOLoop.instance().start()


if __name__ == "__main__":
      main()

如果此时在浏览器中输入http://localhost:8001/recommended/,会读取并显示数据库中的图书信息。执行效果如图1-14所示。

图1-14 执行效果

(3)编写 Python 文件 burts_books_rwdb.py 实现图书添加和修改两个功能。具体实现代码如下。

define("port", default=8001, help="请运行在给定的端口", type=int)

class Application(tornado.web.Application):
      def __init__(self):
            handlers = [
                 (r"/", MainHandler),
                 (r"/recommended/", RecommendedHandler),
                 (r"/edit/([0-9Xx\-]+)", BookEditHandler),
                 (r"/add", BookEditHandler)
            ]
            settings = dict(
                  template_path=os.path.join(os.path.dirname(__file__), "templates"),
                  static_path=os.path.join(os.path.dirname(__file__), "static"),
                  ui_modules={"Book": BookModule},
                  debug=True,
                  )
            conn = pymongo.MongoClient("localhost", 27017)
            self.db = conn["bookstore"]
            tornado.web.Application.__init__(self, handlers, **settings)


class MainHandler(tornado.web.RequestHandler):
      def get(self):

            self.render(
                   "index.html",
                   page_title = "图书管理 | 主页",
                   header_text = "欢迎使用图书管理系统!",
          )

class BookEditHandler(tornado.web.RequestHandler):
      def get(self, isbn=None):
            book = dict()
            if isbn:
                   coll = self.application.db.books
                   book = coll.find_one({"isbn": isbn})
            self.render("book_edit.html",
                   page_title="Burt's Books",
                   header_text="Edit book",
                   book=book)

      def post(self, isbn=None):
            import time
            book_fields = ['isbn', 'title', 'subtitle', 'image', 'author',
                   'date_released', 'description']
            coll = self.application.db.books
            book = dict()
            if isbn:
                   book = coll.find_one({"isbn": isbn})
            for key in book_fields:
                   book[key] = self.get_argument(key, None)

            if isbn:
                   coll.save(book)
            else:
                   book['date_added'] = int(time.time())
                   coll.insert(book)
            self.redirect("/recommended/")

class RecommendedHandler(tornado.web.RequestHandler):
      def get(self):
            coll = self.application.db.books
            books = coll.find()
            self.render(
                   "recommended.html",
                   page_title = "Burt's Books | Recommended Reading",
                   header_text = "Recommended Reading",
                   books = books
            )

class BookModule(tornado.web.UIModule):
      def render(self, book):
            return self.render_string(
                   "modules/book.html", 
                   book=book,
            )

      def css_files(self):
            return "css/recommended.css"

      def javascript_files(self):
            return "js/recommended.js"


def main():
      tornado.options.parse_command line()
      http server = tornado.httpserver.HTTPServer(Application())
      http server.listen(options.port)
      tornado.ioloop.IOLoop.instance().start()


if __name__ == "__main__":
      main()

在上述代码中,BookEditHandler主要完成如下两个功能。

BookEditHandler实现了两个不同路径模式的请求:其中一个是实现图书添加功能的/add,用于提供不存在信息的编辑表单,因此你可以向数据库中添加一本新的图书;另一个是实现图书修改功能的/edit/([0-9Xx-]+),用于根据图书的ISBN参数修改已存在图书的信息。

函数 get()的功能是从数据库中取出图书信息,如果该函数作为/add请求的结果被调用,Tornado将调用一个没有第二个参数的get方法(因为路径中没有正则表达式的匹配组)。在这种情况下,默认将一个空的book字典传递给book_edit.html模板。如果该方法作为/edit/0-123-456请求的结果被调用,那么isbn参数被设置为0-123-456。在这种情况下,我们从程序实例中取得books集合,并用它查询isbn匹配的图书,然后传递book字典给模板。

函数post()的功能是将表单中的数据保存到数据库中,具体来说有两个功能:处理修改已存在图书信息的请求以及添加新图书信息的请求。如果有isbn参数(即路径的请求类似于/edit/0-123-4567),则编辑给定isbn。如果这个参数没有提供,则添加新图书信息。先设置一个空的字典变量book,如果正在编辑已存在的图书信息,则使用book集合的find_one()函数从数据库中加载和传入与isbn值对应的文档。无论是哪一种情况,book_fields列表都指定哪些域应该出现在图书文档中,再迭代这个列表,使用RequestHandler对象的get_argument方法从POST请求中抓取对应的值。

图书修改功能的模板文件是book_edit.html。具体实现代码如下所示。

{% extends "main.html" %}
{% autoescape None %}

{% block body %}
<form method="POST">
     ISBN <input type="text" name="isbn"
          value="{{ book.get('isbn', '') }}"><br>
     书名  <input type="text" name="title"
          value="{{ book.get('title', '') }}"><br>
     标题  <input type="text" name="subtitle"
          value="{{ book.get('subtitle', '') }}"><br>
     图片  <input type="text" name="image"
          value="{{ book.get('image', '') }}"><br>
     作者  <input type="text" name="author"
          value="{{ book.get('author', '') }}"><br>
     出版时间 <input type="text" name="date_released"
          value="{{ book.get('date_released', '') }}"><br>
     内容简介<br>
     <textarea name="description" rows="5"
           cols="40">{% raw book.get('description', '')%}</textarea><br>
     <input type="submit" value="Save">
</form>
{% end %}

上述代码实现了一个基本的HTML表单,如果请求处理函数传进来了book字典,那么将用它预填充带有已存在图书数据的表单中。如果键不在字典中,则使用Python字典对象的get方法为其提供默认值,标签input中的name属性被设置为book字典的对应键。因为form标签没有action属性,所以表单中的POST将会定向到当前URL。如果页面以/edit/0-123-4567进行加载,POST请求将转向/edit/0-123-4567;如果页面以/add进行加载,则POST将转向/add。

添加新图书的界面如图1-15所示。

单击图1-14中的“编辑”链接后会弹出图书修改界面,在此界面中显示修改此图书信息的界面。执行效果如图1-16所示。

图1-15 添加新图书的界面

图1-16 图书修改界面


Django是一个开放源代码的Web应用框架,由Python写成。Django遵守BSD版权,初次发布于2005年7月,并于2008年9月发布了第一个正式版本Django 1.0。Django采用了模型-视图-控制器MVC(Model-View-Controller,MVC)的软件设计模式。本章将详细讲解使用Django框架开发动态Web程序的核心知识。

Django自称是“能够很好地应对应用上线期限的Web框架”。其由劳伦斯日报负责在线业务的Web开发者创建。

在安装Django之前,必须先安装Python。Apache是Web服务器中使用量最多的,因此大多数部署都会使用这款服务器。Django团队建议使用mod_wdgi这个Apache模块,并提供了安装指南和完整的开发文档,具体内容见Django官方网站。

在使用Django框架时需要使用数据库,当前的标准版Django只能够运行基于SQL的关系数据库管理系统(RDBMS)。开发者主要使用4种数据库,分别是PostgreSQL、MySQL、Oracle 和SQLite。其中最容易设置的是SQLite。另外,SQLite是这4个数据库中唯一一个无须部署数据库服务器的工具,所以使用起来也是最简单的。当然,简单并不代表功能弱,SQLite的功能和另外3个一样强大。另外还需要提醒读者,在生产环境的服务器中并不是一定要使用Apache,还可以有其他选择,其中有些服务器的内存占用量更少,速度更快。可以在Django官网中查找符合要求的Web服务器。

为什么SQLite很容易设置?因为SQLite数据库适配器是所有Python版本中自带的(从2.5版本开始)。注意,这里说的是适配器。有些Python发行版自带了SQLite本身,有些会使用系统上安装的SQLite。

Django支持众多关系数据库,SQLite只是其中一种,所以如果不喜欢SQLite,可以使用其他数据库。最近还有快速发展的非关系数据库(NoSQL)。这种类型的数据库提供了额外的可扩展性,能面对不断增长的数据量。如果处理像Facebook、Twitter那样的海量数据,关系数据库需要手动分区(切分)。如果需要使用NoSQL类数据库,如MongoDB或Google App Engine的原生数据库,建议尝试Django-nonrel,这样用户就可以选择使用关系或非关系数据库。

本节将详细讲解Django开发的基础知识,包括如何搭建开发环境和常用命令等内容。

在当今技术环境下,有多种安装Django框架的方法。下面对这些安装方法按难易程度进行排序,其中越靠前的越简单:

最简单的下载和安装方式是使用Python包管理工具,建议读者使用这种安装方式。例如,可以使用Setuptools中的easy_install或pip。目前在所有的操作系统平台上都可以使用这两个工具。对于Windows用户来说,在使用Setuptools时需要将easy_install.exe文件放在Python安装目录下的Scripts文件夹中。此时只要在DOS命令行窗口中使用一条命令就可以安装Django。可以使用easy_install命令进行安装:

easy_install django

也可以使用pip命令进行安装:

pip install django

本书使用的Django版本是1.10.4,控制台安装界面如图2-1所示。

图2-1 控制台中安装Django的界面

接下来将要讲解Django框架中常用的命令。读者需要打开Linux或Mac OS的终端,直接在终端中输入这些命令(不是Python的shell中)。如果读者使用的是Windows系统,则在命令行窗口中输入操作命令。

1.新建一个Django项目

要新建Django项目,可使用以下命令。

django-admin.py startproject project-name

其中,“project-name”表示项目名称。在Windows系统中需要使用如下命令创建项目:

django-admin startproject project-name

2.新建应用

要新建应用,可使用以下命令。

python manage.py startapp app-name

或:

django-admin.py startapp app-name

通常一个项目有多个应用。当然,通用的应用也可以在多个项目中使用。

3.同步数据库

要同步数据库,可使用以下命令。

python manage.py syncdb

读者需要注意,在Django 1.7.1及以上的版本中需要用以下命令。

python manage.py makemigrations
python manage.py migrate

这种方法可以创建表。当在models.py中新增类时,运行以上命令就可以自动在数据库中创建表,不需要手动创建。

4.使用开发服务器

开发服务器在开发时使用,在修改代码后会自动重启,这会方便程序的调试和开发。但是由于性能问题,建议开发服务器只用来测试,不要用于生产环境。

python manage.py runserver
# 当提示端口被占用的时候,可以用其他端口
python manage.py runserver 8001
python manage.py runserver 9999
(当然,也可以终止占用端口的进程)

#监听所有可用IP(计算机可能有一个或多个内网IP地址,一个或多个外网IP地址,即有多个IP地址)
python manage.py runserver 0.0.0.0:8000
#访问对应的地址,比如 http://172.16.20.2:8000

5.清空数据库

要清空数据库,可使用以下命令。

python manage.py flush

此命令会询问是yes还是no,选择yes会把数据全部清空掉,只留下空表。

6.创建超级管理员

要创建超级管理员,可使用以下命令。

python manage.py createsuperuser
# 按照提示输入用户名和对应的密码即可,邮箱可以为空,用户名和密码必填
# 修改用户密码
python manage.py changepassword username

7.导出数据与导入数据

要导出数据,可使用以下命令。

python manage.py dumpdata appname > appname.json

要导入数据,可使用以下命令。

python manage.py loaddata appname.json

8.启动Django项目的终端并打开命令行操作界面

要启动Django项目的终端并打开命令行操作界面,可使用以下命令。

python manage.py shell

如果安装了bpython或ipython,会自动调用它们的界面,推荐安装bpython。这个命令和直接运行Python或bpython进入shell的区别是:可以在这个shell里面调用当前项目的models.py中的API。

9.显示数据库的基本信息

要显示数据库的基本信息,可使用以下命令。

python manage.py dbshell

Django会自动进入settings.py中设置的数据库,如果是MySQL或postgreSQL,会要求输入用户名和密码。在这个终端可以执行数据库的SQL语句。如果用户对SQL比较熟悉,可能喜欢这种方式。

下面的实例代码演示了创建并运行第一个Django项目的过程。

源码路径:daima\2\2-2\mysite

(1)在命令行窗口中定位到H盘,然后通过如下命令创建一个“mysite”目录。

django-admin startproject mysite

创建成功后会看到如下所示的目录样式。

mysite
├── manage.py
└── mysite
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

也就是说,在H盘中新建了一个mysite目录,其中还有一个mysite子目录,这个子目录mysite中是一些项目的设置文件settings.py、总的urls配置文件urls.py以及部署服务器时用到的wsgi.py文件。文件__init__.py是Python包的目录结构必需的项,与调用有关。

(2)在命令行窗口中定位到mysite目录下(注意,不是mysite中的mysite目录),然后通过如下命令新建一个应用,名称为learn。

H:\mysite>python manage.py startapp learn

此时可以看到在主mysite目录中多出了一个learn文件夹,在里面有如下所示的文件。

learn/
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── tests.py
└── views.py

(3)为了将新创建的应用添加到settings.py文件的INSTALL_APPS中,需要对文件mysite/mysite/settings.py进行如下修改。

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'learn',
]

这一步的目的是将新建的应用“learn”添加到INSTALL_APPS中,如果不这样做,Django就不能自动找到应用中的模板文件(app-name/templates/下的文件)和静态文件(app-name/static/中的文件)。

(4)定义视图函数,用于显示访问页面时的内容。在learn目录中打开文件views.py,然后进行如下所示的修改。

#编码:utf-8
from django.http import HttpResponse
def index(request):
     return HttpResponse(u"欢迎光临,浪潮软件欢迎您!")

对上述代码的具体说明如下。

在request变量里面包含get/post的内容、浏览器和系统等信息。函数index()返回了一个HttpResponse对象,经过一些处理,最终可以在网页上显示几个字。

现在问题来了:用户应该访问什么网址才能看到刚才写的这个函数呢?怎么让网址和函数关联起来呢?接下来需要定义和视图函数相关的URL。

(5)开始定义和视图函数相关的URL,对文件mysite/mysite/urls.py进行如下修改。

from django.conf.urls import url
from django.contrib import admin
from learn import views as learn_views  # new

urlpatterns = [
     url(r'^$', learn_views.index),  # new
     url(r'^admin/', admin.site.urls),
]

(6)在终端运行如下命令进行测试。

python manage.py runserver

测试成功后显示图2-2所示的执行效果。

在浏览器中的执行效果如图2-3所示。

图2-2 控制台执行效果

图2-3 执行效果

和前面学习的Tornado框架一样,使用Django框架也可以实现对URL参数的处理。下面的实例代码演示了使用Django框架实现参数相加功能的过程。

源码路径:daima\2\2-2\zqxt_views

(1)在命令行窗口中定位到H盘,然后通过如下命令创建一个“zqxt_views”目录。

django-admin startproject zqxt_views

也就是说,在H盘中新建了一个zqxt_views目录,其中还有一个zqxt_views子目录。这个子目录mysite中是一些项目的设置文件settings.py,总的urls配置文件urls.py,以及部署服务器时用到的wsgi.py文件。文件__init__.py是Python包的目录结构必需的项,与调用有关。

(2)在命令行窗口中定位到zqxt_views目录下(注意,不是zqxt_views中的zqxt_views目录),然后通过如下命令新建一个应用,名称为calc。

cd zqxt_views
python manage.py startapp calc

此时自动生成的目录结构大致如下。

zqxt_views/
├── calc
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── manage.py
└── zqxt_views
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

(3)为了将新定义的应用添加到 settings.py 文件的 INSTALLED_APPS 中,需要对文件zqxt_views/zqxt_views/settings.py进行如下修改。

INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'calc',
]

这一步的目的是将新建的应用“calc”添加到INSTALLED_APPS中,如果不这样做,Django就不能自动找到应用中的模板文件(app-name/templates/下的文件)和静态文件(app-name/static/中的文件)。

(4)定义视图函数,用于显示访问页面时的内容。对文件calc/views.py的代码进行如下修改。

from django.shortcuts import render
from django.http import HttpResponse

def add(request):
     a = request.GET['a']
     b = request.GET['b']
     c = int(a)+int(b)
     return HttpResponse(str(c))

在上述代码中,request.GET类似于一个字典,当没有传递a的值时,a的默认值为0。

(5)开始定义视图函数相关的URL,添加一个网址来对应刚才新建的视图函数。对文件zqxt_views/zqxt_views/urls.py进行如下修改。

from django.conf.urls import url
from django.contrib import admin
from learn import views as learn_views  # new

urlpatterns = [
     url(r'^$', learn_views.index),  # new
     url(r'^admin/', admin.site.urls),
]

(6)在终端运行如下命令进行测试。

python manage.py runserver

在浏览器中输入“http://localhost:8000/add/”后的执行效果如图2-4所示。

图2-4 执行效果

如果在URL中输入数字参数,例如,在浏览器地址栏中输入“http://localhost:8000/ add/ ?a=4&b=5”,执行后会显示这两个数字(4与5)的和,执行效果如图2-5所示。

图2-5 执行效果

在Python程序中,也可以采用“/add/3/4/”这样的方式对URL中的参数进行求和处理。这时需要修改文件calc/views.py的代码,在里面新定义一个求和函数add2()。具体代码如下所示。

def add2(request, a, b):
     c = int(a) + int(b)
     return HttpResponse(str(c))

接着修改文件zqxt_views/urls.py的代码,再添加一个新的URL。具体代码如下所示。

url(r'^add/(\d+)/(\d+)/$', calc_views.add2, name='add2'),

此时可以看到网址中多了“\d+”,正则表达式中的“\d”代表一个数字,“+”代表一个或多个前面的字符,写在一起“\d+”就表示是一个或多个数字,用括号括起来的意思是另存为一个子组,每一个子组将作为一个参数,被文件views.py中的对应视图函数接收。此时输入如下网址,执行后就可以看到和图2-1一样的执行效果。

http://localhost:8000/add/?add/4/5/

在Tornado框架中,模板是一个文本,用于分离文档的表现形式和具体内容。为了方便开发者进行开发,Tornado框架提供了很多模板标签,具体说明如下。

{% autoescape on %}
    {{ body }}
{% endautoescape %}
{% for o in some_list %}
     <tr class="{% cycle 'row1' rowvalue2 'row3' %}">
          ...
     </tr>
{% endfor %}

值得注意的是,这里的变量值默认不是自动转义的,下面给出一段示例代码。

{% for o in some_list %}
    <tr class="{% filter force_escape %}{% cycle rowvalue1 rowvalue2 %}{% endfilter %}">
         ...
    </tr>
{% endfor %}

在某些情况下,可能想在循环外部引用循环中的下一个值,这时需要用as给cycle标签设置一个名字,这个名字代表的是当前循环的值。但是在cycle标签里面可以用这个变量来获得循环中的下一个值。下面给出一段示例代码。

<tr>
    ﹤td class = "{%cycle 'row1' 'row2' as rowcolors %}"﹥...﹤/td﹥
    <td class="{% cycle 'row1' 'row2' as rowcolors %}">...</td>
    <td class="{{ rowcolors }}">...</td>
</tr>
<tr>
    <td class="{% cycle rowcolors %}">...</td>
    <td class="{{ rowcolors }}">...</td>
</tr>

对应的渲染结果是:

<tr>
    <td class="row1">...</td>
    <td class="row1">...</td>
</tr>
<tr>
    <td class="row2">...</td>
    <td class="row2">...</td>
</tr>

但是一旦定义了cycle标签,默认就会使用循环中的第一个值。当你只是想定义一个循环而不想输出循环的值时(比如,在父模板中定义变量以方便继承),可以用cycle的silent参数(必须保证silent是cycle的最后一个参数),并且silent也具有继承的特点。尽管﹤td class = "{% cycle 'row1' 'row2' as rowcolors %}"﹥…﹤/td﹥中的cycle没有silent参数,但是因为rowcoclors是前面定义的且包含silent参数,所以第2个cycle也具有silent 循环的特点。

{% cycle 'row1' 'row2' as rowcolors silent %}
{% cycle rowcolors %}
{% filter force_escape|lower %}
    This text will be HTML-escaped, and will appear in all lowercase.
{% endfilter %}
{% firstof var1 var2 var3 "fallback value" %}
{% for obj in list reversed %

还可以根据列表的数据来写for语句,例如,下面是对应字典类型数据的for循环。

{% for key, value in data.items %}
    {{ key }}: {{ value }}
{% endfor %}

另外,在for循环中还有一系列有用的变量,具体说明如表2-1所示。

表2-1 for循环中的变量

变量

描述

forloop.counter

当前循环的索引,从1开始

forloop.counter0

当前循环的索引,从0开始

forloop.revcounter

当前循环的索引(从后面算起),从1开始

forloop.revcounter0

当前循环的索引(从后面算起),从0开始

forloop.first

如果这是第一次循环,返回真

forloop.last

如果这是最后一次循环,返回真

forloop.parentloop

如果是嵌套循环,指的是外一层循环

<ul>
{% for athlete in athlete_list %}
    <li>{{ athlete.name }}</li>
{% empty %}
    <li>Sorry, no athlete in this list!</li>
{% endfor %}
<ul>
{% if athlete_list %}
    Number of athletes: {{ athlete_list|length }}
{% elif athlete_in_locker_room_list %}
    Athletes should be out of the locker room soon!
{% else %}
    No athletes.
{% endif %}

① 当没有接受参数时,比较的是ifchange标签里面的内容相比以前是否有变化,如果有变化,生效。

② 当接受一个或一个以上的参数的时候,如果有一个或者一个以上的参数发生变化,生效。

在ifchange中可以有else标签。例如:

{% for match in matches %}
    <div style="background-color:
        {% ifchanged match.ballot_id %}
            {% cycle "red" "blue" %}
        {% else %}
            grey
        {% endifchanged %}
    ">{{ match }}</div>
{% endfor %}
{% ifequal user.username "adrian" %}
    ...
{% endifequal %}
{% include "name_snippet.html" with person="Jane" greeting="Hello" %}

如果只想接受传递的参数,不接受当前模板的上下文,可以使用only参数。例如:

{% include "name_snippet.html" with greeting="Hi" only %}
It is {% now "jS F Y H:i" %}

在现实中已经定义好了一些格式化的字符串参数。例如:

cities = [
     {'name': 'Mumbai', 'population': '19,000,000', 'country': 'India'},
     {'name': 'Calcutta', 'population': '15,000,000', 'country': 'India'},
     {'name': 'New York', 'population': '20,000,000', 'country': 'USA'},
     {'name': 'Chicago', 'population': '7,000,000', 'country': 'USA'},
     {'name': 'Tokyo', 'population': '33,000,000', 'country': 'Japan'},
]

如果想按照country属性来重新分组,得到每个国家(地区)中不同城市的人口,则可以通过如下代码实现分组功能。

{% regroup cities by country as country_list %}
<ul>
{% for country in country_list %}
    <li>{{ country.grouper }}
    <ul>
        {% for item in country.list %}
          <li>{{ item.name }}: {{ item.population }}</li>
        {% endfor %}
    </ul>
    </li>
{% endfor %}
</ul>

值得注意的是,regroup并不会重新排序,所以必须确保city在regroup之前已经按照country排好序,否则将得不到预期的结果。如果不确定city在regroup之前已经按照country排好序,可以用dictsort进行过滤器排序。例如:

{% regroup cities|dictsort:"country" by country as country_list %}
{% spaceless %}
    <p>
        <a href="foo/">Foo</a>
    </p>
{% endspaceless %}

运行结果是:

<p><a href="foo/">Foo</a></p>
{% ssi /home/html/ljworld.com/includes/right_generic.html %}

使用参数parsed可以使得输入的内容作为一个模板,从而可以使用当前模板的上下文。例如:

{% ssi /home/html/ljworld.com/includes/right_generic.html parsed %}
<img src="bar.gif" height="10" width="{% widthratio this_value max_value 100 %}" />
{% with total=business.employees.count %}
    {{ total }} employee{{ total|pluralize }}
{% endwith %}

下面的实例代码演示了在Django框架中使用模板的过程。

源码路径:daima\2\2-2\zqxt_tmpl

(1)分别创建一个名为“zqxt_tmpl”的项目和一个名为“learn”的应用。

(2)将“learn”应用添加到settings.INSTALLED_APPS中。具体实现代码如下。

INSTALLED_APPS = (
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'learn',
)

(3)打开文件learn/views.py编写一个首页的视图。具体实现代码如下。

from django.shortcuts import render
def home(request):
    return render(request, 'home.html')

(4)在“learn”目录下新建一个“templates”文件夹用于保存模板文件,然后在里面新建一个home.html文件作为模板。文件home.html的具体实现代码如下。

<!DOCTYPE html>
<html>
<head>
    <title>欢迎光临</title>
</head>
<body>
欢迎选择浪潮产品!
</body>
</html>

(5)为了将视图函数对应到网址,对文件zqxt_tmpl/urls.py的代码进行如下修改。

from django.conf.urls import include, url
from django.contrib import admin
from learn import views as learn_views
urlpatterns = [
     url(r'^$', learn_views.home, name='home'),
     url(r'^admin/', admin.site.urls),
]

(6)输入如下命令启动服务器。

python manage.py runserver

执行后将显示模板的内容,执行效果如图2-6所示。

图2-6 执行效果

在动态Web应用中,表单是实现动态网页效果的核心。下面的实例代码演示了在Django框架中使用表单计算数字和的过程。

源码路径:daima\2\2-2\zqxt_form2

(1)新建一个名为“zqxt_form2”的项目,然后进入“zqxt_form2”文件夹并新建一个名为“tools”的应用。

django-admin startproject zqxt_form2
python manage.py startapp tools

(2)在“tools”文件夹中新建文件forms.py。具体实现代码如下。

from django import forms
class AddForm(forms.Form):
     a = forms.IntegerField()
     b = forms.IntegerField()

(3)编写视图文件views.py,求两个数字的和。具体实现代码如下。

# coding:utf-8
from django.shortcuts import render
from django.http import HttpResponse

#导入我们创建的表单类
from .forms import AddForm

def index(request):
     if request.method == 'POST':#当提交表单时

          form = AddForm(request.POST) # form包含提交的数据

          if form.is_valid():#如果提交的数据合法
                a = form.cleaned_data['a']
                b = form.cleaned_data['b']
                return HttpResponse(str(int(a) + int(b)))

     else:#当正常访问时
          form = AddForm()
     return render(request, 'index.html', {'form': form})

(4)编写模板文件index.html,实现一个简单的表单。具体实现代码如下。

<form method='post'>
{% csrf_token %}
{{ form }}
<input type="submit" value="提交">
</form>

(5)在文件urls.py中将视图函数对应到网址。具体实现代码如下。

from django.conf.urls import include, url
from django.contrib import admin
from tools import views as tools_views
urlpatterns = [
     url(r'^$', tools_views.index, name='home'),
     url(r'^admin/', admin.site.urls),
]

在浏览器中运行后会显示一个表单,在表单中输入两个数字,如图2-7所示。

单击“提交”按钮后会计算这两个数字的和,并显示求和结果。执行效果如图2-8所示。

图2-7 在表单中输入数字

图2-8 显示求和结果

在动态Web应用中,数据库技术永远是核心技术。Django模型是与数据库相关的,与数据库相关的代码一般保存在文件models.py中。Django框架支持SQLite3、MySQL和PostgreSQL等数据库工具,开发者只需要在文件settings.py中进行配置,不用修改文件models.py中的代码。下面的实例代码演示了在Django框架中实现数据库操作的过程。

源码路径:daima\2\2-2\learn_models

(1)新建一个名为“learn_models”的项目,然后进入“learn_models”文件夹中并新建一个名为“people”的应用。

django-admin startproject learn_models # 新建一个项目
cd learn_models # 进入该项目的文件夹
django-admin startapp people # 新建一个 people 应用

(2)将新建的应用(people)添加到文件settings.py中的INSTALLED_APPS中,也就是告诉Django有这么一个应用。

INSTALLED_APPS = (
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'people',
)

(3)打开文件people/models.py,新建一个继承自类models.Model的子类Person,此类中有姓名和年龄这两个字段。具体实现代码如下。

from django.db import models
class Person(models.Model):
     name = models.CharField(max_length=30)
     age = models.IntegerField()
     def __str__(self):
          return self.name

在上述代码中,name和age这两个字段中不能有双下划线“ ”,这是因为它在Django QuerySetAPI中有特殊含义(表示关系、包含、不区分大小写、以什么指定字符或结尾、日期的大于或小于、正则等)。另外,在代码中也不能有Python中的关键字、所以name是合法的,student_name也是合法的,但是student_ _name不合法,try、class和continue也不合法,因为它们是Python的关键字。

(4)开始同步数据库操作,在此使用默认数据库SQLite3,无须进行额外配置,具体命令如下。

# 进入 manage.py 所在的那个文件夹并输入这个命令
python manage.py makemigrations
python manage.py migrate

通过上述命令可以创建一个数据库表,当在前面的文件models.py中新增类people时,运行上述命令后就可以自动在数据库中创建对应数据库表,不用开发者手动创建。命令行运行后会发现Django生成了一系列的表,也生成了上面刚刚新建的表people_person。命令运行界面中显示的内容如图2-9所示。

图2-9 命令行运行界面中显示的内容

(5)输入命令进行测试。整个测试过程如下。

$ python manage.py shell
>>> from people.models import Person
>>> Person.objects.create(name="haoren", age=24)
<Person: haoren>
>>> Person.objects.get(name="haoren")
<Person: haoren>

在动态Web应用中,后台管理系统十分重要,网站管理员通过后台实现对整个网站的管理。Django框架的功能十分强大,为开发者提供了现成的后台管理系统,程序员只需要编写很少的代码就可以实现功能强大的后台管理系统。下面的实例代码演示了使用Django框架开发一个博客系统的过程。

源码路径:daima\2\2-3\

(1)新建一个名为“zqxt_admin”的项目,然后进入 “zqxt_admin”文件夹并新建一个名为“blog”的应用。

django-admin startproject zqxt_admin
cd zqxt_admin
# 创建 blog 应用
python manage.py startapp blog

(2)修改“blog”文件夹中的文件models.py。具体实现代码如下。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class Article(models.Model):
     title = models.CharField('标题', max_length=256)
     content = models.TextField('内容')
     pub_date = models.DateTimeField('发表时间', auto_now_add=True, editable=True)
     update_time = models.DateTimeField('更新时间', auto_now=True, null=True)
     def __str__(self):
          return self.title
class Person(models.Model):
     first_name = models.CharField(max_length=50)
     last_name = models.CharField(max_length=50)
     def my_property(self):
          return self.first_name + ' ' + self.last_name
     my_property.short_description = "Full name of the person"
     full_name = property(my_property)

(3)将“blog”添加到settings.py文件中的INSTALLED_APPS中。具体实现代码如下。

INSTALLED_APPS = (
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'blog',
)

(4)通过如下命令同步所有的数据库表。

# 进入包含有 manage.py 的文件夹
python manage.py makemigrations
python manage.py migrate

(5)进入文件夹“blog”,修改里面的文件admin.py(如果没有此文件,则新建一个)。具体实现代码如下。

from django.contrib import admin
from .models import Article, Person
class ArticleAdmin(admin.ModelAdmin):
     list_display = ('title', 'pub_date', 'update_time',)
class PersonAdmin(admin.ModelAdmin):
     list_display = ('full_name',)
admin.site.register(Article, ArticleAdmin)
admin.site.register(Person, PersonAdmin)

输入下面的命令启动服务器。

python manage.py runserver

然后在浏览器地址栏中输入“http://localhost:8000/admin”会显示一个用户登录界面,如图2-10所示。

图2-10 用户登录界面

我们可以创建一个超级管理员用户,使用命令进入包含manage.py的文件夹“zqxt_admin”。然后输入如下命令创建一个超级账号,根据提示分别输入账号、邮箱地址和密码。

python manage.py createsuperuser

此时可以使用超级账号登录后台管理系统。登录成功后的界面如图2-11所示。

图2-11 登录成功后的界面

管理员可以修改、删除或添加账号信息,如图2-12所示。

图2-12 账号管理

也可以对系统内已经发布的博客信息进行管理维护,如图2-13所示。

图2-13 博客信息管理

还可以直接修改用户账号信息中的密码,如图2-14所示。

图2-14 修改用户账号信息中的密码

本实例使用聚合技术抓取某网站中的新闻信息。通过聚合技术,可以直接调用其他网站的内容,而无须自己创建或维护网站的内容。本实例的功能是使用知乎网提供的API抓取知乎当日新闻,并显示新闻列表。

源码路径:daima\2\2-4\

通过以下步骤完成基本设置。

(1)新建一个名为“news”的项目,在文件settings.py中设置使用的模块和系统目录。涉及的变动代码如下。

INSTALLED_APPS = (
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'news',
   'bs4',

)

STATIC_URL = '/static/'
STATIC_ROOT = 'static_files'
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, "static"),
)
MAIN_SERVER = True

(2)在“main_app”目录下的urls.py文件中设置网页的地址。具体实现代码如下。

urlpatterns = [
     url('^$', home),
     url(r'^admin/', admin.site.urls),
     url(r'^news/', include('news.urls')),
]

(3)在“news”目录下的urls.py文件中设置地址的映射关系。一定不要忘记创建名为app_name的“news”,这将作为命名空间来使用。具体实现代码如下。

urlpatterns = [
     url(r'^$', news.views.news_home, name='news_home'),
     url(r'^about/$', news.views.about, name='about'),
     url(r'^(?P<source>\w+)/$', news.views.NewsList.as_view(), name='story_list'),
     url(r'^detail/(?P<story_id>\d+)/$', news.views.StoryDetail.as_view(), name="story_detail"),
     url(r'^convertlist/(?P<source>\w+)/$', news.views.ConvertList.as_view(), name='convert_list'),
     url(r'^convertdetail/(?P<source>\w+)/(?P<id>\d+)/$', news.views.ConvertDetail.as_view(),
         name='convert_detail'),

]
app_name = 'news'

编写文件fetcher.py来获取知乎新闻客户端的新闻。具体实现流程如下。

(1)定义类FetchError实现错误处理功能,并输出对应的错误类型。对应代码如下。

class FetchError(Exception):

      def __init__(self, errtype=''):
           self.errtype = errtype

      def __str__(self, *args, **kwargs):
           return self.__class__.__name__ + ':' + self.errtype

class PageNotFoundError(FetchError):
      pass

(2)定义类NewsFetcher。首先设置解析目标网页的文件头信息,然后分别通过函数fetch_json()和fetch_html()获取目标JSON与HTML信息。具体实现代码如下。

class NewsFetcher():
     headers = {"Accept": "text/html,application/xhtml+xml,application/xml;",
                  "Accept-Encoding": "gzip",
                  "Accept-Language": "zh-CN,zh;q=0.8",
                  "Referer": "http://www.example.com/",
                  "User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 
                      (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36"
               }

     def fetch_json(self, url, method='get'):
          r = requests.request(
               method=method,
               url=url,
               headers=self.headers
          )
          try:

               r.raise_for_status()
               if r.encoding.lower() == 'iso-8859-1':
                     r.encoding = 'utf-8'
               json = r.json()
          except HTTPError as e:
               raise FetchError('invalid_url')
          return json

     def fetch_html(self, url, method='get'):
          r = requests.request(
               method=method,
               url=url,
               headers=self.headers
          )
          try:
               r.raise_for_status()
               if r.encoding.lower() == 'iso-8859-1':
                    r.encoding = 'utf-8'
               html = r.text
          except HTTPError as e:
               raise FetchError('invalid_url')
          return html

(3)编写类ZhihuDailyFetcher,通过函数get_latest_news()解析当日知乎客户端的新闻;通过函数get_before_news()解析过去某日知乎客户端的新闻;通过函数get_story_detail()解析知乎客户端某具体ID的新闻,并显示这条新闻的详细内容。具体实现代码如下。

class ZhihuDailyFetcher(NewsFetcher):

      def get_latest_news(self):
           response_json = self.fetch_json(
               'http://news.at.知乎网站域名 /api/4/news/latest')
           return response_json

      def get_before_news(self, date_str):
           response_json = self.fetch_json(
               'http://news.at.zhihu.com/api/4/news/before/' + date_str)
           return response_json

      def get_story_detail(self, story_id):
           response_json = self.fetch_json(
               'http://news.at.知乎网站域名/api/4/news/' + str(story_id))
           return response_json

(4)编写类CBFetcher实现具体的解析功能,这是整个实例的核心——聚合解析。通过函数get_news_list()解析新闻列表;通过函数get_story_comment()解析指定ID新闻的具体内容;通过函数get_story_detail()解析某ID新闻的详细内容;通过函数get_before_news()解析过去某日新闻的内容。类CBFetcher的具体实现代码如下。

class CBFetcher(NewsFetcher):

      def get_news_list(self, page_number=1):
           url = 'http://m.cnbeta.com/list_latest_' + str(page_number) + '.htm'
           html = self.fetch_html(url)

           soup = BeautifulSoup(html, "html.parser")
           news_list = []
           ul = soup.html.body.find('ul')
           if not ul:
                 raise FetchError('parse_error')
           for li in ul.find_all('li'):
                 a = li.div.a
                 news_id = int(a.attrs['href'].split('.')[0].split('/')[2])
                 news_list.append((news_id, a.text))
           if news_list:
                 first_id = news_list[0][0]
                 last_id = news_list[-1][0]
                 return {'news_list': news_list, 'first_id': first_id, 'last_id': last_id}
           else:
                 raise FetchError('parse_error')

      def get_story_comment(self, story_id):
           url = 'http://m.cnbeta.com/comments_' + str(story_id) + '.htm'
           html = self.fetch_html(url)
           soup = BeautifulSoup(html, "html.parser")
           J_commt_list = soup.html.body.find('ul', id="J_commt_list")
           if not J_commt_list:
                 return [], 0, 0
           comments = str(J_commt_list)
           comment_count_list = soup.html.body.find(
                 'span', class_="morComment").find_all('b')
           comment_count_all = int(comment_count_list[0].string)
           comment_count_show = int(comment_count_list[1].string)
           return comments, comment_count_all, comment_count_show

      def get_story_detail(self, story_id, update_comment=True):
           url = 'http://www.cnbeta.com/articles/' + str(story_id) + '.htm'
           html = self.fetch_html(url)
           soup = BeautifulSoup(html, "html.parser")
           introduction = soup.html.body.find('div', class_="introduction")
           if not introduction:
                 raise PageNotFoundError()
           theme = introduction.find('img')
           theme_id = introduction.find('a')['href'].split('/')[2].split('.')[0]
           theme_text = theme['title']
           theme_img = theme['src']
           summary = introduction.p
           title = soup.html.body.find('h2', id="news_title").string
           title_bar = soup.html.body.find('div', class_="title_bar")
           time = datetime.datetime.strptime(
                title_bar.find('span', class_="date").string, "%Y-%m-%d %H:%M:%S")
           where = title_bar.find('span', class_="where").string
           body = soup.html.body.find('div', class_="content")
           author = soup.html.body.find('span', class_="author")
           if update_comment:
                comments, comment_count_all, comment_count_show = self.get_story_comment(
                   story_id)
           else:
                comments, comment_count_all, comment_count_show = '', 0, 0
           result = {'body': str(body),
                        'share_url': url,
                        'image': str(theme_img),
                        'section': {'thumbnail': str(theme_img),
                                        'id': str(theme_id),
                                        'name': str(theme_text),
                                         },
                        'title': str(title),
                        'id': story_id,

                        'author': str(author),
                        'summary': (summary),
                        'time': time,
                        'where': str(where),
                        'comments': comments,
                        'comment_count_all': comment_count_all,
                        'comment_count_show': comment_count_show,
                        }
           return result

      def get_before_news(self, date_str, ratio=3):

           date = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
           page_number = 1
           a_page = self.get_news_list(page_number)
           b_page = self.get_news_list(page_number * ratio)
           top_id = int(a_page['news_list'][0][0])
           bottom_id = int(b_page['news_list'][-1][0])
           top_time = self.get_story_detail(top_id, update_comment=False)['time']
           bottom_time = self.get_story_detail(
                bottom_id, update_comment=False)['time']
           print(top_time)
           print(bottom_time)
           while bottom_time.date() > date:
                page_number *= ratio
                a_page = b_page
                top_id = int(a_page['news_list'][0][0])
                top_time = self.get_story_detail(
                     top_id, update_comment=False)['time']
                try:
                     b_page = self.get_news_list(page_number * ratio)
                except FetchError:
                     break

                bottom_id = int(b_page['news_list'][-1][0])
                bottom_time = self.get_story_detail(
                     bottom_id, update_comment=False)['time']
                print(top_time)
                print(bottom_time)
           print('finish')
           a_number = page_number
           b_number = page_number * ratio
           while b_number - a_number > 1:
                print(str(a_number) + ' ' + str(b_number))
                mid_number = int(math.ceil((a_number + b_number) / 2))
                try:
                    mid_page = self.get_news_list(mid_number)
                except FetchError:
                    b_number = mid_number
                    continue
                mid_page_top_id = int(mid_page['news_list'][0][0])
                mid_page_top_time = self.get_story_detail(
                    mid_page_top_id, update_comment=False)['time']
                if mid_page_top_time.date() > date:
                    a_number = mid_number
                else:
                    b_number = mid_number
           while True:
                news_list = self.get_news_list(a_number)['news_list']
                for news in news_list:
                    news_id = int(news[0])
                    news_dict = self.get_story_detail(news_id, False)
                    print(news_dict['title'])

                a_number += 1

编写文件views.py获取知乎新闻客户端的新闻信息。具体实现流程如下。

(1)编写函数news_home()设置系统主页。具体实现代码如下。

def news_home(request):
     return HttpResponseRedirect(reverse('news:story_list', kwargs={'source': 'zhihudaily'}))

(2)编写函数about()设置“关于我们”页面。具体实现代码如下。

@gzip_page
def about(request):
     return render_to_response('about.html', {'nav_item': 'about'})

(3)定义类NewsViewBase实现基本的视图功能,通过函数get()获取指定页面的视图;通过函数media_display()设置是否显示新闻中的图片信息;通过函数get_date()获取新闻的时间;通过函数hide_media()隐藏新闻中的图片信息。具体实现代码如下。

class NewsViewBase(TemplateView):

      @method_decorator(gzip_page)
      def get(self, request, *args, **kwargs):
           return TemplateView.get(self, request, *args, **kwargs)

      def media_display(self):
           image_show = self.request.GET.get("image", None)
           if image_show:
                self.request.session['image'] = image_show
           else:
                image_show = self.request.session.get('image', 'hide')
           if image_show == 'show':
                return True
           else:
                return False

      def get_date(self):
           date_str = self.request.GET.get("date", None)
           if date_str:
                date = datetime.datetime.strptime(date_str, '%Y-%m-%d').date()
           else:
                date = datetime.datetime.now().date()
           date_str = date.strftime('%Y-%m-%d')
           last_date = date - datetime.timedelta(days=1)
           last_date_str = last_date.strftime('%Y-%m-%d')
           next_date = date + datetime.timedelta(days=1)
           next_date_str = next_date.strftime('%Y-%m-%d')
           if date == datetime.datetime.now().date():
                next_date_str = ''
           return date, last_date_str, date_str, next_date_str

      def hide_media(self, raw_html):
           soup = BeautifulSoup(raw_html, "html.parser")
           for img in soup.find_all('img'):
                tag = soup.new_tag("a")
                tag['href'] = img['src']
                tag.string = img.get('title', '图片')
                img.insert_before(tag)
                img.decompose()

           for embed in soup.find_all('embed'):
                tag = soup.new_tag("a")
                tag['href'] = embed['src']
                tag.string = '视频' + embed['src']
                embed.insert_before(tag)
                embed.decompose()

           for script in soup.find_all('script'):
                script.decompose()

           return str(soup)

      def get_context_data(self, request, *args, **kwargs):
           context = {}
           return context

(4)定义类NewsList显示新闻列表。具体实现代码如下。

class NewsList(NewsViewBase):
     template_name = 'newslist.html'

     def get_context_data(self, source, **kwargs):
         if not self.media_display():
              self.template_name = 'newslist_no_pic.html'

         date, last_date_str, date_str, next_date_str = self.get_date()
         dailydate = DailyDate.objects.update_daily_date_with_date(
              date, source=source)
         story_qs = dailydate.get_daily_stories()
         context = {}
         context['date_str'] = date_str
         context['story_qs'] = story_qs
         context['last_date_str'] = last_date_str
         context['next_date_str'] = next_date_str
         context['nav_item'] = 'zhihu'
         return context

(5)定义类StoryDetail显示新闻的详细内容。具体实现代码如下。

class StoryDetail(NewsViewBase):
     template_name = 'story_detail.html'

     def get_context_data(self, story_id, **kwargs):
          story = Story.objects.get(story_id=story_id)
          story.update()

          if not self.media_display():
                story.body = self.hide_media(story.body)
          context = {}
          context['story'] = story
          context['nav_item'] = 'zhihu'
          return context

(6)定义类ConvertList实现“上一页”和“下一页”功能。具体实现代码如下。

class ConvertList(NewsViewBase):
     template_name = 'convert_list.html' 

     def get_context_data(self, source, **kwargs):
          page_number = int(self.request.GET.get('page', 1))
          if source == 'cb':
               result = CBFetcher().get_news_list(page_number)
               news_list = result['news_list']
               context = {}
               context['page_number'] = page_number
               if page_number > 1:
                    context['previous_page'] = page_number - 1
               context['next_page'] = page_number + 1

               context['news_list'] = news_list
               context['nav_item'] = 'cnbeta'
               return context
          else:
               raise Exception('wrong source name')

“关于我们”页面对应的模板文件是about.html。具体实现代码如下。

{% extends 'bootstrap_base.html' %}

{% block title %}
     关于
{% endblock %}

{% block content %}
     <div class="row">
          <h1>啥</h1>
            <h3>都没有</h3>
    </div><!-- /.row -->
{% endblock %}

新闻列表页面的模板文件是newslist.html。具体实现代码如下。

% extends 'bootstrap_base.html' %}

{% block title %}
     知乎日报 {{date_str}}
{% endblock %}

{% block extra_js %}

{% endblock %}

{% block content %}

     <script type="text/javascript"> 
           function showImg( url ) { 
           var frameid = 'frameimg' + Math.random(); 
           window.img = '<img id="img" jump_url class="img-thumbnail"
               src=\''+url+'?'+Math.random()+'\' /><script>window.onload = function()
               { parent.document.getElementById(\''+frameid+'\').height =
               document.getElementById(\'img\').height+\'px\'; }<'+'/script>'; 
           document.write('<iframe id="'+frameid+'" src="javascript:parent.img;" frameBorder="0"
               scrolling="no" width="100%"></iframe>'); 
           } 
     </script> 

     <div class="row">
           {% for story in story_qs %}
                <div class="col-xs-6 col-sm-4 col-md-3">
                <a href="{% url 'news:story_detail' story_id=story.story_id  %}">
                          <!--<img class="img-thumbnail" src="
                              //v4.bootcss.com/examples/screenshots/starter-template.
                              jpg" alt="" />-->
                          <div id="hotlinking"><script
                              type="text/javascript">showImg("{{story.cover_picture_fi
                              rst}}");</script></div>
                   </a>
                   <div style= "height:50px;"><a href="{% url 'news:story_detail'
                        story_id=story.story_id  %}">{{story.title}}</a></div>
                </div>
           {% endfor %}

     </div><!-- /.row -->
          <nav>
             <ul class="pager">
               <li><a href="?date={{last_date_str}}">Previous</a></li>

               {% if next_date_str %}
               <li><a href="?date={{next_date_str}}">Next</a></li>
               {% endif %}

             </ul>

             <form method="get" action="" >
               <input type="date" name="date" value="{{date_str}}">
               <button type="submit">跳转</button>
               </form>

          </nav>
{% endblock %}

隐藏图片的新闻列表页面的模板文件是newslist_no_pic.html。具体实现代码如下。

{% extends 'bootstrap_base.html' %} 
{% block title %} 知乎日报 {{date_str}}
{% endblock %} {% block extra_js %} {% endblock %} 

{% block content %}
<table class='table'>
      {% for story in story_qs %}
      <tr>
           <td><a href={% url 'news:story_detail' story_id=story.story_id%}>{{story.title}}</a></td>
      </tr>
      {% endfor %}
</table>

<nav>
      <ul class="pager">
           <li><a href="?date={{last_date_str}}">Previous</a></li> 
           {% if next_date_str %}
                 <li><a href="?date={{next_date_str}}">Next</a></li> 
           {% endif %}

      </ul>

      <form method="get" action="">
           <input type="date" name="date" value="{{date_str}}">
           <button type="submit">跳转</button>
      </form>

</nav>
{% endblock %}

无图版系统主页如图2-15所示。

图2-15 无图版系统主页

有图版系统主页如图2-16所示。

图2-16 有图版系统主页

无图版新闻详情页面如图2-17所示。

图2-17 无图版新闻详情页面

有图版新闻详情页面如图2-18所示。

图2-18 有图版新闻详情页面

本实例的功能是使用Django框架开发一个在线商城系统,在该系统中不但可以实现后台商品数据的添加和修改操作,而且能实现对商品分类的添加、修改和删除操作,还实现了商城系统的大量核心功能——订单处理和购物车处理。

源码路径:daima\2\2-5\

在“myshop”子目录下的文件settings.py中实现系统设置功能,分别添加shop、cart和orders三大模块,设置数据库信息,设置URL链接路径。主要实现代码如下。

源码路径:daima\2\2-5\myshop\myshop\settings.py

INSTALLED_APPS = (
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'shop',
   'cart',
   'orders',
)

DATABASES = {
   'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

STATIC_URL = '/static/'

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

CART_SESSION_ID = 'cart'

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

cwd = os.getcwd()
if cwd == '/app' or cwd[:4] == '/tmp':
     import dj_database_url
     DATABASES = {
        'default': dj_database_url.config(default='postgres://localhost')
     }

     SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

     ALLOWED_HOSTS = ['*']
     DEBUG = False

     BASE_DIR = os.path.dirname(os.path.abspath(__file__))
     STATIC_ROOT = 'staticfiles'
     STATICFILES_DIRS = (
         os.path.join(BASE_DIR, 'static'),
     )

在“myshop”子目录下的文件urls.py中实现系统所有功能模块页面的布局功能。整个系统分为四大模块——前台商城展示、后台管理、订单处理和购物车处理。文件urls.py的具体实现代码如下。

源码路径:daima\2\2-5\myshop\myshop\urls.py

urlpatterns = [
     url(r'^admin/', admin.site.urls),
     url(r'^cart/', include('cart.urls', namespace='cart')),
     url(r'^orders/', include('orders.urls', namespace='orders')),
     url(r'^', include('shop.urls', namespace='shop')),
]

if settings.DEBUG:
     urlpatterns += static(settings.MEDIA_URL,
                                  document_root=settings.MEDIA_ROOT)

在前台商城展示模块中显示系统内所有商品的分类信息,并展示各个分类商品的信息,单击某个商品后可以展示这个商品的详细信息。本模块主要由以下3个文件实现:

在文件models.py中编写两个类。其中,类Category实现商品分类展示功能,类Product实现产品展示功能。文件models.py的具体实现代码如下。

源码路径:daima\2\2-5\myshop\shop\models.py

class Category(models.Model):
      name = models.CharField(max_length=200, db_index=True)
      slug = models.SlugField(max_length=200, db_index=True, unique=True)

      class Meta:
           ordering = ('name',)
           verbose_name = 'category'
           verbose_name_plural = 'categories'

      def __str__(self):
           return self.name

      def get_absolute_url(self):
           return reverse('shop:product_list_by_category', args=[self.slug])

class Product(models.Model):
      category = models.ForeignKey(Category, related_name='products',on_delete=models.CASCADE)
      name = models.CharField(max_length=200, db_index=True)
      slug = models.SlugField(max_length=200, db_index=True)
      image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
      description = models.TextField(blank=True)
      price = models.DecimalField(max_digits=10, decimal_places=2)
      stock = models.PositiveIntegerField()
      available = models.BooleanField(default=True)
      created = models.DateTimeField(auto_now_add=True)
      updated = models.DateTimeField(auto_now=True)

      class Meta:
           ordering = ('-created',)
           index_together = (('id', 'slug'),)

      def __str__(self):
           return self.name

      def get_absolute_url(self):
           return reverse('shop:product_detail', args=[self.id, self.slug])

文件urls.py的功能是实现前台页面的URL处理,并分别实现“分类展示”和“产品详情展示”。具体实现代码如下。

源码路径:daima\2\2-5\myshop\shop\urls.py

urlpatterns = [
     url(r'^$', views.product_list, name='product_list'),
     url(r'^(?P<category_slug>[-\w]+)/$', views.product_list, name='product_list_by_category'),
     url(r'^(?P<id>\d+)/(?P<slug>[-\w]+)/$', views.product_detail, name='product_detail'),
]
app_name = 'myshop'

在文件views.py中实现视图展示功能,通过函数product_list()实现商品列表展示功能,通过函数product_detail()实现商品详情展示功能。文件views.py的具体实现代码如下。

源码路径:daima\2\2-5\myshop\shop\views.py

def product_list(request, category_slug=None):
     category = None
     categories = Category.objects.all()
     products = Product.objects.filter(available=True)
     if category_slug:
           category = get_object_or_404(Category, slug=category_slug)
           products = products.filter(category=category)
     return render(request, 'shop/product/list.html', {'category': category,
                                                            'categories': categories,
                                                            'products': products})

def product_detail(request, id, slug):
     product = get_object_or_404(Product, id=id, slug=slug, available=True)
     cart_product_form = CartAddProductForm()
     return render(request,
                      'shop/product/detail.html',
                      {'product': product,
                      'cart_product_form': cart_product_form
                      })

前台商品展示主页的模板文件是base.html。具体实现代码如下。

{% load static %}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>{% block title %}My shop 我的商店{% endblock %}</title>
    <link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
    <div id="header">
         <a href="/" class="logo">My shop 我的商店</a>
    </div>
    <div id="subheader">
         <div class="cart">
              {% with total_items=cart|length %}
                  {% if cart|length > 0 %}
                      Your cart: 
                      <a href="{% url "cart:cart_detail" %}">
                           {{ total_items }} item{{ total_items|pluralize }}, ${{ cart.
                           get_total_price }}
                      </a>
                  {% else %}
                      Your cart is empty.
                  {% endif %}
              {% endwith %}
         </div>
    </div>
    <div id="content">
         {% block content %}
         {% endblock %}
    </div>
</body>
</html>

商品列表展示功能的模板文件是list.html。具体实现代码如下。

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
    {% if category %}{{ category.name }}{% else %}Products{% endif %}
{% endblock %}

{% block content %}
    <div id="sidebar">
         <h3>Categories</h3>
         <ul>
             <li {% if not category %}class="selected"{% endif %}>
                  <a href="{% url "shop:product_list" %}">All</a>
             </li>
         {% for c in categories %}
             <li {% if category.slug == c.slug %}class="selected"{% endif %}>
                  <a href="{{ c.get_absolute_url }}">{{ c.name }}</a>
             </li>
         {% endfor %}
         </ul>
    </div>
    <div id="main" class="product-list">
         <h1>{% if category %}{{ category.name }}{% else %}Products{% endif %}</h1>
         {% for product in products %}
             <div class="item">
                  <a href="{{ product.get_absolute_url }}">
                       <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static
                          "img/no_image.png" %}{% endif %}">
                  </a>
                  <a href="{{ product.get_absolute_url }}">{{ product.name }}</a><br>
                  ${{ product.price }}
             </div>
         {% endfor %}
    </div>
{% endblock %}

某个商品详情展示功能的模板文件是detail.html。具体实现代码如下。

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
    {{ product.name }}
{% endblock %}

{% block content %}
    <div class="product-detail">
         <img src="{% if product.image %}{{ product.image.url }}{% else %}{% static
             "img/no_image.png" %}{% endif %}">
         <h1>{{ product.name }}</h1>
         <h2><a href="{{ product.category.get_absolute_url }}">{{ product.category }}</a></h2>
         <p class="price">${{ product.price }}</p>
         <form action="{% url "cart:cart_add" product.id %}" method="post">
              {{ cart_product_form }}
              {% csrf_token %}
              <input type="submit" value="Add to cart">
         </form>

         {{ product.description|linebreaks }}
    </div>
{% endblock %}

前台商品展示主页如图2-19所示。单击左侧分类链接后,会只显示这个分类下的所有商品信息。

图2-19 前台商品展示主页

某商品详情展示页面如图2-20所示。

图2-20 某商品详情展示页面

购物车是在线商城系统的核心功能之一。该模块主要由如下4个文件实现:

文件urls.py的功能是实现购物车页面的URL处理,以及实现购物车详情展示、商品的添加和商品的删除。具体实现代码如下。

源码路径:daima\2\2-5\myshop\cart\urls.py

urlpatterns = [
     url(r'^$', views.cart_detail, name='cart_detail'),
     url(r'^add/(?P<product_id>\d+)/$', views.cart_add, name='cart_add'),
     url(r'^remove/(?P<product_id>\d+)/$', views.cart_remove, name='cart_remove'),
]
app_name = 'myshop'

在文件cart.py中实现和购物车相关的操作:通过函数__init__()获取登录用户的账号信息,通过函数__len__()统计当前账户购物车中的商品数量,通过函数__iter__()展示购物车内各个商品的信息,通过函数add()向购物车内添加新的商品信息,通过函数remove()删除购物车内的某个商品信息,通过函数save()保存当前购物车内的商品信息。文件cart.py的具体实现代码如下。

源码路径:daima\2\2-5\myshop\cart\cart.py

class Cart(object):

     def __init__(self, request):
          """
          Initialize the cart.
          """
          self.session = request.session
          cart = self.session.get(settings.CART_SESSION_ID)
          if not cart:
                cart = self.session[settings.CART_SESSION_ID] = {}
          self.cart = cart

     def __len__(self):
          """
          Count all items in the cart.
          """
          return sum(item['quantity'] for item in self.cart.values())

     def __iter__(self):
          """
          Iterate over the items in the cart and get the products from the database.
          """
          product_ids = self.cart.keys()
          # get the product objects and add them to the cart
          products = Product.objects.filter(id__in=product_ids)
          for product in products:
               self.cart[str(product.id)]['product'] = product

          for item in self.cart.values():
               item['price'] = Decimal(item['price'])
               item['total_price'] = item['price'] * item['quantity']
               yield item

     def add(self, product, quantity=1, update_quantity=False):
          """
          Add a product to the cart or update its quantity.
          """
          product_id = str(product.id)
          if product_id not in self.cart:
               self.cart[product_id] = {'quantity': 0,
                                                 'price': str(product.price)}
          if update_quantity:
               self.cart[product_id]['quantity'] = quantity
          else:
               self.cart[product_id]['quantity'] += quantity
          self.save()

     def remove(self, product):
          """
          Remove a product from the cart.
          """
          product_id = str(product.id)
          if product_id in self.cart:
               del self.cart[product_id]
               self.save()

     def save(self):
          self.session[settings.CART_SESSION_ID] = self.cart
          self.session.modified = True

     def clear(self):
          self.session[settings.CART_SESSION_ID] = {}
          self.session.modified = True

     def get_total_price(self):
          return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())

文件forms.py的功能是处理购物车表单中商品的变动信息。具体实现代码如下。

源码路径:daima\2\2-5\myshop\cart\forms.py

PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]

class CartAddProductForm(forms.Form):
     quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES,
                                             coerce=int)
     update = forms.BooleanField(required=False,
                                      initial=False,
                                      widget=forms.HiddenInput)

在文件views.py中实现视图展示功能,通过函数cart_add()向购物车中添加商品,通过函数cart_remove()从购物车中删除商品,通过函数cart_detail()实现购物车详情展示功能。文件views.py的具体实现代码如下。

源码路径:daima\2\2-5\myshop\cart\views.py

def cart_add(request, product_id):
     cart = Cart(request)
     product = get_object_or_404(Product, id=product_id)
     form = CartAddProductForm(request.POST)
     if form.is_valid():
           cd = form.cleaned_data
           cart.add(product=product,
                      quantity=cd['quantity'],
                      update_quantity=cd['update'])
     return redirect('cart:cart_detail')

def cart_remove(request, product_id):
     cart = Cart(request)
     product = get_object_or_404(Product, id=product_id)
     cart.remove(product)
     return redirect('cart:cart_detail')

def cart_detail(request):
     cart = Cart(request)
     for item in cart:
           item['update_quantity_form'] = CartAddProductForm(initial={'quantity': item['quantity'],
                                                                       'update': True})
     return render(request, 'cart/detail.html', {'cart': cart})

购物车模块的模板文件是detail.html。具体实现代码如下。

{% extends "shop/base.html" %}
{% load static %}

{% block title %}
    Your shopping cart
{% endblock %}

{% block content %}
    <h1>Your shopping cart</h1>
    <table class="cart">
         <thead>
             <tr>
                  <th>Image</th>
                  <th>Product</th>
                  <th>Quantity</th>
                  <th>Remove</th>
                  <th>Unit price</th>                
                  <th>Price</th>
             </tr>
         </thead>
         <tbody>
         {% for item in cart %}
             {% with product=item.product %}
             <tr>
                  <td>
                       <a href="{{ product.get_absolute_url }}">
                            <img src="{% if product.image %}{{ product.image.url }}{% 
                               else %}{% static "img/no_image.png" %}{% endif %}">
                       </a>
                  </td>
                  <td>{{ product.name }}</td>
                  <td>
                      <form action="{% url "cart:cart_add" product.id %}" method="post">
                          {{ item.update_quantity_form.quantity }}
                          {{ item.update_quantity_form.update }}
                          <input type="submit" value="Update">
                          {% csrf_token %}
                      </form>
                  </td>
                  <td><a href="{% url "cart:cart_remove" product.id %}">Remove</a></td>
                  <td class="num">${{ item.price }}</td>
                  <td class="num">${{ item.total_price }}</td>
             </tr>
             {% endwith %}
         {% endfor %}
         <tr class="total">
             <td>Total</td>
             <td colspan="4"></td>
             <td class="num">${{ cart.get_total_price }}</td>
         </tr>
         </tbody>
    </table>
    <p class="text-right">
         <a href="{% url "shop:product_list" %}" class="button light">Continue shopping</a>
         <a href="{% url 'orders:order_create' %}" class="button">Checkout</a>
    </p>

购物车界面的执行效果如图2-21所示。我们可以灵活地增加或删除里面的商品,也可以修改里面的商品数量。

图2-21 购物车界面

订单处理也是在线商城系统的核心功能之一。本模块主要由如下文件实现。

(1)文件urls.py的功能是实现订单页面的URL处理,并创建订单。具体实现代码如下。

源码路径:daima\2\2-5\myshop\orders\urls.py

urlpatterns = [
     url(r'^create/$', views.order_create, name='order_create'),
]
app_name = 'myshop'
{% endblock %}

(2)文件admin.py的功能是实现订单展示和订单管理。具体实现代码如下。

源码路径:daima\2\2-5\myshop\orders\admin.py

class OrderItemInline(admin.TabularInline):
      model = OrderItem
      raw_id_fields = ['product']

class OrderAdmin(admin.ModelAdmin):
      list_display = ['id', 'first_name', 'last_name', 'email', 'address', 'postal_code',
          'city', 'paid', 'created', 'updated']
      list_filter = ['paid', 'created', 'updated']
      inlines = [OrderItemInline]

admin.site.register(Order, OrderAdmin)

(3)文件forms.py的功能是创建订单列表。具体实现代码如下。

源码路径:daima\2\2-5\myshop\orders\forms.py

class OrderCreateForm(forms.ModelForm):
      class Meta:
            model = Order
            fields = ['first_name', 'last_name', 'email', 'address', 'postal_code', 'city']

(4)文件models.py的功能是实现订单和订单列表的处理。具体实现代码如下。

源码路径:daima\2\2-5\myshop\orders\models.py

class Order(models.Model):
      first_name = models.CharField(max_length=50)
      last_name = models.CharField(max_length=50)
      email = models.EmailField()
      address = models.CharField(max_length=250)
      postal_code = models.CharField(max_length=20)
      city = models.CharField(max_length=100)
      created = models.DateTimeField(auto_now_add=True)
      updated = models.DateTimeField(auto_now=True)
      paid = models.BooleanField(default=False)

      class Meta:
           ordering = ('-created',)

      def __str__(self):
           return 'Order {}'.format(self.id)

      def get_total_cost(self):
           return sum(item.get_cost() for item in self.items.all())

class OrderItem(models.Model):
      order = models.ForeignKey(Order, related_name='items',on_delete=models.CASCADE)
      product = models.ForeignKey(Product, related_name='order_items',on_delete=models.CASCADE)
      price = models.DecimalField(max_digits=10, decimal_places=2)
      quantity = models.PositiveIntegerField(default=1)

      def __str__(self):
           return '{}'.format(self.id)

      def get_cost(self):
           return self.price * self.quantity

(5)文件tasks.py的功能是创建新的订单。具体实现代码如下。

源码路径:daima\2\2-5\myshop\orders\tasks.py

def order_created(order_id):
     """
     Task to send an e-mail notification when an order is successfully created.
     """
     order = Order.objects.get(id=order_id)
     subject = 'Order nr. {}'.format(order.id)
     message = 'Dear {},\n\nYou have successfully placed an order. Your order id is 
        {}.'.format(order.first_name,
                                                                             order.id)
     mail_sent = send_mail(subject, message, 'admin@myshop.com', [order.email])
     return mail_sent

(6)文件views.py的功能是创建订单视图。具体实现代码如下。

源码路径:daima\2\2-5\myshop\orders\views.py

def order_create(request):
     cart = Cart(request)
     if request.method == 'POST':
          form = OrderCreateForm(request.POST)
          if form.is_valid():
                order = form.save()
                for item in cart:
                      OrderItem.objects.create(order=order,
                                                    product=item['product'],
                                                    price=item['price'],
                                                    quantity=item['quantity'])
                cart.clear()
                # order_created.delay(order.id)
                return render(request, 'orders/order/created.html', {'order': order})
     else:
          form = OrderCreateForm()
     return render(request, 'orders/order/create.html', {'cart': cart,
                                                                         'form': form})

创建订单功能的模板文件是create.html。具体实现代码如下。

{% extends "shop/base.html" %}

{% block title %}
    Checkout
{% endblock %}

{% block content %}
    <h1>Checkout</h1>

    <div class="order-info">
         <h3>Your order</h3>
         <ul>
             {% for item in cart %}
                 <li>{{ item.quantity }}x {{ item.product.name }}
                     <span>${{ item.total_price }}</span></li>
             {% endfor %}
         </ul>
         <p>Total: ${{ cart.get_total_price }}</p>
    </div>

    <form action="." method="post" class="order-form">
         {{ form.as_p }}
         <p><input type="submit" value="Place order"></p>
         {% csrf_token %}
    </form>
{% endblock %}

创建订单成功的页面的模板文件是created.html。具体实现代码如下。

{% extends "shop/base.html" %}

{% block title %}
    Thank you
{% endblock %}

{% block content %}
    <h1>Thank you</h1>
    <p>Your order has been successfully completed. Your order number is
       <strong>{{ order.id }}</strong>.</p>
{% endblock %}

在购物车界面中单击“Checkout”按钮后会弹出创建订单界面,如图2-22所示。填写配送信息完毕,并单击“Place order”按钮,会成功创建订单。

图2-22 创建订单界面

整个实例全部介绍完毕,后台系统是通过Django框架自动实现的。订单管理界面如图2-23所示。

图2-23 订单管理界面

商品分类管理界面如图2-24所示。

图2-24 商品分类管理界面

商品管理界面如图2-25所示。

图2-25 商品管理界面

添加商品的界面如图2-26所示。

图2-26 添加商品的界面

Mezzanine是一款开源的、基于Django的CMS (Content Management System)框架。其实任何一个网站都可以被看作一个特定的内容管理系统,只不过每个网站发布和管理的具体内容不一样。例如,携程网发布的是航班、酒店的信息,而淘宝网发布的是商品的信息。下面将详细介绍框架Mezzanine的使用知识。

在安装Mezzanine之前需要先确保已经安装了Django,然后使用如下命令安装Mezzanine。

pip install mezzanine

接下来便可以使用Mezzanine快速创建一个CMS。具体实现流程如下。

源码路径:daima\2\2-6\testing

(1)使用如下命令创建一个Mezzanine项目。项目名为“testing”。

mezzanine-project testing

(2)使用如下命令进入项目目录。

cd testing

(3)使用如下命令初始化创建一个数据库。

python manage.py createdb

这个过程需要填写如下基本信息。

(4)使用如下命令启动这个项目。

python manage.py runserver

当显示如下信息时,说明成功运行了新建的Mezzanine项目“testing”。

              .....
          _d^^^^^^^^^b_
       .d''           ``b.
     .p'                `q.
    .d'                   `b.
   .d'                     `b.   * Mezzanine 4.2.3
   ::                       ::   * Django 1.10.8
  ::    M E Z Z A N I N E    ::  * Python 3.6.0
   ::                       ::   * SQLite 3.2.2
   `p.                     .q'   * Windows 10
    `p.                   .q'
     `b.                 .d'
       `q..          ..p'
          ^q........p^
              ''''

Performing system checks...

System check identified no issues (0 silenced).
April 17, 2018 - 14:07:33
Django version 1.10.8, using settings 'testing.settings'
Starting development server at http://127.0.0.1:8000/

在浏览器中输入“http://127.0.0.1:8000/”后进入前台主页,如图2-27所示。

图2-27 前台主页http://127.0.0.1:8000/

(5)后台管理首页是http://127.0.0.1:8000/admin,如图2-28所示。在登录后台管理页面时,使用在创建数据库时设置的管理员账号登录。

图2-28 后台管理首页http://127.0.0.1:8000/admin

后台管理系统的主要功能如下。

(6)系统主页默认显示Home页面,如果想以Blog的列表作为主页,只须将文件url.py中的代码行un-comment修改为:

url("^$", "mezzanine.blog.views.blog_post_list", name="home")

也就是将文件url.py中的如下代码注释掉。

#url("^$", direct_to_template, {"template": "index.html"}, name="home")

在url.py中把如下代码取消注释。

url("^$", "mezzanine.blog.views.blog_post_list", name="home")

(7)如果想去掉导航栏中的Search输入框,需要添加如下配置项。

SEARCH_MODEL_CHOICES = []

如果想去掉左侧导航链接和页脚,则需要添加如下配置项。

PAGE_MENU_TEMPLATES = ( (1, "Top navigation bar", "pages/menus/dropdown.html"), )

(8)Mezzanine默认支持4种数据库,分别是postgresql_psycopg2、MySQL、SQLite3和Oracle,在默认情况下使用SQLite3。我们可以在文件local_settings.py中的如下代码段中进行修改设置。

DATABASES = {
   "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": "dev.db",
        "USER": "",
        "PASSWORD": "",
        "HOST": "",
        "PORT": "",
    }
}

Cartridge库是一个基于Mezzanine构建的购物车应用框架,通过一个基于Mezzanine构建的购物车应用可以快速实现电子商务应用中的购物车程序。下面将详细讲解Cartridge库的使用方法。

在安装Cartridge库之前需要先确保已经安装了Mezzanine。使用如下命令安装Cartridge。

pip install Cartridge

接下来便可以使用Cartridge库快速创建一个购物车应用程序系统,具体实现流程如下。

源码路径:daima\2\2-7\car

(1)使用如下命令创建一个Cartridge项目,项目名称是“car”。

mezzanine-project -a cartridge car

(2)使用如下命令进入项目目录。

cd car

(3)使用如下命令初始化创建一个数据库,默认数据库类型是SQLite。

python manage.py createdb --noinput

在这个过程中需要注意系统默认的管理员账号信息,其中用户名默认为admin,密码默认为default。

(4)使用如下命令启动这个项目。

python manage.py runserver

当显示如下所示的信息时,说明成功运行了新建的Cartridge项目“car”。

              .....
          _d^^^^^^^^^b_
       .d''           ``b.
     .p'                `q.
    .d'                   `b.
   .d'                     `b.   * Mezzanine 4.2.3
   ::                       ::   * Django 1.10.8
  ::    M E Z Z A N I N E    ::  * Python 3.6.0
   ::                       ::   * SQLite 3.2.2
   `p.                     .q'   * Windows 10
    `p.                   .q'
     `b.                 .d'
       `q..          ..p'
          ^q........p^
              ''''

Performing system checks...

System check identified no issues (0 silenced).
April 17, 2018 - 21:05:02
Django version 1.10.8, using settings 'car.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

在浏览器地址栏中输入“http://127.0.0.1:8000/”后,进入前台主页,如图2-29所示。

图2-29 前台主页http://127.0.0.1:8000/

(5)后台管理首页地址是http://127.0.0.1:8000/admin,如图2-30所示。在登录后台管理页面时,使用在创建数据库时提供的默认账号信息。

图2-30 后台管理首页http://127.0.0.1:8000/admin

与传统的Django和Mezzanine项目相比,Cartridge提供了和电子商务功能密切相关的模块,具体说明如下。

(6)系统主页默认显示Home页面,如果想以Blog的列表作为主页,只须将文件url.py中的代码行un-comment修改为如下内容即可。

url("^$", "mezzanine.blog.views.blog_post_list", name="home")

也就是将文件url.py中的如下代码注释掉。

#url("^$", direct_to_template, {"template": "index.html"}, name="home")

然后在url.py中对如下代码取消注释。

url("^$", "mezzanine.blog.views.blog_post_list", name="home")

(7)如果想去掉导航栏中的Search输入框,需要添加如下配置项。

SEARCH_MODEL_CHOICES = []

如果想去掉左侧导航链接和页脚,则需要添加如下配置项。

PAGE_MENU_TEMPLATES = ( (1, "Top navigation bar", "pages/menus/dropdown.html"), )

(8)Mezzanine默认支持4种数据库,分别是postgresql_psycopg2、MySQL、SQLite3和Oracle,在默认情况下使用SQLite3。我们可以在文件local_settings.py中的如下代码段中进行修改设置。

DATABASES = {
   "default": {
       "ENGINE": "django.db.backends.sqlite3",
       "NAME": "dev.db",
       "USER": "",
       "PASSWORD": "",
       "HOST": "",
       "PORT": "",
   }
}

django-oscar库是一个基于Django的、开源的电子商务框架,其官网是oscarcommerce网站,其源码参见GitHub网站。在安装django-oscar库之前需要先确保已经安装了Django库。使用如下命令安装django-oscar库。

pip install django-oscar

虽然安装django-oscar库很简单,但是django-oscar库的一些依赖项不支持Windows系统,并且安装起来十分困难,可能会遇到一些错误从而阻止安装成功。所以,建议读者在Linux系统下安装django-oscar库,或者使用虚拟环境进行安装。

在浏览器地址栏中输入“http://127.0.0.1:8000/”后,进入前台主页,如图2-31所示。可见,这是一个在线商城风格的页面。

图2-31 前台主页http://127.0.0.1:8000/

商品展示界面如图2-32所示。

图2-32 商品展示界面


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程