Python书籍

python相关书籍

python

python宝典(基础书)

书籍每章节编写时间
章节标题名 时间
第一章、初识Python  
第二章、Python起步必备  
第三章、Python数据类型与基本语句  
第四章、可复用的函数与模块  
第五章、数据结构与算法  
第六章、面向对象的Python  
第七章、异常处理与程序调试  
第八章、Python多媒体编程  
第九章、使用PIL处理图片  
第十章、系统编程  
第十一章、使用PythonWin编写GUI  
第十二章、使用tkinter编写GUI  
第十三章、使用wxPython编写GUI  
第十四章、使用PyGTK编写GUI  
第十五章、使用PyQT编写GUI  
第十六章、Python与数据库  
第十七章、Python Web应用  
第十八章、Python网络编程  
第十九章、处理HTML和XML  
第二十章、功能强大的正则表达式  
第二十一章、科学计算  
第二十二章、Python扩展与嵌入  
第二十三章、多线程编程  
第二十四章、案例1:用Python优化Windows  
第二十五章、案例2:用Python玩转大数据  
第二十六章、案例3:植物大战僵尸  
第一章、初识Python

python自身已经提供了大量的模块来实现各种功能,初次之外,还可以使用C/C++来扩展Python,甚至可以将Python嵌入到其他语言中。

1.1Python是什么

Python最大的特点是其独特而简洁的语法。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第二章、Python起步必备
第三章、Python数据类型与基本语句
第四章、可复用的函数与模块
第五章、数据结构与算法
第六章、面向对象的Python
第七章、异常处理与程序调试
第八章、Python多媒体编程
第九章、使用PIL处理图片
第十章、系统编程
第十一章、使用PythonWin编写GUI
第十二章、使用tkinter编写GUI
第十三章、使用wxPython编写GUI
第十四章、使用PyGTK编写GUI
第十五章、使用PyQT编写GUI
第十六章、Python与数据库
第十七章、Python Web应用
第十八章、Python网络编程
第十九章、处理HTML和XML
第二十章、功能强大的正则表达式
第二十一章、科学计算
第二十二章、Python扩展与嵌入
第二十三章、多线程编程
第二十四章、案例1:用Python优化Windows
第二十五章、案例2:用Python玩转大数据
第二十六章、案例3:植物大战僵尸

python核心编程(第三版)

本来是想写这本书的,但是翻看了一遍不是很友好。内容不合适新手,老手又没必要去看,比如GUI一章节,新手会耗时去学完这一章,皮毛功夫基本之后又会忘记完了,老鸟 会专门找GUI对应的框架去看也不会看这里,还有就是WEB那一章,总是很多章节都是,新手看了很懵懂,老手不屑去看的感觉。所以暂时放着。

书籍每章节编写时间
章节标题名 时间
第一章:正则表达式
第二章:网络编程 2018-09
第三章:因特网客户端编程  
第四章:多线程编程  
第五章:GUI编程  
第六章:数据库编程  
第七章:Microsoft office编程  
第八章:扩展python  
第九章:web客户端和服务器  
第十章:web编程 cgi和WSGI  
第十一章:web框架 django  
第十二章:云计算 google app  
第十三章:web服务  
第十四章:文本处理  
第十五章:其他内容  
附录B:参考表  
附录C:python3一种编程进化的产物  
附录D:python2迁移python3  
第一章:正则表达式

这是第一章,讲解正则表达式的使用,。不用死记硬背。做好笔记用到再查。

书中很多啰嗦的内容就不摘要了。无非就是知道python中re模块是如何处理正则的。

网上已经有很多教程了,这里就不不重复写了,(懒,逃)

http://www.runoob.com/python/python-reg-expressions.html

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第二章:网络编程

在本节中,我们将简要的介绍如何使用套接字进行网络编程。首先,我们将给出一些网络编程 方面的背景资料和 Python 中使用套接字的方法,然后介绍如何使用 Python 的一些模块来创建网络 化的应用程序。

什么是客户/服务器架构?

什么是客户/服务器架构?不同的人有不同的答案。这要看你问的是什么人,以及指的是软件 系统还是硬件系统了。但是,有一点是共通的:服务器是一个软件或硬件,用于提供客户需要的“服 务”。服务器存在的唯一目的就是等待客户的请求,给这些客户服务,然后再等待其它的请求。

另一方面,客户连上一个(预先已知的)服务器,提出自己的请求,发送必要的数据,然后就 等待服务器的完成请求或说明失败原因的反馈。服务器不停地处理外来的请求,而客户一次只能提 出一个服务的请求,等待结果。然后结束这个事务。客户之后也可以再提出其它的请求,只是,这 个请求会被视为另一个不同的事务了。

硬件的客户/服务器架构

打印(机)服务是一个硬件服务器的例子。它们处理打印任务,并把任务发给相连的打印机(或 其它打印设备)。这样的电脑一般是可以通过网络访问并且客户机器可以远程发送打印请求给它。

另一个硬件服务器的例子是文件服务器。它们一般拥有大量的存储空间,客户可以远程访问。 客户机器可以把服务器的磁盘映射到自己本地,就像本地磁盘一样使用它们。其中, SunMicrosystems 公司的 Network File System(NFS)是使用最广泛的网络文件系统之一。如果你正 在访问网络磁盘,并且区分不出是本地的还是网络上的,那客户/服务器系统就很好的完成了它们 的工作。其目的就是要让用户使用起来感觉就像使用本地磁盘一样。“抽象”到一般的磁盘访问这一 层上后,所有的操作都是一样的,而让所有操作都一样的“实现”则要依靠各自的程序了。

软件客户/服务器架构

软件服务器也是运行在某个硬件上的。但不像硬件服务器那样,有专门的设备,如打印机,磁盘等。软件服务器提供的服务主要是程序的运行,数据的发送与接收,合并,升级或其它的程序或数据的操作。

如今,最常用的软件服务器是 Web 服务器。一台机器里放一些网页或 Web 应用程序,然后启动 服务。这样的服务器的任务就是接受客户的请求,把网页发给客户(如用户计算机上的浏览器),然 后等待下一个客户请求。这些服务启动后的目标就是“永远运行下去”。虽然它们不可能实现这样的 目标,但只要没有关机或硬件出错等外力干扰,它们就能运行非常长的一段时间。

数据库服务器是另一种软件服务器。它们接受客户的保存或读取请求,完成请求,然后再等待 其它的请求。它们也被设计为要能“永远”运行。

我们要讨论的最后一种软件服务器是窗口服务器。这些服务器几乎可以认为是硬件服务器。它 们运行于一个有显示器的机器上。窗口的客户是那些在运行时需要窗口环境的程序,它们一般会被 叫做图形界面(GUI)程序。在一个 DOS 窗口或 Unix 的 shell 中等没有窗口服务器的环境中,它们将 无法启动。一旦窗口服务器可以使用时,那一切就正常了。

当世界有了网络,那这样的环境就开始变得更有趣了。一般情况下,窗口客户的显示和窗口服 务器的提供都在同一台电脑上。但在 X Window 之类的网络化的窗口环境中,你可以选 择其它电脑的窗口服务器来做显示即你可以在一台电脑上运行 GUI 程序,而在另一台电脑上显 示它!

客户/服务器网络编程

什么是套接字?
套接字是一种具有之前所说的“通讯端点”概念的计算机网络数据结构。网络化的应用程序在
开始任何通讯之前都必需要创建套接字。就像电话的插口一样,没有它就完全没办法通讯。

套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种,分别是基于文 件型的和基于网络型的。

Unix 套接字是我们要介绍的第一个套接字家族。其“家族名”为 AF_UNIX(在 POSIX1.g 标准中 也叫 AF_LOCAL),表示“地址家族:UNIX”。包括 Python 在内的大多数流行平台上都使用术语“地址 家族”及其缩写“AF”。而老一点的系统中,地址家族被称为“域”或“协议家族”,并使用缩写“PF” 而不是“AF”。同样的,AF_LOCAL(在 2000-2001 年被列为标准)将会代替 AF_UNIX。不过,为了向后 兼容,很多系统上,两者是等价的。Python 自己则仍然使用 AF_UNIX。

由于两个进程都运行在同一台机器上,而且这些套接字是基于文件的。所以,它们的底层结构 是由文件系统来支持的。这样做相当有道理,因为,同一台电脑上,文件系统的确是不同的进程都 能访问的。

Python 只支持 AF_UNIX,AF_NETLINK,和 AF_INET 家族。由于我们只关心网络编程,所以在本 章的大部分时候,我们都只用 AF_INET。

套接字地址:主机与端口

如果把套接字比做电话的插口——即通讯的最底层结构,那主机与端口就像区号与电话号码的 一对组合。有了能打电话的硬件还不够,你还要知道你要打给谁,往哪打。一个 Internet 地址由网 络通讯所必需的主机与端口组成。而且不用说,另一端一定要有人在听才可以。否则,你就会听到 熟悉的声音“对不起,您拨的是空号,请查对后再播”。你在上网的时候,可能也见过类似的情况, 如“不能连接该服务器。服务器无响应或不可达”。

合法的端口号范围为 0 到 65535。其中,小于 1024 的端口号为系统保留端口。如果你所使用的 是 Unix 操作系统,保留的端口号(及其对应的服务/协议和套接字类型)可以通过/etc/services 文件获得。常用端口号列表可以从下面这个网站获得: http://www.iana.org/assignments/port-numbers

Python 中的网络编程

现在,你已经有了足够的客户/服务器,套接字和网络方面的知识。我们现在就开始把这些概 念带到 Python 中来。本节中,我们将主要使用 socket 模块。模块中的 socket()函数被用来创建套 接字。套接字也有自己的一套函数来提供基于套接字的网络通讯。

socket()模块函数

要使用 socket.socket()函数来创建套接字。其语法如下:

socket(socket_family, socket_type, protocol=0)

socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。 这几个常量的意义可以参考之前的解释。protocol 一般不填,默认值为 0。

创建一个 TCP/IP 的套接字,你要这样调用 socket.socket():

tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

同样地,创建一个 UDP/IP 的套接字,你要这样:

udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。

tcpSock = socket(AF_INET, SOCK_STREAM)

当我们创建了套接字对象后,所有的交互都将通过对该套接字对象的方法调用进行。

套接字对象(内建)方法

我们列出了最常用的套接字对象的方法。在下一个小节中,我们将分别创建 TCP 和 UDP 的客户和服务器,它们都要用到这些方法。虽然我们只关心 Internet 套接字,但是这些方法在 Unix 套接字中的也有类似的意义。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第三章:因特网客户端编程
3.1因特网客户端简介

3.2文件传输 3.2.1文件传输因特网协议 3.2.2文件传输协议 3.2.3 Python和FTP 3.2.4 4 ftplib.fTP类的方法 3.2.5交互式FTP示例 3.2.6客户端FTP程序示例 3.2.7FTP的其他内容

3.3网络新闻 3.3.1 Usenet与新闻组 3.3.2网络新闻传输协议 3.3.3 Python和NNTP 3.3.4 nntplib.NTP类方法 3.3.5交互式NNTP示例 3.3.6客户端程序NNTP示例 3.3.7NNTP的其他内容

3.4电子邮件 3.4.1电子邮件系统组件和协议 3.4.2发送电子邮件 3.4.3 Python和sm 3.4.4 smtplib.MTP类方法 3.4.5交互式SMTP示例 3.4.6 SMTP的其他 3.4.7接收电子邮件 3.4.8 POP和IMAP 3.4.9 Python和POP3 3.4.10 交互式POP3示例 3.4.l1 poplib.POP3类方法 3.4.12 客户端程序SMTP和POP3示例 3.4.13 Python和IMAP4 3.4.14 交互式IMAP4示例 3.4.15 imaplib.IMAP4类中的常用方法

3.5实战 3.5.1生成电子邮件 3.5.2解析电子邮件 3.5.3基于Web的云电子邮件服务…35.4最佳实践:安全、重构 3.5.5 Yahoo! Mail" 3.5.6 Gmail"

3.6相关模块 3.6.1电子邮件 3.6.2其他因特网客户端协议 3.7练习

第四章:多线程编程

4.1简介/动机

4.2线程和进程 4.2.1进程 4.2.2线程

4.3线程和Python 4.3.1全局解释额锁 4.3.2退出线程 4.3.3在 Python中使用线程 4.3.4不使用线程的情况 4.3.5 Python threading模块 4.4 thread模块

4.5 threading模块 4.5.1 Thread类 4.5.2 threading模块的其他函数

4.6单线程和多线程执行对比

4.7多线程实践 4.7.1图书排名示例 4.7.2同步原语 4.7.3锁示例 4.7.4信号量示例

4.8生产者消费者问题和Queuequeue模块

4.9线程的替代方案 4.9.1 subprocess模块 4.9.2 multiprocessing模块 4.9.3 concurrent futures模块

4.10相关模块

4.11练习

第五章:GUI编程

第六章:数据库编程

6.1简介 6.1.1持久化存储 6.1.2数据库基本操作和SQL6.1.3数据库和 python 6.2 Python的DB-apl 6.2.1模块属性 6.2.2 Connection xtuo. 6.2.3 Cursor对象 6.2.4类型对象和构造函数6.2.5关系数据库 6.2.6数据库和 Python:适配器6.2.7使用数据库适配器的示例6.2.8数据库适配器示例应用 6.3 ORM. 6.3.1考虑对象,而不是SQL6.3.2 Python和ORM 6.3.3员工角色数据库示例 6.3.4 SQLAlchemy"" 6.3.5 SQLObject 6.4非关系数据库 6.4.1 NoSQL介绍 6.4.2 MongoDB 6.4.3 PyMongo: MongoDB FI Python 6.4.4总结 6.5相关文献 6.6练习

第七章:Microsoft office编程

7.1简介 7.2使用 Python进行COM客户端编程 7.2.1客户端COM编程 7.2.2入门 7.3入门示例 7.3.1 Excel". 7.3.2 Word". 7.3.3 PowerPoint" 7.3.4 oUtlook

7.4中级示例 7.4.1 Excel". 7.4.2 Outlook" 7.4.3 PowerPoint" 7.4.4总结 7.5相关模块/包 7.6练习

第八章:扩展python

8.1简介和动机… 8.1.1 Python扩展简介… 8.1.2什么情况下需要扩展 Python 8.1.3什么情况下不应该扩展 Python 8.2编写 Python扩展 8.2.1创建应用代码 8.2.2根据样板编写封装代码 8.2.3编译 8.2.4导入并测试 8.2.5引用计数 8.2.6线程和全局解释器锁 8.3相关主题 8.3.1 SWIG 8.3.2 Pyrex 8.3.3 Cython 8.3.4 Psyco"" 8.3.5PyPyPy 8.3.6嵌aPython入 8.4练习

第九章:web客户端和服务器
第十章:web编程 cgi和WSGI
第十一章:web框架 django
第十二章:云计算 google app
第十三章:web服务
第十四章:文本处理
第十五章:其他内容
附录B:参考表
附录C:python3一种编程进化的产物
附录D:python2迁移python3

python编程实战

第一章:python的创建型设计模式
第二章:python的结构型设计模式
第三章:python的行为型设计模式
第四章:python的高级并发模式
第五章:扩充python
第六章:python高级网络编程
第七章:用tkinter开发图形用户界面
第八章:用OpenGL绘制3D图形

改善python程序的91个建议

第一章:引言
第二章:编程惯用法
第三章:基础语法
第四章:库
第五章:设计模式
第六章:内部机制
第七章:使用工具辅助项目开发
第八章:性能剖析与优化

python_cookbook第三版

第一章:数据结构与算法
第二章:字符串与文本
第三章:数据日期和时间
第四章:迭代器和生成器
第五章:文件与IO
第六章:数据编码和处理
第七章:函数
第八章:类与对象
第九章:元编程
第十章:模块与包
第十一章:网络与web编程
第十二章:并发编程
第十三章:脚本编程与系统管理
第十四章:测试、调试与异常
第十五章:C语言扩展

python科学计算

第一章:软件包的安装和介绍
第二章:numpy快速处理数据
第三章:scipy数值计算库
第四章:sympy符号运算帮手
第五章:matplotlib绘制精美的图表
第六章:traits为python添加类型定义
第七章:traits_ui轻松制作用户界面
第八章:chaco交互式图表
第九章:TVTK数据的三维可视化
第十章:mayavi更方便的可视化
第十一章:VPython制作3D演示动画
第十二章:OpenCV图像处理和计算机视觉
第十三章:数据和文件
第十四章:数字信号系统
第十五章:频域信号处理
第十六章:用C语言提高计算效率
第十七章:自适应滤波器
第十八章:单摆和双摆模拟
第十九章:分形几何

python网络编程攻略

第一章:套接字、IPv4和简单的客户端/服务器编程
第二章:使用多路复用套接字IO提升性能
第三章:IPv6/unix域套接字和网络接口
第四章:HTTP协议网络编程
第五章:电子邮件协议、FTP和CGI编程
第六章:屏幕抓取和其他实用程序
第七章:跨设备编程
第八章:使用web服务 xml-rpc、soap、rest
第九章:网络监控和安全

python性能分析与优化

第一章:性能分析基础
第二章:性能分析器
第三章:可视化-利用GUI理解性能分析数据
第四章:优化每一个细节
第五章:多线程与多进程
第六章:常用优化方法
第七章:用numba、parakeet、pandas实现极速数据处理
第八章:付诸实践

python开发实战

第一章:开始使用python
第二章:开发web应用程序
第三章:准备团队开发环境
第四章:编写开发文档的准备
第五章:问题跟踪与审评
第六章:模块的分和设计与单元测试
第七章:打包与自动建立环境
第八章:使用mercurial管理源代码
第九章:使用Jenkins持续集成
第十章:自动构建和部署环境
第十一章:改善应用程序的性能
第十二章:Google APP engine
第十三章:测试时不可分割的一部分
第十四章:便捷的使用django
第十五章:使用方便的python模块

python自动化运维技术与最佳实践

第一章:系统基础信息模块详解
1.1、系统性能信息模块psutil

psutil是一个跨平台库,能够获取系统的运行进程和系统利用率(包括CPU、内存、磁盘、网络等)

1.1.1、获取系统性能信息
1.CPU信息:
  • User Time 执行用户进程的时间百分比
  • System Time 执行内核进程和中断的时间百分比
  • Wait IO 由于IO等待而使CPU处于idle(空闲)状态的时间百分比
  • Idle,CPU处于idle状态的时间百分比
import pustil
pustil.cpu_times()
pustil.cpu_times().user
pustil.cpu_count()
pustil.cpu_count(logical=False)#获得物理CPU个数
1.内存信息:
  • total(内存总数)
  • used (已使用的内存数)
  • free (空闲内存)
  • buffers(缓冲使用数)
  • cache(缓存使用)
  • swap (交换分区使用数)

分别使用psutil。virtaul_memory()和psutil.swap_memory()方法获取这些信息。

3.磁盘信息:
  • read_count:读IO数
  • write_count:写IO数
  • read_bytes:IO读字节数
  • write_bytes:IO写字节数
  • read_time:磁盘读数据
  • write_time:磁盘写时间
  • 利用率可使用:psutil.disk_usage()获取
  • IO信息科使用:psutil.disk_io_counters()获取
4.网络信息:
  • bytes_sent:发送字节数
  • bytes_recv:接收字节数
  • packets_sent:发送数据包数
  • packets_recv:接收数据包数

这些信息可以使用psutil.net_io_counters()获取

5.获取其他信息(开机时间、用户登录等):
  • psutil.users()
  • psutil.boot_time()
1.1.2、系统进程管理方法

获取当前系统的进程信息,包括进程的启动时间、查看或设置CPU亲和度、内存使用率、IO信息、socket连接、线程数等。

1.进程信息

psutil.pids()获取所有PID,使用psutil.Process()接收单个进程的PID,获取进程名、路径、状态、系统资源等。

2.popen类的使用

psutil提供了popen类的作用是获取用户启动的应用程序进程信息,以便跟踪进程的运行状态。

1.2、实用的IP地址处理模块IPy

IPy模块可以很好的辅助我们完成高效的规划工作。

1.3、DNS处理模块dnspython

dnspython支出几乎所有的记录类型,可以用于查询、传输并动态更新ZONE信息。

dnspython模块提供了大量的DNS处理方法,最常用的方法是域名查询、dnspython提供了一个DNS解析类-reslover,使用它的query方法来实现域名的查询功能。

DNS域名轮询业务监控:

#!/usr/bin/python

import dns.resolver
import os
import httplib

iplist=[]    #定义域名IP列表变量
appdomain="www.google.com.hk"    #定义业务域名

def get_iplist(domain=""):    #域名解析函数,解析成功IP将追加到iplist
    try:
        A = dns.resolver.query(domain, 'A')    #解析A记录类型
    except Exception,e:
        print "dns resolver error:"+str(e)
        return
    for i in A.response.answer:
        for j in i.items:
            iplist.append(j.address)    #追加到iplist
    return True

def checkip(ip):
    checkurl=ip+":80"
    getcontent=""
    httplib.socket.setdefaulttimeout(5)    #定义http连接超时时间(5秒)
    conn=httplib.HTTPConnection(checkurl)    #创建http连接对象

    try:
        conn.request("GET", "/",headers = {"Host": appdomain})  #发起URL请求,添加host主机头
        r=conn.getresponse()
        getcontent =r.read(15)   #获取URL页面前15个字符,以便做可用性校验
    finally:
        if getcontent=="<!doctype html>":  #监控URL页的内容一般是事先定义好,比如“HTTP200”等
            print ip+" [OK]"
        else:
            print ip+" [Error]"    #此处可放告警程序,可以是邮件、短信通知

if __name__=="__main__":
    if get_iplist(appdomain) and len(iplist)>0:    #条件:域名解析正确且至少要返回一个IP
        for ip in iplist:
            checkip(ip)
    else:
        print "dns resolver error."

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第二章:业务服务监控详解
2.1、文件内存差异对比方法

本章通过difflib模块实现内存差异对比。difflib作为python标准库模块无须安装,作用是对比文件之间的差异,且支持输出可读性的html文件。

与Linuxdiff命令类似:

#!/usr/bin/python
import difflib

text1 = """text1:
This module provides classes and functions for comparing sequences.
including HTML and context and unified diffs.
difflib document v7.4
add string
"""

text1_lines = text1.splitlines()

text2 = """text2:
This module provides classes and functions for Comparing sequences.
including HTML and context and unified diffs.
difflib document v7.5"""

text2_lines = text2.splitlines()

d = difflib.Differ()
diff = d.compare(text1_lines, text2_lines)
print '\n'.join(list(diff))

生成HTML:

d = difflib.HtmlDiff()
print d.make_file(text1_lines,text2_lines)
运行simple2.py > diff.html即可

示例:对比nginx配置文件的差异:

#!/usr/bin/python
import difflib
import sys

try:
    textfile1=sys.argv[1]
    textfile2=sys.argv[2]
except Exception,e:
    print "Error:"+str(e)
    print "Usage: simple3.py filename1 filename2"
    sys.exit()

def readfile(filename):
    try:
        fileHandle = open (filename, 'rb' )
        text=fileHandle.read().splitlines()
        fileHandle.close()
        return text
    except IOError as error:
       print('Read file Error:'+str(error))
       sys.exit()

if textfile1=="" or textfile2=="":
    print "Usage: simple3.py filename1 filename2"
    sys.exit()


text1_lines = readfile(textfile1)
text2_lines = readfile(textfile2)

d = difflib.HtmlDiff()
print d.make_file(text1_lines, text2_lines)
2.2、文件与目录差异对比方法

自带的filecmp模块就可以实现文件、目录、遍历子目录的差异对比功能。

filecmp提供了三个操作方法。分别是:cmp单文件对比、cmpfile多文件对比、dircmp目录对比。

import filecmp

a="/home/test/filecmp/dir1"
b="/home/test/filecmp/dir2"

dirobj=filecmp.dircmp(a,b,['test.py'])

print "-------------------report---------------------"
dirobj.report()
print "-------------report_partial_closure-----------"
dirobj.report_partial_closure()
print "-------------report_full_closure--------------"
dirobj.report_full_closure()

print "left_list:"+ str(dirobj.left_list)
print "right_list:"+ str(dirobj.right_list)
print "common:"+ str(dirobj.common)
print "left_only:"+ str(dirobj.left_only)
print "right_only:"+ str(dirobj.right_only)
print "common_dirs:"+ str(dirobj.common_dirs)
print "common_files:"+ str(dirobj.common_files)
print "common_funny:"+ str(dirobj.common_funny)
print "same_file:"+ str(dirobj.same_files)
print "diff_files:"+ str(dirobj.diff_files)
print "funny_files:"+ str(dirobj.funny_files)

示例2、校验源于备份目录差异:

#!/usr/bin/env python

import os, sys
import filecmp
import re
import shutil
holderlist=[]

def compareme(dir1, dir2):
    dircomp=filecmp.dircmp(dir1,dir2)
    only_in_one=dircomp.left_only
    diff_in_one=dircomp.diff_files
    dirpath=os.path.abspath(dir1)
    [holderlist.append(os.path.abspath( os.path.join(dir1,x) )) for x in only_in_one]
    [holderlist.append(os.path.abspath( os.path.join(dir1,x) )) for x in diff_in_one]
    if len(dircomp.common_dirs) > 0:
        for item in dircomp.common_dirs:
            compareme(os.path.abspath(os.path.join(dir1,item)), \
            os.path.abspath(os.path.join(dir2,item)))
        return holderlist

def main():
    if len(sys.argv) > 2:
        dir1=sys.argv[1]
        dir2=sys.argv[2]
    else:
        print "Usage: ", sys.argv[0], "datadir backupdir"
        sys.exit()

    source_files=compareme(dir1,dir2)
    dir1=os.path.abspath(dir1)

    if not dir2.endswith('/'): dir2=dir2+'/'
    dir2=os.path.abspath(dir2)
    destination_files=[]
    createdir_bool=False

    for item in source_files:
        destination_dir=re.sub(dir1, dir2, item)
        destination_files.append(destination_dir)
        if os.path.isdir(item):
            if not os.path.exists(destination_dir):
                os.makedirs(destination_dir)
                createdir_bool=True

    if createdir_bool:
        destination_files=[]
        source_files=[]
        source_files=compareme(dir1,dir2)
        for item in source_files:
            destination_dir=re.sub(dir1, dir2, item)
            destination_files.append(destination_dir)

    print "update item:"
    print source_files

    copy_pair=zip(source_files,destination_files)
    for item in copy_pair:
        if os.path.isfile(item[0]):
            shutil.copyfile(item[0], item[1])

if __name__ == '__main__':
    main()
2.3、发送电子邮件
import smtplib
import string

HOST = "smtp.gmail.com"
SUBJECT = "Test email from Python"
TO = "test@qq.com"
FROM = "test@gmail.com"
text = "Python rules them all!"
BODY = string.join((
        "From: %s" % FROM,
        "To: %s" % TO,
        "Subject: %s" % SUBJECT ,
        "",
        text
        ), "\r\n")
server = smtplib.SMTP()
server.connect(HOST,"25")
server.starttls()
server.login("test@gmail.com","123456")
server.sendmail(FROM, [TO], BODY)
server.quit()
#coding: utf-8
import smtplib
from email.mime.text import MIMEText

HOST = "smtp.gmail.com"
SUBJECT = u"官网流量数据报表"
TO = "test@qq.com"
FROM = "test@gmail.com"

msg = MIMEText("""
    <table width="800" border="0" cellspacing="0" cellpadding="4">
      <tr>
        <td bgcolor="#CECFAD" height="20" style="font-size:14px">*官网数据  <a href="monitor.domain.com">更多>></a></td>
      </tr>
      <tr>
        <td bgcolor="#EFEBDE" height="100" style="font-size:13px">
        1)日访问量:<font color=red>152433</font>  访问次数:23651 页面浏览量:45123 点击数:545122  数据流量:504Mb<br>
        2)状态码信息<br>
        &nbsp;&nbsp;500:105  404:3264  503:214<br>
        3)访客浏览器信息<br>
        &nbsp;&nbsp;IE:50%  firefox:10% chrome:30% other:10%<br>
        4)页面信息<br>
        &nbsp;&nbsp;/index.php 42153<br>
        &nbsp;&nbsp;/view.php 21451<br>
        &nbsp;&nbsp;/login.php 5112<br>
    </td>
      </tr>
    </table>""","html","utf-8")
msg['Subject'] = SUBJECT
msg['From']=FROM
msg['To']=TO
try:
    server = smtplib.SMTP()
    server.connect(HOST,"25")
    server.starttls()
    server.login("test@gmail.com","123456")
    server.sendmail(FROM, TO, msg.as_string())
    server.quit()
    print "邮件发送成功!"
except Exception, e:
    print "失败:"+str(e)
#coding: utf-8
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

HOST = "smtp.gmail.com"
SUBJECT = u"业务性能数据报表"
TO = "test@qq.com"
FROM = "test@gmail.com"

def addimg(src,imgid):
    fp = open(src, 'rb')
    msgImage = MIMEImage(fp.read())
    fp.close()
    msgImage.add_header('Content-ID', imgid)
    return msgImage

msg = MIMEMultipart('related')
msgtext = MIMEText("""
<table width="600" border="0" cellspacing="0" cellpadding="4">
      <tr bgcolor="#CECFAD" height="20" style="font-size:14px">
        <td colspan=2>*官网性能数据  <a href="monitor.domain.com">更多>></a></td>
      </tr>
      <tr bgcolor="#EFEBDE" height="100" style="font-size:13px">
        <td>
         <img src="cid:io"></td><td>
         <img src="cid:key_hit"></td>
      </tr>
      <tr bgcolor="#EFEBDE" height="100" style="font-size:13px">
         <td>
         <img src="cid:men"></td><td>
         <img src="cid:swap"></td>
      </tr>
    </table>""","html","utf-8")
msg.attach(msgtext)
msg.attach(addimg("img/bytes_io.png","io"))
msg.attach(addimg("img/myisam_key_hit.png","key_hit"))
msg.attach(addimg("img/os_mem.png","men"))
msg.attach(addimg("img/os_swap.png","swap"))

msg['Subject'] = SUBJECT
msg['From']=FROM
msg['To']=TO
try:
    server = smtplib.SMTP()
    server.connect(HOST,"25")
    server.starttls()
    server.login("test@gmail.com","123456")
    server.sendmail(FROM, TO, msg.as_string())
    server.quit()
    print "邮件发送成功!"
except Exception, e:
    print "失败:"+str(e)
#coding: utf-8
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

HOST = "smtp.gmail.com"
SUBJECT = u"官网业务服务质量周报"
TO = "test@qq.com"
FROM = "test@gmail.com"

def addimg(src,imgid):
    fp = open(src, 'rb')
    msgImage = MIMEImage(fp.read())
    fp.close()
    msgImage.add_header('Content-ID', imgid)
    return msgImage

msg = MIMEMultipart('related')
msgtext = MIMEText("<font color=red>官网业务周平均延时图表:<br><img src=\"cid:weekly\" border=\"1\"><br>详细内容见附件。</font>","html","utf-8")
msg.attach(msgtext)
msg.attach(addimg("img/weekly.png","weekly"))

attach = MIMEText(open("doc/week_report.xlsx", "rb").read(), "base64", "utf-8")
attach["Content-Type"] = "application/octet-stream"
#attach["Content-Disposition"] = "attachment; filename=\"业务服务质量周报(12周).xlsx\"".decode("utf-8").encode("gb18030")
msg.attach(attach)

msg['Subject'] = SUBJECT
msg['From']=FROM
msg['To']=TO
try:
    server = smtplib.SMTP()
    server.connect(HOST,"25")
    server.starttls()
    server.login("test@gmail.com","123456")
    server.sendmail(FROM, TO, msg.as_string())
    server.quit()
    print "邮件发送成功!"
except Exception, e:
    print "失败:"+str(e)
2.4、探测web服务质量方法

pycurl是一个用C语言编写的python实现、支持的操作协议有:ftp、http、https、telnet等,可以理解成Linux下curl命令功能的python封装。

# -*- coding: utf-8 -*-
import os,sys
import time
import sys
import pycurl

URL="http://www.google.com.hk"
c = pycurl.Curl()
c.setopt(pycurl.URL, URL)

#连接超时时间,5秒
c.setopt(pycurl.CONNECTTIMEOUT, 5)

#下载超时时间,5秒
c.setopt(pycurl.TIMEOUT, 5)
c.setopt(pycurl.FORBID_REUSE, 1)
c.setopt(pycurl.MAXREDIRS, 1)
c.setopt(pycurl.NOPROGRESS, 1)
c.setopt(pycurl.DNS_CACHE_TIMEOUT,30)
indexfile = open(os.path.dirname(os.path.realpath(__file__))+"/content.txt", "wb")
c.setopt(pycurl.WRITEHEADER, indexfile)
c.setopt(pycurl.WRITEDATA, indexfile)
try:
    c.perform()
except Exception,e:
    print "connecion error:"+str(e)
    indexfile.close()
    c.close()
    sys.exit()

NAMELOOKUP_TIME =  c.getinfo(c.NAMELOOKUP_TIME)
CONNECT_TIME =  c.getinfo(c.CONNECT_TIME)
PRETRANSFER_TIME =   c.getinfo(c.PRETRANSFER_TIME)
STARTTRANSFER_TIME = c.getinfo(c.STARTTRANSFER_TIME)
TOTAL_TIME = c.getinfo(c.TOTAL_TIME)
HTTP_CODE =  c.getinfo(c.HTTP_CODE)
SIZE_DOWNLOAD =  c.getinfo(c.SIZE_DOWNLOAD)
HEADER_SIZE = c.getinfo(c.HEADER_SIZE)
SPEED_DOWNLOAD=c.getinfo(c.SPEED_DOWNLOAD)

print "HTTP状态码:%s" %(HTTP_CODE)
print "DNS解析时间:%.2f ms"%(NAMELOOKUP_TIME*1000)
print "建立连接时间:%.2f ms" %(CONNECT_TIME*1000)
print "准备传输时间:%.2f ms" %(PRETRANSFER_TIME*1000)
print "传输开始时间:%.2f ms" %(STARTTRANSFER_TIME*1000)
print "传输结束总时间:%.2f ms" %(TOTAL_TIME*1000)

print "下载数据包大小:%d bytes/s" %(SIZE_DOWNLOAD)
print "HTTP头部大小:%d byte" %(HEADER_SIZE)
print "平均下载速度:%d bytes/s" %(SPEED_DOWNLOAD)

indexfile.close()
c.close()

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第三章:定制业务质量报表详解
3.1、数据报表excel操作模块wlsxWriter
#coding: utf-8
import xlsxwriter


# Create an new Excel file and add a worksheet.
workbook = xlsxwriter.Workbook('demo1.xlsx')
worksheet = workbook.add_worksheet()

# Widen the first column to make the text clearer.
worksheet.set_column('A:A', 20)

# Add a bold format to use to highlight cells.
#bold = workbook.add_format({'bold': True})
bold = workbook.add_format()
bold.set_bold()

# Write some simple text.
worksheet.write('A1', 'Hello')

# Text with formatting.
worksheet.write('A2', 'World', bold)

worksheet.write('B2', u'中文测试', bold)

# Write some numbers, with row/column notation.
worksheet.write(2, 0, 32)
worksheet.write(3, 0, 35.5)
worksheet.write(4, 0, '=SUM(A3:A4)')

# Insert an image.
worksheet.insert_image('B5', 'img/python-logo.png')

workbook.close()
3.1.1、模块常用方法说明
  1. workbook类

    add_workshee方法,添加一个新的工作表。

    add_format():在工作表中创建一个新的格式对象来格式化单元格。

  2. worksheet类

    worksheet类代表了一个excel工作表。是最核心的一个类

  3. char类

    char类实现在xlsxWriter模块中图表组件的基类,支持的图表类型包括面积、条形图、柱状图、折线图、饼图、散点图、股票和雷达等。

    • area:创建一个面积样式的图表
    • bar:创建一个条形图
    • column:创建一个柱形图
    • line:创建一个线条图
    • pie:创建一个饼图
    • scatter:创建一个散点图
    • stock:创建一个股票图
    • radar:创建一个雷达图
3.1.2、实践:定制自动化业务流量报表图
#coding: utf-8
import xlsxwriter

workbook = xlsxwriter.Workbook('chart.xlsx')
worksheet = workbook.add_worksheet()

chart = workbook.add_chart({'type': 'column'})

title = [u'业务名称',u'星期一',u'星期二',u'星期三',u'星期四',u'星期五',u'星期六',u'星期日',u'平均流量']
buname= [u'业务官网',u'新闻中心',u'购物频道',u'体育频道',u'亲子频道']

data = [
    [150,152,158,149,155,145,148],
    [89,88,95,93,98,100,99],
    [201,200,198,175,170,198,195],
    [75,77,78,78,74,70,79],
    [88,85,87,90,93,88,84],
]
format=workbook.add_format()
format.set_border(1)

format_title=workbook.add_format()
format_title.set_border(1)
format_title.set_bg_color('#cccccc')
format_title.set_align('center')
format_title.set_bold()

format_ave=workbook.add_format()
format_ave.set_border(1)
format_ave.set_num_format('0.00')

worksheet.write_row('A1',title,format_title)
worksheet.write_column('A2', buname,format)
worksheet.write_row('B2', data[0],format)
worksheet.write_row('B3', data[1],format)
worksheet.write_row('B4', data[2],format)
worksheet.write_row('B5', data[3],format)
worksheet.write_row('B6', data[4],format)

def chart_series(cur_row):
    worksheet.write_formula('I'+cur_row, \
     '=AVERAGE(B'+cur_row+':H'+cur_row+')',format_ave)
    chart.add_series({
        'categories': '=Sheet1!$B$1:$H$1',
        'values':     '=Sheet1!$B$'+cur_row+':$H$'+cur_row,
        'line':       {'color': 'black'},
        'name': '=Sheet1!$A$'+cur_row,
    })

for row in range(2, 7):
    chart_series(str(row))

#chart.set_table()
#chart.set_style(30)
chart.set_size({'width': 577, 'height': 287})
chart.set_title ({'name': u'业务流量周报图表'})
chart.set_y_axis({'name': 'Mb/s'})

worksheet.insert_chart('A8', chart)
workbook.close()
3.2、Python与rrdtool的结合模块

rrdtool工具最为环状数据库的存储格式, rrdtool robin是一种处理定量数据以及当前元素指针的技术。rrdtool主要用来跟踪对象的变化情况,生成这些变化的走势图。比如业务的访问流量、系统性能、磁盘利用率等趋势图,很多流行监控平台都是用到例如rrdtool。

3.2.1、rrdtool模块常用方法
  1. create方法:创建一个后缀为rrd的rrdtool数据库。
  2. update方法:存储一个新值到rrdtool数据库。
  3. graph方法:根据指定的rrdtool数据库进行绘图
  4. fetch方法:根据指定的rrdtool数据库进行查询
3.3.2、实践:实现网卡流量图表绘制

create.py:

# -*- coding: utf-8 -*-
#!/usr/bin/python
import rrdtool
import time

cur_time=str(int(time.time()))
rrd=rrdtool.create('Flow.rrd','--step','300','--start',cur_time,
        'DS:eth0_in:COUNTER:600:0:U',
        'DS:eth0_out:COUNTER:600:0:U',
        'RRA:AVERAGE:0.5:1:600',
    'RRA:AVERAGE:0.5:6:700',
    'RRA:AVERAGE:0.5:24:775',
    'RRA:AVERAGE:0.5:288:797',
    'RRA:MAX:0.5:1:600',
    'RRA:MAX:0.5:6:700',
    'RRA:MAX:0.5:24:775',
    'RRA:MAX:0.5:444:797',
    'RRA:MIN:0.5:1:600',
    'RRA:MIN:0.5:6:700',
    'RRA:MIN:0.5:24:775',
    'RRA:MIN:0.5:444:797')
if rrd:
    print rrdtool.error()

update.py:

# -*- coding: utf-8 -*-
#!/usr/bin/python
import rrdtool
import time,psutil

total_input_traffic = psutil.net_io_counters()[1]
total_output_traffic = psutil.net_io_counters()[0]
starttime=int(time.time())

update=rrdtool.updatev('/home/test/rrdtool/Flow.rrd','%s:%s:%s' % (str(starttime),str(total_input_traffic),str(total_output_traffic)))
print update

graph.py:

# -*- coding: utf-8 -*-
#!/usr/bin/python
import rrdtool
import time

title="Server network  traffic flow ("+time.strftime('%Y-%m-%d',time.localtime(time.time()))+")"
rrdtool.graph( "Flow.png", "--start", "-1d","--vertical-label=Bytes/s","--x-grid","MINUTE:12:HOUR:1:HOUR:1:0:%H",\
 "--width","650","--height","230","--title",title,
 "DEF:inoctets=Flow.rrd:eth0_in:AVERAGE",
 "DEF:outoctets=Flow.rrd:eth0_out:AVERAGE",
 "CDEF:total=inoctets,outoctets,+",
 "LINE1:total#FF8833:Total traffic",
 "AREA:inoctets#00FF00:In traffic",
 "LINE1:outoctets#0000FF:Out traffic",
 "HRULE:6144#FF0000:Alarm value\\r",
 "CDEF:inbits=inoctets,8,*",
 "CDEF:outbits=outoctets,8,*",
 "COMMENT:\\r",
 "COMMENT:\\r",
 "GPRINT:inbits:AVERAGE:Avg In traffic\: %6.2lf %Sbps",
 "COMMENT:   ",
 "GPRINT:inbits:MAX:Max In traffic\: %6.2lf %Sbps",
 "COMMENT:  ",
 "GPRINT:inbits:MIN:MIN In traffic\: %6.2lf %Sbps\\r",
 "COMMENT: ",
 "GPRINT:outbits:AVERAGE:Avg Out traffic\: %6.2lf %Sbps",
 "COMMENT: ",
 "GPRINT:outbits:MAX:Max Out traffic\: %6.2lf %Sbps",
 "COMMENT: ",
 "GPRINT:outbits:MIN:MIN Out traffic\: %6.2lf %Sbps\\r")
3.3、生成动态路由轨迹图scapy模块

scapy可能怼数据包进行伪造或解包,包括发送数据包、包嗅探、应答和反馈匹配等功能。可以用在处理网络扫描、路由跟踪、服务探测、单元测试等方面。

3.3.1、模块常用方法

scapy模块提供了众多网络数据包操作方法。包括发包send()、SYN/ACK扫描、嗅探、sniff()、抓包wrpcap()、TCP路由跟踪traceroute()等

3.3.2、实践:实现TCP探测目标服务路由轨迹
# -*- coding: utf-8 -*-
import os,sys,time,subprocess
import warnings,logging
warnings.filterwarnings("ignore", category=DeprecationWarning)
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import traceroute
domains = raw_input('Please input one or more IP/domain: ')
target =  domains.split(' ')
dport = [80]
if len(target) >= 1 and target[0]!='':
    res,unans = traceroute(target,dport=dport,retry=-2)
    res.graph(target="> test.svg")
    time.sleep(1)
    subprocess.Popen("/usr/bin/convert test.svg test.png", shell=True)
else:
    print "IP/domain number of errors,exit"

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第四章:python与系统安全
4.1、构建集中式的病毒扫描机制

clam antivirus 是一款免费而且开放源代码的防毒软件。pyClamd是python第三方库,可以让python直接使用clamAV病毒扫描守护进程clamd来实现一个高效的病毒检测功能。

描述略

4.1.2、实践:实现集中式病毒扫描
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import pyclamd
from threading import Thread

class Scan(Thread):

    def __init__ (self,IP,scan_type,file):
        """构造方法"""
        Thread.__init__(self)
        self.IP = IP
        self.scan_type=scan_type
        self.file = file
        self.connstr=""
        self.scanresult=""


    def run(self):
        """多进程run方法"""

        try:
            cd = pyclamd.ClamdNetworkSocket(self.IP,3310)
            if cd.ping():
                self.connstr=self.IP+" connection [OK]"
                cd.reload()
                if self.scan_type=="contscan_file":
                    self.scanresult="{0}\n".format(cd.contscan_file(self.file))
                elif self.scan_type=="multiscan_file":
                    self.scanresult="{0}\n".format(cd.multiscan_file(self.file))
                elif self.scan_type=="scan_file":
                    self.scanresult="{0}\n".format(cd.scan_file(self.file))
                time.sleep(1)
            else:
                self.connstr=self.IP+" ping error,exit"
                return
        except Exception,e:
            self.connstr=self.IP+" "+str(e)


IPs=['192.168.1.21','192.168.1.22']
scantype="multiscan_file"
scanfile="/data/www"
i=1
threadnum=2
scanlist = []

for ip in IPs:

    currp = Scan(ip,scantype,scanfile)
    scanlist.append(currp)

    if i%threadnum==0 or i==len(IPs):
        for task in scanlist:
            task.start()

        for task in scanlist:
            task.join()
            print task.connstr
            print task.scanresult
        scanlist = []
    i+=1
4.2、实现高效的端口扫描器python-nmap模块
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import nmap

scan_row=[]
input_data = raw_input('Please input hosts and port: ')
scan_row = input_data.split(" ")
if len(scan_row)!=2:
    print "Input errors,example \"192.168.1.0/24 80,443,22\""
    sys.exit(0)
hosts=scan_row[0]    #接收用户输入的主机
port=scan_row[1]    #接收用户输入的端口

try:
    nm = nmap.PortScanner()    #创建端口扫描对象
except nmap.PortScannerError:
    print('Nmap not found', sys.exc_info()[0])
    sys.exit(0)
except:
    print("Unexpected error:", sys.exc_info()[0])
    sys.exit(0)

try:
    nm.scan(hosts=hosts, arguments=' -v -sS -p '+port)    #调用扫描方法,参数指定扫描主机hosts,nmap扫描命令行参数arguments
except Exception,e:
    print "Scan erro:"+str(e)

for host in nm.all_hosts():    #遍历扫描主机
    print('----------------------------------------------------')
    print('Host : %s (%s)' % (host, nm[host].hostname()))    #输出主机及主机名
    print('State : %s' % nm[host].state())    #输出主机状态,如up、down

    for proto in nm[host].all_protocols():    #遍历扫描协议,如tcp、udp
        print('----------')
        print('Protocol : %s' % proto)    #输入协议名

        lport = nm[host][proto].keys()    #获取协议的所有扫描端口
        lport.sort()    #端口列表排序
        for port in lport:    #遍历端口及输出端口与状态
            print('port : %s\tstate : %s' % (port, nm[host][proto][port]['state']))

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第五章:系统批量运维管理器pexpeet详解

pexpect可以理解成Linux下的expect的python封装。可以实现对ssh、ftp、passwd、telnet等命令进行自动化交互。

5.1、安装
pip install pexpect

一个简单的ssh登录:

import pexpect
child = pexpect.spawn('scp foo user@example.com')
child.expect('Password:')
child.sendline(passwd)
5.2、pexpect的核心组件

5.2.1、spawn类

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第六章:系统批量运维管理器paramiko详解
第七章:系统批量运维管理器fabric详解
第八章:从零开发一个轻量级webserver
第九章:集中化管理平台ansible详解
第十章:集中化管理平台saltstack详解
第十一章:统一网络控制器func详解
第十二章:python大数据应用详解
第十三章:从零打造bs自动化运维平台
第十四章:打造Linux系统安全审计功能
第十五章:构建分布式质量监控平台
第十六章:构建桌面版cs自动化运维平台

numpy学习指南(数据分析基础教程)

第一章:numpy快速入门

完成时间:2018-11-07

安装:

pip install numpy

标量:普通的数

矩阵:多维数组

向量(一维数组)加法:

python代码实现:

def pythonsum(n):
    a = range(n)
    b = range(n)
    c = []

    for i in range(len(a)):
        a[i] = i ** 2
        b[i] = i ** 3
        c.append(a[i]+b[i])

    return c

numpy实现:

def numpysum(n):

    a = numpy.arange(n) ** 2
    b = numpy.arange(n) ** 3

    c = a +b

    return c

numpy等价的代码比纯python的运行速度要快得多。

第二章:numpy基础

完成时间:2018-11-07

本章涵盖知识:
  • 数据类型
  • 数组类型
  • 类型转换
  • 创建数组
  • 数组切片
  • 改变维度
numpy中的ndarray是一个多维数组对象,该对象由两部分组成:
  • 实际的数据
  • 描述这些数据的元数据

创建多维数组:

array([arange(2),arange(2)])

ndarray对象的维度属性是以 元祖 python tuple对象存储的。

numpy数据类型:
  • bool :布尔
  • inti :平台决定精度的整数
  • int8 :整数
  • int16 :整数
  • int32 :整数
  • int64 :整数
  • uint8 :无符号整数
  • uint16 :无符号整数
  • uint32 :无符号整数
  • uint64 :无符号整数
  • float16 :半精度浮点数
  • float32 :单精度浮点数
  • float64或float:双精度浮点数
  • complex64 :复数
  • complex128或complex :复数

数据类型对象时numpy.dtype类的实例。numpy数组中的每一个元素均为相同的数据类型。 数据类型对象可以给出单个数组元素在内存中占用的字节数。即dtype类的itemsize属性:

a.dtype.itemsize
字符编码:
  • i:整数
  • u:无符号整数
  • f:单精度浮点数
  • d:双精度浮点数
  • b:布尔值
  • D:复数
  • S:字符串
  • U:unicode字符串
  • V:void空

创建单精度浮点数数组:

arange(7,dtype='f')

获取数据类型的字符编码:

t = dtype('float64')
t.char
>>d
#对应的数据类型
t.type

一维数组切片:

a = arange(9)
a[3:7]
#下标0-7,步长为2
a[:7:2]

创建一个数组并改变其维度:

arange(24).reshape(2,3,4)

获取维度:

b.shape

多维数组的切片:可用逗号隔开切片

展平维度:转换为一维数组:

#1.ravel
b.ravel()

#2.flatten,与上面ravel不同的是,这里会请求内存来保存结果,上面只是返回数组的一个视图。
b.flatten()

#3.用元祖设置维度
b.shape = (6,4)

#4.transpose 在线性代数中,转置矩阵是很常见的操作。
b.transpose()

#5.resize和reshape函数功能一样,但是resize会直接修改所操作的数组
b.resize((2,12))

组合数组:

hstack()

concatenate()

#垂直组合:

vstack()
#concatenate的axis设置为0也是垂直组合。
concatenate((a,b),axis=0)

#深度组合
dstack()

#列组合
column_stack((a,b))

#行组合

row_stack()

分割数组:

#1.水平分割
hsplit()

#2.垂直分割
vsplit()

#3.深度分割
dsplit()
数组属性:
  • ndim属性:给出数组的未读 数轴的个数
  • size属性:数组元素的总个数
  • itemsize:给出数组中的元素在内存中所占的字节数
  • nbytes:数组所占的存储空间
  • T属性:和transpose函数一样
  • 对于一维数组,T属性就是原数组
  • 复数的虚部是用j表示
  • real属性给出复数数组的实部
  • imag属性给出复数数组的虚部

numpy数组转换python数组:

tolist()
第三章:常用函数

完成时间:2018-11-07

创建方阵:

np.eye(2)

读取csv文件:

c,v = np.loadtext('data.csv',delimiter=',',usecols=(6,7),unpack=True)
#读取文件设置分隔符为逗号,usecols参数设置为一个元祖

需要理解加权平均的含义

加权 平均价格:

average(c,weights=v)

算数平均值:

np.mean(c)

时间加权平均值:

t = np.arange(len(c))
np.average(c,weights=t)

找到最大、最小值:

np.max(c)
np.min(c)

取值范围,最大最小的差值(极差):

np.ptp(c)

中位数:

np.median(c)

#排序:
np.msort()

需要理解 方差 意思:

方差 是指各个数据与所有数据算数平均数的离差平方和除以数据个数所得到的值

计算 方差

np.var(c)

计算标准差:

np.std()

结果对比:

np.diff(np.log(c))

创建包含5个元素的数组:

np.zeros(5)
np.onew(N)
#创建一个长度为iN的元素均初始化为1的数组

exp函数计算数组每个元素的指数:

np.exp(x)

理解 布林带

数组的修建和压缩:

chip方法返回一个修建过的数组

compress方法返回一个根据给定条件筛选后的数组

计算乘积:

prod方法计算数组中所有元素的乘积

第四章:便捷函数

完成时间:2018-11-07

使用 cov 函数计算协方差矩阵

使用 diagonal 函数查看对角线上的元素

使用 trace 函数计算矩阵的迹,即对角线行元素之和。

使用 conrrcoef 函数计算相关系数(或者更精确地,相关系数的矩阵)

numpy中的 ployfit 函数可以用多项式去拟合一系列数据点,无论这些数据点数是否来自连续函数都适用。

用上面的ployfit得到的多项式对象以及 polyval 函数,就可以推断出下一个值

使用 roots 函数找出拟合的多项式函数什么时候到达0值。

使用 polyder 函数对多项式函数求导。

使用 argmaxargmin 找出最大值点和最小值点

使用 vectorize 函数可以减少程序中使用循环的次数

numpy中的vectorize函数相当于python中的map函数

要避免使用循环的技巧。

hanning 函数式是一个加权余弦的窗函数。用于平滑处理。

isreal 函数判断数组元素是否为实数

select 函数可以根据一组给定的条件,从一组元素中挑选符合条件的元素并返回

trim_zeros 函数可以去掉一维数组中开头和末尾为0的元素。

第五章:矩阵和通用函数

矩阵作为一种重要的数学概念,在numpy中也有专门的表示方法,通过函数可以逐个处理数组中的元素, 也可以直接处理标量。通过函数的输入是一组标量,输出也是一组标量,他们通常可以对应于基本数学运算、

本章涵盖内容:
  • 矩阵创建
  • 矩阵运算
  • 基本通用函数
  • 三角函数
  • 位运算函数
  • 比较函数

在numpy中,矩阵是ndarray的子类,可以由专门的字符串格式来创建。 与数学概念中的矩阵一样,NumPy中的矩阵也是二维的。

我们可以使用mat、matrix、bmat函数来创建矩阵。

mat函数创建矩阵时,若输入已为matrix或者ndarray对象,则不会为他们创建副本。 因此,调用mat函数和调用matrix(data,copy=False)等价。

使用字符串调用mat函数创建矩阵:

np.mat('1 2 3; 4 5 6; 7 8 9')

T属性获得转置矩阵

I属性获得逆矩阵

除了字符串创建矩阵意外,我们还可以使用NumPy数组进行创建:

np.mat(np.arange(9).reshape(3,3))

有时候我们希望利用一些已有的较小的矩阵来创建一个新的大矩阵,这可以用bmat函数来实现。 这里的b表示分块,bmat即分块矩阵。

创建通用函数

我们可以使用NumPy职工的 frompyfunc 函数,通过一个Python函数来创建通用函数。

通用函数方法?

通用函数有四个方法,不过这些方法只对输入两个参数、输出一个参数的func对象有效,例如add函数。
  • reduce
  • accumulate
  • reduceat
  • outer

在add上调用通用函数的方法:

np.add.reduce(a)

除法运算:

np.divide(a,b)

模运算:

计算模数或者余数,可以使用NumPy中的mod、remainder和fmod函数。当然也可以使用%运算符。 这些函数的主要差异在于处理负数的方式。fmod函数在这方面异于其他函数。

斐波那契数列:

斐波那契数列是基于递推关系生成的,直接用NumPy代码来解释递推关系是比较麻烦的。 不过我们可以用矩阵的形式或者黄金分割公式来解释它 。因此我们将介绍matrix和rint函数。 使用matrix函数创建矩阵,rint函数对浮点数取整,但结果仍为浮点数类型。

利萨茹曲线:

在NumPy中吗,所有的标准三角函数如sin、cos、tan等均有对应的通用函数。 利萨茹曲线是一种很有趣的使用三角函数的方式。

由以下参数方程定义:

x = A sin(at + n/2)
y = B sin(bt)

方波

方波也是一种可以在显波器上显示的波形。方波可以近似表示多个正弦波的叠加。 事实上,任意一个防波信号都可以用无穷傅里叶级数来表示。

第六章:深入学习numpy模块
第七章:专用函数
第八章:质量控制
第九章:使用matplotlib绘图
第十章:numpy的扩展:scipy
第十一章:玩转pygame

python计算机视觉编程

前言

今天,图像和视频无处不在,在线照片分享网站和社交网络上的图像有数十亿之多。几乎对于任意可能的查询图像,搜索引擎都会给用户返回检索的图像。实际上,几乎所有手机和计算机都有内置的摄像头,所以在人们的设备中,有几G的图像和视频是一件很寻常的事。

计视计算机视觉就是用计算机编程,并设计算法来理解在这些图像中有什么。计算机觉的有力应用有图像搜索、机器人导航、医学图像分析、照片管理等。

本书旨在为计算机视觉实战提供一个简单的切入点,让学生、研究者和爱好者充分理解其基础理论和算法。本书中的编程语言是 Python,Python自带了很多可以免费获取的强大而便捷的图像处理、数学计算和数据挖掘模块,可以免费获取。

写作本书的时候,我遵循了以下原则。
  • 鼓励探究式学习,让读者在阅读本书的时候,在计算机上跟着书中示例进行练习。
  • 推广和使用免费且开源的软件,设立较低的学习门槛。显然,我们选择了Python
  • 保持内容完整性和独立性。本书没有介绍计算机视觉的全部内容,而是完整呈现并解释所有代码。你应该能够重现这些示例,并可以直接在它们之上构建其他应用。
  • 内容追求广泛而非详细,且相对于理论更注重鼓舞和激励。

总之,如果你对计算机视觉编程感兴趣,希望它能给你带来启发。

先决条件和概述

本书主要针对各种应用和问题探讨理论及算法,下面简单概括一下。

读者须知:
  • 基本的编程经验。你需要会使用编辑器,能够运行脚本,知道如何构建代码以及基本数据类型。熟悉 Python Matlab等其他脚本语言,这也会对你理解本书有所帮助。
  • 数学基础。如果你知道矩阵、向量、矩阵乘法、标准数学函数以及导数和梯度等概念,这对于充分利用其中示例非常有益。对于一些较高级的数学例子,你可以轻松跳过。
本书内容:
  • 用 Python对图像进行实战编程。
  • 现实世界中各种应用背后的计算机视觉技术。
  • 一些基本算法,以及如何实现并应用这些算法。

本书中的代码示例会向你展示物体识别、基于内容的图像检索、图像搜索、光学字符识别、光流、跟踪、三维重建、立体成像、增强现实、姿态估计、全景创建、图像分割、降噪、图像分组等内容。

第一章:基本的图像操作和处理
需要理解:
  • 1.3.4:直方图均衡化
  • 1.3.5:图像平均

本章讲解操作和处理图像的基础知识,将通过大量示例介绍处理图像所需的 Python工具包,并介绍用于读取图像、图像转换和缩放、计算导数、画图和保存结果等的基本工具。这些工具的使用将贯穿本书的剩余章节。

1.1、PIL: Python图像处理类库

PIL(Python Imaging Library Python,图像处理类库)提供了通用的图像处理功能,以及大量有用的基本图像操作,比如图像缩放、裁剪、旋转、颜色转换等。

利用 PIL 中的函数,我们可以从大多数图像格式的文件中读取数据,然后写入最常见的图像格式文件中。 PIL 中最重要的模块为 Image。要读取一幅图像,可以使用:

from PIL  import Image
pil_im = Image.open('../img/1.png')

上述代码的返回值 pil_im 是一个 PIL 图像对象。

图像的颜色转换可以使用 convert() 方法来实现。要读取一幅图像,并将其转换成 灰度图像,只需要加上 convert('L'),如下所示:

pil_im = Image.open('empire.jpg').convert('L')

在 PIL 文档中有一些例子,参见 http://www.pythonware.com/library/pil/handbook/index.htm

1.1.1、转换图像格式

通过 save() 方法, PIL 可以将图像保存成多种格式的文件。下面的例子从文件名列 表(filelist) 中读取所有的图像文件,并转换成 JPEG 格式:

from PIL import Image
import os
for infile in filelist:
    outfile = os.path.splitext(infile)[0] + ".jpg"
if infile != outfile:
    try:
        Image.open(infile).save(outfile)
    except IOError:
        print("cannot convert", infile)

PIL 的 open() 函数用于创建 PIL 图像对象, save() 方法用于保存图像到具有指定文件名的文件。除了后缀变为“.jpg”,上述代码的新文件名和原文件名相同。 PIL 是个足够智能的类库,可以根据文件扩展名来判定图像的格式。 PIL 函数会进行简单的检查,如果文件不是 JPEG 格式,会自动将其转换成 JPEG 格式;如果转换失败,它会在控制台输出一条报告失败的消息。

本书会处理大量图像列表。下面将创建一个包含文件夹中所有图像文件的文件名列 表。首先新建一个文件,命名为 imtools.py,来存储一些经常使用的图像操作,然 后将下面的函数添加进去:

import os
def get_imlist(path):
    """ 返回目录中所有 JPG 图像的文件名列表 """
    return [os.path.join(path,f) for f in os.listdir(path) if f.endswith('.jpg')]

现在,回到 PIL。

1.1.2、创建缩略图

使用 PIL 可以很方便地创建图像的缩略图。 thumbnail() 方法接受一个元组参数(该 参数指定生成缩略图的大小),然后将图像转换成符合元组参数指定大小的缩略图。 例如,创建最长边为 128 像素的缩略图,可以使用下列命令:

pil_im.thumbnail((128,128))
1.1.3、复制和粘贴图像区域

使用 crop() 方法可以从一幅图像中裁剪指定区域:

box = (100,100,400,400)
region = pil_im.crop(box)

该区域使用四元组来指定。四元组的坐标依次是(左,上,右,下)。 PIL 中指定 坐标系的左上角坐标为(0, 0)。我们可以旋转上面代码中获取的区域,然后使用 paste() 方法将该区域放回去,具体实现如下:

region = region.transpose(Image.ROTATE_180)
pil_im.paste(region,box)
1.1.4、调整尺寸和旋转

要调整一幅图像的尺寸,我们可以调用 resize() 方法。该方法的参数是一个元组, 用来指定新图像的大小:

out = pil_im.resize((128,128))

要旋转一幅图像,可以使用逆时针方式表示旋转角度,然后调用 rotate() 方法:

out = pil_im.rotate(45)

上述例子的输出结果如图 1-1 所示。最左端是原始图像,然后是灰度图像、粘贴有 旋转后裁剪图像的原始图像,最后是缩略图。

1.2、Matplotlib

2018-10-02mac运行报错,经过Google搜索,得到解决需要在前面输入这两行代码设置一下:

import matplotlib
matplotlib.use('TkAgg')
from PIL import Image
from pylab import *

我们处理数学运算、绘制图表,或者在图像上绘制点、直线和曲线时, Matplotlib是个很好的类库,具有比 PIL 更强大的绘图功能。 Matplotlib 可以绘制出高质量的图表,就像本书中的许多插图一样。 Matplotlib 中的 PyLab 接口包含很多方便用户创建图像的函数。 Matplotlib 是开源工具,可以从 http://matplotlib.sourceforge.net/免费下载。该链接中包含非常详尽的使用说明和教程。下面的例子展示了本书中需要使用的大部分函数。

1.2.1、绘制图像、点和线

尽管 Matplotlib 可以绘制出较好的条形图、饼状图、散点图等,但是对于大多数计算机视觉应用来说,仅仅需要用到几个绘图命令。最重要的是,我们想用点和线来表示一些事物,比如兴趣点、对应点以及检测出的物体。下面是用几个点和一条线绘制图像的例子:

from PIL import Image
from pylab import *
# 读取图像到数组中
im = array(Image.open('empire.jpg'))
# 绘制图像
imshow(im)
# 一些点
x = [100,100,400,400]
y = [200,500,200,500]
# 使用红色星状标记绘制点
plot(x,y,'r*')
# 绘制连接前两个点的线
plot(x[:2],y[:2])
# 添加标题,显示绘制的图像
title('Plotting: "empire.jpg"')
show()

上面的代码首先绘制出原始图像,然后在 x 和 y 列表中给定点的 x 坐标和 y 坐标上绘制出红色星状标记点,最后在两个列表表示的前两个点之间绘制一条线段(默认为蓝色)。该例子的绘制结果如图 1-2 所示。 show() 命令首先打开图形用户界面(GUI),然后新建一个图像窗口。该图形用户界面会循环阻断脚本,然后暂停,直到最后一个图像窗口关闭。在每个脚本里,你只能调用一次 show() 命令,而且通常是在脚本的结尾调用。注意,在 PyLab 库中,我们约定图像的左上角为坐标原点。图像的坐标轴是一个很有用的调试工具;但是,如果你想绘制出较美观的图像,加 上下列命令可以使坐标轴不显示:

axis('off')

在绘图时,有很多选项可以控制图像的颜色和样式。最有用的一些短命令如表 1-1、 表 1-2 和表 1-3 所示。使用方法见下面的例子:

plot(x,y) # 默认为蓝色实线
plot(x,y,'r*') # 红色星状标记
plot(x,y,'go-') # 带有圆圈标记的绿线
plot(x,y,'ks:') # 带有正方形标记的黑色虚线

用PyLab库绘图的基本颜色格式命令

颜色:
  • 'b' 蓝色
  • 'g' 绿色
  • 'r' 红色
  • 'c' 青色
  • 'm' 品红
  • 'y' 黄色
  • 'k' 黑色
  • 'w' 白色

用PyLab库绘图的基本线型格式命令

线型:
  • '-' 实线
  • '--' 虚线
  • ':' 点线

用PyLab库绘图的基本绘制标记格式命令

标记:
  • '.' 点
  • 'o' 圆圈
  • 's' 正方形
  • '*' 星形
  • '+' 加号
  • 'x' 叉号
1.2.2、图像轮廓和直方图

下面来看两个特别的绘图示例:图像的轮廓和直方图。绘制图像的轮廓(或者其他二维函数的等轮廓线)在工作中非常有用。因为绘制轮廓需要对每个坐标 [x, y] 的像素值施加同一个阈值,所以首先需要将图像灰度化:

from PIL import Image
from pylab import *
# 读取图像到数组中
im = array(Image.open('empire.jpg').convert('L'))
# 新建一个图像
figure()
# 不使用颜色信息
gray()
# 在原点的左上角显示轮廓图像
contour(im, origin='image')
axis('equal')
axis('off')

像之前的例子一样,这里用 PIL 的 convert() 方法将图像转换成灰度图像。

图像的直方图用来表征该图像像素值的分布情况。用一定数目的小区间(bin)来 指定表征像素值的范围,每个小区间会得到落入该小区间表示范围的像素数目。该 (灰度)图像的直方图可以使用 hist() 函数绘制:

figure()
hist(im.flatten(),128)
show()

hist() 函数的第二个参数指定小区间的数目。需要注意的是,因为 hist() 只接受一维数组作为输入,所以我们在绘制图像直方图之前,必须先对图像进行压平处理。flatten() 方法将任意数组按照行优先准则转换成一维数组。

1.2.3、交互式标注

有时用户需要和某些应用交互,例如在一幅图像中标记一些点,或者标注一些训练数据。 PyLab 库中的 ginput() 函数就可以实现交互式标注。下面是一个简短的例子:

import matplotlib
matplotlib.use('TkAgg')

from PIL import Image
from pylab import *
im = array(Image.open('../../images/img1.jpg'))
imshow(im)
print('Please click 3 points')
x = ginput(3)
print('you clicked:',x)
show()

上面的脚本首先绘制一幅图像,然后等待用户在绘图窗口的图像区域点击三次。程序将这些点击的坐标 [x, y] 自动保存在 x 列表里。

1.3、Numpy

NumPy(http://www.scipy.org/NumPy/)是非常有名的 Python 科学计算工具包,其中包含了大量有用的思想,比如数组对象(用来表示向量、矩阵、图像等)以及线性代数函数。 NumPy 中的数组对象几乎贯穿用于本书的所有例子中 1 数组对象可以帮助你实现数组中重要的操作,比如矩阵乘积、转置、解方程系统、向量乘积和归一化,这为图像变形、对变化进行建模、图像分类、图像聚类等提供了基础。

NumPy 可以从 http://www.scipy.org/Download 免费下载,在线说明文档(http://docs.scipy.org/doc/numpy/)包含了你可能遇到的大多数问题的答案。关于 NumPy 的更多内容,请参考开源书籍 [24]。

1.3.1、图像数组表示

在先前的例子中,当载入图像时,我们通过调用 array() 方法将图像转换成 NumPy 的数组对象,但当时并没有进行详细介绍。 NumPy 中的数组对象是多维的,可以用来表示向量、矩阵和图像。一个数组对象很像一个列表(或者是列表的列表),但是数组中所有的元素必须具有相同的数据类型。除非创建数组对象时指定数据类型,否则数据类型会按照数据的类型自动确定。

对于图像数据,下面的例子阐述了这一点:

im = array(Image.open('empire.jpg'))
print(im.shape, im.dtype)
im = array(Image.open('empire.jpg').convert('L'),'f')
print(im.shape, im.dtype)

控制台输出结果如下所示:

(800, 569, 3) uint8
(800, 569) float32

每行的第一个元组表示图像数组的大小(行、列、颜色通道),紧接着的字符串表示数组元素的数据类型。因为图像通常被编码成无符号八位整数(uint8),所以在第一种情况下,载入图像并将其转换到数组中,数组的数据类型为“uint8”。在第二种情况下,对图像进行灰度化处理,并且在创建数组时使用额外的参数“f”;该参数将数据类型转换为浮点型。关于更多数据类型选项,可以参考图书 [24]。注意,由于灰度图像没有颜色信息,所以在形状元组中,它只有两个数值。

数组中的元素可以使用下标访问。位于坐标 i、 j,以及颜色通道 k 的像素值可以像下面这样访问:

value = im[i,j,k]

多个数组元素可以使用数组切片方式访问。 切片方式返回的是以指定间隔下标访问 该数组的元素值。下面是有关灰度图像的一些例子:

im[i,:] = im[j,:] # 将第 j 行的数值赋值给第 i 行
im[:,i] = 100 # 将第 i 列的所有数值设为 100
im[:100,:50].sum() # 计算前 100 行、前 50 列所有数值的和
im[50:100,50:100] # 50~100 行, 50~100 列(不包括第 100 行和第 100 列)
im[i].mean() # 第 i 行所有数值的平均值
im[:,-1] # 最后一列
im[-2,:] (or im[-2]) # 倒数第二行

注意,示例仅仅使用一个下标访问数组。如果仅使用一个下标,则该下标为行下标。注意,在最后几个例子中,负数切片表示从最后一个元素逆向计数。我们将会频繁地使用切片技术访问像素值,这也是一个很重要的思想。我们有很多操作和方法来处理数组对象。本书将在使用到的地方逐一介绍。你可以查阅在线文档或者开源图书 [24] 获取更多信息。

1.3.2、灰度变换

将图像读入 NumPy 数组对象后,我们可以对它们执行任意数学操作。一个简单的例子就是图像的灰度变换。考虑任意函数 f,它将 0...255 区间(或者 0...1 区间)映射到自身(意思是说,输出区间的范围和输入区间的范围相同)。下面是关于灰度变换的一些例子:

from PIL import Image
from numpy import *
im = array(Image.open('empire.jpg').convert('L'))
im2 = 255 - im # 对图像进行反相处理
im3 = (100.0/255) * im + 100 # 将图像像素值变换到 100...200 区间
im4 = 255.0 * (im/255.0)**2 # 对图像像素值求平方后得到的图像

第一个例子将灰度图像进行反相处理;第二个例子将图像的像素值变换到 100...200区间;第三个例子对图像使用二次函数变换,使较暗的像素值变得更小。图 1-4 为所使用的变换函数图像。图 1-5 是输出的图像结果。你可以使用下面的命令查看图像中的最小和最大像素值:

print(int(im.min()), int(im.max()))

如果试着对上面例子查看最小值和最大值,可以得到下面的输出结果:

2 255
0 253
100 200
0 255

array() 变换的相反操作可以使用 PIL 的 fromarray() 函数完成:

pil_im = Image.fromarray(im)

如果你通过一些操作将“uint8”数据类型转换为其他数据类型,比如之前例子中的 im3 或者 im4,那么在创建 PIL 图像之前,需要将数据类型转换回来:

pil_im = Image.fromarray(uint8(im))

如果你并不十分确定输入数据的类型,安全起见,应该先转换回来。注意, NumPy总是将数组数据类型转换成能够表示数据的“最低”数据类型。对浮点数做乘积或除法操作会使整数类型的数组变成浮点类型。

1.3.3、图像缩放

NumPy 的数组对象是我们处理图像和数据的主要工具。想要对图像进行缩放处理没有现成简单的方法。我们可以使用之前 PIL 对图像对象转换的操作,写一个简单的用于图像缩放的函数。把下面的函数添加到 imtool.py 文件里:

def imresize(im,sz):
    """ 使用 PIL 对象重新定义图像数组的大小 """
    pil_im = Image.fromarray(uint8(im))
    return array(pil_im.resize(sz))

我们将会在接下来的内容中使用这个函数。

1.3.4、直方图均衡化

图像灰度变换中一个非常有用的例子就是直方图均衡化。直方图均衡化是指将一幅图像的灰度直方图变平,使变换后的图像中每个灰度值的分布概率都相同。在对图像做进一步处理之前,直方图均衡化通常是对图像灰度值进行归一化的一个非常好的方法,并且可以增强图像的对比度。

在这种情况下,直方图均衡化的变换函数是图像中像素值的累积分布函数(cumulativedistribution function, 简写为 cdf,将像素值的范围映射到目标范围的归一化操作)。

下面的函数是直方图均衡化的具体实现。将这个函数添加到 imtool.py 里:

def histeq(im,nbr_bins=256):
    """ 对一幅灰度图像进行直方图均衡化 """
    # 计算图像的直方图
    imhist,bins = histogram(im.flatten(),nbr_bins,normed=True)
    cdf = imhist.cumsum() # cumulative distribution function
    cdf = 255 * cdf / cdf[-1] # 归一化
    # 使用累积分布函数的线性插值,计算新的像素值
    im2 = interp(im.flatten(),bins[:-1],cdf)
    return im2.reshape(im.shape), cdf

该函数有两个输入参数,一个是灰度图像,一个是直方图中使用小区间的数目。函数返回直方图均衡化后的图像,以及用来做像素值映射的累积分布函数。注意,函数中使用到累积分布函数的最后一个元素(下标为 -1),目的是将其归一化到 0...1范围。你可以像下面这样使用该函数:

from PIL import Image
from numpy import *
im = array(Image.open('AquaTermi_lowcontrast.jpg').convert('L'))
im2,cdf = imtools.histeq(im)
1.3.5、图像平均

图像平均操作是减少图像噪声的一种简单方式,通常用于艺术特效。我们可以简单地从图像列表中计算出一幅平均图像。假设所有的图像具有相同的大小,我们可以将这些图像简单地相加,然后除以图像的数目,来计算平均图像。下面的函数可以用于计算平均图像,将其添加到 imtool.py 文件里:

def compute_average(imlist):
    """ 计算图像列表的平均图像 """
    # 打开第一幅图像,将其存储在浮点型数组中
    averageim = array(Image.open(imlist[0]), 'f')
    for imname in imlist[1:]:
        try:
            averageim += array(Image.open(imname))
        except:
            print(imname + '...skipped')
    averageim /= len(imlist)
    # 返回 uint8 类型的平均图像
    return array(averageim, 'uint8')

该函数包括一些基本的异常处理技巧,可以自动跳过不能打开的图像。我们还可以使用 mean() 函数计算平均图像。 mean() 函数需要将所有的图像堆积到一个数组中;也就是说,如果有很多图像,该处理方式需要占用很多内存。我们将会在下一节中使用该函数。

1.3.6、图像的主成分分析(PCA)

PCA(Principal Component Analysis,主成分分析)是一个非常有用的降维技巧。它可以在使用尽可能少维数的前提下,尽量多地保持训练数据的信息,在此意义上是一个最佳技巧。即使是一幅 100×100 像素的小灰度图像,也有 10 000 维,可以看成 10 000 维空间中的一个点。一兆像素的图像具有百万维。由于图像具有很高的维数,在许多计算机视觉应用中,我们经常使用降维操作。 PCA 产生的投影矩阵可以被视为将原始坐标变换到现有的坐标系,坐标系中的各个坐标按照重要性递减排列.

为了对图像数据进行 PCA 变换,图像需要转换成一维向量表示。我们可以使用NumPy 类库中的 flatten() 方法进行变换。

将变平的图像堆积起来,我们可以得到一个矩阵,矩阵的一行表示一幅图像。在计算 主方向之前,所有的行图像按照平均图像进行了中心化。我们通常使用 SVD(Singular Value Decomposition,奇异值分解)方法来计算主成分;但当矩阵的维数很大时, SVD 的计算非常慢,所以此时通常不使用 SVD 分解。下面就是 PCA 操作的代码:

from PIL import Image
from numpy import *
def pca(X):
    """ 主成分分析:
    输入:矩阵 X,其中该矩阵中存储训练数据,每一行为一条训练数据
    返回:投影矩阵(按照维度的重要性排序)、方差和均值 """
    # 获取维数
    num_data,dim = X.shape
    # 数据中心化
    mean_X = X.mean(axis=0)
    X = X - mean_X

    if dim>num_data:
        # PCA- 使用紧致技巧
        M = dot(X,X.T) # 协方差矩阵
        e,EV = linalg.eigh(M) # 特征值和特征向量
        tmp = dot(X.T,EV).T # 这就是紧致技巧
        V = tmp[::-1] # 由于最后的特征向量是我们所需要的,所以需要将其逆转
        S = sqrt(e)[::-1] # 由于特征值是按照递增顺序排列的,所以需要将其逆转
        #这里S得到0下面的除法就错误了
        for i in range(V.shape[1]):
            V[:,i] /= S
    else:
        # PCA- 使用 SVD 方法
        U,S,V = linalg.svd(X)
        V = V[:num_data] # 仅仅返回前 nun_data 维的数据才合理

    # 返回投影矩阵、方差和均值
    return V,S,mean_x

这里报错了。上面S为0除法错误,最后错误:ValueError: cannot reshape array of size 3072000 into shape (800,1280)

该函数首先通过减去每一维的均值将数据中心化,然后计算协方差矩阵对应最大特征值的特征向量,此时可以使用简明的技巧或者 SVD 分解。这里我们使用了range() 函数,该函数的输入参数为一个整数 n,函数返回整数 0...(n-1) 的一个列表。你也可以使用 arange() 函数来返回一个数组,或者使用 xrange() 函数返回一个产生器(可能会提升速度)。我们在本书中贯穿使用 range() 函数。

如果数据个数小于向量的维数,我们不用 SVD 分解,而是计算维数更小的协方差矩阵 XXT 的特征向量。通过仅计算对应前 k(k 是降维后的维数)最大特征值的特征向量,可以使上面的 PCA 操作更快。由于篇幅所限,有兴趣的读者可以自行探索。矩阵 V 的每行向量都是正交的,并且包含了训练数据方差依次减少的坐标方向。

我们接下来对字体图像进行 PCA 变换。 fontimages.zip 文件包含采用不同字体的字符 a 的缩略图。所有的 2359 种字体可以免费下载 1。假定这些图像的名称保存在列表 imlist 中,跟之前的代码一起保存传在 pca.py 文件中,我们可以使用下面的脚本计算图像的主成分:

from PIL import Image
from numpy import *
from pylab import *
import pca
im = array(Image.open(imlist[0])) # 打开一幅图像,获取其大小
m,n = im.shape[0:2] # 获取图像的大小
imnbr = len(imlist) # 获取图像的数目
# 创建矩阵,保存所有压平后的图像数据
immatrix = array([array(Image.open(im)).flatten() for im in imlist],'f')
# 执行 PCA 操作
V,S,immean = pca.pca(immatrix)
# 显示一些图像(均值图像和前 7 个模式)
figure()
gray()
subplot(2,4,1)
imshow(immean.reshape(m,n))
for i in range(7):
    subplot(2,4,i+2)
    imshow(V[i].reshape(m,n))

show()

注意,图像需要从一维表示重新转换成二维图像;可以使用 reshape() 函数。如图1-8 所示,运行该例子会在一个绘图窗口中显示 8 个图像。这里我们使用了 PyLab 库的 subplot() 函数在一个窗口中放置多个图像。

1.3.7、使用 pickle模块

如果想要保存一些结果或者数据以方便后续使用, Python 中的 pickle 模块非常有用。 pickle 模块可以接受几乎所有的 Python 对象,并且将其转换成字符串表示,该过程叫做封装(pickling)。从字符串表示中重构该对象,称为拆封(unpickling)。这些字符串表示可以方便地存储和传输。

我们来看一个例子。假设想要保存上一节字体图像的平均图像和主成分,可以这样来完成:

# 保存均值和主成分数据
f = open('font_pca_modes.pkl', 'wb')
pickle.dump(immean,f)
pickle.dump(V,f)
f.close()

在上述例子中,许多对象可以保存到同一个文件中。 pickle 模块中有很多不同的协议可以生成 .pkl 文件;如果不确定的话,最好以二进制文件的形式读取和写入。在其他 Python 会话中载入数据,只需要如下使用 load() 方法:

# 载入均值和主成分数据
f = open('font_pca_modes.pkl', 'rb')
immean = pickle.load(f)
V = pickle.load(f)
f.close()

注意,载入对象的顺序必须和先前保存的一样。 Python 中有个用C语言写的优化版本,叫做 cpickle 模块,该模块和标准 pickle 模块完全兼容。关于 pickle 模块的更多内容,参见 pickle 模块文档页 http://docs.python.org/library/pickle.html

在本书接下来的章节中,我们将使用 with 语句处理文件的读写操作。这是 Python2.5 引入的思想,可以自动打开和关闭文件(即使在文件打开时发生错误)。下面的例子使用 with() 来实现保存和载入操作:

# 打开文件并保存
with open('font_pca_modes.pkl', 'wb') as f:
pickle.dump(immean,f)
pickle.dump(V,f)

# 打开文件并载入
with open('font_pca_modes.pkl', 'rb') as f:
immean = pickle.load(f)
V = pickle.load(f)

上面的例子乍看起来可能很奇怪,但 with() 确实是个很有用的思想。如果你不喜欢它,可以使用之前的 open 和 close 函数。

作为 pickle 的一种替代方式, NumPy 具有读写文本文件的简单函数。如果数据中不包含复杂的数据结构,比如在一幅图像上点击的点列表, NumPy 的读写函数会很有用。保存一个数组 x 到文件中,可以使用:

savetxt('test.txt',x,'%i')

最后一个参数表示应该使用整数格式。类似地,读取可以使用:

x = loadtxt('test.txt')

你可以从在线文档 http://docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt. html 了解更多内容。

最后, NumPy 有专门用于保存和载入数组的函数。你可以在上面的在线文档里查看 关于 save() 和 load() 的更多内容。

1.4、SciPy

SciPy(http://scipy.org/)是建立在 NumPy 基础上,用于数值运算的开源工具包。SciPy 提供很多高效的操作,可以实现数值积分、优化、统计、信号处理,以及对我们来说最重要的图像处理功能。接下来,本节会介绍 SciPy 中大量有用的模块。SciPy 是个开源工具包,可以从 http://scipy.org/Download 下载。

1.4.1、图像模糊

图像的高斯模糊是非常经典的图像卷积例子。本质上,图像模糊就是将(灰度)图像 I 和一个高斯核进行卷积操作

_images/20181101132451.png

其中 * 表示卷积操作; Gσ 是标准差为 σ 的二维高斯核,定义为

_images/20181101132518.png

高斯模糊通常是其他图像处理操作的一部分,比如图像插值操作、兴趣点计算以及 很多其他应用.

SciPy 有用来做滤波操作的 scipy.ndimage.filters 模块。该模块使用快速一维分离 的方式来计算卷积。你可以像下面这样来使用它:

from PIL import Image
from numpy import *
from scipy.ndimage import filters
im = array(Image.open('empire.jpg').convert('L'))
im2 = filters.gaussian_filter(im,5)

上面 guassian_filter() 函数的最后一个参数表示标准差。

图 1-9 显示了随着 σ 的增加,一幅图像被模糊的程度。 σ 越大,处理后的图像细节丢失越多。如果打算模糊一幅彩色图像,只需简单地对每一个颜色通道进行高斯模糊:

im = array(Image.open('empire.jpg'))
im2 = zeros(im.shape)
for i in range(3):
im2[:,:,i] = filters.gaussian_filter(im[:,:,i],5)
im2 = uint8(im2)

在上面的脚本中,最后并不总是需要将图像转换成 uint8 格式,这里只是将像素值用八位来表示。我们也可以使用:

im2 = array(im2,'uint8')

来完成转换。

关于该模块更多的内容以及不同参数的选择,请查看 http://docs.scipy.org/doc/scipy/reference/ndimage.html 上 SciPy 文档中的 scipy.ndimage 部分。

1.4.2、图像导数

整本书中可以看到,在很多应用中图像强度的变化情况是非常重要的信息。强度的 变化可以用灰度图像 I(对于彩色图像,通常对每个颜色通道分别计算导数)的 x 和 y 方向导数 Ix 和 Iy 进行描述。 图像的梯度向量为dI I = [ , x y I ] T 。梯度有两个重要的属性,一是梯度的大小:

_images/20181101145657.png

它描述了图像强度变化的强弱,一是梯度的角度:

_images/20181101145722.png

描述了图像中在每个点(像素)上强度变化最大的方向。 NumPy 中的 arctan2() 函数返回弧度表示的有符号角度,角度的变化区间为 -π...π。我们可以用离散近似的方式来计算图像的导数。图像导数大多数可以通过卷积简单地实现:

_images/20181101145852.png

对于 Dx 和 Dy,通常选择 Prewitt 滤波器:

_images/20181101150412.png

或者 Sobel 滤波器:

_images/20181101150520.png

这些导数滤波器可以使用 scipy.ndimage.filters 模块的标准卷积操作来简单地实 现,例如:

from PIL import Image
from numpy import *
from scipy.ndimage import filters

im = array(Image.open('empire.jpg').convert('L'))

# Sobel 导数滤波器
imx = zeros(im.shape)
filters.sobel(im,1,imx)

imy = zeros(im.shape)
filters.sobel(im,0,imy)
magnitude = sqrt(imx**2+imy**2)

上面的脚本使用 Sobel 滤波器来计算 x 和 y 的方向导数,以及梯度大小。 sobel() 函数的第二个参数表示选择 x 或者 y 方向导数,第三个参数保存输出的变量。图 1-10显示了用 Sobel 滤波器计算出的导数图像。在两个导数图像中,正导数显示为亮的像素,负导数显示为暗的像素。灰色区域表示导数的值接近于零。

上述计算图像导数的方法有一些缺陷:在该方法中,滤波器的尺度需要随着图像分辨率的变化而变化。为了在图像噪声方面更稳健,以及在任意尺度上计算导数,我们可以使用高斯导数滤波器:

_images/20181101150701.png

其中, Gσx 和 Gσy 表示 Gσ 在 x 和 y 方向上的导数, Gσ 为标准差为 σ 的高斯函数。

我们之前用于模糊的 filters.gaussian_filter() 函数可以接受额外的参数,用来计算高斯导数。可以简单地按照下面的方式来处理:

sigma = 5 # 标准差

imx = zeros(im.shape)
filters.gaussian_filter(im, (sigma,sigma), (0,1), imx)

imy = zeros(im.shape)
filters.gaussian_filter(im, (sigma,sigma), (1,0), imy)

该函数的第三个参数指定对每个方向计算哪种类型的导数,第二个参数为使用的标准差。你可以查看相应文档了解详情。

1.4.3、形态学:对象计数

形态学(或数学形态学)是度量和分析基本形状的图像处理方法的基本框架与集合。形态学通常用于处理二值图像,但是也能够用于灰度图像。 二值图像是指图像的每个像素只能取两个值,通常是 0 和 1。二值图像通常是,在计算物体的数目,或者度量其大小时,对一幅图像进行阈值化后的结果。你可以从 http://en.wikipedia.org/wiki/Mathematical_morphology 大体了解形态学及其处理图像的方式。

scipy.ndimage 中的 morphology 模块可以实现形态学操作。你可以使用 scipy.ndimage 中的 measurements 模块来实现二值图像的计数和度量功能。下面通过一个简单的例子介绍如何使用它们。

考虑在图 1-12a ^1 里的二值图像,计算该图像中的对象个数可以通过下面的脚本实现:

from scipy.ndimage import measurements,morphology

# 载入图像,然后使用阈值化操作,以保证处理的图像为二值图像
im = array(Image.open('houses.png').convert('L'))
im = 1*(im<128)

labels, nbr_objects = measurements.label(im)
print "Number of objects:", nbr_objects

上面的脚本首先载入该图像,通过阈值化方式来确保该图像是二值图像。通过和 1相乘,脚本将布尔数组转换成二进制表示。然后,我们使用 label() 函数寻找单个的物体,并且按照它们属于哪个对象将整数标签给像素赋值。图 1-12b 是 labels 数组的图像。图像的灰度值表示对象的标签。可以看到,在一些对象之间有一些小的连接。进行二进制开(binary open)操作,我们可以将其移除:

# 形态学开操作更好地分离各个对象
im_open = morphology.binary_opening(im,ones((9,5)),iterations=2)

labels_open, nbr_objects_open = measurements.label(im_open)
print "Number of objects:", nbr_objects_open

binary_opening() 函数的第二个参数指定一个数组结构元素。该数组表示以一个像素为中心时,使用哪些相邻像素。在这种情况下,我们在 y 方向上使用 9 个像素(上面 4 个像素、像素本身、下面 4 个像素),在 x 方向上使用 5 个像素。你可以指定任意数组为结构元素,数组中的非零元素决定使用哪些相邻像素。参数iterations 决定执行该操作的次数。你可以尝试使用不同的迭代次数 iterations 值,看一下对象的数目如何变化。你可以在图 1-12c 与图 1-12d 中查看经过开操作后的图像,以及相应的标签图像。正如你想象的一样, binary_closing() 函数实现相反的操作。我们将该函数和在 morphology 和 measurements 模块中的其他函数的用法留作练习。你可以从 scipy.ndimage 模块文档 http://docs.scipy.org/doc/scipy/reference/ndimage.html 中了解关于这些函数的更多知识。

1.4.4、一些有用的 SciPy模块

SciPy 中包含一些用于输入和输出的实用模块。下面介绍其中两个模块: io 和 misc。

  1. 读写.mat文件

    如果你有一些数据,或者在网上下载到一些有趣的数据集,这些数据以 Matlab的 .mat 文件格式存储,那么可以使用 scipy.io 模块进行读取:

    data = scipy.io.loadmat('test.mat')
    

    上面代码中, data 对象包含一个字典,字典中的键对应于保存在原始 .mat 文件中的变量名。由于这些变量是数组格式的,因此可以很方便地保存到 .mat 文件中。你仅需创建一个字典(其中要包含你想要保存的所有变量),然后使用 savemat() 函数:

    data = {}
    data['x'] = x
    scipy.io.savemat('test.mat',data)
    

    因为上面的脚本保存的是数组 x,所以当读入到 Matlab 中时,变量的名字仍为 x。关 于 scipy.io 模 块 的 更 多 内 容, 请 参 见 在 线 文 档 http://docs.scipy.org/doc/scipy/reference/io.html

  2. 以图像形式保存数组

    因为我们需要对图像进行操作,并且需要使用数组对象来做运算,所以将数组直接保存为图像文件 1 非常有用。本书中的很多图像都是这样的创建的。

    imsave() 函数可以从 scipy.misc 模块中载入。要将数组 im 保存到文件中,可以使用

    下面的命令:

    from scipy.misc import imsave
    imsave('test.jpg',im)
    

    scipy.misc 模块同样包含了著名的 Lena 测试图像:

    lena = scipy.misc.lena()
    

    该脚本返回一个 512×512 的灰度图像数组。

1.5、高级示例:图像去噪

我们通过一个非常实用的例子——图像的去噪——来结束本章。图像去噪是在去除图像噪声的同时,尽可能地保留图像细节和结构的处理技术。我们这里使用 ROF(Rudin-Osher-Fatemi) 去噪模型。该模型最早出现在文献 [28] 中。图像去噪对于很多应用来说都非常重要;这些应用范围很广,小到让你的假期照片看起来更漂亮,大到提高卫星图像的质量。 ROF 模型具有很好的性质:使处理后的图像更平滑,同时保持图像边缘和结构信息。

ROF 模型的数学基础和处理技巧非常高深,不在本书讲述范围之内。在讲述如何基于 Chambolle 提出的算法 [5] 实现 ROF 求解器之前,本书首先简要介绍一下 ROF模型。

一幅(灰度)图像 I 的全变差(Total Variation, TV)定义为梯度范数之和。在连续表示的情况下,全变差表示为:

_images/20181101152857.png

在离散表示的情况下,全变差表示为:

_images/20181101152953.png

其中,上面的式子是在所有图像坐标 x=[x, y] 上取和。

在 Chambolle 提出的 ROF 模型里,目标函数为寻找降噪后的图像 U,使下式最小:

_images/20181101153109.png

其中范数|| I - U || 是去噪后图像 U 和原始图像 I 差异的度量。也就是说,本质上该 模型使去噪后的图像像素值“平坦”变化,但是在图像区域的边缘上,允许去噪后 的图像像素值“跳跃”变化。

按照论文 [5] 中的算法,我们可以按照下面的代码实现 ROF 模型去噪:

from numpy import *
def denoise(im,U_init,tolerance=0.1,tau=0.125,tv_weight=100):
    """ 使用 A. Chambolle(2005)在公式(11)中的计算步骤实现 Rudin-Osher-Fatemi(ROF)去噪模型
    输入:含有噪声的输入图像(灰度图像)、 U 的初始值、 TV 正则项权值、步长、停业条件
    输出:去噪和去除纹理后的图像、纹理残留 """

    m,n = im.shape # 噪声图像的大小

    # 初始化
    U = U_init
    Px = im # 对偶域的 x 分量
    Py = im # 对偶域的 y 分量
    error = 1

    while (error > tolerance):
        Uold = U

        # 原始变量的梯度
        GradUx = roll(U,-1,axis=1)-U # 变量 U 梯度的 x 分量
        GradUy = roll(U,-1,axis=0)-U # 变量 U 梯度的 y 分量

        # 更新对偶变量
        PxNew = Px + (tau/tv_weight)*GradUx
        PyNew = Py + (tau/tv_weight)*GradUy
        NormNew = maximum(1,sqrt(PxNew**2+PyNew**2))

        Px = PxNew/NormNew # 更新 x 分量(对偶)
        Py = PyNew/NormNew # 更新 y 分量(对偶)

        # 更新原始变量
        RxPx = roll(Px,1,axis=1) # 对 x 分量进行向右 x 轴平移
        RyPy = roll(Py,1,axis=0) # 对 y 分量进行向右 y 轴平移

        DivP = (Px-RxPx)+(Py-RyPy) # 对偶域的散度
        U = im + tv_weight*DivP # 更新原始变量

        # 更新误差
        error = linalg.norm(U-Uold)/sqrt(n*m);

    return U,im-U # 去噪后的图像和纹理残余

在这个例子中,我们使用了 roll() 函数。顾名思义,在一个坐标轴上,它循环“滚动”数组中的元素值。该函数可以非常方便地计算邻域元素的差异,比如这里的导数。我们还使用了 linalg.norm() 函数,该函数可以衡量两个数组间(这个例子中是指图像矩阵 U 和 Uold)的差异。我们将这个 denoise() 函数保存到 rof.py 文件中。

下面使用一个合成的噪声图像示例来说明如何使用该函数:

from numpy import *
from numpy import random
from scipy.ndimage import filters
import rof

# 使用噪声创建合成图像
im = zeros((500,500))
im[100:400,100:400] = 128
im[200:300,200:300] = 255
im = im + 30*random.standard_normal((500,500))

U,T = rof.denoise(im,im)
G = filters.gaussian_filter(im,10)

# 保存生成结果
from scipy.misc import imsave
imsave('synth_rof.pdf',U)
imsave('synth_gaussian.pdf',G)

原始图像和图像的去噪结果如图 1-13 所示。正如你所看到的, ROF 算法去噪后的图像很好地保留了图像的边缘信息。

_images/20181101154102.png

图 1-13:使用 ROF 模型对合成图像去噪:(a)为原始噪声图像;(b)为经过高斯模糊的图像 (σ=10);(c)为经过 ROF 模型去噪后的图像

下面看一下在实际图像中使用 ROF 模型去噪的效果:

from PIL import Image
from pylab import *
import rof

im = array(Image.open('empire.jpg').convert('L'))
U,T = rof.denoise(im,im)

figure()
gray()
imshow(U)
axis('equal')
axis('off')
show()

经过 ROF 去噪后的图像如图 1-14c 所示。为了方便比较,该图中同样显示了模糊后的图像。可以看到, ROF 去噪后的图像保留了边缘和图像的结构信息,同时模糊了“噪声”。

_images/20181101154319.png
练习

(1) 如图 1-9 所示,将一幅图像进行高斯模糊处理。随着 σ 的增加,绘制出图像轮廓。 在你绘制出的图中,图像的轮廓有何变化?你能解释为什么会发生这些变化吗?

(2) 通过将图像模糊化,然后从原始图像中减去模糊图像,来实现反锐化图像掩模操 作(http://en.wikipedia.org/wiki/Unsharp_masking)。反锐化图像掩模操作可以实 现图像锐化效果。试在彩色和灰度图像上使用反锐化图像掩模操作,观察该操作 的效果。

(3) 除了直方图均衡化,商图像是另一种图像归一化的方法。商图像可以通过除以模 糊后的图像 I/(I * Gσ) 获得。尝试使用该方法,并使用一些样本图像进行验证。

(4) 使用图像梯度,编写一个在图像中获得简单物体(例如,白色背景中的正方形) 轮廓的函数。

(5) 使用梯度方向和大小检测图像中的线段。估计线段的长度以及线段的参数,并在 原始图像中重新绘制该线段。

(6) 使用 label() 函数处理二值化图像,并使用直方图和标签图像绘制图像中物体的 大小分布。

(7) 使用形态学操作处理阈值化图像。在发现一些参数能够产生好的结果后,使用 morphology 模块里面的 center_of_mass() 函数寻找每个物体的中心坐标,将其在 图像中绘制出来。

代码示例约定

从第 2 章起,我们假定 PIL、 NumPy 和 Matplotlib 都包括在你所创建的每个文件和每 个代码例子的开头:

from PIL import Image
from numpy import *
from pylab import *

这种约定使得示例代码更清晰,同时也便于读者理解。除此之外,我们使用 SciPy模块时,将会在代码示例中显式声明。

一些纯化论者会反对这种将全体模块导入的方式,坚持如下使用方式:

import numpy as np
import matplotlib.pyplot as plt

这种方式能够保持命名空间(知道每个函数从哪儿来)。因为我们不需要 PyLab 中的NumPy 部分,所以该例子只从 Matplotlib 中导入 pyplot 部分。纯化论者和经验丰富的程序员们知道这些区别,他们能够选择自己喜欢的方式。但是,为了使本书的内容和例子更容易被读者接受,我们不打算这样做。

请读者注意。

第二章、局部图像描述子

本章旨在寻找图像间的对应点和对应区域。本章将介绍用于图像匹配的两种局部描述子算法。本书的很多内容中都会用到这些局部特征,它们在很多应用中都有重要作用,比如创建全景图、增强现实技术以及计算图像的三维重建

2.1 Harris角点检测器

Harris 角点检测算法(也称 Harris & Stephens 角点检测器)是一个极为简单的角点检测算法。该算法的主要思想是,如果像素周围显示存在多于一个方向的边,我们认为该点为兴趣点。该点就称为角点。

我们把图像域中点 x 上的对称半正定矩阵 MI=MI(x)定义为:

_images/20181101155404.png

其中 ▽I 为包含导数 Ix 和 Iy 的图像梯度(我们已经在第 1 章定义了图像的导数和梯度)。由于该定义,MI 的秩为 1,特征值为 λ1=| ▽I |2 和 λ2=0。现在对于图像的每一个像素,我们可以计算出该矩阵。

选择权重矩阵 W(通常为高斯滤波器 Gσ),我们可以得到卷积:

_images/20181101155951.png

该卷积的目的是得到 MI 在周围像素上的局部平均。计算出的矩阵 M I 有称为 Harris矩阵。 W 的宽度决定了在像素 x 周围的感兴趣区域。像这样在区域附近对矩阵 M I取平均的原因是,特征值会依赖于局部图像特性而变化。如果图像的梯度在该区域变化,那么 M I 的第二个特征值将不再为 0。如果图像的梯度没有变化, M I 的特征值也不会变化。

取决于该区域 I 的值, Harris 矩阵 M I 的特征值有三种情况:
  • 如果 λ1 和 λ2 都是很大的正数,则该 x 点为角点;
  • 如果 λ1 很大, λ2 ≈ 0,则该区域内存在一个边,该区域内的平均 MI 的特征值不
会变化太大;
  • 如果 λ1≈λ2≈ 0,该区域内为空。

在不需要实际计算特征值的情况下,为了把重要的情况和其他情况分开, Harris 和Stephens 在文献 [12] 中引入了指示函数:

_images/20181101160146.png

为了去除加权常数 κ,我们通常使用商数:

_images/20181101160229.png

作为指示器。

下面我们写出 Harris 角点检测程序。像 1.4.2 节介绍的一样,对于这个函数,我们需要使用 scipy.ndimage.filters 模块中的高斯导数滤波器来计算导数。使用高斯滤波器的道理同样是,我们需要在角点检测过程中抑制噪声强度。

首先,将角点响应函数添加到 harris.py 文件中,该函数使用高斯导数实现。同样地,参数 σ 定义了使用的高斯滤波器的尺度大小。你也可以修改这个函数,对 x 和y 方向上不同的尺度参数,以及尝试平均操作中的不同尺度,来计算 Harris 矩阵。

from scipy.ndimage import filters
def compute_harris_response(im,sigma=3):
    """ 在一幅灰度图像中,对每个像素计算 Harris 角点检测器响应函数 """

    # 计算导数
    imx = zeros(im.shape)
    filters.gaussian_filter(im, (sigma,sigma), (0,1), imx)
    imy = zeros(im.shape)
    filters.gaussian_filter(im, (sigma,sigma), (1,0), imy)

    # 计算 Harris 矩阵的分量
    Wxx = filters.gaussian_filter(imx*imx,sigma)
    Wxy = filters.gaussian_filter(imx*imy,sigma)
    Wyy = filters.gaussian_filter(imy*imy,sigma)

    # 计算特征值和迹
    Wdet = Wxx*Wyy - Wxy**2
    Wtr = Wxx + Wyy

    return Wdet / Wtr

上面的函数会返回像素值为 Harris 响应函数值的一幅图像。现在,我们需要从这幅图像中挑选出需要的信息。然后,选取像素值高于阈值的所有图像点;再加上额外的限制,即角点之间的间隔必须大于设定的最小距离。这种方法会产生很好的角点检测结果。为了实现该算法,我们获取所有的候选像素点,以角点响应值递减的顺序排序,然后将距离已标记为角点位置过近的区域从候选像素点中删除。将下面的函数添加到 harris.py 文件中:

def get_harris_points(harrisim,min_dist=10,threshold=0.1):
    """ 从一幅 Harris 响应图像中返回角点。 min_dist 为分割角点和图像边界的最少像素数目 """

    # 寻找高于阈值的候选角点
    corner_threshold = harrisim.max() * threshold
    harrisim_t = (harrisim > corner_threshold) * 1

    # 得到候选点的坐标
    coords = array(harrisim_t.nonzero()).T

    # 以及它们的 Harris 响应值
    candidate_values = [harrisim[c[0],c[1]] for c in coords]

    # 对候选点按照 Harris 响应值进行排序
    index = argsort(candidate_values)

    # 将可行点的位置保存到数组中
    allowed_locations = zeros(harrisim.shape)
    allowed_locations[min_dist:-min_dist,min_dist:-min_dist] = 1

    # 按照 min_distance 原则,选择最佳 Harris 点
    filtered_coords = []

    for i in index:
        if allowed_locations[coords[i,0],coords[i,1]] == 1:
            filtered_coords.append(coords[i])
            allowed_locations[(coords[i,0]-min_dist):(coords[i,0]+min_dist),(coords[i,1]-min_dist):(coords[i,1]+min_dist)] = 0

    return filtered_coords

现在你有了检测图像中角点所需要的所有函数。为了显示图像中的角点,你可以使用 Matplotlib 模块绘制函数,将其添加到 harris.py 文件中,如下:

def plot_harris_points(image,filtered_coords):
    """ 绘制图像中检测到的角点 """
    figure()
    gray()
    imshow(image)
    plot([p[1] for p in filtered_coords],[p[0] for p in filtered_coords],'*')
    axis('off')
    show()

试着运行下面的命令:

im = array(Image.open('empire.jpg').convert('L'))
harrisim = harris.compute_harris_response(im)
filtered_coords = harris.get_harris_points(harrisim,6)
harris.plot_harris_points(im, filtered_coords)

首先,打开该图像,转换成灰度图像。然后,计算响应函数,基于响应值选择角点。最后,在原始图像中覆盖绘制检测出的角点。绘制出的结果图像如图 2-1 所示。

_images/20181101160707.png

图 2-1:使用 Harris 角点检测器检测角点:(a)为 Harris 响应函数;(b-d)分别为使用阈值 0.01、 0.05 和 0.1 检测出的角点

如果你想概要了解角点检测的不同方法,包括 Harris 角点检测器的改进和进一步的开发应用,可以查找资源,如网站 http://en.wikipedia.org/wiki/Corner_detection

在图像间寻找对应点

Harris 角点检测器仅仅能够检测出图像中的兴趣点,但是没有给出通过比较图像间的兴趣点来寻找匹配角点的方法。我们需要在每个点上加入描述子信息,并给出一个比较这些描述子的方法。

兴趣点描述子是分配给兴趣点的一个向量,描述该点附近的图像的表观信息。描述子越好,寻找到的对应点越好。我们用对应点或者点的对应来描述相同物体和场景点在不同图像上形成的像素点。

Harris 角点的描述子通常是由周围图像像素块的灰度值,以及用于比较的归一化互相关矩阵构成的。图像的像素块由以该像素点为中心的周围矩形部分图像构成。

通常,两个(相同大小)像素块 I1(x) 和 I2(x) 的相关矩阵定义为:

_images/20181101160907.png _images/20181101160955.png

其中,n为像素块中像素的数目, μ1 和 μ2 表示每个像素块中的平均像素值强度, σ1和 σ2 分别表示每个像素块中的标准差。通过减去均值和除以标准差,该方法对图像亮度变化具有稳健性。

为获取图像像素块,并使用归一化的互相关矩阵来比较它们,你需要另外两个函数。将它们添加到 harris.py 文件中:

def get_descriptors(image,filtered_coords,wid=5):
    """ 对于每个返回的点,返回点周围 2*wid+1 个像素的值(假设选取点的 min_distance > wid) """

    desc = []
    for coords in filtered_coords:
        patch = image[coords[0]-wid:coords[0]+wid+1,coords[1]-wid:coords[1]+wid+1].flatten()

    desc.append(patch)

    return desc

def match(desc1,desc2,threshold=0.5):
    """ 对于第一幅图像中的每个角点描述子,使用归一化互相关,选取它在第二幅图像中的匹配角点 """
    n = len(desc1[0])
    # 点对的距离
    d = -ones((len(desc1),len(desc2)))
    for i in range(len(desc1)):
        for j in range(len(desc2)):
            d1 = (desc1[i] - mean(desc1[i])) / std(desc1[i])
            d2 = (desc2[j] - mean(desc2[j])) / std(desc2[j])
            ncc_value = sum(d1 * d2) / (n-1)
            if ncc_value > threshold:
                d[i,j] = ncc_value
    ndx = argsort(-d)
    matchscores = ndx[:,0]
    return matchscores

第一个函数的参数为奇数大小长度的方形灰度图像块,该图像块的中心为处理的像素点。该函数将图像块像素值压平成一个向量,然后添加到描述子列表中。第二个函数使用归一化的互相关矩阵,将每个描述子匹配到另一个图像中的最优的候选点。由于数值较高的距离代表两个点能够更好地匹配,所以在排序之前,我们对距离取相反数。为了获得更稳定的匹配,我们从第二幅图像向第一幅图像匹配,然后过滤掉在两种方法中不都是最好的匹配。下面的函数可以实现该操作:

def match_twosided(desc1,desc2,threshold=0.5):
    """ 两边对称版本的 match()"""

    matches_12 = match(desc1,desc2,threshold)
    matches_21 = match(desc2,desc1,threshold)

    ndx_12 = where(matches_12 >= 0)[0]
    # 去除非对称的匹配
    for n in ndx_12:
        if matches_21[matches_12[n]] != n:
            matches_12[n] = -1

    return matches_12

这些匹配可以通过在两边分别绘制出图像,使用线段连接匹配的像素点来直观地可视化。下面的代码可以实现匹配点的可视化。将这两个函数添加到 harris.py 文件中:

def appendimages(im1,im2):
    """ 返回将两幅图像并排拼接成的一幅新图像 """

    # 选取具有最少行数的图像,然后填充足够的空行
    rows1 = im1.shape[0]
    rows2 = im2.shape[0]

    if rows1 < rows2:
        im1 = concatenate((im1,zeros((rows2-rows1,im1.shape[1]))),axis=0)
    elif rows1 > rows2:
        im2 = concatenate((im2,zeros((rows1-rows2,im2.shape[1]))),axis=0)

    # 如果这些情况都没有,那么它们的行数相同,不需要进行填充
    return concatenate((im1,im2), axis=1)

def plot_matches(im1,im2,locs1,locs2,matchscores,show_below=True):
    """ 显示一幅带有连接匹配之间连线的图片
    输入: im1, im2(数组图像), locs1, locs2(特征位置), matchscores(match() 的输出),
    show_below(如果图像应该显示在匹配的下方) """

    im3 = appendimages(im1,im2)
    if show_below:
        im3 = vstack((im3,im3))
    imshow(im3)

    cols1 = im1.shape[1]
    for i,m in enumerate(matchscores):
        if m>0:
            plot([locs1[i][1],locs2[m][1]+cols1],[locs1[i][0],locs2[m][0]],'c')
    axis('off')

图 2-2 为使用归一化的互相关矩阵(在这个例子中,每个像素块的大小为 11×11)来寻找对应点的例子。该图像可以通过下面的命令实现:

wid = 5
harrisim = harris.compute_harris_response(im1,5)
filtered_coords1 = harris.get_harris_points(harrisim,wid+1)

d1 = harris.get_descriptors(im1,filtered_coords1,wid)

harrisim = harris.compute_harris_response(im2,5)
filtered_coords2 = harris.get_harris_points(harrisim,wid+1)
d2 = harris.get_descriptors(im2,filtered_coords2,wid)

print 'starting matching'
matches = harris.match_twosided(d1,d2)

figure()
gray()
harris.plot_matches(im1,im2,filtered_coords1,filtered_coords2,matches)
show()

为了看得更清楚,你可以画出匹配的子集。在上面的代码中,可以通过将数组matches 替换成 matches[:100] 或者任意子集来实现。

如图 2-2 所示,该算法的结果存在一些不正确匹配。这是因为,与现代的一些方法相比,图像像素块的互相关矩阵具有较弱的描述性。实际运用中,我们通常使用更稳健的方法来处理这些对应匹配。这些描述符还有一个问题,它们不具有尺度不变性和旋转不变性,而算法中像素块的大小也会影响对应匹配的结果。

近年来诞生了很多用来提高特征点检测和描述性能的方法。在下一节中,我们来学习其中最好的一种算法。

2.2 SIFT(尺度不变特征变换)

David Lowe 在文献 [17] 中提出的 SIFT(Scale-Invariant Feature Transform,尺度不变特征变换)是过去十年中最成功的图像局部描述子之一。 SIFT 特征后来在文献[18] 中得到精炼并详述,经受住了时间的考验。 SIFT 特征包括兴趣点检测器和描述子。 SIFT 描述子具有非常强的稳健性,这在很大程度上也是 SIFT 特征能够成功和流行的主要原因。自从 SIFT 特征的出现,许多其他本质上使用相同描述子的方法也相继出现。现在, SIFT 描述符经常和许多不同的兴趣点检测器相结合使用(有些情况下是区域检测器),有时甚至在整幅图像上密集地使用。 SIFT 特征对于尺度、旋转和亮度都具有不变性,因此,它可以用于三维视角和噪声的可靠匹配。你可以在 http://en.wikipedia.org/wiki/Scale-invariant_feature_transform 获得 SIFT 特征的简要介绍。

2.2.1 兴趣点

SIFT 特征使用高斯差分函数来定位兴趣点:

_images/20181101163300.png

其中, Gσ 是上一章中介绍的二维高斯核, Iσ 是使用 Gσ 模糊的灰度图像, κ 是决定相差尺度的常数。兴趣点是在图像位置和尺度变化下 D(x,σ) 的最大值和最小值点。这些候选位置点通过滤波去除不稳定点。基于一些准则,比如认为低对比度和位于边上的点不是兴趣点,我们可以去除一些候选兴趣点。你可以参考文献 [17, 18] 了解更多。

2.2.2 描述子

上面讨论的兴趣点(关键点)位置描述子给出了兴趣点的位置和尺度信息。为了实现旋转不变性,基于每个点周围图像梯度的方向和大小, SIFT 描述子又引入了参考方向。 SIFT 描述子使用主方向描述参考方向。主方向使用方向直方图(以大小为权重)来度量。

下面我们基于位置、尺度和方向信息来计算描述子。为了对图像亮度具有稳健性,SIFT 描述子使用图像梯度(之前 Harris 描述子使用图像亮度信息计算归一化互相关矩阵)。 SIFT 描述子在每个像素点附近选取子区域网格,在每个子区域内计算图像梯度方向直方图。每个子区域的直方图拼接起来组成描述子向量。 SIFT 描述子的标准设置使用 4×4 的子区域,每个子区域使用 8 个小区间的方向直方图,会产生共128 个小区间的直方图(4×4×8=128)。图 2-3 所示为描述子的构造过程。感兴趣的读者可以参考文献 [18] 获取更多内容,或者从 http://en.wikipedia.org/wiki/Scaleinvariant_feature_transform 概要了解 SIFT 特征描述子。

_images/20181101163521.png
2.2.3 检测兴趣点

我们使用开源工具包 VLFeat 提供的二进制文件来计算图像的 SIFT 特征 [36]。用完整的 Python 实现 SIFT 特征的所有步骤可能效率不是很高,并且超出了本书的范围。VLFeat 工具包可以从 http://www.vlfeat.org/ 下载,二进制文件可以在所有主要的平台上运行。 VLFeat 库是用 C 语言来写的,但是我们可以使用该库提供的命令行接口。如果你认为使用 Matlab 接口或者 Python 包装器比二进制文件更方便,可以从http://github.com/mmmikael/vlfeat/ 下载相应的版本。由于 Python 包装器对平台的依赖性,安装 Python 包装器在某些平台上需要一定的技巧,所以我们这里使用二进制文件版本。 Lowe 的个人网站上也有 SIFT 特征的实现,可以参见 http://www.cs.ubc.ca/~lowe/keypoints/,该代码仅适用于 Windows 系统和 Linux 系统。

创建 sift.py 文件,将下面调用可执行文件的函数添加到该文件中:

def process_image(imagename,resultname,params="--edge-thresh 10 --peak-thresh 5"):
    """ 处理一幅图像,然后将结果保存在文件中 """
    if imagename[-3:] != 'pgm':
        # 创建一个 pgm 文件
        im = Image.open(imagename).convert('L')
        im.save('tmp.pgm')
        imagename = 'tmp.pgm'
    cmmd = str("sift "+imagename+" --output="+resultname+" "+params)
    os.system(cmmd)
    print 'processed', imagename, 'to', resultname

由于该二进制文件需要的图像格式为灰度 .pgm,所以如果图像为其他格式,我们需要首先将其转换成 .pgm 格式文件。转换的结果以易读的格式保存在文本文件中。文本文件如下:

318.861 7.48227 1.12001 1.68523 0 0 0 1 0 0 0 0 0 11 16 0 ...
318.861 7.48227 1.12001 2.99965 11 2 0 0 1 0 0 0 173 67 0 0 ...
54.2821 14.8586 0.895827 4.29821 60 46 0 0 0 0 0 0 99 42 0 0 ...
155.714 23.0575 1.10741 1.54095 6 0 0 0 150 11 0 0 150 18 2 1 ...
42.9729 24.2012 0.969313 4.68892 90 29 0 0 0 1 2 10 79 45 5 11 ...
229.037 23.7603 0.921754 1.48754 3 0 0 0 141 31 0 0 141 45 0 0 ...
232.362 24.0091 1.0578 1.65089 11 1 0 16 134 0 0 0 106 21 16 33 ...
201.256 25.5857 1.04879 2.01664 10 4 1 8 14 2 1 9 88 13 0 0 ...
…

上面数据的每一行前 4 个数值依次表示兴趣点的坐标、尺度和方向角度,后面紧接着的是对应描述符的 128 维向量。这里的描述子使用原始整数数值表示,没有经过归一化处理。当你需要比较这些描述符时,要做一些处理。更多的内容请见后面的介绍。

上面的例子显示的是在一幅图像中前 8 个特征的前面部分数值。注意前两行的坐标值相同,但是方向不同。当同一个兴趣点上出现不同的显著方向,这种情况就会出现的。

下面是如何从像上面的输出文件中,将特征读取到 NumPy 数组中的函数。将该函数添加到 sift.py 文件中:

def read_features_from_file(filename):
    """ 读取特征属性值,然后将其以矩阵的形式返回 """
    f = loadtxt(filename)
    return f[:,:4],f[:,4:] # 特征位置,描述子

在上面的函数中,我们使用 NumPy 库中的 loadtxt() 函数来处理所有的工作。

如果在 Python 会话中修改描述子,你需要将输出结果保存到特征文件中。下面的函数使用 NumPy 库中的 savetxt() 函数,可以帮你实现该功能:

def write_features_to_file(filename,locs,desc):
    """ 将特征位置和描述子保存到文件中 """
    savetxt(filename,hstack((locs,desc)))

上面的函数使用了 hstack() 函数。该函数通过拼接不同的行向量来实现水平堆叠两个向量的功能。在这个例子中,每一行中前几列为位置信息,紧接着是描述子。

读取特征后,通过在图像上绘制出它们的位置,可以将其可视化。将下面的 plot_features() 函数添加到 sift.py 文件中,可以实现该功能:

def plot_features(im,locs,circle=False):
    """ 显示带有特征的图像
    输入: im(数组图像), locs(每个特征的行、列、尺度和朝向) """
    def draw_circle(c,r):
        t = arange(0,1.01,.01)*2*pi
        x = r*cos(t) + c[0]
        y = r*sin(t) + c[1]
        plot(x,y,'b',linewidth=2)
        imshow(im)
        if circle:
        for p in locs:
        draw_circle(p[:2],p[2])
        else:
        plot(locs[:,0],locs[:,1],'ob')
        axis('off')

该函数在原始图像上使用蓝色的点绘制出 SIFT 特征点的位置。将参数 circle 的选项设置为 True,该函数将使用 draw_circle() 函数绘制出圆圈,圆圈的半径为特征的尺度。

你可以通过下面的命令绘制出如图 2-4b 中 SIFT 特征位置的图像:

import sift

imname = 'empire.jpg'
im1 = array(Image.open(imname).convert('L'))
sift.process_image(imname,'empire.sift')
l1,d1 = sift.read_features_from_file('empire.sift')

figure()
gray()
sift.plot_features(im1,l1,circle=True)
show()

为了比较 Harris 角点和 SIFT 特征的不同,右图(图 2-4c)显示的是同一幅图像的 Harris 角点。你可以看到,两个算法所选择特征点的位置不同。

_images/20181101173955.png
2.2.4 匹配描述子

对于将一幅图像中的特征匹配到另一幅图像的特征,一种稳健的准则(同样是由Lowe 提出的)是使用这两个特征距离和两个最匹配特征距离的比率。相比于图像中的其他特征,该准则保证能够找到足够相似的唯一特征。使用该方法可以使错误的匹配数降低。下面的代码实现了匹配函数。将 match() 函数添加到 sift.py 文件中:

def match(desc1,desc2):
    """ 对于第一幅图像中的每个描述子,选取其在第二幅图像中的匹配
    输入: desc1(第一幅图像中的描述子), desc2(第二幅图像中的描述子)
    """

    desc1 = array([d/linalg.norm(d) for d in desc1])
    desc2 = array([d/linalg.norm(d) for d in desc2])

    dist_ratio = 0.6
    desc1_size = desc1.shape

    matchscores = zeros((desc1_size[0],1),'int')
    desc2t = desc2.T # 预先计算矩阵转置

    for i in range(desc1_size[0]):
        dotprods = dot(desc1[i,:],desc2t) # 向量点乘
        dotprods = 0.9999*dotprods
        # 反余弦和反排序,返回第二幅图像中特征的索引
        indx = argsort(arccos(dotprods))
        # 检查最近邻的角度是否小于 dist_ratio 乘以第二近邻的角度
        if arccos(dotprods)[indx[0]] < dist_ratio * arccos(dotprods)[indx[1]]:
            matchscores[i] = int(indx[0])

    return matchscores

该函数使用描述子向量间的夹角作为距离度量。在此之前,我们需要将描述子向量归一化到单位长度 1。因为这种匹配是单向的,即我们将每个特征向另一幅图像中的所有特征进行匹配,所以我们可以先计算第二幅图像兴趣点描述子向量的转置矩阵。这样,我们就不需要对每个特征分别进行转置操作。

为了进一步增加匹配的稳健性,我们可以再反过来执行一次该步骤,用另外的方法匹配(从第二幅图像中的特征向第一幅图像中的特征匹配)。最后,我们仅保留同时满足这两种匹配准则的对应(和我们对 Harris 角点的处理方法相同)。下面的match_twosided() 函数可以实现该操作:

def match_twosided(desc1,desc2):
    """ 双向对称版本的 match()"""

    matches_12 = match(desc1,desc2)
    matches_21 = match(desc2,desc1)

    ndx_12 = matches_12.nonzero()[0]
    # 去除不对称的匹配
    for n in ndx_12:
        if matches_21[int(matches_12[n])] != n:
            matches_12[n] = 0

    return matches_12

为了绘制出这些匹配点,我们可以使用在 harris.py 用到的相同函数。方便起见,将appendimages() 函数和 plot_matches() 函数复制 过来。然后,将它们添加到 sift.py文件中。如果你喜欢,也可以通过载入 harris.py 来使用这两个函数。

通过检测和匹配特征点,我们可以将这些局部描述子应用到很多例子中。为了稳健地过滤掉这些不正确的匹配,接下来的两个章节将会在对应上加入几何学的约束关系,并将局部描述子应用到一些例子中,比如自动创建全景图、照相机姿态估计以及三维结构计算。

2.3 匹配地理标记图像

我们将通过一个示例应用来结束本章节。在这个例子中,我们使用局部描述子来匹 配带有地理标记的图像

2.3.1 从 Panoramio 下载地理标记图像

你 可 以 从 谷 歌 提 供 的 照 片 共 享 服 务 Panoramio(http://www.panoramio.com/) 获得地理标记图像。像许多网络资源一样, Panoramio 提供一个 API 接口,方便用户使用程序访问这些内容。 Panoramio 的 API 非常简单直接,可以在 http://www.panoramio.com/api/ 上找到 API 的使用方式。你可以通过 HTTP GET 方式访问网址内容,如下

http://www.panoramio.com/map/get_panoramas.php?order=popularity&set=public& from=0&to=20&minx=-180&miny=-90&maxx=180&maxy=90&size=medium

其中的 minx、 miny、 maxx 和 maxy 定义了选取照片的地理区域位置(分别表示最小经度、最小纬度、最大经度和最大纬度),你会得到可以简单解析的 JSON 格式的响应。 JSON 是用于网络服务间数据传输的常用格式,比 XML 和其他格式更轻便。你可以从 http://en.wikipedia.org/wiki/JSON 获取更多关于 JSON 的内容。

你可以使用两个不同的视点来看华盛顿白宫的位置,通常从宾夕法尼亚大街南侧拍摄,或者从北侧拍摄。其坐标(纬度、经度)如下:

lt=38.897661
ln=-77.036564

为了转换成 API 调用需要的格式,需要在这些坐标值上减去或者加上一个数值,来获得以白宫为中心的正方形范围内的所有图像。调用如下:

http://www.panoramio.com/map/get_panoramas.php?order=popularity&set=public&from=0&to=20&minx=-77.037564&miny=38.896662&maxx=-77.035564&maxy=38.898662&size=medium

该调用返回在坐标边界内(±0.001)的前 20 幅图像,这些图像按照用户访问情况 排序。调用的响应格式如下:

{ "count": 349,
"photos": [{"photo_id": 7715073, "photo_title": "White House", "photo_url":
"http://www.panoramio.com/photo/7715073", "photo_file_url":
"http://mw2.google.com/mw-panoramio/photos/medium/7715073.jpg", "longitude":
-77.036583, "latitude": 38.897488, "width": 500, "height": 375, "upload_date":
"10 February 2008", "owner_id": 1213603, "owner_name": "***", "owner_url":
"http://www.panoramio.com/user/1213603"}
,
{"photo_id": 1303971, "photo_title": "White House balcony", "photo_url":
"http://www.panoramio.com/photo/1303971", "photo_file_url":
"http://mw2.google.com/mw-panoramio/photos/medium/1303971.jpg", "longitude":
-77.036353, "latitude": 38.897471, "width": 500, "height": 336, "upload_date":
"13 March 2007", "owner_id": 195000, "owner_name": "***", "owner_url":
"http://www.panoramio.com/user/195000"}
...
]}

为了解析这个 JSON 格式的响应,我们可以使用 simplejson 工具包,可以从 http://github.com/simplejson/simplejson 下载。在项目界面上,可以看到在线的说明文档。

如果你使用的 Python 是 2.6 或之后的版本,因为在这些后来版本中已经包含 JSON库,所以不需要使用 simplejson 工具包。如果想使用内置的 JSON 库,你只需要像这样导入即可:

import json

如果你想使用上面链接中的 simplejson 工具包(速度很快,并且比内置包含更新的内容),一个非常好的办法是使用可靠的方式导入它,如下:

try: import simplejson as json
except ImportError: import json

下面的代码将使用 Python 里的 urllib 工具包来处理请求,然后使用 simplejson 工具包解析返回结果:

import os
import urllib, urlparse
import simplejson as json
# 查询图像
url = 'http://www.panoramio.com/map/get_panoramas.php?order=popularity&\
    set=public&from=0&to=20&minx=-77.037564&miny=38.896662&\
    maxx=-77.035564&maxy=38.898662&size=medium'
c = urllib.urlopen(url)

# 从 JSON 中获得每个图像的 url
j = json.loads(c.read())
imurls = []
for im in j['photos']:
    imurls.append(im['photo_file_url'])

# 下载图像
for url in imurls:
    image = urllib.URLopener()
    image.retrieve(url, os.path.basename(urlparse.urlparse(url).path))
    print 'downloading:', url

通过 JSON 的输出可以看到,我们需要的是 photo_file_url 字段。运行上面的代码,在控制台上你应该能够看到类似下面的数据:

downloading: http://mw2.google.com/mw-panoramio/photos/medium/7715073.jpg
downloading: http://mw2.google.com/mw-panoramio/photos/medium/1303971.jpg
downloading: http://mw2.google.com/mw-panoramio/photos/medium/270077.jpg
downloading: http://mw2.google.com/mw-panoramio/photos/medium/15502.jpg
...
2.3.2 使用局部描述子匹配

我们刚才已经下载了这些图像,下面需要对这些图像提取局部描述子。在这种情况下,我们将使用前面部分讲述的 SIFT 特征描述子。我们假设已经对这些图像使用 SIFT 特征提取代码进行了处理,并且将特征保存在和图像同名(但文件名后缀是 .sift,而不是 .jpg)的文件中。假设 imlist 和 featlist 列表中包含这些文件名。我们可以对所有组合图像对进行逐个匹配,如下:

import sift
nbr_images = len(imlist)
matchscores = zeros((nbr_images,nbr_images))
for i in range(nbr_images):
    for j in range(i,nbr_images): # 仅仅计算上三角
        print 'comparing ', imlist[i], imlist[j]
        l1,d1 = sift.read_features_from_file(featlist[i])
        l2,d2 = sift.read_features_from_file(featlist[j])
        matches = sift.match_twosided(d1,d2)
        nbr_matches = sum(matches > 0)
        print 'number of matches = ', nbr_matches
        matchscores[i,j] = nbr_matches
    # 复制值
    for i in range(nbr_images):
        for j in range(i+1,nbr_images): # 不需要复制对角线
            matchscores[j,i] = matchscores[i,j]

我们将每对图像间的匹配特征数保存在 matchscores 数组中。因为该“距离度量”是 对称的,所以我们可以不在代码的最后部分复制数值,来将 matchscores 矩阵填充完 整;填充完整后的 matchscores 矩阵只是看起来更好。这些特定图像的 matchscores 矩阵里的数值如下:

662 0 0 2 0 0 0 0 1 0 0 1 2 0 3 0 19 1 0 2
0 901 0 1 0 0 0 1 1 0 0 1 0 0 0 0 0 0 1 2
0 0 266 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0
2 1 0 1481 0 0 2 2 0 0 0 2 2 0 0 0 2 3 2 0
0 0 0 0 1748 0 0 1 0 0 0 0 0 2 0 0 0 0 0 1
0 0 0 0 0 1747 0 0 1 0 0 0 0 0 0 0 0 1 1 0
0 0 0 2 0 0 555 0 0 0 1 4 4 0 2 0 0 5 1 0
0 1 0 2 1 0 0 2206 0 0 0 1 0 0 1 0 2 0 1 1
1 1 0 0 0 1 0 0 629 0 0 0 0 0 0 0 1 0 0 20
0 0 0 0 0 0 0 0 0 829 0 0 1 0 0 0 0 0 0 2
0 0 0 0 0 0 1 0 0 0 1025 0 0 0 0 0 1 1 1 0
1 1 0 2 0 0 4 1 0 0 0 528 5 2 15 0 3 6 0 0
2 0 0 2 0 0 4 0 0 1 0 5 736 1 4 0 3 37 1 0
0 0 1 0 2 0 0 0 0 0 0 2 1 620 1 0 0 1 0 0
3 0 0 0 0 0 2 1 0 0 0 15 4 1 553 0 6 9 1 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2273 0 1 0 0
19 0 0 2 0 0 0 2 1 0 1 3 3 0 6 0 542 0 0 0
1 0 0 3 0 1 5 0 0 0 1 6 37 1 9 1 0 527 3 0
0 1 0 2 0 1 1 1 0 0 1 0 1 0 1 0 0 3 1139 0
2 2 0 0 1 0 0 1 20 2 0 0 0 0 0 0 0 0 0 499

使用该 matchscores 矩阵作为图像间简单的距离度量方式(具有相似内容的图像间拥有更多的匹配特征数),下面我们可以使用相似的视觉内容来将这些图像连接起来。

2.3.3 可视化连接的图像

我们首先通过图像间是否具有匹配的局部描述子来定义图像间的连接,然后可视化这些连接情况。为了完成可视化,我们可以在图中显示这些图像,图的边代表连接。我们将会使用 pydot 工具包(http://code.google.com/p/pydot/),该工具包是功能强大的 GraphViz 图形库的 Python 接口。 Pydot 使用 Pyparsing(http://pyparsing.wikispaces.com/)和 GraphViz(http://www.graphviz.org/);不用担心,这些都非常容易安装,只需要几分钟就可以安装成功。

Pydot 非常容易使用。下面的一小段代码很好地展示了这一点。该代码会创建一个图,该图表示深度为 2 的树,具有 5 个分支,将分支的编号添加到分支节点上。图的结构如图 2-9 所示。我们有很多方法来修改图的布局和外观。如果你想了解更多内容,可以查看 Pydot 的说明文档,或者在 http://www.graphviz.org/Documentation.php 查看 GraphViz 使用的 DOT 语言介绍。

import pydot
g = pydot.Dot(graph_type='graph')
g.add_node(pydot.Node(str(0),fontcolor='transparent'))
for i in range(5):
    g.add_node(pydot.Node(str(i+1)))
    g.add_edge(pydot.Edge(str(0),str(i+1)))
    for j in range(5):
        g.add_node(pydot.Node(str(j+1)+'-'+str(i+1)))
        g.add_edge(pydot.Edge(str(j+1)+'-'+str(i+1),str(j+1)))
g.write_png('graph.jpg',prog='neato')

我们接下来继续探讨地理标记图像处理的例子。为了创建显示可能图像组的图,如果匹配的数目高于一个阈值,我们使用边来连接相应的图像节点。为了得到图中的图像,需要使用图像的全路径(在下面例子中,使用 path 变量表示)。为了使图像看起来漂亮,我们需要将每幅图像尺度化为缩略图形式,缩略图的最大边为 100 像素。下面是具体实现代码:

import pydot
threshold = 2 # 创建关联需要的最小匹配数目
g = pydot.Dot(graph_type='graph') # 不使用默认的有向图

for i in range(nbr_images):
    for j in range(i+1,nbr_images):
        if matchscores[i,j] > threshold:
            # 图像对中的第一幅图像
            im = Image.open(imlist[i])
            im.thumbnail((100,100))
            filename = str(i)+'.png'
            im.save(filename) # 需要一定大小的临时文件
            g.add_node(pydot.Node(str(i),fontcolor='transparent',shape='rectangle',image=path+filename))
            # 图像对中的第二幅图像
            im = Image.open(imlist[j])
            im.thumbnail((100,100))
            filename = str(j)+'.png'
            im.save(filename) # 需要一定大小的临时文件
            g.add_node(pydot.Node(str(j),fontcolor='transparent',shape='rectangle',image=path+filename))
            g.add_edge(pydot.Edge(str(i),str(j)))

g.write_png('whitehouse.png')

代码运行结果如图 2-10 所示。图的具体内容和结构取决于你下载的图像。对于这个特定的例子,我们使用两组图像,每组分别是两个视角的白宫图像。

这个应用是使用局部描述子来匹配图像间区域的一个简单例子。在该应用中,我们没有使用针对任何匹配的限制约束。匹配的约束(具有很强的稳健性)可以通过接下来两章中的内容来实现。

练习
  1. 为了让匹配具有更强的稳健性,修改用于匹配 Harris 角点的函数,使其输入参数中包含认为两点存在对应关系允许的最大像素距离。
  2. 对一幅图像不断地应用模糊操作(或者 ROF 去噪),使得模糊效果越来越强,然后提取 Harris 角点,会出现什么问题?
  3. 另一种 Harris 角点检测器是快速角点检测器。有很多快速角点检测器的实现方法,包括纯 Python 语言实现的版本,可以在 http://www.edwardrosten.com/work/fast.html 下载。尝试使用该检测器,使用敏感性的阈值,然后将结果和 Harris 角点检测器检测出的角点比较。
  4. 以不同分辨率创建一幅图像的副本(例如,可以尝试多次将图像的尺寸减半)。对每幅图像提取 SIFT 特征。绘制以及匹配特征,来发现尺度的独立性是如何以及何时失效的。
  5. VLFeat 命令行工具同样实现了最大稳定极值区域(MSER, http://en.wikipedia.org/wiki/Maximally_stable_extremal_regions) 算 法。 该 算 法 是 个 能 够 找 到 角点一侧区域的区域检测器。创建一个用于提取 MSER 区域的函数,然后使用-read-frames 选项将它们传递给 SIFT 特征描述子部分,最后写出一个用于绘制该区域边界的函数。
  6. 基于对应关系,写出在图像对间匹配特征的函数,以实现估计尺度差异以及场景的平面旋转。
  7. 任意选取一个位置,然后下载该位置的图像,像白宫例子一样将它们匹配起来。你能发现用于连接这些图像的更好方式吗?你是如何利用图来选取用于地理位置具有代表性的图像的?
第三章:图像到图像的映射

本章讲解图像之间的变换,以及一些计算变换的实用方法。这些变换可以用于图像 扭曲变形和图像配准。 最后,我们将会介绍一个自动创建全景图像的例子。

3.1 单应性变换

单应性变换是将一个平面内的点映射到另一个平面内的二维投影变换。在这里,平面是指图像或者三维中的平面表面。 单应性变换具有很强的实用性,比如图像配准、 图像纠正和纹理扭曲,以及创建全景图像。我们将频繁地使用单应性变换。 本质上, 单应性变换 H,按照下面的方程映射二维中的点(齐次坐标意义下):

_images/1541119944695.jpg

对于图像平面内(甚至是三维中的点,后面我们会介绍到)的点,齐次坐 标是个非常有用的表示方式。 点的齐次坐标是依赖于其尺度定义的,所以, x=[x,y,w]=[αx,αy,αw]=[x/w,y/w,1] 都表示同一个二维点。因此, 单应性矩阵 H 也仅 依赖尺度定义,所以,单应性矩阵具有 8 个独立的自由度。我们通常使用 w=1 来归 一化点,这样, 点具有唯一的图像坐标 x 和 y。这个额外的坐标使得我们可以简单 地使用一个矩阵来表示变换。

创建 homography.py 文件。下面的函数可以实现对点进行归一化和转换齐次坐标的功能,将其添加到 homography.py 文件中:

def normalize(points):
    """ 在齐次坐标意义下,对点集进行归一化,使最后一行为 1 """
    for row in points:
        row /= points[-1]
    return points

def make_homog(points):
    """ 将点集(dim×n 的数组)转换为齐次坐标表示 """

    return vstack((points,ones((1,points.shape[1]))))

进行点和变换的处理时,我们会按照列优先的原则存储这些点。因此,n 个二维点 集将会存储为齐次坐标意义下的一个 3×n 数组。 这种格式使得矩阵乘法和点的变换 操作更加容易。对于其他的例子,比如对于聚类和分类的特征,我们将使用典型的 行数组来存储数据。

在这些投影变换中,有一些特别重要的变换。比如,仿射变换:

_images/1541121999594.jpg

保持了 w=1, 不具有投影变换所具有的强大变形能力。仿射变换包含一个可逆矩阵 A和一个平移向量 t=[tx,ty]。 仿射变换可以用于很多应用,比如图像扭曲。

相似变换:

_images/1541122132955.jpg

是一个包含尺度变化的二维刚体变换。上式中的向量 s 指定了变换的尺度,R 是角 度为 θ 的旋转矩阵, t=[tx,ty] 在这里也是一个平移向量。如果 s=1,那么该变换能够 保持距离不变。此时,变换为刚体变换。 相似变换可以用于很多应用,比如图像 配准。

下面让我们一起探讨如何设计用于估计单应性矩阵的算法,然后看一下使用仿射变 换进行图像扭曲,使用相似变换进行图像匹配, 以及使用完全投影变换进行创建全 景图像的一些例子。

3.1.1 直接线性变换算法

单应性矩阵可以由两幅图像(或者平面)中对应点对计算出来。前面已经提到过, 一个完全射影变换具有 8 个自由度。 根据对应点约束,每个对应点对可以写出两个 方程,分别对应于 x 和 y 坐标。因此,计算单应性矩阵 H 需要4个对应点对。

DLT(Direct Linear Transformation,直接线性变换)是给定4个或者更多对应点对 矩阵,来计算单应性矩阵 H 的算法。 将单应性矩阵 H 作用在对应点对上,重新写出 该方程,我们可以得到下面的方程:

_images/1541122425863.jpg

或者 Ah=0,其中 A 是一个具有对应点对二倍数量行数的矩阵。将这些对应点对方 程的系数堆叠到一个矩阵中,我们可以使用 SVD(Singular Value Decomposition, 奇异值分解)算法找到 H 的最小二乘解。 下面是该算法的代码。将下面的函数添加 到 homography.py 文件中:

def H_from_points(fp,tp):
    """ 使用线性 DLT 方法,计算单应性矩阵 H,使 fp 映射到 tp。点自动进行归一化 """

    if fp.shape != tp.shape:
        raise RuntimeError('number of points do not match')
    # 对点进行归一化(对数值计算很重要)
    # --- 映射起始点 ---
    m = mean(fp[:2], axis=1)
    maxstd = max(std(fp[:2], axis=1)) + 1e-9 C1 = diag([1/maxstd, 1/maxstd, 1]) C1[0][2] = -m[0]/maxstd
    C1[1][2] = -m[1]/maxstd
    fp = dot(C1,fp)

    # --- 映射对应点 ---
    m = mean(tp[:2], axis=1)
    maxstd = max(std(tp[:2], axis=1)) + 1e-9

    C2 = diag([1/maxstd, 1/maxstd, 1])
    C2[0][2] = -m[0]/maxstd
    C2[1][2] = -m[1]/maxstd
    tp = dot(C2,tp)

    # 创建用于线性方法的矩阵,对于每个对应对,在矩阵中会出现两行数值 nbr_correspondences = fp.shape[1]
    A = zeros((2*nbr_correspondences,9))
    for i in range(nbr_correspondences):
        A[2*i] = [-fp[0][i],-fp[1][i],-1,0,0,0,tp[0][i]*fp[0][i],tp[0][i]*fp[1][i],tp[0][i]]
        A[2*i+1] = [0,0,0,-fp[0][i],-fp[1][i],-1,tp[1][i]*fp[0][i],tp[1][i]*fp[1][i],tp[1][i]]

    U,S,V = linalg.svd(A)
    H = V[8].reshape((3,3))

    # 反归一化
    H = dot(linalg.inv(C2),dot(H,C1))

    # 归一化,然后返回
    return H / H[2,2]

上面函数的第一步操作是检查点对的两个数组中点的数目是否相同。如果不相同, 函数将会抛出异常信息。 这对于写出稳健的代码来说非常有用。但是,为了使得代 码例子更简单、更容易理解, 我们在本书中仅在很少的例子中使用异常处理技巧。 你可以在 http://docs.python.org/library/exceptions.html 查阅更多关于异常类型的内 容,以及在 http://docs.python.org/tutorial/errors.html 上了解如何使用它们。

对这些点进行归一化操作,使其均值为 0,方差为 1。因为算法的稳定性取决于坐 标的表示情况和部分数值计算的问题, 所以归一化操作非常重要。接下来我们使用 对应点对来构造矩阵 A。最小二乘解即为矩阵 SVD 分解后所得矩阵 V 的最后一行。 该行经过变形后得到矩阵 H。然后对这个矩阵进行处理和归一化,返回输出。

3.1.2 仿射变换

由于仿射变换具有 6 个自由度,因此我们需要三个对应点对来估计矩阵 H。通过将 最后两个元素设置为 0,即 h7=h8=0,仿射变换可以用上面的 DLT 算法估计得出。

这里我们将使用不同的方法来计算单应性矩阵 H,这在文献 [13] 中有详细的描 述(第 130 页)。 下面的函数使用对应点对来计算仿射变换矩阵,将其添加到 homograph.py 文件中:

def Haffine_from_points(fp,tp):
    """ 计算 H,仿射变换,使得 tp 是 fp 经过仿射变换 H 得到的 """
    if fp.shape != tp.shape:
        raise RuntimeError('number of points do not match')

        # 对点进行归一化
        # --- 映射起始点 ---
        m = mean(fp[:2], axis=1)
        maxstd = max(std(fp[:2], axis=1)) + 1e-9
        C1 = diag([1/maxstd, 1/maxstd, 1])
        C1[0][2] = -m[0]/maxstd

        C1[1][2] = -m[1]/maxstd
        fp_cond = dot(C1,fp)

        # --- 映射对应点 ---
        m = mean(tp[:2], axis=1)
        C2 = C1.copy() # 两个点集,必须都进行相同的缩放

        C2[0][2] = -m[0]/maxstd
        C2[1][2] = -m[1]/maxstd
        tp_cond = dot(C2,tp)

        # 因为归一化后点的均值为 0,所以平移量为 0
        A = concatenate((fp_cond[:2],tp_cond[:2]), axis=0)
        U,S,V = linalg.svd(A.T)

        # 如 Hartley 和 Zisserman 著的 Multiple View Geometry in Computer , Scond Edition 所示,
        # 创建矩阵B和C
        tmp = V[:2].T
        B = tmp[:2]
        C = tmp[2:4]
        tmp2 = concatenate((dot(C,linalg.pinv(B)),zeros((2,1))), axis=1)
        H = vstack((tmp2,[0,0,1]))

        # 反归一化
        H = dot(linalg.inv(C2),dot(H,C1))
        return H / H[2,2]

同样地,类似于 DLT 算法,这些点需要经过预处理和去处理化操作。在下一节中, 让我们一起来看这些仿射变换是如何处理图像的。

3.2 图像扭曲

对图像块应用仿射变换,我们将其称为图像扭曲(或者仿射扭曲)。该操作不仅经常应用在计算机图形学中, 而且经常出现在计算机视觉算法中。扭曲操作可以使用 SciPy 工具包中的 ndimage 包来简单完成。命令:

transformed_im = ndimage.affine_transform(im,A,b,size)

使用如上所示的一个线性变换 A 和一个平移向量 b 来对图像块应用仿射变换。选项 参数 size 可以用来指定输出图像的大小。 默认输出图像设置为和原始图像同样大小。为了研究该函数是如何工作的,我们可以试着运行下面的命令:

from scipy import ndimage
im = array(Image.open('empire.jpg').convert('L'))
H = array([[1.4,0.05,-100],[0.05,1.5,-100],[0,0,1]])
im2 = ndimage.affine_transform(im,H[:2,:2],(H[0,2],H[1,2]))

figure()
gray()
imshow(im2)
show()

该命令输出结果图像如图 3-1(右)所示。可以看到,输出图像结果中丢失的像素用 零来填充。

_images/1541123400179.jpg
3.2.1 图像中的图像

仿射扭曲的一个简单例子是,将图像或者图像的一部分放置在另一幅图像中,使得它们能够和指定的区域或者标记物对齐。

将函数 image_in_image() 添加到 warp.py 文件中。该函数的输入参数为两幅图像和 一个坐标。 该坐标为将第一幅图像放置到第二幅图像中的角点坐标:

def image_in_image(im1,im2,tp):
    """ 使用仿射变换将 im1 放置在 im2 上,使 im1 图像的角和 tp 尽可能的靠近
    tp 是齐次表示的,并且是按照从左上角逆时针计算的
    """
    # 扭曲的点
    m,n = im1.shape[:2]
    fp = array([[0,m,m,0],[0,0,n,n],[1,1,1,1]])

    # 计算仿射变换,并且将其应用于图像 im1
    H = homography.Haffine_from_points(tp,fp)
    im1_t = ndimage.affine_transform(im1,H[:2,:2],(H[0,2],H[1,2]),im2.shape[:2])
    alpha = (im1_t > 0)

    return (1-alpha)*im2 + alpha*im1_t

正如你所看到的,该函数没有很多繁杂的操作。将扭曲的图像和第二幅图像融合, 我们就创建了 alpha 图像。 该图像定义了每个像素从各个图像中获取的像素值成分 多少。这里我们基于以下事实, 扭曲的图像是在扭曲区域边界之外以 0 来填充的图 像,来创建一个二值的 alpha 图像。 严格意义上说,我们需要在第一幅图像中的潜 在 0 像素上加上一个小的数值, 或者合理地处理这些 0 像素(参见本章结尾的练习 部分)。注意,这里我们使用的图像坐标是齐次坐标意义下的。

试着使用该函数将公告牌中的一幅图像插入另一幅图像。下面几行代码会将图 3-2 中最左端的图像插入到第二幅图像中。 这些坐标值是通过查看绘制的图像(在 PyLab 图像中,鼠标的坐标显示在图像底部附近)手工确定的。 当然,也可以用 PyLab 类 库中的 ginput() 函数获得。

_images/1541123726904.jpg
import warp
# 仿射扭曲im1到im2的例子
im1 = array(Image.open('beatles.jpg').convert('L'))
im2 = array(Image.open('billboard_for_rent.jpg').convert('L'))

# 选定一些目标点
tp = array([[264,538,540,264],[40,36,605,605],[1,1,1,1]])

im3 = warp.image_in_image(im1,im2,tp)

figure()
gray()
imshow(im3)
axis('equal')
axis('off')
show()

上面的代码将图像放置在公告牌的上半部分。需要注意,标记物的坐标 tp 是用齐次 坐标意义下的坐标表示的。将这些坐标换成:

tp = array([[675,826,826,677],[55,52,281,277],[1,1,1,1]])

会将图像放置在公告牌的左下“for rent”部分。

函数 Haffine_from_points() 会返回给定对应点对的最优仿射变换。在上面的例子 中,对应点对为图像和公告牌的角点。 如果透视效应比较弱,那么这种方法会返回 很好的结果。图 3-3 的上面一行显示出,在具有很强透视效应的情况下, 在公告牌 图像上使用射影变换输出图像的情况。在这种情况下, 我们不可能使用同一个仿射 变换将全部 4 个角点变换到它们的目标位置(尽管我们可以使用完全投影变换来完 成该任务)。 所以,当你打算使用仿射变换时,有一个很有用的技巧。

_images/1541123924692.jpg

对于三个点,仿射变换可以将一幅图像进行扭曲,使这三对对应点对可以完美地匹配上。 这是因为,仿射变换具有 6 个自由度,三个对应点对可以给出 6 个约束条件 (对于这三个对应点对,x 和 y 坐标必须都要匹配)。 所以,如果你真的打算使用仿 射变换将图像放置到公告牌上,可以将图像分成两个三角形,然后对它们分别进行 扭曲图像操作。下面是具体实现的代码:

# 选定 im1 角上的一些点
m,n = im1.shape[:2]
fp = array([[0,m,m,0],[0,0,n,n],[1,1,1,1]])

# 第一个三角形
tp2 = tp[:,:3]
fp2 = fp[:,:3]
# 计算H
H = homography.Haffine_from_points(tp2,fp2)
im1_t = ndimage.affine_transform(im1,H[:2,:2],(H[0,2],H[1,2]),im2.shape[:2])

# 三角形的 alpha
alpha = warp.alpha_for_triangle(tp2,im2.shape[0],im2.shape[1])

im3 = (1-alpha)*im2 + alpha*im1_t
# 第二个三角形
tp2 = tp[:,[0,2,3]] fp2 = fp[:,[0,2,3]]

# 计算H
H = homography.Haffine_from_points(tp2,fp2)
im1_t = ndimage.affine_transform(im1,H[:2,:2],(H[0,2],H[1,2]),im2.shape[:2])

# 三角形的 alpha 图像
alpha = warp.alpha_for_triangle(tp2,im2.shape[0],im2.shape[1])
im4 = (1-alpha)*im3 + alpha*im1_t

figure()
gray()
imshow(im4)
axis('equal')
axis('off')
show()

这里我们简单地为每个三角形创建了 alpha 图像,然后将所有的图像合并起来。 该三 角形的 alpha 图像可以简单地通过检查像素的坐标是否能够写成三角形顶点坐标的凸 组合来计算得出 1。 如果坐标可以表示成这种形式,那么该像素就位于三角形的内部。 上面的例子使用了下面的函数 alpha_for_triangle(),将其添加到 warp.py 文件中。

def alpha_for_triangle(points,m,n):
    """ 对于带有由 points 定义角点的三角形,创建大小为 (m,n) 的 alpha 图
    (在归一化的齐次坐标意义下)
    """
    alpha = zeros((m,n))
    for i in range(min(points[0]),max(points[0])):
        for j in range(min(points[1]),max(points[1])):
            x = linalg.solve(points,[i,j,1])
            if min(x) > 0: # 所有系数都大于零
                alpha[i,j] = 1
    return alpha

你的显卡可以极其快速地操作上面的代码。Python 语言的处理速度比你的显卡(或 者 C/C++ 实现)慢很多, 但是对于我们来说已经够用了。正如在图 3-3 下半部分所 看到的,角点可以很好地匹配。

3.2.2 分段仿射扭曲

正如上面的例子所示,三角形图像块的仿射扭曲可以完成角点的精确匹配。 让我们 看一下对应点对集合之间最常用的扭曲方式:分段仿射扭曲。给定任意图像的标记 点,通过将这些点进行三角剖分, 然后使用仿射扭曲来扭曲每个三角形,我们可以 将图像和另一幅图像的对应标记点扭曲对应。对于任何图形和图像处理库来说, 这些都是最基本的操作。下面我们来演示一下如何使用Matplotlib 和 SciPy 来完成该 操作。

为了三角化这些点,我们经常使用狄洛克三角剖分方法。 在 Matplotlib(但是不在 PyLab 库中)中有狄洛克三角剖分,我们可以用下面的方式使用它:

import matplotlib.delaunay as md
x,y = array(random.standard_normal((2,100)))
centers,edges,tri,neighbors = md.delaunay(x,y)

figure()
for t in tri:
    t_ext = [t[0], t[1], t[2], t[0]] # 将第一个点加入到最后
    plot(x[t_ext],y[t_ext],'r')

plot(x,y,'*')
axis('off')
show()

图 3-4 显示了一些实例点和三角剖分的结果。狄洛克三角剖分选择一些三角形, 使三角剖分中所有三角形的最小角度最大 1。 函数 delaunay() 有 4 个输出,其中 我们仅需要三角形列表信息(第三个输出)。 在 warp.py 文件中创建用于三角剖分 的函数:

import matplotlib.delaunay as md
def triangulate_points(x,y):
    """ 二维点的 Delaunay 三角剖分 """
    centers,edges,tri,neighbors = md.delaunay(x,y)
    return tri

函数输出的是一个数组,该数组的每一行包含对应数组 x 和 y 中每个三角形三个点 的切片。

现在让我们将该算法应用于一个例子,在该例子中,在 5×6 的网格上使用 30 个控 制点,将一幅图像扭曲到另一幅图像中的非平坦区域。 图 3-5b 所示的是将一幅图像 扭曲到“turning torso”的表面。目标点是使用 ginput() 函数手工选取出来的, 将结果保存在 turningtorso_points.txt 文件中。

首先,我们需要写出一个用于分段仿射图像扭曲的通用扭曲函数。下面的代码可以 实现该功能。 在该代码中,我们也展示了如何扭曲一幅彩色图像(你仅需要对每个 颜色通道进行扭曲)。

def pw_affine(fromim,toim,fp,tp,tri):
    """ 从一幅图像中扭曲矩形图像块
    fromim= 将要扭曲的图像
    toim= 目标图像
    fp= 齐次坐标表示下,扭曲前的点
    tp= 齐次坐标表示下,扭曲后的点
    tri= 三角剖分
    """
    im = toim.copy()

    # 检查图像是灰度图像还是彩色图象
    is_color = len(fromim.shape) == 3

    # 创建扭曲后的图像(如果需要对彩色图像的每个颜色通道进行迭代操作,那么有必要这样做)
    im_t = zeros(im.shape, 'uint8')

    for t in tri:
        # 计算仿射变换
        H = homography.Haffine_from_points(tp[:,t],fp[:,t])
        if is_color:
            for col in range(fromim.shape[2]):
                im_t[:,:,col] = ndimage.affine_transform(fromim[:,:,col],H[:2,:2],\
                    (H[0,2],H[1,2]),im.shape[:2])
        else:
            im_t = ndimage.affine_transform(
                fromim,H[:2,:2],(H[0,2],H[1,2]),im.shape[:2])

    # 三角形的 alpha
    alpha = alpha_for_triangle(tp[:,t],im.shape[0],im.shape[1])

    # 将三角形加入到图像中
    im[alpha>0] = im_t[alpha>0]

    return im

在该代码中,我们首先检查该图像是灰度图像还是彩色图像。如果图像为彩色图像, 则对每个颜色通道进行扭曲处理。 因为对于每个三角形来说,仿射变换是唯一确定 的,所以我们这里使用 Haffine_from_points() 函数来处理。 将上面的函数添加到 warp.py 文件中。

为了将该函数应用到当前例子中,接下来的简短脚本将这些操作统一起来:

import homography
import warp

# 打开图像,并将其扭曲
fromim = array(Image.open('sunset_tree.jpg'))
x,y = meshgrid(range(5),range(6))
x = (fromim.shape[1]/4) * x.flatten()
y = (fromim.shape[0]/5) * y.flatten()

# 三角剖分
tri = warp.triangulate_points(x,y)

# 打开图像和目标点
im = array(Image.open('turningtorso1.jpg'))

tp = loadtxt('turningtorso1_points.txt') # destination points

# 将点转换成齐次坐标
fp = vstack((y,x,ones((1,len(x)))))
tp = vstack((tp[:,1],tp[:,0],ones((1,len(tp)))))

# 扭曲三角形
im = warp.pw_affine(fromim,im,fp,tp,tri)

# 绘制图像
figure()
imshow(im)
warp.plot_mesh(tp[1],tp[0],tri)
axis('off')
show()

输出结果如图 3-5c 所示。我们通过下面的辅助函数(将其添加到 warp.py 文件中) 来绘制出图像中的这些三角形:

def plot_mesh(x,y,tri):
    """ 绘制三角形 """
    for t in tri:
        t_ext = [t[0], t[1], t[2], t[0]] # 将第一个点加入到最后
        plot(x[t_ext],y[t_ext],'r')
_images/1541125396988.jpg

这个例子应该能够帮助你在应用中做图像的分段仿射扭曲。我们可以对该例子中的 函数进行改进。 我们将其中一部分留作练习,剩下的留给你自己解决。

3.2.3 图像配准

图像配准是对图像进行变换,使变换后的图像能够在常见的坐标系中对齐。 配准可 以是严格配准,也可以是非严格配准。为了能够进行图像对比和更精细的图像分析, 图像配准是一步非常重要的操作。

让我们一起看一个对多个人脸图像进行严格配准的例子。该配准使得我们计算的平均人脸和人脸表观的变化具有意义。 因为,图像中的人脸并不都有相同的大小、位 置和方向,所以,在这种类型的配准中, 我们实际上是寻找一个相似变换(带有尺 度变化的刚体变换),在对应点对之间建立映射。

在 jkface.zip 文件中有 366 幅单人图像(2008 年,每天一幅)。1 这些图像都对眼睛 和嘴的坐标进行了标记, 结果保存在 jkface.xml 文件中。使用这些点,我们可以计 算出一个相似变换, 然后将可以使用该变换(包含尺度变换)的这些图像扭曲到一 个归一化的坐标系中。为了读取 XML 格式的文件, 我们将会使用 Python 中内置 xml.dom 模块中的 minidom 类库。

该 XML 文件看起来类似于下面的格式:

<?xml version="1.0" encoding="utf-8"?>
<faces>
<face file="jk-002.jpg" xf="46" <face file="jk-006.jpg" xf="38" <face file="jk-004.jpg" xf="40" <face file="jk-010.jpg" xf="33"
xm="56" xs="67" yf="38" ym="65" ys="39"/>
xm="48" xs="59" yf="38" ym="65" ys="38"/>
xm="50" xs="61" yf="38" ym="66" ys="39"/>
xm="44" xs="55" yf="38" ym="65" ys="38"/>
...
</faces>

为了从该文件中读取这些坐标,我们需要将使用 minidom 的函数添加到新文件imregistration.py 中:

from xml.dom import minidom
def read_points_from_xml(xmlFileName):
    """ 读取用于人脸对齐的控制点 """

    xmldoc = minidom.parse(xmlFileName)
    facelist = xmldoc.getElementsByTagName('face')
    faces = {}

    for xmlFace in facelist:
        fileName = xmlFace.attributes['file'].value
        xf = int(xmlFace.attributes['xf'].value)
        yf = int(xmlFace.attributes['yf'].value)
        xs = int(xmlFace.attributes['xs'].value)
        ys = int(xmlFace.attributes['ys'].value)
        xm = int(xmlFace.attributes['xm'].value)
        ym = int(xmlFace.attributes['ym'].value)
        faces[fileName] = array([xf, yf, xs, ys, xm, ym])
    return faces

这些标记点会在 Python 中以字典的形式返回,字典的键值为图像的文件名。 格式 为:图像中左眼(人脸右侧)的坐标为 xf 和 yf,右眼的坐标为 xs 和 ys,嘴的坐标 为 xm 和 ym。 为了计算相似变换中的参数,我们可以使用最小二乘解来解决。对于 每个点 xi=[xi, yi](在这个例子中,每幅图像有三个点), 这些点应该被映射到目标位tt置[xi, yi],如下所示:

_images/1541125671577.jpg

将这三个点都表示成该形式,我们可以重新将其写成方程组的形式。该方程组中含有 a、b、tx、ty 未知量,如下所示:

_images/1541125717262.jpg

下面我们使用相似矩阵的参数化表示方式:

_images/1541125761369.jpg

如果存在更多的对应点对,其计算公式相同,只需在矩阵中额外添加几行。 你可以 使用 linalg.lstsq() 函数来计算该问题的最小二乘解。 使用最小二乘解的思想是一 个标准技巧, 我们还会在本书中多次使用。实际上,这和之前在 DLT 算法中使用的 方式相同。

函数的具体代码如下(将其添加到 imregistration.py 文件中):

from scipy import linalg
def compute_rigid_transform(refpoints,points):
    """ 计算用于将点对齐到参考点的旋转、尺度和平移量 """
    A = array([ [points[0], -points[1], 1, 0],
                [points[1],  points[0], 0, 1],
                [points[2], -points[3], 1, 0],
                [points[3],  points[2], 0, 1],
                [points[4], -points[5], 1, 0],
                [points[5],  points[4], 0, 1]])

    y = array([ refpoints[0],
                refpoints[1],
                refpoints[2],
                refpoints[3],
                refpoints[4],
                refpoints[5]])

    # 计算最小化 ||Ax-y|| 的最小二乘解
    a,b,tx,ty = linalg.lstsq(A,y)[0]
    R = array([[a, -b], [b, a]]) # 包含尺度的旋转矩阵

    return R,tx,ty

该函数返回一个具有尺度的旋转矩阵,以及在 x 和 y 方向上的平移量。为了扭曲图 像,并保存对齐后的新图像, 我们可以对每个颜色通道(这些图像都是彩色图像) 应用 ndimage.affine_transform() 函数操作。作为参考坐标系, 你可以使用任何三 个点的坐标。这里我们为了简单起见,直接使用第一幅图像中的标记位置:

from scipy import ndimage
from scipy.misc import imsave
import os

def rigid_alignment(faces,path,plotflag=False):
    """ 严格对齐图像,并将其保存为新的图像
        path 决定对齐后图像保存的位置
        设置 plotflag=True,以绘制图像
    """
    # 将第一幅图像中的点作为参考点 refpoints = faces.values()[0]
    # 使用仿射变换扭曲每幅图像
    for face in faces:
        points = faces[face]
    R,tx,ty = compute_rigid_transform(refpoints, points)
    T = array([[R[1][1], R[1][0]], [R[0][1], R[0][0]]])

    im = array(Image.open(os.path.join(path,face)))
    im2 = zeros(im.shape, 'uint8')

    # 对每个颜色通道进行扭曲
    for i in range(len(im.shape)):
        im2[:,:,i] = ndimage.affine_transform(im[:,:,i],linalg.inv(T),offset=[-ty,-tx])

    if plotflag:
        imshow(im2)
        show()
    # 裁剪边界,并保存对齐后的图像
    h,w = im2.shape[:2]
    border = (w+h)/20

    # 裁剪边界
    imsave(os.path.join(path, 'aligned/'+face),im2[border:h-border,border:w-border,:])

这里我们使用 imsave() 函数来将对齐后的图像保存到 aligned 子文件夹中。

接下来的简短脚本会读取 XML 文件,其中文件名为键,点的坐标为键值。然后配准所有的图像,将它们与第一幅图像对齐:

import imregistration

# 载入控制点的位置
xmlFileName = 'jkfaces2008_small/jkfaces.xml'
points = imregistration.read_points_from_xml(xmlFileName)

# 注册
imregistration.rigid_alignment(points,'jkfaces2008_small/')

运行这些代码,你能够在子目录中得到这些对齐后的人脸图像。图 3-6 所示为配准 前后的 6 幅样本图像。 由于配准后图像的边界可能会出现不想要的黑色填充像素, 所以我们对配准后的图像进行轻微的修剪,来删除这些黑色填充像素。

_images/1541126160462.jpg

现在让我们看配准操作如何影响平均图像。图 3-7 为未对齐人脸图像的平均图像, 旁边是对齐后图像的平均图像。 (注意,由于对齐后图像的边界有裁剪,所以两幅图 像的大小有差异)尽管在原始图像中,人脸的尺寸、方向和位置变化都很小, 但是 配准操作对平均图像的计算结果影响很大。

自然地,使用未准确配准的图像同样对主成分的计算结果有着很大的影响。图 3-8 表示, 从未经过配准和经过配准的数据集中选取前 150 幅图像,PCA 的计算结果。 正如平均图像一样,未配准的 PCA 模式是模糊的。 在计算主成分时,我们使用以平 均人脸位置为中心的椭圆掩膜。在堆叠这些图像之前,将这些图像和掩膜相乘, 我 们能够避免将背景变化带入到 PCA 模式中。将 1.3 节 PCA 例子中创建矩阵的一行 替换为:

immatrix = array([mask*array(Image.open(imlist[i]).convert('L')).flatten() for i in range(150)],'f')

其中 mask 是一副同样大小的二值图像,已经经过压平处理。

3.3 创建全景图

在同一位置(即图像的照相机位置相同)拍摄的两幅或者多幅图像是单应性相关的 (如图 3-9 所示)。 我们经常使用该约束将很多图像缝补起来,拼成一个大的图像来创建全景图像。在本节中,我们将探讨如何创建全景图像。

_images/1541126364831.jpg
3.3.1 RANSAC

RANSAC 是“RANdom SAmple Consensus”(随机一致性采样)的缩写。该方法是 用来找到正确模型来拟合带有噪声数据的迭代方法。 给定一个模型,例如点集之间 的单应性矩阵,RANSAC 基本的思想是,数据中包含正确的点和噪声点, 合理的模 型应该能够在描述正确数据点的同时摒弃噪声点。

RANSAC 的标准例子:用一条直线拟合带有噪声数据的点集。简单的最小二乘在该 例子中可能会失效, 但是 RANSAC 能够挑选出正确的点,然后获取能够正确拟合 的直线。下面来看使用 RANSAC 的例子。 你可以从 http://www.scipy.org/Cookbook/ RANSAC 下载 ransac.py,里面包含了特定的例子作为测试用例。 图 3-10 为运行 ransac.text() 的例子。可以看到,该算法只选择了和直线模型一致的数据点,成功 地找到了正确的解。

RANSAC 是个非常有用的算法,我们将在下一节估计单应性矩阵和其他一些例子中 使用它。 关于 RANSAC 更多的信息,参见 Fischler 和 Bolles 的原始论文 [11]、 维基 百科 http://en.wikipedia.org/wiki/RANSAC 或者技术报告 [40]。

3.3.2 稳健的单应性矩阵估计

我们在任何模型中都可以使用 RANSAC 模块。在使用 RANSAC 模块时, 我们只需 要在相应 Python 类中实现 fit() 和 get_error() 方法,剩下就是正确地使用 ransac.py。 我们这里使用可能的对应点集来自动找到用于全景图像的单应性矩阵。图 3-11 所示 为使用 SIFT 特征自动找到匹配对应。 这可以通过运行下面的命令来实现:

import sift
featname = ['Univ'+str(i+1)+'.sift' for i in range(5)]
imname = ['Univ'+str(i+1)+'.jpg' for i in range(5)]
l = {}
d = {}

for i in range(5):
    sift.process_image(imname[i],featname[i])
    l[i],d[i] = sift.read_features_from_file(featname[i])

matches = {}
for i in range(4):
    matches[i] = sift.match(d[i+1],d[i])

显然,并不是所有图像中的对应点对都是正确的。实际上,SIFT 是具有很强稳健性 的描述子,能够比其他描述子, 例如图像块相关的 Harris 角点,产生更少的错误的 匹配。但是该方法仍然远非完美。

_images/1541126589118.jpg

我们使用 RANSAC 算法来求解单应性矩阵,首先需要将下面模型类添加到homography.py 文件中:

class RansacModel(object):
    """ 用于测试单应性矩阵的类,其中单应性矩阵是由网站 http://www.scipy.org/Cookbook/RANSAC 上
    的 ransac.py 计算出来的
    """

    def __init__(self,debug=False):
        self.debug = debug def fit(self, data):

        """ 计算选取的 4 个对应的单应性矩阵 """
        # 将其转置,来调用 H_from_points() 计算单应性矩阵
        data = data.T
        # 映射的起始点
        fp = data[:3,:4]
        # 映射的目标点
        tp = data[3:,:4]
        # 计算单应性矩阵,然后返回
        return H_from_points(fp,tp)

    def get_error( self, data, H):
        """ 对所有的对应计算单应性矩阵,然后对每个变换后的点,返回相应的误差 """
        data = data.T
        # 映射的起始点
        fp = data[:3]
        # 映射的目标点
        tp = data[3:]
        # 变换fp

        fp_transformed = dot(H,fp)
        # 归一化齐次坐标
        for i in range(3):
            fp_transformed[i] /= fp_transformed[2]

        # 返回每个点的误差
        return sqrt( sum((tp-fp_transformed)**2,axis=0) )

可以看到,这个类包含 fit() 方法。该方法仅仅接受由 ransac.py 选择的4个对应点 对(data 中的前4个点对), 然后拟合一个单应性矩阵。记住,4个点对是计算单 应性矩阵所需的最少数目。 由于 get_error() 方法对每个对应点对使用该单应性矩 阵,然后返回相应的平方距离之和, 因此 RANSAC 算法能够判定哪些点对是正确 的,哪些是错误的。在实际中, 我们需要在距离上使用一个阈值来决定哪些单应性 矩阵是合理的。为了方便使用, 将下面的函数添加到 homography.py 文件中:

def H_from_ransac(fp,tp,model,maxiter=1000,match_theshold=10):
    """ 使用 RANSAC 稳健性估计点对应间的单应性矩阵 H(ransac.py 为从
        http://www.scipy.org/Cookbook/RANSAC 下载的版本)
        # 输入:齐次坐标表示的点 fp,tp(3×n 的数组)"""

        import ransac

        # 对应点组
        data = vstack((fp,tp))
        # 计算 H,并返回
        H,ransac_data = ransac.ransac(data.T,model,4,maxiter,match_theshold,10,
                return_all=True)

        return H,ransac_data['inliers']

该函数同样允许提供阈值和最小期望的点对数目。最重要的参数是最大迭代次数: 程序退出太早可能得到一个坏解; 迭代次数太多会占用太多时间。函数的返回结果 为单应性矩阵和对应该单应性矩阵的正确点对。

类似于下面的操作,你可以将 RANSAC 算法应用于对应点对上:

# 将匹配转换成齐次坐标点的函数
def convert_points(j):
    ndx = matches[j].nonzero()[0]
    fp = homography.make_homog(l[j+1][ndx,:2].T)
    ndx2 = [int(matches[j][i]) for i in ndx]
    tp = homography.make_homog(l[j][ndx2,:2].T)
    return fp,tp

# 估计单应性矩阵
model = homography.RansacModel()

fp,tp = convert_points(1)
H_12 = homography.H_from_ransac(fp,tp,model)[0] # im1 到 im2 的单应性矩阵

fp,tp = convert_points(0)
H_01 = homography.H_from_ransac(fp,tp,model)[0] # im0 到 im1 的单应性矩阵

tp,fp = convert_points(2) # 注意:点是反序的
H_32 = homography.H_from_ransac(fp,tp,model)[0] # im3 到 im2 的单应性矩阵

tp,fp = convert_points(3) # 注意:点是反序的
H_43 = homography.H_from_ransac(fp,tp,model)[0] # im4 到 im3 的单应性矩阵

在该例子中,图像 2 是中心图像,也是我们希望将其他图像变成的图像。图像 0 和 图像 1 应该从右边扭曲, 图像 3 和图像 4 从左边扭曲。在每个图像对中,由于匹配 是从最右边的图像计算出来的,所以我们将对应的顺序进行了颠倒, 使其从左边图 像开始扭曲。因为我们不关心该扭曲例子中的正确点对,所以仅需要该函数的第一 个输出(单应性矩阵)。

3.3.3 拼接图像

估计出图像间的单应性矩阵(使用 RANSAC 算法),现在我们需要将所有的图像扭 曲到一个公共的图像平面上。 通常,这里的公共平面为中心图像平面(否则,需要 进行大量变形)。一种方法是创建一个很大的图像,比如图像中全部填充 0, 使其和 中心图像平行,然后将所有的图像扭曲到上面。由于我们所有的图像是由照相机水平 旋转拍摄的, 因此我们可以使用一个较简单的步骤:将中心图像左边或者右边的区域填充0,以便为扭曲的图像腾出空间。 将下面的代码添加到 warp.py 文件中:

def panorama(H,fromim,toim,padding=2400,delta=2400):
    """ 使用单应性矩阵 H(使用 RANSAC 健壮性估计得出),协调两幅图像,创建水平全景图像。结果
        为一幅和 toim 具有相同高度的图像。padding 指定填充像素的数目,delta 指定额外的平移量
    """

    # 检查图像是灰度图像,还是彩色图像

    is_color = len(fromim.shape) == 3
    # 用于 geometric_transform() 的单应性变换

    def transf(p):
        p2 = dot(H,[p[0],p[1],1])
        return (p2[0]/p2[2],p2[1]/p2[2])

    if H[1,2]<0: # fromim 在右边
        print 'warp - right'
        # 变换 fromim
        if is_color:
            # 在目标图像的右边填充 0
            toim_t = hstack((toim,zeros((toim.shape[0],padding,3))))
            fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))
            for col in range(3):
                fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],
                    transf,(toim.shape[0],toim.shape[1]+padding))

        else:
            # 在目标图像的右边填充 0
            toim_t = hstack((toim,zeros((toim.shape[0],padding))))
            fromim_t = ndimage.geometric_transform(fromim,transf,
                        (toim.shape[0],toim.shape[1]+padding))

    else:
        print 'warp - left'
        # 为了补偿填充效果,在左边加入平移量
        H_delta = array([[1,0,0],[0,1,-delta],[0,0,1]])
        H = dot(H,H_delta)
        # fromim 变换
        if is_color:
            # 在目标图像的左边填充 0
            toim_t = hstack((zeros((toim.shape[0],padding,3)),toim))
            fromim_t = zeros((toim.shape[0],toim.shape[1]+padding,toim.shape[2]))
            for col in range(3):
                fromim_t[:,:,col] = ndimage.geometric_transform(fromim[:,:,col],
                    transf,(toim.shape[0],toim.shape[1]+padding))

        else:
            # 在目标图像的左边填充 0
            toim_t = hstack((zeros((toim.shape[0],padding)),toim))
            fromim_t = ndimage.geometric_transform(fromim,
                transf,(toim.shape[0],toim.shape[1]+padding))

            # 协调后返回(将 fromim 放置在 toim 上)
            if is_color:
                # 所有非黑色像素
                alpha = ((fromim_t[:,:,0] * fromim_t[:,:,1] * fromim_t[:,:,2] ) > 0)
                for col in range(3):
                    toim_t[:,:,col] = fromim_t[:,:,col]*alpha + toim_t[:,:,col]*(1-alpha)

            else:
                alpha = (fromim_t > 0)
                toim_t = fromim_t*alpha + toim_t*(1-alpha)

    return toim_t

书中 代码对齐补全,缩进可能会有问题

对于通用的 geometric_transform() 函数,我们需要指定能够描述像素到像素间映射 的函数。 在这个例子中,transf() 函数就是该指定的函数。该函数通过将像素和 H 相乘,然后对齐次坐标进行归一化来实现像素间的映射。 通过查看 H 中的平移量, 我们可以决定应该将该图像填补到左边还是右边。当该图像填补到左边时, 由于目 标图像中点的坐标也变化了,所以在“左边”情况中,需要在单应性矩阵中加入平 移。简单起见, 我们同样使用 0 像素的技巧来寻找 alpha 图。

现在在图像中使用该操作,函数如下所示:

# 扭曲图像
delta = 2000 # 用于填充和平移
im1 = array(Image.open(imname[1]))
im2 = array(Image.open(imname[2]))
im_12 = warp.panorama(H_12,im1,im2,delta,delta)

im1 = array(Image.open(imname[0]))
im_02 = warp.panorama(dot(H_12,H_01),im1,im_12,delta,delta)

im1 = array(Image.open(imname[3]))
im_32 = warp.panorama(H_32,im1,im_02,delta,delta)

im1 = array(Image.open(imname[j+1]))
im_42 = warp.panorama(dot(H_32,H_43),im1,im_32,delta,2*delta)

注意,在最后一行中,im_32 图像已经发生了一次平移。创建的全景图结果如 图 3-12 所示。 正如你所看到的,图像曝光不同,在单个图像的边界上存在边缘效 应。 商业的创建全景图像软件里有额外的操作来对强度进行归一化,并对平移进行 平滑场景转换,以使得结果看上去更好。

_images/1541127332817.jpg
练习

1.写出一个函数,其输入参数为正方形(或者长方形)物体(例如,一本书、一张 海报,或者二维条形码)图像的坐标。 然后,计算将该长方形映射归一化坐标系 中正视图全图的变换。你可以使用 ginput(), 或者最强的 Harris 角点来发现长方 形物体的稳健性角点。

2.写出一个函数,对于如图 3-1 所示的扭曲能够正确地找到 alpha 图像。

3.在你自己的数据集中找出包含三个公共的标记物(像人脸例子一样,或者使用著 名的景物,比如埃菲尔铁塔)的那个。 创建对齐后的图像,其中这些标记物在同一个位置上。计算平均和中值图像,然后可视化。

4.进行亮度归一化操作,找出在全景图像例子中更好地拼接图像的方法。该方法能够去除图 3-12 中的边缘效应。

5.与将图像扭曲到中心图像上不同,全景图像可以通过将图像扭曲到圆柱体上来创建。

6.使用 RANSAC 算法来找到一些主要的正确单应性矩阵集合。一个简单的方式是,首先运行一次 RANSAC 算法, 找到具有最大一致子集的单应性矩阵,然后 将与该单应性矩阵一致的对应点对从匹配集合中删除, 再运行 RANSAC 算法找 到下一个最大的集合,以此类推。

7.修改单应性矩阵的 RANSAC 估计算法,来使用三个对应点对计算仿射变换。 使 用该算法来判断图像对之间是否包含平面场景,例如使用正确点的个数。 对于仿 射变化,平面场景中正确点的个数会很多。

8.通过匹配局部特征,以及使用最小二乘刚体配准, 用多个图像(例如,从 Flickr 下载)创建一个全景图(http://en.wikipedia.org/wiki/Panography)。

第四章:照相机模型与增强实现
4.1 针孔照相机模型
4.1.1 照相机矩阵
4.1.2 三维点的投影
4.1.3 照相机矩阵的分解
4.1.4 计算照相机中心
4.2 照相机标定
4.3 以平面和标记物进行姿态估计
4.4 增强现实
4.4.1 PyGame 和 PyOpenGL
4.4.2 从照相机矩阵到 OpenGL 格式
4.4.3 在图像中放置虚拟物体
4.4.4 综合集成
4.4.5 载入模型
练习
第五章:多视图几何
5.1 外极几何
5.1.1 一个简单的数据集
5.1.2 用 Matplotlib 绘制三维数据
5.1.3 计算 F:八点法
5.1.4 外极点和外极线
5.2 照相机和三维结构的计算
5.2.1 三角剖分
5.2.2 由三维点计算照相机矩阵
5.2.3 由基础矩阵计算照相机矩阵
5.3 多视图重建
5.3.1 稳健估计基础矩阵
5.3.2 三维重建示例
5.3.3 多视图的扩展示例
5.4 立体图像
练习
第六章:图像聚类
6.1 K-means 聚类
6.1.1 SciPy 聚类包
6.1.2 图像聚类
6.1.3 在主成分上可视化图像
6.1.4 像素聚类
6.2 层次聚类
6.3 谱聚类
练习
第七章:图像搜索
7.1 基于内容的图像检索
7.2 视觉单词
7.3 图像索引
7.3.1 建立数据库
7.3.2 添加图像
7.4 在数据库中搜索图像
7.4.1 利用索引获取候选图像
7.4.2 用一幅图像进行查询
7.4.3 确定对比基准并绘制结果
7.5 使用几何特性对结果排序
7.6 建立演示程序及 Web 应用
7.6.1 用 CherryPy 创建 Web 应用
7.6.2 图像搜索演示程序
练习
第八章:图像内容分类
8.1 K 邻近分类法(KNN)
8.1.1 一个简单的二维示例
8.1.2 用稠密 SIFT 作为图像特征
8.1.3 图像分类:手势识别
8.2 贝叶斯分类器
8.3 支持向量机
8.3.1 使用 LibSVM
8.3.2 再论手势识别
8.4 光学字符识别
8.4.1 训练分类器
8.4.2 选取特征
8.4.3 多类支持向量机
8.4.4 提取单元格并识别字符
8.4.5 图像校正
练习
第九章:图像分割
9.1 图割(Graph Cut)
9.1.1 从图像创建图
9.1.2 用户交互式分割
9.2 利用聚类进行分割
9.3 变分法
练习
第十章:OpenCV
10.1 OpenCV 的 Python 接口
10.2 OpenCV 基础知识
10.2.1 读取和写入图像
10.2.2 颜色空间
10.2.3 显示图像及结果
10.3 处理视频
10.3.1 视频输入
10.3.2 将视频读取到 NumPy 数组中
10.4 跟踪
10.4.1 光流
10.4.2 Lucas-Kanade 算法
10.5 更多示例
10.5.1 图像修复
10.5.2 利用分水岭变换进行分割
10.5.3 利用霍夫变换检测直线
练习

python计算与编程实践(多媒体方法)

第一章:计算机科学与媒体计算导论
第二章:编程导论
第三章:使用循环修改图片
第四章:修改区域中的像素
第五章:高级图片技术
第六章:使用循环修改声音
第七章:修改一段样本区域
第八章:通过合并片段制作声音
第九章:构建更大的程序
第十章:构建和修改文本
第十一章:高级文本技术.web和信息
第十二章:产生web文本
第十三章:制作和修改电影
第十四章:速度
第十五章:函数式编程
第十六章:面向对象编程

利用python进行数据分析

第一章:准备工作
第二章:引言
第三章:ipython交互式和开发环境
第四章:numpy基础 数组和矢量计算
第五章:pandas入门
第六章:数据加载、存储与文件格式
第七章:数据规整化.清理、转换、合并、重塑
第八章:绘图和可视化
第九章:数据聚合与分组运算
第十章:时间序列
第十一章:金融和经济数据应用
第十二章:numpy高级应用

面向ArcGIS的Python脚本编程

python在abaqus中的应用

python高手之路

第一章:项目开始
第二章:模块和库
第三章:文档
第四章:分发
第五章:虚拟环境
第六章:单元测试
第七章:方法和装饰器
第八章:函数式编程
第九章:抽象语法树
第十章:性能与优化
第十一章:扩展与架构
第十二章:RDBMS和ORM
第十二章:python3支持策略
第十二章:少即是多

菜鸟教程的python3教程

查看版本:

python -V

第一个Python3.x程序:

print("Hello, World!")
Python3 环境搭建
Python3 可应用于多平台包括 Windows、Linux 和 Mac OS X。
  • Unix (Solaris, Linux, FreeBSD, AIX, HP/UX, SunOS, IRIX, 等等。)
  • Win 9x/NT/2000
  • Macintosh (Intel, PPC, 68K)
  • OS/2
  • DOS (多个DOS版本)
  • PalmOS
  • Nokia 移动手机
  • Windows CE
  • Acorn/RISC OS
  • BeOS
  • Amiga
  • VMS/OpenVMS
  • QNX
  • VxWorks
  • Psion
  • Python 同样可以移植到 Java 和 .NET 虚拟机上。
Python3 下载

python3 最新源码,二进制文档,新闻资讯等可以在 Python 的官网查看到:

Python 官网:https://www.python.org/

你可以在以下链接中下载 Python 的文档,你可以下载 HTML、PDF 和 PostScript 等格式的文档。

Python文档下载地址:https://www.python.org/doc/

Python 安装

Python 已经被移植在许多平台上(经过改动使它能够工作在不同平台上)。

您需要下载适用于您使用平台的二进制代码,然后安装 Python。

如果您平台的二进制代码是不可用的,你需要使用C编译器手动编译源代码。

编译的源代码,功能上有更多的选择性, 为 Python 安装提供了更多的灵活性。

Unix & Linux 平台安装 Python3:

这里菜鸟教程的安装方式会有一定的问题,请参照我的笔记,Linux下升级Python

Window 平台安装 Python:

直接下载双击运行exe安装即可,没这么复杂。

环境变量配置

程序和可执行文件可以在许多目录,而这些路径很可能不在操作系统提供可执行文件的搜索路径中。

path(路径)存储在环境变量中,这是由操作系统维护的一个命名的字符串。这些变量包含可用的命令行解释器和其他程序的信息。

Unix或Windows中路径变量为PATH(UNIX区分大小写,Windows不区分大小写)。

在Mac OS中,安装程序过程中改变了python的安装路径。如果你需要在其他目录引用Python,你必须在path中添加Python目录。

在 Unix/Linux 设置环境变量

在 csh shell: 输入:

setenv PATH "$PATH:/usr/local/bin/python"

在 bash shell (Linux): 输入

export PATH="$PATH:/usr/local/bin/python"

在 sh 或者 ksh shell: 输入

PATH="$PATH:/usr/local/bin/python"

注意: /usr/local/bin/python 是 Python 的安装目录。

在 Windows 设置环境变量

在环境变量中添加Python目录:

在命令提示框中(cmd) : 输入

path=%path%;C:\Python

按下"Enter"。

注意: C:Python 是Python的安装目录。

也可以通过以下方式设置:
  • 右键点击"计算机",然后点击"属性"
  • 然后点击"高级系统设置"
  • 选择"系统变量"窗口下面的"Path",双击即可!
  • 然后在"Path"行,添加python安装路径即可(我的D:Python32),所以在后面,添加该路径即可。

ps:记住,路径直接用分号";"隔开!最后设置成功以后,在cmd命令行,输入命令"python",就可以有相关显示。

运行Python

有三种方式可以运行Python:

1、交互式解释器:

你可以通过命令行窗口进入python并开在交互式解释器中开始编写Python代码。

你可以在Unix,DOS或任何其他提供了命令行或者shell的系统进行python编码工作。

$ python # Unix/Linux
或者
C:>python # Windows/DOS
以下为Python命令行参数:
  • 选项 描述

  • -d

    在解析时显示调试信息

  • -O

    生成优化代码 ( .pyo 文件 )

  • -S

    启动时不引入查找Python路径的位置

  • -V

    输出Python版本号

  • -X

    从 1.6版本之后基于内建的异常(仅仅用于字符串)已过时。

  • -c cmd

    执行 Python 脚本,并将运行结果作为 cmd 字符串。

  • file 在给定的python文件执行python脚本。

Python3 基础语法

编码

默认情况下,Python 3 源码文件以 UTF-8 编码,所有字符串都是 unicode 字符串。 当然你也可以为源码文件指定不同的编码:

# -*- coding: cp-1252 -*-
标识符
  • 第一个字符必须是字母表中字母或下划线 _ 。
  • 标识符的其他的部分由字母、数字和下划线组成。
  • 标识符对大小写敏感。

在 Python 3 中,非 ASCII 标识符也是允许的了。

python保留字

保留字即关键字,我们不能把它们用作任何标识符名称。Python 的标准库提供了一个 keyword 模块,可以输出当前版本的所有关键字:

>>> import keyword
>>> keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']
注释

Python中单行注释以 # 开头,实例如下:

#!/usr/bin/python3

# 第一个注释
print ("Hello, Python!") # 第二个注释

执行以上代码,输出结果为:

Hello, Python!

多行注释可以用多个 # 号,还有 ''' 和 """:

#!/usr/bin/python3

# 第一个注释
# 第二个注释

'''
第三注释
第四注释
'''

"""
第五注释
第六注释
"""
print ("Hello, Python!")

执行以上代码,输出结果为:

Hello, Python!
行与缩进

python最具特色的就是使用缩进来表示代码块,不需要使用大括号 {} 。

缩进的空格数是可变的,但是同一个代码块的语句必须包含相同的缩进空格数。实例如下:

if True:
    print ("True")
else:
    print ("False")

以下代码最后一行语句缩进数的空格数不一致,会导致运行错误:

if True:
    print ("Answer")
    print ("True")
else:
    print ("Answer")
  print ("False")    # 缩进不一致,会导致运行错误

以上程序由于缩进不一致,执行后会出现类似以下错误:

File "test.py", line 6
    print ("False")    # 缩进不一致,会导致运行错误
                                  ^
IndentationError: unindent does not match any outer indentation level
多行语句

Python 通常是一行写完一条语句,但如果语句很长,我们可以使用反斜杠()来实现多行语句,例如:

total = item_one + \
        item_two + \
        item_three

在 [], {}, 或 () 中的多行语句,不需要使用反斜杠(),例如:

total = ['item_one', 'item_two', 'item_three',
        'item_four', 'item_five']
数字(Number)类型
python中数字有四种类型:整数、布尔型、浮点数和复数。
  • int (整数), 如 1, 只有一种整数类型 int,表示为长整型,没有 python2 中的 Long。
  • bool (布尔), 如 True。
  • float (浮点数), 如 1.23、3E-2
  • complex (复数), 如 1 + 2j、 1.1 + 2.2j
字符串(String)
  • python中单引号和双引号使用完全相同。
  • 使用三引号('''或""")可以指定一个多行字符串。
  • 转义符 ''
  • 反斜杠可以用来转义,使用r可以让反斜杠不发生转义。。 如 r"this is a line with n" 则n会显示,并不是换行。
  • 按字面意义级联字符串,如"this " "is " "string"会被自动转换为this is string。
  • 字符串可以用 + 运算符连接在一起,用 * 运算符重复。
  • Python 中的字符串有两种索引方式,从左往右以 0 开始,从右往左以 -1 开始。
  • Python中的字符串不能改变。
  • Python 没有单独的字符类型,一个字符就是长度为 1 的字符串。
  • 字符串的截取的语法格式如下:变量[头下标:尾下标]
::
word = '字符串' sentence = "这是一个句子。" paragraph = """这是一个段落, 可以由多行组成"""
#!/usr/bin/python3

str='Runoob'

print(str)                 # 输出字符串
print(str[0:-1])           # 输出第一个到倒数第二个的所有字符
print(str[0])              # 输出字符串第一个字符
print(str[2:5])            # 输出从第三个开始到第五个的字符
print(str[2:])             # 输出从第三个开始的后的所有字符
print(str * 2)             # 输出字符串两次
print(str + '你好')        # 连接字符串

print('------------------------------')

print('hello\nrunoob')      # 使用反斜杠(\)+n转义特殊字符
print(r'hello\nrunoob')     # 在字符串前面添加一个 r,表示原始字符串,不会发生转义

这里的 r 指 raw,即 raw string。输出结果为:

Runoob
Runoo
R
noo
noob
RunoobRunoob
Runoob你好
------------------------------
hello
runoob
hello\nrunoob
空行

函数之间或类的方法之间用空行分隔,表示一段新的代码的开始。类和函数入口之间也用一行空行分隔,以突出函数入口的开始。

空行与代码缩进不同,空行并不是Python语法的一部分。书写时不插入空行,Python解释器运行也不会出错。但是空行的作用在于分隔两段不同功能或含义的代码,便于日后代码的维护或重构。

记住:空行也是程序代码的一部分。

等待用户输入

执行下面的程序在按回车键后就会等待用户输入:

#!/usr/bin/python3
input("\n\n按下 enter 键后退出。")

以上代码中 ,"nn"在结果输出前会输出两个新的空行。一旦用户按下 enter 键时,程序将退出。

同一行显示多条语句

Python可以在同一行中使用多条语句,语句之间使用分号(;)分割,以下是一个简单的实例:

#!/usr/bin/python3
import sys; x = 'runoob'; sys.stdout.write(x + '\n')

使用脚本执行以上代码,输出结果为:

runoob

使用交互式命令行执行,输出结果为:

>>> import sys; x = 'runoob'; sys.stdout.write(x + '\n')
runoob
7

此处的 7 表示字符数。

多个语句构成代码组

缩进相同的一组语句构成一个代码块,我们称之代码组。

像if、while、def和class这样的复合语句,首行以关键字开始,以冒号( : )结束,该行之后的一行或多行代码构成代码组。

我们将首行及后面的代码组称为一个子句(clause)。

如下实例:

if expression :
   suite
elif expression :
   suite
else :
   suite
Print 输出

print 默认输出是换行的,如果要实现不换行需要在变量末尾加上 end="":

x="a" y="b" # 换行输出 print( x ) print( y )

print('---------') # 不换行输出 print( x, end=" " ) print( y, end=" " ) print()

以上实例执行结果为:

a
b
---------
a b
import 与 from...import

在 python 用 import 或者 from...import 来导入相应的模块。

将整个模块(somemodule)导入,格式为: import somemodule

从某个模块中导入某个函数,格式为: from somemodule import somefunction

从某个模块中导入多个函数,格式为: from somemodule import firstfunc, secondfunc, thirdfunc

将某个模块中的全部函数导入,格式为: from somemodule import *

导入 sys 模块:

import sys
print('================Python import mode==========================');
print ('命令行参数为:')
for i in sys.argv:
    print (i)
print ('\n python 路径为',sys.path)

导入 sys 模块的 argv,path 成员:

from sys import argv,path  #  导入特定的成员

print('================python from import===================================')
print('path:',path) # 因为已经导入path成员,所以此处引用时不需要加sys.path
命令行参数

很多程序可以执行一些操作来查看一些基本信息,Python可以使用-h参数查看各参数帮助信息:

$ python -h
usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ...
Options and arguments (and corresponding environment variables):
-c cmd : program passed in as string (terminates option list)
-d     : debug output from parser (also PYTHONDEBUG=x)
-E     : ignore environment variables (such as PYTHONPATH)
-h     : print this help message and exit

[ etc. ]

我们在使用脚本形式执行 Python 时,可以接收命令行输入的参数,具体使用可以参照 Python 3 命令行参数。

Python3 基本数据类型

Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。

在 Python 中,变量就是变量,它没有类型,我们所说的"类型"是变量所指的内存中对象的类型。

等号(=)用来给变量赋值。

等号(=)运算符左边是一个变量名,等号(=)运算符右边是存储在变量中的值。例如:

#!/usr/bin/python3

counter = 100          # 整型变量
miles   = 1000.0       # 浮点型变量
name    = "runoob"     # 字符串

print (counter)
print (miles)
print (name)

执行以上程序会输出如下结果:

100
1000.0
runoob
多个变量赋值

Python允许你同时为多个变量赋值。例如:

a = b = c = 1

以上实例,创建一个整型对象,值为 1,从后向前赋值,三个变量被赋予相同的数值。

您也可以为多个对象指定多个变量。例如:

a, b, c = 1, 2, "runoob" 以上实例,两个整型对象 1 和 2 的分配给变量 a 和 b,字符串对象 "runoob" 分配给变量 c。

标准数据类型
Python3 中有六个标准的数据类型:
  • Number(数字)
  • String(字符串)
  • List(列表)
  • Tuple(元组)
  • Set(集合)
  • Dictionary(字典)
Python3 的六个标准数据类型中:
  • 不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组);
  • 可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。
Number(数字)

Python3 支持 int、float、bool、complex(复数)。

在Python 3里,只有一种整数类型 int,表示为长整型,没有 python2 中的 Long。

像大多数语言一样,数值类型的赋值和计算都是很直观的。

内置的 type() 函数可以用来查询变量所指的对象类型。

>>> a, b, c, d = 20, 5.5, True, 4+3j
>>> print(type(a), type(b), type(c), type(d))
<class 'int'> <class 'float'> <class 'bool'> <class 'complex'>
此外还可以用 isinstance 来判断:
>>>a = 111
>>> isinstance(a, int)
True
>>>

isinstance 和 type 的区别在于:

class A:
    pass

class B(A):
    pass

isinstance(A(), A)  # returns True
type(A()) == A      # returns True
isinstance(B(), A)    # returns True
type(B()) == A        # returns False
区别就是:
  • type()不会认为子类是一种父类类型。
  • isinstance()会认为子类是一种父类类型。

注解

注意:在 Python2 中是没有布尔型的,它用数字 0 表示 False,用 1 表示 True。到 Python3 中,把 True 和 False 定义成关键字了,但它们的值还是 1 和 0,它们可以和数字相加。

当你指定一个值时,Number 对象就会被创建:

var1 = 1
var2 = 10

您也可以使用del语句删除一些对象引用。

del语句的语法是:

del var1[,var2[,var3[....,varN]]]

您可以通过使用del语句删除单个或多个对象。例如:

del var
del var_a, var_b
数值运算
实例
>>>5 + 4  # 加法
9
>>> 4.3 - 2 # 减法
2.3
>>> 3 * 7  # 乘法
21
>>> 2 / 4  # 除法,得到一个浮点数
0.5
>>> 2 // 4 # 除法,得到一个整数
0
>>> 17 % 3 # 取余
2
>>> 2 ** 5 # 乘方
32
注意:
1、Python可以同时为多个变量赋值,如a, b = 1, 2。 2、一个变量可以通过赋值指向不同类型的对象。 3、数值的除法包含两个运算符:/ 返回一个浮点数,// 返回一个整数。 4、在混合计算时,Python会把整型转换成为浮点数。
数值类型实例:
  • int float complex

  • 10 0.0 3.14j

  • 100 15.20 45.j

  • -786 -21.9 9.322e-36j

  • 080 32.3e+18 .876j

  • -0490 -90. -.6545+0J

  • -0x260

    -32.54e100 3e+26J

  • 0x69 70.2E-12 4.53e-7j

Python还支持复数,复数由实数部分和虚数部分构成,可以用a + bj,或者complex(a,b)表示, 复数的实部a和虚部b都是浮点型

String(字符串)

Python中的字符串用单引号 ' 或双引号 " 括起来,同时使用反斜杠 转义特殊字符。

字符串的截取的语法格式如下:

变量[头下标:尾下标]

索引值以 0 为开始值,-1 为从末尾的开始位置。

图略

加号 + 是字符串的连接符, 星号 * 表示复制当前字符串,紧跟的数字为复制的次数。实例如下:

实例:

#!/usr/bin/python3

str = 'Runoob'

print (str)          # 输出字符串
print (str[0:-1])    # 输出第一个到倒数第二个的所有字符
print (str[0])       # 输出字符串第一个字符
print (str[2:5])     # 输出从第三个开始到第五个的字符
print (str[2:])      # 输出从第三个开始的后的所有字符
print (str * 2)      # 输出字符串两次
print (str + "TEST") # 连接字符串

执行以上程序会输出如下结果:

Runoob
Runoo
R
noo
noob
RunoobRunoob
RunoobTEST

Python 使用反斜杠()转义特殊字符,如果你不想让反斜杠发生转义,可以在字符串前面添加一个 r,表示原始字符串:

>>> print('Ru\noob')
Ru
oob
>>> print(r'Ru\noob')
Ru\noob
>>>

另外,反斜杠()可以作为续行符,表示下一行是上一行的延续。也可以使用 """...""" 或者 '''...''' 跨越多行。

注意,Python 没有单独的字符类型,一个字符就是长度为1的字符串。

>>>word = 'Python'
>>> print(word[0], word[5])
P n
>>> print(word[-1], word[-6])
n P

与 C 字符串不同的是,Python 字符串不能被改变。向一个索引位置赋值,比如word[0] = 'm'会导致错误。

注意:
1、反斜杠可以用来转义,使用r可以让反斜杠不发生转义。 2、字符串可以用+运算符连接在一起,用*运算符重复。 3、Python中的字符串有两种索引方式,从左往右以0开始,从右往左以-1开始。 4、Python中的字符串不能改变。
List(列表)

List(列表) 是 Python 中使用最频繁的数据类型。

列表可以完成大多数集合类的数据结构实现。列表中元素的类型可以不相同,它支持数字,字符串甚至可以包含列表(所谓嵌套)。

列表是写在方括号 [] 之间、用逗号分隔开的元素列表。

和字符串一样,列表同样可以被索引和截取,列表被截取后返回一个包含所需元素的新列表。

列表截取的语法格式如下:

变量[头下标:尾下标] 索引值以 0 为开始值,-1 为从末尾的开始位置。

加号 + 是列表连接运算符,星号 * 是重复操作。如下实例:

#!/usr/bin/python3

list = [ 'abcd', 786 , 2.23, 'runoob', 70.2 ]
tinylist = [123, 'runoob']

print (list)            # 输出完整列表
print (list[0])         # 输出列表第一个元素
print (list[1:3])       # 从第二个开始输出到第三个元素
print (list[2:])        # 输出从第三个元素开始的所有元素
print (tinylist * 2)    # 输出两次列表
print (list + tinylist) # 连接列表

以上实例输出结果:

['abcd', 786, 2.23, 'runoob', 70.2]
abcd
[786, 2.23]
[2.23, 'runoob', 70.2]
[123, 'runoob', 123, 'runoob']
['abcd', 786, 2.23, 'runoob', 70.2, 123, 'runoob']

与Python字符串不一样的是,列表中的元素是可以改变的:

>>>a = [1, 2, 3, 4, 5, 6]
>>> a[0] = 9
>>> a[2:5] = [13, 14, 15]
>>> a
[9, 2, 13, 14, 15, 6]
>>> a[2:5] = []   # 将对应的元素值设置为 []
>>> a
[9, 2, 6]

List内置了有很多方法,例如append()、pop()等等,这在后面会讲到。

注意:
1、List写在方括号之间,元素用逗号隔开。 2、和字符串一样,list可以被索引和切片。 3、List可以使用+操作符进行拼接。 4、List中的元素是可以改变的。
Tuple(元组)

元组(tuple)与列表类似,不同之处在于元组的元素不能修改。元组写在小括号 () 里,元素之间用逗号隔开。

元组中的元素类型也可以不相同:

#!/usr/bin/python3

tuple = ( 'abcd', 786 , 2.23, 'runoob', 70.2  )
tinytuple = (123, 'runoob')

print (tuple)             # 输出完整元组
print (tuple[0])          # 输出元组的第一个元素
print (tuple[1:3])        # 输出从第二个元素开始到第三个元素
print (tuple[2:])         # 输出从第三个元素开始的所有元素
print (tinytuple * 2)     # 输出两次元组
print (tuple + tinytuple) # 连接元组

以上实例输出结果:

('abcd', 786, 2.23, 'runoob', 70.2)
abcd
(786, 2.23)
(2.23, 'runoob', 70.2)
(123, 'runoob', 123, 'runoob')
('abcd', 786, 2.23, 'runoob', 70.2, 123, 'runoob')

元组与字符串类似,可以被索引且下标索引从0开始,-1 为从末尾开始的位置。也可以进行截取(看上面,这里不再赘述)。

其实,可以把字符串看作一种特殊的元组:

>>>tup = (1, 2, 3, 4, 5, 6)
>>> print(tup[0])
1
>>> print(tup[1:5])
(2, 3, 4, 5)
>>> tup[0] = 11  # 修改元组元素的操作是非法的
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>>

虽然tuple的元素不可改变,但它可以包含可变的对象,比如list列表。

构造包含 0 个或 1 个元素的元组比较特殊,所以有一些额外的语法规则:

tup1 = ()    # 空元组
tup2 = (20,) # 一个元素,需要在元素后添加逗号
string、list和tuple都属于sequence(序列)。
注意:
1、与字符串一样,元组的元素不能修改。 2、元组也可以被索引和切片,方法一样。 3、注意构造包含0或1个元素的元组的特殊语法规则。 4、元组也可以使用+操作符进行拼接。
Set(集合)

集合(set)是由一个或数个形态各异的大小整体组成的,构成集合的事物或对象称作元素或是成员。

基本功能是进行成员关系测试和删除重复元素。

可以使用大括号 { } 或者 set() 函数创建集合,注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。

创建格式:

parame = {value01,value02,...}
或者
set(value)

#!/usr/bin/python3

student = {'Tom', 'Jim', 'Mary', 'Tom', 'Jack', 'Rose'}

print(student)   # 输出集合,重复的元素被自动去掉

# 成员测试
if 'Rose' in student :
    print('Rose 在集合中')
else :
    print('Rose 不在集合中')


# set可以进行集合运算
a = set('abracadabra')
b = set('alacazam')

print(a)

print(a - b)     # a和b的差集

print(a | b)     # a和b的并集

print(a & b)     # a和b的交集

print(a ^ b)     # a和b中不同时存在的元素

以上实例输出结果:

{'Mary', 'Jim', 'Rose', 'Jack', 'Tom'}
Rose 在集合中
{'b', 'a', 'c', 'r', 'd'}
{'b', 'd', 'r'}
{'l', 'r', 'a', 'c', 'z', 'm', 'b', 'd'}
{'a', 'c'}
{'l', 'r', 'z', 'm', 'b', 'd'}
Dictionary(字典)

字典(dictionary)是Python中另一个非常有用的内置数据类型。

列表是有序的对象集合,字典是无序的对象集合。两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。

字典是一种映射类型,字典用"{ }"标识,它是一个无序的键(key) : 值(value)对集合。

键(key)必须使用不可变类型。

在同一个字典中,键(key)必须是唯一的:

#!/usr/bin/python3

dict = {}
dict['one'] = "1 - 菜鸟教程"
dict[2]     = "2 - 菜鸟工具"

tinydict = {'name': 'runoob','code':1, 'site': 'www.runoob.com'}


print (dict['one'])       # 输出键为 'one' 的值
print (dict[2])           # 输出键为 2 的值
print (tinydict)          # 输出完整的字典
print (tinydict.keys())   # 输出所有键
print (tinydict.values()) # 输出所有值
以上实例输出结果:
1 - 菜鸟教程 2 - 菜鸟工具
{'name': 'runoob', 'code': 1, 'site': 'www.runoob.com'}
dict_keys(['name', 'code', 'site'])
dict_values(['runoob', 1, 'www.runoob.com'])

构造函数 dict() 可以直接从键值对序列中构建字典如下:

>>>dict([('Runoob', 1), ('Google', 2), ('Taobao', 3)])
{'Taobao': 3, 'Runoob': 1, 'Google': 2}

>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}

>>> dict(Runoob=1, Google=2, Taobao=3)
{'Runoob': 1, 'Google': 2, 'Taobao': 3}

另外,字典类型也有一些内置的函数,例如clear()、keys()、values()等。

注意:
1、字典是一种映射类型,它的元素是键值对。 2、字典的关键字必须为不可变类型,且不能重复。 3、创建空字典使用 { }。
Python数据类型转换

有时候,我们需要对数据内置的类型进行转换,数据类型的转换,你只需要将数据类型作为函数名即可。

以下几个内置的函数可以执行数据类型之间的转换。这些函数返回一个新的对象,表示转换的值。

函数 描述
  • int(x [,base]) 将x转换为一个整数
  • float(x) 将x转换到一个浮点数
  • complex(real [,imag]) 创建一个复数
  • str(x) 将对象 x 转换为字符串
  • repr(x) 将对象 x 转换为表达式字符串
  • eval(str) 用来计算在字符串中的有效Python表达式,并返回一个对象
  • tuple(s) 将序列 s 转换为一个元组
  • list(s) 将序列 s 转换为一个列表
  • set(s) 转换为可变集合
  • dict(d) 创建一个字典。d 必须是一个序列 (key,value)元组。
  • frozenset(s) 转换为不可变集合
  • chr(x) 将一个整数转换为一个字符
  • ord(x) 将一个字符转换为它的整数值
  • hex(x) 将一个整数转换为一个十六进制字符串
  • oct(x) 将一个整数转换为一个八进制字符串
Python3 注释

确保对模块, 函数, 方法和行内注释使用正确的风格

Python中的注释有单行注释和多行注释:

Python中单行注释以 # 开头,例如:

# 这是一个注释
print("Hello, World!")

多行注释用三个单引号 ''' 或者三个双引号 """ 将注释括起来,例如:

1、单引号(''')
#!/usr/bin/python3
'''
这是多行注释,用三个单引号
这是多行注释,用三个单引号
这是多行注释,用三个单引号
'''
print("Hello, World!")
2、双引号(""")
#!/usr/bin/python3
"""
这是多行注释,用三个双引号
这是多行注释,用三个双引号
这是多行注释,用三个双引号
"""
print("Hello, World!")

web开发

python web开发实战—董伟明

本书知识点比较散、且大,很多并不是python方面的知识,只属于一些通用概念,比如服务化,只是介绍了一些概念。

这里只做内容总结,不详细记录。

2018-12-02完成,此书有些内容可以学习,但是挺多都是为了凑页数。相当于花钱买了技能目录

第一章:初识Python web开发

python应用广泛,在大数据、算法、运维等领域都有对应的工具和库

django2.0不在支持python2。

第二章:web开发前的准备

搭建虚拟环境virtualenv

介绍了vagrant虚拟机工具,这个后期可以详细了解。
  1. vagrant是一个操作虚拟机的工具,他会很快的完成一套开发环境的部署,也解决了开发环境不一致的问题。

介绍了virtualBox虚拟机工具。

简单介绍docker

包管理和虚拟环境

使用pip 代替 easy_install

其他的包管理可以忽略了

重点注意虚拟环境 virtualenv的使用。

virtualenvwrapper 是对virtualenv的功能扩展,他的用途:
  1. 用来管理全部的虚拟环境
  2. 能方便的创建、删除和拷贝虚拟环境
  3. 用单个命令就可以切换不同的虚拟环境。
  4. 可以使用tab补全虚拟环境
  5. 支持用户颗粒度的钩子支持

详细不知道使用后期再学习。

virtualenv-burrito 是一个安装、配置virtualenv和virtualenvwrapper及其依赖的傻瓜式工具

autoenv 是在其切换目录的时候可以完成自动激活虚拟环境等定制操作。

第三章:flask web开发

入门:

from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
    return 'index'

if __name__ == '__main__':
    app.run(host='0.0.0.0',port=5000)

#python index.py
动态url支持的规则:
  • string: 接受任何没有斜杠的文本,默认
  • int:整数
  • float:浮点数
  • path:和默认相似,但是接受斜杠
  • uuid:只接受uuid字符串
  • any:可以指定多种路径,但是需要传入参数

any示例:@app.route('/<any(a,b)>:page_name/'),类似枚举

自定义url转换器 略 具体需要到在查询。

构造url:

url_for

跳转和重定向:

redirect

静态文件:

url_for('static',filename='style.css')

app = Flask(__name__,static_folder='/tmp')

即插视图: 略

蓝图:略

子域名 :略

命令行接口:略

模板:略

使用mysql:略

书中的内容对以上的知识都是简单说明所以内容全略。

理解Context: 重点 ,但是书中内容略 ,书中也是简单概括,可以去官网查询。

次此章节最后 又一个文件托管服务的例子 但是感觉不是很全,具体可以看这里的代码:https://github.com/dongweiming/web_develop/tree/master/chapter3/section5

这个案例涉及的知识点有:
  • python-magic:libmagic的python绑定,用于确定文件类型。
  • pillow:处理图片
  • cropresize2:用来剪切和调整图片大小
  • short_url:创建短连接。
第四章:flask 开发进阶

信号:用于业务解耦,需要了解下 感觉还是很实用的。

自定义信号:blinker

flask_login中带有6种信号

可以用来做登录成功后记录登录次数和ip地址。用于解耦,不用再登录验证成功后那里写这些代码。

flask_script:略 过时了不推荐。

flask_debugtoolbar:调试工具栏

flask_migrate:迁移数据库用

flask_wtf:表单插件

flask_security:提供角色管理、权限验证、用户登录、邮箱验证、密码重置、密码加密、跟踪用户登录状态等等功能,很强大,详细需要去了解。

flask_restful:做api接口使用

flask_admin:可以快速搭建后台,但是很细节的东西功能性较多的后台还是手动创建。

flask_assets:对静态文件的打包压缩管理。 性能提升时需要使用。

第五章:RESTt 和Ajax
速度限制:
  • X-RateLimit-Limit:当前时间段允许的并发请求数
  • X-RateLimit-Remaining:当前时间段保留的请求数
  • X-RateLimit-Reset:当前时间段剩余的秒数

缓存:可通过If-Modified-Since实现缓存

好像没有什么实际内容,js前端代码一堆没有用的

第六章:网站架构

常见的WSGI容器

gunicorn:豆瓣广泛使用。

uWSGI:这个也有很多人用。

nginx做转发端口。

缓存:

memcached:介绍略

书中这里介绍了简单的Redis的操作

也举例了几个例子:
  1. 取最新N个数据的操作
  2. 取Top操作(排行榜应用)
  3. 实时统计

具体代码请看:https://github.com/dongweiming/web_develop/tree/master/chapter6

分片和集群管理:
  1. Twemproxy
  2. Redis Cluster
  3. Coids

Nosql:略

书中后面都是简单文字性介绍了一下内容:
  1. 高可用方案
  2. 分片方案
  3. 缓存
  4. 均衡负载
  5. 群集

以上内容都是简单文件描述,没有实际意义。

第七章:系统管理

supervisor:进程管理工具。

应用部署工具:fabric

介绍saltstack和ansible 这都是自动化部署的东西,这里应该算是凑字数。

psutil:获取系统硬件等信息。

使用sentry收集错误信息,有错误自己记录?详细需要在看这个插件文件 ,书中相当于目录。

使用StatsD、Graphite等搭建web监控。

第八章:测试和持续集成

unittest单元测试

doctest测试:是一种只管的表述型测试方法,它通常查找代码里文档字符串中的交互式会话部分。

给功能函数添加doctest是一个好习惯?

介绍了py.test和mock

tox是一个通用的virtualenv管理和测试命令行工具。

第九章:消息队列和celery

Beanstalkd:是消息队列的后起之秀,是一个高性能、轻量级的分布式内存队列系统。

RabbitMQ:是一个实现了AMQP协议标准的开源消息代理和队列服务器。和Beanstalkd不同的是,他是企业级消息系统,自带了集群、管理、插件系统等特性。

AMQP:是一个一步消息传递所使用的应用层协议规范。

Celery:是一个专注于实时处理和任务调度的分布式任务队列。

使用场景:
  • WEB应用,需要长时间才能执行完成时的任务。
  • 定时任务。
  • 其他可以异步执行的任务。

书中后面都是使用celery的例子。

实际中也是需要使用这个比较重量级、安全性的celery。

第十章:服务化

RPC框架:这里介绍了REST和RPC的差别,前者使用http协议,后者使用二进制。

对于对接第三方服务时,通常使用http/RESTful等公有协议,对于内部的服务调用,应该选择更高性能的二进制私有协议。

简单文件介绍了**微服务**

介绍了Thrift

介绍了PIDL

第十一章:数据处理

使用MapReduce做日志分析

内容略,属于大数据分析的知识,这里只是列出简单的做法凑页数。

DPark是一个基于Mesos的集群计算框架,是Spark的Python实现版本,类似于MapReduce,但是比其更灵活。

分布式文件系统MooseFS

NFS 及网络文件系统,允许网络中的计算机之间通过TCP/IP网络共享资源。

Mesos是一个集群管理器,用来作为资源统一管理与调度平台。让你就像使用一台服务器一样使用 整个集群,

介绍了使用xlsxWriter处理xlsx文件

简单介绍了pandas:结构化数据分析工具。

第十二章:帮助工具

介绍ipython略。

Jupyter Notebook:web端交互式 略。

介绍了一些linux命令:

uptime:获取当前系统的平均负载

free:获取当前内存的使用情况

htop:运行于linux系统的监控与进程管理软件

dstat:是用python实现的多功能系统资源统计生成工具。

Glances:是基于psutil的跨平台的系统监控工具。

iftop:类似于top命令的实时流量监控工具。

sysdig:是linux服务器监控和故障排除工具

nethogs:查看进程暂用宽带情况

iptraf:网络流量监控工具。

性能测试

boom:是apache Bench的代替品

tcpcopy:测试的数据更真实

第十三章:python并发编程
爬虫基本要素:
  • 使用代理
  • 伪造UA字符串
  • 选择解析HTML的方式
  • 使用Referer

随机UA生成库:fake-useragent

高并发编程时,采用多线程或者进程是一种不可取的解决方案。

采用gevent。是相对可取的方案

Gevent是一个基于微线程库Greenlet的并发框架。

多进程:multiprocessing

Future: 是python3.2引入的并发模块,,他提供了用多线程或多进程实现并发的方式。

协程:asyncio,听说最牛x

aiohttp:爬虫框架?可用代替requests作为http客户端。

队列:Queue:用生产者/消费者模型的队列。

第十四章:python进阶

errno:捕获错误类型。

subprocess: 可代替以下模块和函数:
  • os.system
  • os.spawn*
  • os.popen*
  • popen2.*
  • commands.*

直接执行系统命令。

挺多函数的 ,感觉都是凑页数的。

第十五章:web开发项目实践

内容略

Flask Web开发(狗书)

start_time:2018-08-28

第一章:安装

flask非常小,但是,小并不意味着它比其他框架的功能少。Flask 自开发伊始就被设计为可扩展的框架, 它具有一个包含基本服务的强健核心,其他功能则可通过扩展实现。

Flask有两个主要依赖:路由、调试和Web服务器网关接口(Web Server Gateway Interface, WSGI)子系统由 Werkzeug(http://werkzeug.pocoo.org/)提供;模板系统由 Jinja2(http:// jinja.pocoo.org/)提供。Werkzeug 和 Jinjia2 都是由 Flask 的核心开发者开发而成。

使用虚拟环境
  • 首先需要安装python,linux和mac都自带python,Windows需要下载一个安装包安装,注意选择32/64版本。
  • 安装pip。https://bootstrap.pypa.io/get-pip.py,然后运行 python get-pip.py。如果报错根据报错原因搜索一般都会有答案。
  • 安装虚拟环境。pip install virtualenv 这里pip 报错一般是window没有把python的环境变量加上。然后使用 virtualenv --version 即可看到虚拟环境的版本信息。注意Linux权限 .可以使用 pip install virtualenv --user
$ git clone https://github.com/miguelgrinberg/flasky.git
$ cd flasky
$ git checkout 1a
这里说 需要clone下来  如果没有安装git是报错的,可自行查阅安装git。

创建虚拟环境:

$ virtualenv venv
New python executable in venv/bin/python2.7
Also creating executable in venv/bin/python
Installing setuptools............done.
Installing pip...............done.

激活:

source venv/bin/activate
Windows:
venv\Scripts\activate
退出:
deactivate

之前virtualenv venv有报错过,网络原因尝试多次后就可以了

使用pip安装Python包
pip install flask

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第二章:程序基本结构

初始化:

from flask import Flask
app = Flask(__name__)

路由和视图函数:

@app.route('/')
def index():
    return '<h1>Hello World!</h1>'

动态可变的部分name:

@app.route('/user/<name>')
def user(name):
    return '<h1>Hello, %s!</h1>' % name

启动服务器:

if __name__ == '__main__':
    app.run(debug=True)

完整的程序:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Hello World!</h1>'

if __name__ == '__main__':
    app.run(debug=True)

运行程序:

(venv) $ python hello.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader

上下文的概念重之之重,现在刚开始学知道有这么一个东西就好

Flask上下文全局变量:

current_app:当前激活程序的程序实例
g:处理请求时用作临时存储的对象。每次请求都会重设这个变量
request:请求对象,封装了客户端发出的 HTTP 请求中的内容
session:用户会话,用于存储请求之间需要“记住”的值的词典
请求钩子

也是常用的模块,比如登录验证:

before_first_request:注册一个函数,在处理第一个请求之前运行。
before_request:注册一个函数,在每次请求之前运行。
after_request:注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。
teardown_request:注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行。
响应

:这个http协议内容,可以这样设置返回的状态:

@app.route('/')
def index():
    return '<h1>Bad Request</h1>', 400

Response:

from flask import make_response
@app.route('/')
def index():
    response = make_response('<h1>This document carries a cookie!</h1>')
response.set_cookie('answer', '42')
return response

redirect跳转及404:

from flask import redirect
@app.route('/')
def index():
    return redirect('http://www.example.com')

from flask import abort
@app.route('/user/<id>')
def get_user(id):
    user = load_user(id)
    if not user:
        abort(404)
    return '<h1>Hello, %s</h1>' % user.name
Flask扩展

Flask 被设计为可扩展形式,故而没有提供一些重要的功能,例如数据库和用户认证,所 以开发者可以自由选择最适合程序的包,或者按需求自行开发。

社区成员开发了大量不同用途的扩展,如果这还不能满足需求,你还可使用所有 Python 标 准包或代码库。

使用Flask-Script支持命令行选项

Flask 的开发 Web 服务器支持很多启动设置选项,但只能在脚本中作为参数传给 app.run()函数。这种方式并不十分方便,传递设置选项的理想方式是使用命令行参数。

Flask-Script 是一个 Flask 扩展,为 Flask程序添加了一个命令行解析器。Flask-Script 自带了一组常用选项,而且还支持自定义命令。(版本更新这个插件已经弃用了。)

Flask-Script扩展使用pip安装:

(venv) $ pip install flask-script

from flask.ext.script import Manager
manager = Manager(app)
# ...
if __name__ == '__main__':
    manager.run()

可以这样启动:python hello.py runserver --help

这样可以其他电脑访问:python hello.py runserver --host 0.0.0.0 mac下可能有绑定限制

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第三章:模板

默认情况下,Flask 在程序文件夹中的 templates子文件夹中寻找模板。

渲染模板
from flask import Flask, render_template 

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')


@app.route('/user/<name>')
def user(name):
    return render_template('user.html', name=name)
变量

在模板中使用的{{ name }}结构表示一个变量,它是一种特殊的占位符,告诉模 板引擎这个位置的值从渲染模板时使用的数据中获取。

Jinja2能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。在模板 中使用变量的一些示例如下

<p>A value from a dictionary: {{ mydict['key'] }}.</p>
<p>A value from a list: {{ mylist[3] }}.</p>
<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
<p>A value from an object's method: {{ myobj.somemethod() }}.</p>

可以使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述 模板以首字母大写形式显示变量 name 的值:

Hello, {{ name|capitalize }}

Jinja2 提供的部分常用过滤器:

safe:渲染值时不转义
capitalize:把值的首字母转换成大写,其他字母转换成小写 lower 把值转换成小写形式
upper:把值转换成大写形式
title:把值中每个单词的首字母都转换成大写
trim:把值的首尾空格去掉
striptags:渲染之前把值中所有的 HTML 标签都删掉

safe 过滤器值得特别说明一下。默认情况下,出于安全考虑,Jinja2 会转义所有变量。例 如,如果一个变量的值为 '<h1>Hello</h1>',Jinja2 会将其渲染成 '&lt;h1&gt;Hello&lt;/ h1&gt;',浏览器能显示这个 h1 元素,但不会进行解释。很多情况下需要显示变量中存储 的 HTML 代码,这时就可使用 safe 过滤器。

控制结构

条件控制语句和循环语句:

{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}

<ul>
{% for comment in comments %}
<li>{{ comment }}</li> {% endfor %}
</ul>

Jinja2还支持宏,宏类似于Python代码中的函数《本人觉得不常用,欢迎反驳他的好处,请加18977771077一起讨论》。例如:

{% macro render_comment(comment) %} <li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }} {% endfor %}
</ul>

为了重复使用宏,我们可以将其保存在单独的文件中,然后在需要使用的模板中导入:

{% import 'macros.html' as macros %} <ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %} </ul>

需要在多处重复使用的模板代码片段可以写入单独的文件,再包含在所有模板中,以避免 重复《这个就很实用必须要用》:

{% include 'common.html' %}

还有另外一种很实用学会很有帮助的,另一种重复使用代码的强大方式是模板继承,它类似于 Python 代码中的类继承。首先,创建一个名为 base.html 的基模板:

<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %} </body>
</html>

block 标签定义的元素可在衍生模板中修改。在本例中,我们定义了名为 head、title 和body 的块。注意,title 包含在 head 中。下面这个示例是基模板的衍生模板:

{% extends "base.html" %}
{% block title %}Index{% endblock %} {% block head %}
{{ super() }}
<style>
</style>
{% endblock %}
{% block body %}
<h1>Hello, World!</h1>
{% endblock %}

extends 指令声明这个模板衍生自 base.html。在 extends 指令之后,基模板中的 3 个块被 重新定义,模板引擎会将其插入适当的位置。注意新定义的 head 块,在基模板中其内容不 是空的,**所以使用 super() 获取原来的内容**。

使用Flask-Bootstrap集成Twitter Bootstrap

Bootstrap 是客户端框架,因此不会直接涉及服务器。服务器需要做的只是提供引用了 Bootstrap 层叠样式表(CSS)和 JavaScript 文件的 HTML 响应,并在 HTML、CSS 和 JavaScript 代码中实例化所需组件。这些操作最理想的执行场所就是模板。

(venv) $ pip install flask-bootstrap

hello.py:初始化 Flask-Bootstrap:

from flask.ext.bootstrap import Bootstrap
#...
bootstrap = Bootstrap(app)

templates/user.html:使用 Flask-Bootstrap 的模板

{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
     <div class="container">
         <div class="navbar-header">
             <button type="button" class="navbar-toggle"
              data-toggle="collapse" data-target=".navbar-collapse">
                 <span class="sr-only">Toggle navigation</span>
                 <span class="icon-bar"></span>
                 <span class="icon-bar"></span>
                 <span class="icon-bar"></span>
             </button>
             <a class="navbar-brand" href="/">Flasky</a>
         </div>
         <div class="navbar-collapse collapse">
             <ul class="nav navbar-nav">
                 <li><a href="/">Home</a></li>
             </ul>
         </div>
     </div>
</div>
{% endblock %}
{% block content %} <div class="container">
         <div class="page-header">
             <h1>Hello, {{ name }}!</h1>
         </div>
     </div>
{% endblock %}

Jinja2 中的 extends 指令从 Flask-Bootstrap 中导入 bootstrap/base.html,从而实现模板继 承。Flask-Bootstrap 中的基模板提供了一个网页框架,引入了 Bootstrap 中的所有 CSS 和JavaScript 文件。

很多块都是 Flask-Bootstrap 自用的,如果直接重定义可能会导致一些问题。例 如,Bootstrap 所需的文件在 styles 和 scripts 块中声明。 如果程序需要向已经有内容的块 中添加新内容,必须使用 Jinja2 提供的 super() 函数。 例如,如果要在衍生模板中添加新 的 JavaScript 文件,需要这么定义 scripts 块:

{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}
自定义错误页面

如果你在浏览器的地址栏中输入了不可用的路由,那么会显示一个状态码为 404 的错误页 面。现在这个错误页面太简陋、平庸,而且样式和使用了 Bootstrap 的页面不一致。 像常规路由一样,Flask 允许程序使用基于模板的自定义错误页面。最常见的错误代码有 两个:404,客户端请求未知页面或路由时显示;500,有未处理的异常时显示。

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500

和视图函数一样,错误处理程序也会返回响应。它们还返回与该错误对应的数字状态码。

错误处理程序中引用的模板也需要编写。

示例 3-7 templates/base.html:包含导航条的程序基模板:

.. literalinclude:: code/code3-7.html
   :language: html

这个模板的 content 块中只有一个 <div> 容器,其中包含了一个名为 page_content 的新的空块,块中的内容由衍生模板定义。

现在,程序使用的模板继承自这个模板,而不直接继承自 Flask-Bootstrap 的基模板。通过继承 templates/base.html 模板编写自定义的 404 错误页面很简单.

{% extends "base.html" %}
{% block title %}Flasky - Page Not Found{% endblock %}
{% block page_content %} <div class="page-header">
         <h1>Not Found</h1>
     </div>
{% endblock %}
链接

任何具有多个路由的程序都需要可以连接不同页面的链接,例如导航条。

在模板中直接编写简单路由的 URL 链接不难,但对于包含可变部分的动态路由,在模板 中构建正确的 URL 就很困难。而且,直接编写 URL 会对代码中定义的路由产生不必要的 依赖关系。如果重新定义路由,模板中的链接可能会失效。

为了避免这些问题,Flask 提供了 url_for() 辅助函数,它可以使用程序 URL 映射中保存 的信息生成 URL。

url_for() 函数最简单的用法是以视图函数名(或者 app.add_url_route() 定义路由时使用 的端点名)作为参数,返回对应的 URL。例如,在当前版本的 hello.py 程序中调用 url_for('index')得到的结果是/。调用 url_for('index', _external=True) 返回的则是绝对地 址,在这个示例中是 http://localhost:5000/

使用 url_for() 生成动态地址时,将动态部分作为关键字参数传入。例如, url_for ('user', name='john', _external=True) 的返回结果是http://localhost:5000/user/john。

传入 url_for()的关键字参数不仅限于动态路由中的参数。函数能将任何额外参数添加到 查询字符串中。例如,url_for('index', page=2)的返回结果是/?page=2。

静态文件

调用 url_for('static', filename='css/styles.css', _external=True) 得 到 的 结 果 是 http:// localhost:5000/static/css/styles.css。

默认设置下,Flask 在程序根目录中名为 static 的子目录中寻找静态文件。如果需要,可在 static 文件夹中使用子文件夹存放文件。服务器收到前面那个 URL 后,会生成一个响应, 包含文件系统中 static/css/styles.css 文件的内容。

{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename = 'favicon.ico') }}"
type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename = 'favicon.ico') }}"
type="image/x-icon"> {% endblock %}
使用Flask-Moment本地化日期和时间

讲真 一般博主不用。

如果 Web 程序的用户来自世界各地,那么处理日期和时间可不是一个简单的任务。

服务器需要统一时间单位,这和用户所在的地理位置无关,所以一般使用协调世界时 (Coordinated Universal Time,UTC)。不过用户看到UTC格式的时间会感到困惑,他们更希望看到当地时间,而且采用当地惯用的格式。

有一个使用 JavaScript 开发的优秀客户端开源代码库,名为 moment.js(http://momentjs. com/),它可以在浏览器中渲染日期和时间。Flask-Moment 是一个 Flask 程序扩展,能把 moment.js 集成到 Jinja2 模板中。Flask-Moment 可以使用 pip 安装:

(venv) $ pip install flask-moment
from flask.ext.moment import Moment
moment = Moment(app)

除了 moment.js,Flask-Moment 还依赖 jquery.js。要在 HTML 文档的某个地方引入这两个 库,可以直接引入,这样可以选择使用哪个版本,也可使用扩展提供的辅助函数,从内容 分发网络(Content Delivery Network,CDN)中引入通过测试的版本。Bootstrap已经引入 了 jquery.js,因此只需引入 moment.js 即可。示例 3-12 展示了如何在基模板的 scripts 块 中引入这个库。

{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
from datetime import datetime
@app.route('/')
def index():
        return render_template('index.html',current_time=datetime.utcnow())

templates/index.html:

<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
<p>That was {{ moment(current_time).fromNow(refresh=True) }}</p>

format('LLL') 根据客户端电脑中的时区和区域设置渲染日期和时间。参数决定了渲染的方 式,'L' 到 'LLLL' 分别对应不同的复杂度。format() 函数还可接受自定义的格式说明符。

第二行中的 fromNow() 渲染相对时间戳,而且会随着时间的推移自动刷新显示的时间。这 个时间戳最开始显示为“a few seconds ago”,但指定refresh参数后,其内容会随着时 间的推移而更新。如果一直待在这个页面,几分钟后,会看到显示的文本变成“a minute ago”“2 minutes ago”等。

Flask-Moment 实现了 moment.js 中的 format()、fromNow()、fromTime()、calendar()、valueOf() 和 unix() 方法。你可查阅文档(http://momentjs.com/docs/#/displaying/)学习 moment.js 提供的全部格式化选项。

Flask-Moment 渲染的时间戳可实现多种语言的本地化。语言可在模板中选择,把语言代码 传给 lang() 函数即可:

{{ moment.lang('es') }}

使用本章介绍的技术,你应该能为程序编写出现代化且用户友好的网页。下一章将介绍本 章没有涉及的一个模板功能,即如何通过 Web 表单和用户交互。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第四章:web表单

Flask-WTF(http://pythonhosted.org/Flask-WTF/)扩展可以把处理 Web 表单的过程变成一 种愉悦的体验。这个扩展对独立的 WTForms(http://wtforms.simplecodes.com)包进行了包 装,方便集成到 Flask 程序中。

安装:

(venv) $ pip install flask-wtf
跨站请求伪造保护

默认情况下,Flask-WTF能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery,CSRF)的攻击。恶意网站把请求发送到被攻击者已登录的其他网站时就会引发 CSRF 攻击。

为了实现 CSRF 保护,Flask-WTF 需要程序设置一个密钥。Flask-WTF 使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

app.config 字典可用来存储框架、扩展和程序本身的配置变量。使用标准的字典句法就能 把配置值添加到 app.config 对象中。这个对象还提供了一些方法,可以从文件或环境中导 入配置值。

SECRET_KEY 配置变量是通用密钥,可在 Flask 和多个第三方扩展中使用。如其名所示,加 密的强度取决于变量值的机密程度。不同的程序要使用不同的密钥,而且要保证其他人不 知道你所用的字符串。

表单类

使用 Flask-WTF 时,每个 Web 表单都由一个继承自 Form 的类表示。这个类定义表单中的 一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来 验证用户提交的输入值是否符合要求。

hello.py:定义表单类:

from flask.ext.wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
class NameForm(Form):
    name = StringField('What is your name?', validators=[Required()])
    submit = SubmitField('Submit')

flask版本已经升级了,这里按照书上的from flask.ext.wtf import Form应该会报错,应该写成from flask_wtf import Form

这个表单中的字段都定义为类变量,类变量的值是相应字段类型的对象。

在这个示例中, NameForm 表单中有一个名为 name 的文本字段和一个名为 submit 的提交按钮。StringField 类表示属性为 type="text" 的 <input> 元素。SubmitField 类表示属性为 type="submit" 的 <input> 元素。字段构造函数的第一个参数是把表单渲染成 HTML 时使用的标号。

StringField 构造函数中的可选参数 validators 指定一个由验证函数组成的列表,在接受 用户提交的数据之前验证数据。验证函数 Required() 确保提交的字段不为空。

WTForms支持的HTML标准字段

  • StringField :文本字段
  • TextAreaField :多行文本字段
  • PasswordField :密码文本字段
  • HiddenField :隐藏文本字段
  • DateField :文本字段,值为 datetime.date 格式
  • DateTimeField :文本字段,值为 datetime.datetime 格式
  • IntegerField :文本字段,值为整数
  • DecimalField :文本字段,值为 decimal.Decimal
  • FloatField :文本字段,值为浮点数
  • BooleanField :复选框,值为 True 和 False
  • RadioField :一组单选框
  • SelectField :下拉列表
  • SelectMultipleField :下拉列表,可选择多个值
  • FileField :文件上传字段
  • SubmitField :表单提交按钮
  • FormField:把表单作为字段嵌入另一个表单
  • FieldList:一组指定类型的字段

WTForms验证函数:

  • Email :验证电子邮件地址
  • EqualTo :比较两个字段的值;常用于要求输入两次密码进行确认的情况 IPAddress 验证 IPv4 网络地址
  • Length :验证输入字符串的长度
  • NumberRange :验证输入的值在数字范围内
  • Optional :无输入值时跳过其他验证函数
  • Required :确保字段中有数据
  • Regexp :使用正则表达式验证输入值
  • URL :验证 URL
  • AnyOf :确保输入值在可选值列表中
  • NoneOf :确保输入值不在可选值列表中
把表单渲染成HTML
<form method="POST">
    {{ form.hidden_tag() }}
    {{ form.name.label }} {{ form.name() }}
    {{ form.submit() }}
</form>

要想改进表单的外观,可以把参数传入渲染字段的函数,传入 的参数会被转换成字段的 HTML 属性

<form method="POST">
    {{ form.hidden_tag() }}
    {{ form.name.label }} {{ form.name(id='my-text-field') }}
    {{ form.submit() }}
</form>

渲染出来再定义有的场景不合适,还有另外一种渲染方式用render_kw

verification_code = StringField('验证码', validators=[DataRequired(), render_kw={'style':'width:100px;'})

修改flask-bootstrap的js和css为本地,!在配置文件中设置

BOOTSTRAP_SERVE_LOCAL = True

即便能指定 HTML 属性,但按照这种方式渲染表单的工作量还是很大,所以在条件允许的 情况下最好能使用 Bootstrap 中的表单样式。Flask-Bootstrap 提供了一个非常高端的辅助函 数,可以使用 Bootstrap 中预先定义好的表单样式渲染整个 Flask-WTF 表单,而这些操作 只需一次调用即可完成。使用 Flask-Bootstrap,上述表单可使用下面的方式渲染:

{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

import 指令的使用方法和普通 Python 代码一样,允许导入模板中的元素并用在多个模板 中。导入的 bootstrap/wtf.html 文件中定义了一个使用 Bootstrap 渲染 Falsk-WTF 表单对象 的辅助函数。wtf.quick_form() 函数的参数为 Flask-WTF 表单对象,使用 Bootstrap 的默认 样式渲染传入的表单

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %} 
<div class="page-header">
<h1>Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1> 
</div>
{{ wtf.quick_form(form) }} 
{% endblock %}
在视图函数中处理表单

视图函数 index() 不仅要渲染表单,还要接收表单中的数据:

@app.route('/', methods=['GET', 'POST'])
def index():
    name = None
    form = NameForm()
    if form.validate_on_submit():
        name = form.name.data
        form.name.data = ''
    return render_template('index.html', form=form, name=name)

app.route 修饰器中添加的 methods 参数告诉 Flask 在 URL 映射中把这个视图函数注册为 GET 和 POST 请求的处理程序。如果没指定 methods 参数,就只把视图函数注册为 GET 请求 的处理程序。

把 POST 加入方法列表很有必要,因为将提交表单作为 POST 请求进行处理更加便利。表单 也可作为 GET 请求提交,不过 GET 请求没有主体,提交的数据以查询字符串的形式附加到 URL 中,可在浏览器的地址栏中看到。基于这个以及其他多个原因,提交表单大都作为 POST 请求进行处理。

重定向和用户会话

注意使用methods=['GET', 'POST']

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        session['name'] = form.name.data
        return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'))
Flash消息

请求完成后,有时需要让用户知道状态发生了变化。这里可以使用确认消息、警告或者错 误提醒。一个典型例子是,用户提交了有一项错误的登录表单后,服务器发回的响应重新 渲染了登录表单,并在表单上面显示一个消息,提示用户用户名或密码错误。

这种功能是 Flask 的核心特性。

from flask import Flask, render_template, session, redirect, url_for, flash
@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        old_name = session.get('name')
        if old_name is not None and old_name != form.name.data:
            flash('Looks like you have changed your name!') 
        session['name'] = form.name.data
        return redirect(url_for('index'))
    return render_template('index.html',form = form, name = session.get('name'))

仅调用 flash() 函数并不能把消息显示出来,程序使用的模板要渲染这些消息。最好在 基模板中渲染 Flash 消息,因为这样所有页面都能使用这些消息。Flask 把 get_flashed_messages() 函数开放给模板,用来获取并渲染消息,

{% block content %} 
<div class="container">
{% for message in get_flashed_messages() %} 
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">&times;</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}
{% endblock %} 
</div>
{% endblock %}

在模板中使用循环是因为在之前的请求循环中每次调用 flash() 函数时都会生成一个消息, 所以可能有多个消息在排队等待显示。get_flashed_messages() 函数获取的消息在下次调 用时不会再次返回,因此 Flash 消息只显示一次,然后就消失了。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第五章:数据库

数据库这块是很重要的,多敲遇到问题就搜错误信息一般都能查到解决方案

数据库按照一定规则保存程序数据,程序再发起查询取回所需的数据。Web 程序最常用基 于关系模型的数据库,这种数据库也称为 SQL 数据库,因为它们使用结构化查询语言。不 过最近几年文档数据库和键值对数据库成了流行的替代选择,这两种数据库合称 NoSQL 数据库。

SQL数据库

关系型数据库把数据存储在表中,表模拟程序中不同的实体。例如,订单管理程序的数据 库中可能有表 customers、products 和 orders。

表的列数是固定的,行数是可变的。列定义表所表示的实体的数据属性。例如,customers 表中可能有 name、address、phone 等列。表中的行定义各列对应的真实数据。

表中有个特殊的列,称为主键,其值为表中各行的唯一标识符。表中还可以有称为外键的 列,引用同一个表或不同表中某行的主键。行之间的这种联系称为关系,这是关系型数据 库模型的基础。

NoSQL数据库

所有不遵循上节所述的关系模型的数据库统称为 NoSQL 数据库。NoSQL 数据库一般使用 集合代替表,使用文档代替记录。NoSQL 数据库采用的设计方式使联结变得困难,所以大 多数数据库根本不支持这种操作。对于结构如图 5-1 所示的 NoSQL 数据库,若要列出各 用户及其角色,就需要在程序中执行联结操作,即先读取每个用户的 role_id,再在 roles 表中搜索对应的记录。

使用SQL还是NoSQL

SQL 数据库擅于用高效且紧凑的形式存储结构化数据。这种数据库需要花费大量精力保证 数据的一致性。NoSQL 数据库放宽了对这种一致性的要求,从而获得性能上的优势。 对不同类型数据库的全面分析、对比超出了本书范畴。对中小型程序来说,SQL 和 NoSQL 数据库都是很好的选择,而且性能相当。

Python数据库框架

大多数的数据库引擎都有对应的 Python 包,包括开源包和商业包。Flask 并不限制你使 用何种类型的数据库包,因此可以根据自己的喜好选择使用 MySQL、Postgres、SQLite、 Redis、MongoDB 或者 CouchDB。

如果这些都无法满足需求,还有一些数据库抽象层代码包供选择,例如 SQLAlchemy 和 MongoEngine。你可以使用这些抽象包直接处理高等级的 Python 对象,而不用处理如表、 文档或查询语言此类的数据库实体。

FLask集成度选择框架时,你不一定非得选择已经集成了 Flask 的框架,但选择这些框架可以节省 你编写集成代码的时间。使用集成了 Flask 的框架可以简化配置和操作,所以专门为 Flask 开发的扩展是你的首选。

基于以上因素,本书选择使用的数据库框架是 Flask-SQLAlchemy(http://pythonhosted.org/ Flask-SQLAlchemy/),这个 Flask 扩展包装了 SQLAlchemy(http://www.sqlalchemy.org/)框架。

使用Flask-SQLAlchemy管理数据库

安装:

(venv) $ pip install flask-sqlalchemy

在 Flask-SQLAlchemy 中,数据库使用 URL 指定。最流行的数据库引擎采用的数据库 URL 格式如表 5-1 所示。

  • MySQL | mysql://username:password@hostname/database
  • Postgres | postgresql://username:password@hostname/database
  • SQLite(Unix) | sqlite:////absolute/path/to/database
  • SQLite(Windows) | sqlite:///c:/absolute/path/to/database

在这些 URL 中,hostname 表示 MySQL 服务所在的主机,可以是本地主机(localhost), 也可以是远程服务器。数据库服务器上可以托管多个数据库,因此 database 表示要使用的 数据库名。

程序使用的数据库 URL 必须保存到 Flask 配置对象的 SQLALCHEMY_DATABASE_URI 键中。配 置对象中还有一个很有用的选项,即 SQLALCHEMY_COMMIT_ON_TEARDOWN 键,将其设为 True 时,每次请求结束后都会自动提交数据库中的变动。其他配置选项的作用请参阅 Flask- SQLAlchemy 的文档。

from flask.ext.sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__) 
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite') 
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
db = SQLAlchemy(app)

db 对象是 SQLAlchemy 类的实例,表示程序使用的数据库,同时还获得了 Flask-SQLAlchemy提供的所有功能。

定义模型

模型这个术语表示程序使用的持久化实体。在 ORM 中,模型一般是一个 Python 类,类中 的属性对应数据库表中的列。

Flask-SQLAlchemy 创建的数据库实例为模型提供了一个基类以及一系列辅助类和辅助函 数,可用于定义模型的结构。

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    def __repr__(self):
        return '<User %r>' % self.username

类变量 __tablename__ 定义在数据库中使用的表名。如果没有定义 __tablename__,Flask-SQLAlchemy会使用一个默认名字,但默认的表名没有遵守使用复数形式进行命名的约定, 所以最好由我们自己来指定表名。其余的类变量都是该模型的属性,被定义为 db.Column 类的实例。

db.Column 类构造函数的第一个参数是数据库列和模型属性的类型。

最常用的SQLAlchemy列类型:

  • 类型名 Python类型 说明
  • Integer int 普通整数,一般是 32 位
  • SmallInteger int 取值范围小的整数,一般是 16 位
  • BigInteger int 或 long
  • Float float 浮点数
  • Numeric decimal.Decimal 定点数
  • String str 变长字符串
  • Text str 变长字符串,对较长或不限长度的字符串做了优化
  • Unicode unicode 变长 Unicode 字符串
  • UnicodeText unicode 变长 Unicode 字符串,对较长或不限长度的字符串做了优化
  • Boolean bool 布尔值
  • Date datetime.date 日期
  • Time datetime.time 时间
  • DateTime datetime.datetime 日期和时间
  • Interval datetime.timedelta 时间间隔
  • Enum str 一组字符串
  • PickleType 任何Python对象 自动使用Pickle序列化
  • LargeBinary str 二进制文件

db.Column 中其余的参数指定属性的配置选项。

  • primary_key 如果设为 True,这列就是表的主键
  • unique 如果设为 True,这列不允许出现重复的值
  • index 如果设为 True,为这列创建索引,提升查询效率
  • nullable 如果设为 True,这列允许使用空值;如果设为 False,这列不允许使用空值
  • default 为这列定义默认值

虽然没有强制要求,但这两个模型都定义了 __repr()__ 方法,返回一个具有可读性的字符 串表示模型,可在调试和测试时使用。

关系

关系型数据库使用关系把不同表中的行联系起来。图 5-1 所示的关系图表示用户和角色之 间的一种简单关系。这是角色到用户的一对多关系,因为一个角色可属于多个用户,而每 个用户都只能有一个角色。

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    users = db.relationship('User', backref='role')
    def __repr__(self):
        return '<Role %r>' % self.name

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    def __repr__(self):
        return '<User %r>' % self.username

关系使用 users 表中的外键连接了两行。添加到 User 模型中的 role_id 列 被定义为外键,就是这个外键建立起了关系。传给 db.ForeignKey() 的参数 'roles.id' 表 明,这列的值是 roles 表中行的 id 值。

添加到 Role 模型中的 users 属性代表这个关系的面向对象视角。对于一个 Role 类的实例, 其 users 属性将返回与角色相关联的用户组成的列表。db.relationship() 的第一个参数表 明这个关系的另一端是哪个模型。如果模型类尚未定义,可使用字符串形式指定。

db.relationship() 中的 backref 参数向 User 模型中添加一个 role 属性,从而定义反向关 系。这一属性可替代 role_id 访问 Role 模型,此时获取的是模型对象,而不是外键的值。

大多数情况下,db.relationship() 都能自行找到关系中的外键,但有时却无法决定把 哪一列作为外键。例如,如果 User 模型中有两个或以上的列定义为 Role 模型的外键, SQLAlchemy 就不知道该使用哪列。如果无法决定外键,你就要为 db.relationship() 提供 额外参数,从而确定所用外键.

常用的SQLAlchemy关系选项

  • backref 在关系的另一个模型中添加反向引用
  • primaryjoin 明确指定两个模型之间使用的联结条件。只在模棱两可的关系中需要指定
  • lazy 指定如何加载相关记录。可选值有 select(首次访问时按需加载)、immediate(源对象加载后就加载)、joined(加载记录,但使用联结)、subquery(立即加载,但使用子查询),noload(永不加载)和 dynamic(不加载记录,但提供加载记录的查询)
  • uselist 如果设为 Fales,不使用列表,而使用标量值
  • order_by 指定关系中记录的排序方式
  • secondary 指定多对多关系中关系表的名字
  • secondaryjoin SQLAlchemy 无法自行决定时,指定多对多关系中的二级联结条件

其实要很熟这个关系选项,不然多对多的时候非常报错

一对一关系可以用前面介绍的一对多关系 表示,但调用 db.relationship() 时要把 uselist 设为 False ,把“多”变成“一”。多对 一关系也可使用一对多表示,对调两个表即可,或者把外键和 db.relationship() 都放在“多”这一侧。最复杂的关系类型是多对多,需要用到第三张表,这个表称为关系表

数据库操作
创建表

首先,我们要让 Flask-SQLAlchemy 根据模型类创建数据库。方法是使用 db.create_all() 函数:

(venv) $ python hello.py shell
>>> from hello import db
>>> db.create_all()

如果你查看程序目录,会发现新建了一个名为 data.sqlite 的文件。这个 SQLite 数据库文件 的名字就是在配置中指定的。如果数据库表已经存在于数据库中,那么 db.create_all() 不会重新创建或者更新这个表。如果修改模型后要把改动应用到现有的数据库中,这一特 性会带来不便。更新现有数据库表的粗暴方式是先删除旧表再重新创建:

>>> db.drop_all()
>>> db.create_all()

遗憾的是,这个方法有个我们不想看到的副作用,它把数据库中原有的数据都销毁了。本 章末尾将会介绍一种更好的方式用于更新数据库。

博主使用mysql,就算本地测试也一样。因为使用sqlite时多对多报错 换成mysql就没事了

插入行
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
>>> print(admin_role.id)
None
>>> print(mod_role.id)
None
>>> print(user_role.id)
None
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)

or或者简写成:

db.session.add_all([admin_role, mod_role, user_role,user_john, user_susan, user_david])

为了把对象写入数据库,我们要调用 commit() 方法提交会话:

>>> db.session.commit()

修改行:

>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()

删除行:

>>> db.session.delete(mod_role)
>>> db.session.commit()

查询行:

Role.query.all()
User.query.all()
>>> User.query.filter_by(role=user_role).all()

若要查看 SQLAlchemy 为查询生成的原生 SQL 查询语句,只需把 query 对象转换成字符串:

str(User.query.filter_by(role=user_role))

条件查询:

user_role = Role.query.filter_by(name='User').first()

完整的列表参见 SQLAlchemy 文档(http://docs.sqlalchemy.org)。

常用的SQLAlchemy查询过滤器

  • filter() 把过滤器添加到原查询上,返回一个新查询
  • filter_by() 把等值过滤器添加到原查询上,返回一个新查询
  • limit() 使用指定的值限制原查询返回的结果数量,返回一个新查询
  • offset() 偏移原查询返回的结果,返回一个新查询
  • order_by() 根据指定条件对原查询结果进行排序,返回一个新查询
  • group_by() 根据指定条件对原查询结果进行分组,返回一个新查询

在查询上应用指定的过滤器后,通过调用 all() 执行查询,以列表的形式返回结果。除了 all() 之外,还有其他方法能触发查询执行。

最常使用的SQLAlchemy查询执行函数

  • all() 以列表形式返回查询的所有结果
  • first() 返回查询的第一个结果,如果没有结果,则返回 None
  • first_or_404() 返回查询的第一个结果,如果没有结果,则终止请求,返回 404 错误响应
  • get() 返回指定主键对应的行,如果没有对应的行,则返回 None
  • get_or_404() 返回指定主键对应的行,如果没找到指定的主键,则终止请求,返回 404 错误响应 count() 返回查询结果的数量
  • paginate() 返回一个 Paginate 对象,它包含指定范围内的结果

数据库这块是很重要的,多敲遇到问题就搜错误信息一般都能查到解决方案

在视图函数中操作数据库
@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.name.data).first() 
        if user is None:
            user = User(username = form.name.data)
            db.session.add(user)
            session['known'] = False
        else:
            session['known'] = True
            session['name'] = form.name.data
            form.name.data = ''
        return redirect(url_for('index'))
    return render_template('index.html',form = form, name = session.get('name'),known = session.get('known', False))

在这个修改后的版本中,提交表单后,程序会使用 filter_by() 查询过滤器在数据库中查 找提交的名字。变量 known 被写入用户会话中,因此重定向之后,可以把数据传给模板, 用来显示自定义的欢迎消息。注意,要想让程序正常运行,你必须按照前面介绍的方法, 在 Python shell 中创建数据库表。

集成Python shell

每次启动 shell 会话都要导入数据库实例和模型,这真是份枯燥的工作。为了避免一直重复 导入,我们可以做些配置,让 Flask-Script 的 shell 命令自动导入特定的对象。

from flask.ext.script import Shell
def make_shell_context():
    return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))

$ python hello.py shell
>>> app
<Flask 'app'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'>
>>> User
<class 'app.User'>
使用Flask-Migrate实现数据库迁移

在开发程序的过程中,你会发现有时需要修改数据库模型,而且修改之后还需要更新数据库。

仅当数据库表不存在时,Flask-SQLAlchemy 才会根据模型进行创建。因此,更新表的唯一 方式就是先删除旧表,不过这样做会丢失数据库中的所有数据。

SQLAlchemy 的主力开发人员编写了一个迁移框架,称为 Alembic(https://alembic.readthedocs.org/en/latest/index.html)。除了直接使用 Alembic 之外,Flask 程序还可使用 Flask-Migrate (http://flask-migrate.readthedocs.org/en/latest/)扩展。这个扩展对 Alembic 做了轻量级包装,并集成到 Flask-Script 中,所有操作都通过 Flask-Script 命令完成。

创建迁移仓库
(venv) $ pip install flask-migrate
from flask.ext.migrate import Migrate, MigrateCommand
# ...
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
(venv) $ python hello.py db init

创建迁移脚本:

python hello.py db migrate -m "initial migration"

or:

python hello.py db migrate

更新数据库:

(venv) $ python hello.py db upgrade

数据库的设计和使用是很重要的话题,甚至有整本的书对其进行介绍。你应该把本章视作 一个概览,更高级的话题会在后续各章中讨论。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第六章:电子邮件

这章节其实跟flask不是很大关系,主要是与flask的集成,跳过这章学习对之后也没影响

安装:

(venv) $ pip install flask-mail

#配置

import os
# ...
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME'
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

#初始化
from flask.ext.mail import Mail
mail = Mail(app)

保存电子邮件服务器用户名和密码的两个环境变量要在环境中定义。如果你在 Linux 或 Mac OS X 中使用 bash,那么可以按照下面的方式设定这两个变量:

(venv) $ export MAIL_USERNAME=<Gmail username>
(venv) $ export MAIL_PASSWORD=<Gmail password>

微软 Windows 用户可按照下面的方式设定环境变量:

   (venv) $ set MAIL_USERNAME=<Gmail username>
   (venv) $ set MAIL_PASSWORD=<Gmail password>



.. literalinclude:: code/code6-1.py
   :language: python


.. literalinclude:: code/code6-2.py
   :language: python
异步发送电子邮件
from threading import Thread
def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)

def send_email(to, subject, template, **kwargs):
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
        sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to]) 
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr

程序要发送大量电子邮件时,使 用专门发送电子邮件的作业要比给每封邮件都新建一个线程更合适。例如,我们可以把执 行 send_async_email() 函数的操作发给 Celery(http://www.celeryproject.org/)任务队列。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第七章:大型程序的结构

这里组建大型项目结构,其实有更便捷的模板构建工具cookiecutter,省得每次项目都手动创建项目结构

尽管在单一脚本中编写小型 Web 程序很方便,但这种方法并不能广泛使用。程序变复杂 后,使用单个大型源码文件会导致很多问题。 不同于大多数其他的 Web 框架,Flask 并不强制要求大型项目使用特定的组织方式,程序 结构的组织方式完全由开发者决定。在本章,我们将介绍一种使用包和模块组织大型程序 的方式。本书后续示例都将采用这种结构。

配置选项
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:

    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' 
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True
    FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
    FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>' 
    FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')

    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config): 
    DEBUG = True
    MAIL_SERVER = 'smtp.googlemail.com'
    MAIL_PORT = 587
    MAIL_USE_TLS = True
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config): 
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')

config = {
    'development': DevelopmentConfig, 
    'testing': TestingConfig, 
    'production': ProductionConfig,
    'default': DevelopmentConfig 
}

基类 Config 中包含通用配置,子类分别定义专用的配置。如果需要,你还可添加其他配 置类。

为了让配置方式更灵活且更安全,某些配置可以从环境变量中导入。 例如,SECRET_KEY 的值, 这是个敏感信息,可以在环境中设定,但系统也提供了一个默认值,以防环境中没有定义。

在 3 个子类中,SQLALCHEMY_DATABASE_URI 变量都被指定了不同的值。这样程序就可在不同 的配置环境中运行,每个环境都使用不同的数据库。

配置类可以定义 init_app() 类方法,其参数是程序实例。在这个方法中,可以执行对当前 环境的配置初始化。现在,基类 Config 中的 init_app() 方法为空。

在这个配置脚本末尾,config 字典中注册了不同的配置环境,而且还注册了一个默认配置 (本例的开发环境)。

程序包

程序包用来保存程序的所有代码、模板和静态文件。我们可以把这个包直接称为 app(应 用),如果有需求,也可使用一个程序专用名字。templates 和 static 文件夹是程序包的一部 分,因此这两个文件夹被移到了 app 中。数据库模型和电子邮件支持函数也被移到了这个 包中,分别保存为 app/models.py 和 app/email.py。

使用程序工厂函数

在单个文件中开发程序很方便,但却有个很大的缺点,因为程序在全局作用域中创建,所 以无法动态修改配置。运行脚本时,程序实例已经创建,再修改配置为时已晚。这一点对 单元测试尤其重要,因为有时为了提高测试覆盖度,必须在不同的配置环境中运行程序。

这个问题的解决方法是延迟创建程序实例,把创建过程移到可显式调用的工厂函数中。这 种方法不仅可以给脚本留出配置程序的时间,还能够创建多个程序实例,这些实例有时在 测试中非常有用。程序的工厂函数在 app 包的构造文件中定义

构造文件导入了大多数正在使用的 Flask 扩展。由于尚未初始化所需的程序实例,所以没 有初始化扩展,创建扩展类时没有向构造函数传入参数。create_app() 函数就是程序的工 厂函数,接受一个参数,是程序使用的配置名。配置类在 config.py 文件中定义,其中保存 的配置可以使用Flask app.config配置对象提供的from_object()方法直接导入程序。至 于配置对象,则可以通过名字从 config 字典中选择。程序创建并配置好后,就能初始化 扩展了。在之前创建的扩展对象上调用 init_app() 可以完成初始化过程。

app/__init__.py:程序包的构造文件:

from flask import Flask, render_template 
from flask.ext.bootstrap import Bootstrap 
from flask.ext.mail import Mail
from flask.ext.moment import Moment
from flask.ext.sqlalchemy import SQLAlchemy 
from config import config

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()

def create_app(config_name):
    app = Flask(__name__) 
    app.config.from_object(config[config_name]) 
    config[config_name].init_app(app)
    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)
    # 附加路由和自定义的错误页面 
    return app

工厂函数返回创建的程序示例,不过要注意,现在工厂函数创建的程序还不完整,因为没 有路由和自定义的错误页面处理程序。这是下一节要讲的话题。

在蓝本中实现程序功能

转换成程序工厂函数的操作让定义路由变复杂了。在单脚本程序中,程序实例存在于全 局作用域中,路由可以直接使用 app.route 修饰器定义。但现在程序在运行时创建,只 有调用 create_app() 之后才能使用 app.route 修饰器,这时定义路由就太晚了。和路由 一样,自定义的错误页面处理程序也面临相同的困难,因为错误页面处理程序使用 app. errorhandler 修饰器定义。

幸好 Flask 使用蓝本提供了更好的解决方法。蓝本和程序类似,也可以定义路由。不同的 是,在蓝本中定义的路由处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序 的一部分。使用位于全局作用域中的蓝本时,定义路由的方法几乎和单脚本程序一样。

和程序一样,蓝本可以在单个文件中定义,也可使用更结构化的方式在包中的多个模块中 创建。为了获得最大的灵活性,程序包中创建了一个子包,用于保存蓝本。示例 7-4 是这 个子包的构造文件,蓝本就创建于此。

app/main/__init__.py:创建蓝本:

from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors

通过实例化一个 Blueprint 类对象可以创建蓝本。这个构造函数有两个必须指定的参数: 蓝本的名字和蓝本所在的包或模块。和程序一样,大多数情况下第二个参数使用 Python 的 __name__ 变量即可。

程序的路由保存在包里的 app/main/views.py 模块中,而错误处理程序保存在 app/main/ errors.py 模块中。导入这两个模块就能把路由和错误处理程序与蓝本关联起来。注意,这 些模块在 app/main/__init__.py 脚本的末尾导入,这是为了避免循环导入依赖,因为在 views.py 和 errors.py 中还要导入蓝本 main。

蓝本在工厂函数 create_app() 中注册到程序上:

def create_app(config_name):
    #...
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)
    return app

app/main/errors.py:蓝本中的错误处理程序:

from flask import render_template
from . import main

@main.app_errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

@main.app_errorhandler(500)
def internal_server_error(e)
    return render_template('500.html'), 500

在蓝本中编写错误处理程序稍有不同,如果使用 errorhandler 修饰器,那么只有蓝本中的错误才能触发处理程序。要想注册程序全局的错误处理程序,必须使用 app_errorhandler。

app/main/views.py:蓝本中定义的程序路由

from datetime import datetime
from flask import render_template, session, redirect, url_for
from . import main
from .forms import NameForm
from .. import db
from ..models import User
@main.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('.index'))
    return render_template('index.html',
        form=form, name=session.get('name'),
        known=session.get('known', False),
        current_time=datetime.utcnow())

在蓝本中编写视图函数主要有两点不同:第一,和前面的错误处理程序一样,路由修饰器 由蓝本提供;第二,url_for() 函数的用法不同。你可能还记得,url_for() 函数的第一 个参数是路由的端点名,在程序的路由中,默认为视图函数的名字。例如,在单脚本程序 中,index() 视图函数的 URL 可使用 url_for('index') 获取。

在蓝本中就不一样了,Flask 会为蓝本中的全部端点加上一个命名空间,这样就可以在不 同的蓝本中使用相同的端点名定义视图函数,而不会产生冲突。命名空间就是蓝本的名字 (Blueprint 构造函数的第一个参数),所以视图函数 index() 注册的端点名是 main.index,其 URL 使用 url_for('main.index') 获取。

url_for() 函数还支持一种简写的端点形式,在蓝本中可以省略蓝本名,例如url_for('. index')。在这种写法中,命名空间是当前请求所在的蓝本。这意味着同一蓝本中的重定向 可以使用简写形式,但跨蓝本的重定向必须使用带有命名空间的端点名。

为了完全修改程序的页面,表单对象也要移到蓝本中,保存于 app/main/forms.py 模块。

启动脚本

顶级文件夹中的 manage.py 文件用于启动程序。 manage.py:

#!/usr/bin/env python
import os
from app import create_app, db
from app.models import User, Role
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand
app = create_app(os.getenv('FLASK_CONFIG') or 'default') 
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
    return dict(app=app, db=db, User=User, Role=Role)

manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manager.run()

这个脚本先创建程序。如果已经定义了环境变量 FLASK_CONFIG,则从中读取配置名;否则 使用默认配置。然后初始化 Flask-Script、Flask-Migrate 和为 Python shell 定义的上下文。

出于便利,脚本中加入了 shebang 声明,所以在基于 Unix 的操作系统中可以通过 ./manage. py执行脚本,而不用使用复杂的python manage.py。

需求文件

程序中必须包含一个 requirements.txt 文件,用于记录所有依赖包及其精确的版本号。如果 要在另一台电脑上重新生成虚拟环境,这个文件的重要性就体现出来了,例如部署程序时 使用的电脑。pip 可以使用如下命令自动生成这个文件:

(venv) $ pip freeze >requirements.txt

安装或升级包后,最好更新这个文件。

如果你要创建这个虚拟环境的完全副本,可以创建一个新的虚拟环境,并在其上运行以下 命令:

(venv) $ pip install -r requirements.txt

当你阅读本书时,该示例 requirements.txt 文件中的版本号可能已经过期了。如果愿意,你 可以试着使用这些包的最新版。如果遇到问题,你可以随时换回这个需求文件中的版本, 因为这些版本和程序兼容。

单元测试
#!/usr/bin/env python
import unittest
from flask import current_app 
from app import create_app, db

class BasicsTestCase(unittest.TestCase):

    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_app_exists(self):
        self.assertFalse(current_app is None)

    def test_app_is_testing(self):
        self.assertTrue(current_app.config['TESTING'])

这个测试使用 Python 标准库中的 unittest 包编写。setUp() 和 tearDown() 方法分别在各 测试前后运行,并且名字以 test_开头的函数都作为测试执行。

setUp() 方法尝试创建一个测试环境,类似于运行中的程序。首先,使用测试配置创建程 序,然后激活上下文。这一步的作用是确保能在测试中使用 current_app,像普通请求一 样。然后创建一个全新的数据库,以备不时之需。数据库和程序上下文在 tearDown() 方法 中删除。

第一个测试确保程序实例存在。第二个测试确保程序在测试配置中运行。若想把 tests 文 件夹作为包使用,需要添加 tests/__init__.py 文件,不过这个文件可以为空,因为 unittest 包会扫描所有模块并查找测试。

为了运行单元测试,你可以在 manage.py 脚本中添加一个自定义命令:

@manager.command
def test():
    """Run the unit tests."""
    import unittest
    tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)

manager.command 修饰器让自定义命令变得简单。修饰函数名就是命令名,函数的文档字符 串会显示在帮助消息中。test() 函数的定义体中调用了 unittest 包提供的测试运行函数。

单元测试可使用下面的命令运行:

(venv) $ python manage.py test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
.----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
创建数据库

重组后的程序和单脚本版本使用不同的数据库。

首选从环境变量中读取数据库的 URL,同时还提供了一个默认的 SQLite 数据库做备用。3 种配置环境中的环境变量名和 SQLite 数据库文件名都不一样。例如,在开发环境中,数据 库 URL 从环境变量 DEV_DATABASE_URL 中读取,如果没有定义这个环境变量,则使用名为 data-dev.sqlite 的 SQLite 数据库。

不管从哪里获取数据库 URL,都要在新数据库中创建数据表。如果使用 Flask-Migrate 跟 踪迁移,可使用如下命令创建数据表或者升级到最新修订版本:

(venv) $ python manage.py db upgrade

不管你是否相信,第一部分到此就要结束了。现在你已经学到了使用 Flask 开发 Web 程序 的必备基础知识,不过可能还不确定如何把这些知识融贯起来开发一个真正的程序。本书 第二部分的目的就是解决这个问题,带着你一步一步地开发出一个完整的程序。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第八章:用户认证

大多数程序都要进行用户跟踪。用户连接程序时会进行身份认证,通过这一过程,让程序 知道自己的身份。程序知道用户是谁后,就能提供有针对性的体验。

最常用的认证方法要求用户提供一个身份证明(用户的电子邮件或用户名)和一个密码。 本章要为 Flasky 开发一个完整的认证系统。

Flask的认证扩展

优秀的 Python 认证包很多,但没有一个能实现所有功能。本章介绍的认证方案使用了多个 包,并编写了胶水代码让其良好协作。本章使用的包列表如下

  • Flask-Login:管理已登录用户的用户会话。
  • Werkzeug:计算密码散列值并进行核对。
  • itsdangerous:生成并核对加密安全令牌。

除了认证相关的包之外,本章还用到如下常规用途的扩展

  • Flask-Mail:发送与认证相关的电子邮件。
  • Flask-Bootstrap:HTML 模板。
  • Flask-WTF:Web 表单。
密码安全性

设计 Web 程序时,人们往往会高估数据库中用户信息的安全性。如果攻击者入侵服务器获取了数据库,用户的安全就处在风险之中,这个风险比你想象的要大。众所周知,大多数 用户都在不同的网站中使用相同的密码,因此,即便不保存任何敏感信息,攻击者获得存 储在数据库中的密码之后,也能访问用户在其他网站中的账户。

若想保证数据库中用户密码的安全,关键在于不能存储密码本身,而要存储密码的散列 值。计算密码散列值的函数接收密码作为输入,使用一种或多种加密算法转换密码,最终 得到一个和原始密码没有关系的字符序列。核对密码时,密码散列值可代替原始密码,因 为计算散列值的函数是可复现的:只要输入一样,结果就一样。

计算密码散列值是个复杂的任务,很难正确处理。因此强烈建议你不要自己 实现,而是使用经过社区成员审查且声誉良好的库。如果你对生成安全密码 散列值的过程感兴趣,“Salted Password Hashing - Doing it Right”(计算加盐 密码散列值的正确方法,https://crackstation.net/hashing-security.htm)这篇文 章值得一读。

使用Werkzeug实现密码散列

Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要 两个函数,分别用在注册用户和验证用户阶段。

  • generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):这个函数将 原始密码作为输入,以字符串形式输出密码的散列值,输出的值可保存在用户数据库中。 method 和 salt_length 的默认值就能满足大多数需求。
  • check_password_hash(hash, password):这个函数的参数是从数据库中取回的密码散列 值和用户输入的密码。返回值为 True 表明密码正确。

app/models.py:在 User 模型中加入密码散列

from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    # ...
    password_hash = db.Column(db.String(128))

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

计算密码散列值的函数通过名为 password 的只写属性实现。设定这个属性的值时,赋值 方法会调用 Werkzeug 提供的 generate_password_hash() 函数,并把得到的结果赋值给 password_hash 字段。如果试图读取 password 属性的值,则会返回错误,原因很明显,因 为生成散列值后就无法还原成原来的密码了。

verify_password 方法接受一个参数(即密码),将其传给 Werkzeug 提供的 check_password_hash() 函数,和存储在 User 模型中的密码散列值进行比对。如果这个方法返回 True,就表明密码是正确的。

密码散列功能已经完成,可以在 shell 中进行测试:

(venv) $ python manage.py shell
>>> u = User()
>>> u.password = 'cat'
>>> u.password_hash
'pbkdf2:sha1:1000$duxMk0OF$4735b293e397d6eeaf650aaf490fd9091f928bed'
>>> u.verify_password('cat')
True
>>> u.verify_password('dog')
False
>>> u2 = User()
>>> u2.password = 'cat'
>>> u2.password_hash
'pbkdf2:sha1:1000$UjvnGeTP$875e28eb0874f44101d6b332442218f66975ee89'

注意,即使用户 u 和 u2 使用了相同的密码,它们的密码散列值也完全不一样。为了确保 这个功能今后可持续使用,我们可以把上述测试写成单元测试,以便于重复执行。

tests/test_user_model.py:密码散列化测试:

import unittest
from app.models import User
class UserModelTestCase(unittest.TestCase):

    def test_password_setter(self):
        u = User(password = 'cat')
        self.assertTrue(u.password_hash is not None)
    def test_no_password_getter(self):
        u = User(password = 'cat')
        with self.assertRaises(AttributeError):
            u.password

    def test_password_verification(self):
        u = User(password = 'cat')
        self.assertTrue(u.verify_password('cat'))
        self.assertFalse(u.verify_password('dog'))

    def test_password_salts_are_random(self):
        u = User(password='cat')
        u2 = User(password='cat')
        self.assertTrue(u.password_hash != u2.password_hash)
创建认证蓝本

我们在第 7 章介绍过蓝本,把创建程序的过程移入工厂函数后,可以使用蓝本在全局作用 域中定义路由。与用户认证系统相关的路由可在 auth 蓝本中定义。对于不同的程序功能, 我们要使用不同的蓝本,这是保持代码整齐有序的好方法。

auth 蓝本保存在同名 Python 包中。蓝本的包构造文件创建蓝本对象,再从 views.py 模块 中引入路由.

app/auth/__init__.py:创建蓝本:

from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views

app/auth/views.py 模块引入蓝本,然后使用蓝本的 route 修饰器定义与认证相关的路由,这段代码中添加了一个 /login 路由,渲染同名占位模板

from flask import render_template
from . import auth

@auth.route('/login')
def login():
    return render_template('auth/login.html')

注意,为 render_template() 指定的模板文件保存在 auth 文件夹中。这个文件夹必须在 app/templates 中创建,因为 Flask 认为模板的路径是相对于程序模板文件夹而言的。为避 免与 main 蓝本和后续添加的蓝本发生模板命名冲突,可以把蓝本使用的模板保存在单独的文件夹中。

我们也可将蓝本配置成使用其独立的文件夹保存模板。如果配置了多个模板 文件夹,render_template() 函数会首先搜索程序配置的模板文件夹,然后再 搜索蓝本配置的模板文件夹。

auth 蓝本要在 create_app() 工厂函数中附加到程序上

app/__init__.py:附加蓝本:

def create_app(config_name):
    # ...
    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')
    return app

注册蓝本时使用的 url_prefix 是可选参数 。如果使用了这个参数,注册后蓝本中定义的 所有路由都会加上指定的前缀,即这个例子中的 /auth。例如,/login 路由会注册成 /auth/ login,在开发 Web 服务器中,完整的 URL 就变成了 http://localhost:5000/auth/login

使用Flask-Login认证用户

用户登录程序后,他们的认证状态要被记录下来,这样浏览不同的页面时才能记住这个状 态。Flask-Login 是个非常有用的小型扩展,专门用来管理用户认证系统中的认证状态,且 不依赖特定的认证机制。

使用之前,我们要在虚拟环境中安装这个扩展:

(venv) $ pip install flask-login
准备用于登录的用户模型

要想使用 Flask-Login 扩展,程序的 User 模型必须实现几个方法。需要实现的方法如表 8-1 所示。

Flask-Login要求实现的用户方法:

is_authenticated #如果用户已经登录,必须返回True,否则返回False
is_active() #如果允许用户登录,必须返回 True,否则返回 False。如果要禁用账户,可以返回 False
is_anonymous() #对普通用户必须返回 False
get_id() #必须返回用户的唯一标识符,使用 Unicode 编码字符串

这 4个方法可以在模型类中作为方法直接实现,不过还有一种更简单的替代方案。Flask- Login 提供了一个 UserMixin 类,其中包含这些方法的默认实现,且能满足大多数需求。修 改后的 User 模型如示例 8-6 所示。

app/models.py:修改 User 模型,支持用户登录:

from flask.ext.login import UserMixin

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key = True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    password_hash = db.Column(db.String(128))
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

注意,示例中同时还添加了 email 字段。在这个程序中,用户使用电子邮件地址登录,因 为相对于用户名而言,用户更不容易忘记自己的电子邮件地址。

Flask-Login 在程序的工厂函数中初始化

app/__init__.py:初始化 Flask-Login:

from flask.ext.login import LoginManager
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'

def create_app(config_name):
    # ...
    login_manager.init_app(app)
    # ...

LoginManager 对象的 session_protection 属性可以设为 None、'basic' 或 'strong',以提 供不同的安全等级防止用户会话遭篡改。设为 'strong' 时,Flask-Login 会记录客户端 IP 地址和浏览器的用户代理信息,如果发现异动就登出用户。login_view 属性设置登录页面 的端点。回忆一下,登录路由在蓝本中定义,因此要在前面加上蓝本的名字。

最后,Flask-Login 要求程序实现一个回调函数,使用指定的标识符加载用户。

app/models.py:加载用户的回调函数:

from . import login_manager
@login_manager.user_loader

def load_user(user_id):
    return User.query.get(int(user_id))

加载用户的回调函数接收以 Unicode 字符串形式表示的用户标识符。如果能找到用户,这 个函数必须返回用户对象;否则应该返回 None。

保护路由

为了保护路由只让认证用户访问,Flask-Login 提供了一个 login_required 修饰器。用法演示如下:

from flask.ext.login import login_required
@app.route('/secret')
@login_required

def secret():
    return 'Only authenticated users are allowed!'

如果未认证的用户访问这个路由,Flask-Login 会拦截请求,把用户发往登录页面。

添加登录表单

呈现给用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、一 个“记住我”复选框和提交按钮。这个表单使用的 Flask-WTF 类

app/auth/forms.py:登录表单:

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField,SubmitField
from wtforms.validators import Required, Length, Email
class LoginForm(Form):
    email = StringField('Email', validators=[Required(),(1, 64),Email()])
    password = PasswordField('Password', validators=[Required()])
    remember_me = BooleanField('Keep me logged in')
    submit = SubmitField('Log In')

电子邮件字段用到了 WTForms 提供的 Length() 和 Email() 验证函数。PasswordField 类表 示属性为 type="password" 的 <input> 元素。BooleanField 类表示复选框。

登录页面使用的模板保存在 auth/login.html 文件中。这个模板只需使用 Flask-Bootstrap 提 供的 wtf.quick_form() 宏渲染表单即可。

base.html 模板中的导航条使用 Jinja2 条件语句,并根据当前用户的登录状态分别显示 “Sign In”或“Sign Out”链接。

app/templates/base.html:导航条中的 Sign In 和 Sign Out 链接:

<ul class="nav navbar-nav navbar-right">
    {% if current_user.is_authenticated() %}
    <li>
        <a href="{{ url_for('auth.logout') }}">Sign Out</a></li>
    {% else %}
    <li>
        <a href="{{ url_for('auth.login') }}">Sign In</a>
    </li>
    {% endif %}
</ul>

判断条件中的变量 current_user 由 Flask-Login 定义,且在视图函数和模板中自动可用。 这个变量的值是当前登录的用户,如果用户尚未登录,则是一个匿名用户代理对象。如果 是匿名用户,is_authenticated() 方法返回 False。所以这个方法可用来判断当前用户是否 已经登录。

这里其实已经更新了is_authenticated少了括号

登入用户

app/auth/views.py:登录路由:

from flask import render_template, redirect, request, url_for, flash 
from flask.ext.login import login_user

from . import auth
from ..models import User
from .forms import LoginForm

@auth.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user, form.remember_me.data)
            return redirect(request.args.get('next') or url_for('main.index')) 
        flash('Invalid username or password.')
    return render_template('auth/login.html', form=form)

这个视图函数创建了一个 LoginForm 对象,用法和第 4 章中的那个简单表单一样。当请 求类型是 GET 时,视图函数直接渲染模板,即显示表单。当表单在 POST 请求中提交时, Flask-WTF 中的 validate_on_submit() 函数会验证表单数据,然后尝试登入用户。

为了登入用户,视图函数首先使用表单中填写的 email 从数据库中加载用户。如果电子邮 件地址对应的用户存在,再调用用户对象的 verify_password() 方法,其参数是表单中填 写的密码。如果密码正确,则调用 Flask-Login 中的 login_user() 函数,在用户会话中把 用户标记为已登录。login_user() 函数的参数是要登录的用户,以及可选的“记住我”布 尔值,“记住我”也在表单中填写。如果值为 False,那么关闭浏览器后用户会话就过期 了,所以下次用户访问时要重新登录。如果值为 True,那么会在用户浏览器中写入一个长 期有效的 cookie,使用这个 cookie 可以复现用户会话。

按照第 4 章介绍的“Post/ 重定向 /Get 模式”,提交登录密令的 POST 请求最后也做了重定 向,不过目标 URL 有两种可能。用户访问未授权的 URL 时会显示登录表单,Flask-Login 会把原地址保存在查询字符串的 next 参数中,这个参数可从 request.args 字典中读取。 如果查询字符串中没有 next 参数,则重定向到首页。如果用户输入的电子邮件或密码不正 确,程序会设定一个 Flash 消息,再次渲染表单,让用户重试登录。

在生产服务器上,登录路由必须使用安全的 HTTP,从而加密传送给服务器 的表单数据。如果没使用安全的 HTTP,登录密令在传输过程中可能会被截 取,在服务器上花再多的精力用于保证密码安全都无济于事。

我们需要更新登录模板以渲染表单。

app/templates/auth/login.html:渲染登录表单:

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
    <h1>Login</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}
登出用户

app/auth/views.py:退出路由:

from flask.ext.login import logout_user, login_required

@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('You have been logged out.')
    return redirect(url_for('main.index'))

为了登出用户,这个视图函数调用 Flask-Login 中的 logout_user() 函数,删除并重设用户 会话。随后会显示一个 Flash 消息,确认这次操作,再重定向到首页,这样登出就完成了。

测试登录

略过

注册新用户

如果新用户想成为程序的成员,必须在程序中注册,这样程序才能识别并登入用户。程序 的登录页面中要显示一个链接,把用户带到注册页面,让用户输入电子邮件地址、用户名 和密码。

添加用户注册表单

注册页面使用的表单要求用户输入电子邮件地址、用户名和密码。

app/auth/forms.py:用户注册表单

from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField 
from wtforms.validators import Required, Length, Email, Regexp, EqualTo 
from wtforms import ValidationError
from ..models import User

class RegistrationForm(Form):
    email = StringField('Email', validators=[Required(), Length(1, 64),
        Email()]) 
    username = StringField('Username', validators=[
        Required(), Length(1, 64), 
        Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
            'Usernames must have only letters, '
            'numbers, dots or underscores')]) 
    password = PasswordField('Password', validators=[
        Required(), EqualTo('password2', 
            message='Passwords must match.')]) 
    password2 = PasswordField('Confirm password', 
        validators=[Required()]) 
    submit = SubmitField('Register')

    def validate_email(self, field):
        if User.query.filter_by(email=field.data).first():
            raise ValidationError('Email already registered.')

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('Username already in use.')

这个表单使用 WTForms 提供的 Regexp 验证函数,确保 username 字段只包含字母、数字、 下划线和点号。这个验证函数中正则表达式后面的两个参数分别是正则表达式的旗标和验 证失败时显示的错误消息。

安全起见,密码要输入两次。此时要验证两个密码字段中的值是否一致,这种验证可使用 WTForms 提供的另一验证函数实现,即 EqualTo。这个验证函数要附属到两个密码字段中 的一个上,另一个字段则作为参数传入。

这个表单还有两个自定义的验证函数,以方法的形式实现。如果表单类中定义了以 validate_开头且后面跟着字段名的方法,这个方法就和常规的验证函数一起调用。本例 分别为 email 和 username 字段定义了验证函数,确保填写的值在数据库中没出现过。自定 义的验证函数要想表示验证失败,可以抛出 ValidationError 异常,其参数就是错误消息。

显示这个表单的模板是 /templates/auth/register.html。和登录模板一样,这个模板也使用 wtf.quick_form() 渲染表单。

登录页面要显示一个指向注册页面的链接,让没有账户的用户能轻易找到注册页面。

app/templates/auth/login.html:链接到注册页面:

<p>
    New user?
    <a href="{{ url_for('auth.register') }}">Click here to register</a>
</p>
注册新用户

处理用户注册的过程没有什么难以理解的地方。提交注册表单,通过验证后,系统就使用 用户填写的信息在数据库中添加一个新用户。

app/auth/views.py:用户注册路由

@auth.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data,
            username=form.username.data,
            password=form.password.data)
        db.session.add(user)
        flash('You can now login.')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', form=form)
确认账户

略过

使用itsdangerous生成确认令牌

确认邮件中最简单的确认链接是 http://www.example.com/auth/confirm/<id> 这种形式的 URL,其中 id 是数据库分配给用户的数字 id。用户点击链接后,处理这个路由的视图函 数就将收到的用户 id 作为参数进行确认,然后将用户状态更新为已确认。

但这种实现方式显然不是很安全,只要用户能判断确认链接的格式,就可以随便指定 URL 中的数字,从而确认任意账户。解决方法是把 URL 中的 id 换成将相同信息安全加密后得 到的令牌。

回忆一下我们在第 4 章对用户会话的讨论,Flask 使用加密的签名 cookie 保护用户会话, 防止被篡改。这种安全的 cookie 使用 itsdangerous 包签名。同样的方法也可用于确认令 牌上。

下面这个简短的 shell 会话显示了如何使用 itsdangerous 包生成包含用户 id 的安全令牌:

(venv) $ python manage.py shell
>>> from manage import app
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in = 3600)
>>> token = s.dumps({ 'confirm': 23 })
>>> token
'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...' >>> data = s.loads(token)
>>>
data {u'confirm': 23}

itsdangerous 提供了多种生成令牌的方法。其中,TimedJSONWebSignatureSerializer 类生成 具有过期时间的JSON Web签名(JSON Web Signatures,JWS)。这个类的构造函数接收 的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。

dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令 牌字符串。expires_in 参数设置令牌的过期时间,单位为秒。

为了解码令牌,序列化对象提供了 loads() 方法,其唯一的参数是令牌字符串。这个方法 会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的令牌不正 确或过期了,则抛出异常。

我们可以将这种生成和检验令牌的功能可添加到 User 模型中。

app/models.py:确认用户账户:

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from . import db

class User(UserMixin, db.Model):
    # ...
    confirmed = db.Column(db.Boolean, default=False)

    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.id})

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

generate_confirmation_token() 方法生成一个令牌,有效期默认为一小时。confirm() 方 法检验令牌,如果检验通过,则把新添加的 confirmed 属性设为 True。

除了检验令牌,confirm() 方法还检查令牌中的 id 是否和存储在 current_user 中的已登录用户匹配。如此一来,即使恶意用户知道如何生成签名令牌,也无法确认别人的账户。

发送确认邮件

略过

管理账户

拥有程序账户的用户有时可能需要修改账户信息。下面这些操作可使用本章介绍的技术添 加到验证蓝本中。

安全意识强的用户可能希望定期修改密码。这是一个很容易实现的功能,只要用户处于 登录状态,就可以放心显示一个表单,要求用户输入旧密码和替换的新密码。

为避免用户忘记密码无法登入的情况,程序可以提供重设密码功能。安全起见,有必要 使用类似于确认账户时用到的令牌。用户请求重设密码后,程序会向用户注册时提供的 电子邮件地址发送一封包含重设令牌的邮件。用户点击邮件中的链接,令牌验证后,会 显示一个用于输入新密码的表单。

程序可以提供修改注册电子邮件地址的功能,不过接受新地址之前,必须使用确认邮件 进行验证。使用这个功能时,用户在表单中输入新的电子邮件地址。为了验证这个地 址,程序会发送一封包含令牌的邮件。服务器收到令牌后,再更新用户对象。服务器收 到令牌之前,可以把新电子邮件地址保存在一个新数据库字段中作为待定地址,或者将 其和 id 一起保存在令牌中。

下一章,我们使用用户角色扩充 Flasky 的用户子系统。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第九章:用户角色

其实这里是造轮子的过程,要理解二进制的机制。想找下flask的权限管理系统的插件,但是不尽理想。要么不合适 要么太久没更新,如果找到合适插件这里这段话我会更新。

Web 程序中的用户并非都具有同样地位。在大多数程序中,一小部分可信用户具有额外权 限,用于保证程序平稳运行。管理员就是最好的例子,但有时也需要介于管理员和普通用 户之间的角色,例如内容协管员。

有多种方法可用于在程序中实现角色。具体采用何种实现方法取决于所需角色的数量和细 分程度。例如,简单的程序可能只需要两个角色,一个表示普通用户,一个表示管理员。 对于这种情况,在 User 模型中添加一个 is_administrator 布尔值字段就足够了。复杂的 程序可能需要在普通用户和管理员之间再细分出多个不同等级的角色。有些程序甚至不能 使用分立的角色,这时赋予用户某些权限的组合或许更合适。

角色在数据库中的表示

app/models.py:角色的权限:

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    default = db.Column(db.Boolean, default=False, index=True)
    permissions = db.Column(db.Integer)
    users = db.relationship('User', backref='role',lazy='dynamic')

只有一个角色的 default 字段要设为 True,其他都设为False。用户注册时,其角色会被 设为默认角色。

这个模型的第二处改动是添加了 permissions 字段,其值是一个整数,表示位标志。各操 作都对应一个位位置,能执行某项操作的角色,其位会被设为 1。

显然,各操作所需的程序权限是不一样的。

程序的权限
  • 操 作/位 值/说 明
  • 关注用户/ 0b00000001(0x01)/关注其他用户
  • 在他人的文章中发表评/0b00000010(0x02)/在他人撰写的文章中发布评论
  • 写文章/0b00000100(0x04)/写原创文章
  • 管理他人发表的评论/0b00001000(0x08)/查处他人发表的不当评论
  • 管理员权限/0b10000000(0x80)/管理网站

注意,操作的权限使用 8 位表示,现在只用了其中 5 位,其他 3 位可用于将来的扩充。

app/models.py:权限常量:

class Permission:
    FOLLOW = 0x01
    COMMENT = 0x02
    WRITE_ARTICLES = 0x04
    MODERATE_COMMENTS = 0x08
    ADMINISTER = 0x80

支持的用户角色以及定义角色使用的权限位。

用户角色:
  • 匿名 0b00000000(0x00) 未登录的用户。在程序中只有阅读权限
  • 用户 0b00000111(0x07) 具有发布文章、发表评论和关注其他用户的权限。这是新用户的默认角色
  • 协管员 0b00001111(0x0f) 增加审查不当评论的权限
  • 管理员 0b11111111(0xff) 具有所有权限,包括修改其他用户所属角色的权限

使用权限组织角色,这一做法让你以后添加新角色时只需使用不同的权限组合即可。

将角色手动添加到数据库中既耗时又容易出错。作为替代,我们要在 Role 类中添加一个类 方法,完成这个操作,

app/models.py::在数据库中创建角色

class Role(db.Model):
    # ...
    @staticmethod
    def insert_roles():
        roles = {
            'User': (Permission.FOLLOW |
                Permission.COMMENT |
                Permission.WRITE_ARTICLES, True),
            'Moderator': (Permission.FOLLOW |
                Permission.COMMENT |
                Permission.WRITE_ARTICLES |
                Permission.MODERATE_COMMENTS, False),
            'Administrator': (0xff, False)
        }
        for r in roles:
            role = Role.query.filter_by(name=r).first()
            if role is None:
                role = Role(name=r)
            role.permissions = roles[r][0]
            role.default = roles[r][1]
            db.session.add(role)
        db.session.commit()

insert_roles() 函数并不直接创建新角色对象,而是通过角色名查找现有的角色,然后再 进行更新。只有当数据库中没有某个角色名时才会创建新角色对象。如此一来,如果以后 更新了角色列表,就可以执行更新操作了。要想添加新角色,或者修改角色的权限,修改 roles 数组,再运行函数即可。注意,“匿名”角色不需要在数据库中表示出来,这个角色 的作用就是为了表示不在数据库中的用户。

若想把角色写入数据库,可使用 shell 会话:

(venv) $ python manage.py shell
>>> Role.insert_roles()
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>, <Role u'Moderator'>]
赋予角色

用户在程序中注册账户时,会被赋予适当的角色。大多数用户在注册时赋予的角色都是 “用户”,因为这是默认角色。唯一的例外是管理员,管理员在最开始就应该赋予“管理 员”角色。管理员由保存在设置变量 FLASKY_ADMIN 中的电子邮件地址识别,只要这个电子邮件地址出现在注册请求中,就会被赋予正确的角色。示例 9-4 展示了如何在 User 模型的构造函数中完成这一操作。

app/models.py:定义默认的用户角色:

class User(UserMixin, db.Model):
    #...
    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        if self.role is None:
            self.role = Role.query.filter_by(default=True).first()

    #...

User 类的构造函数首先调用基类的构造函数,如果创建基类对象后还没定义角色.

角色验证

为了简化角色和权限的实现过程,我们可在 User 模型中添加一个辅助方法,检查是否有指 定的权限,

app/models.py:检查用户是否有指定的权限

from flask.ext.login import UserMixin, AnonymousUserMixin

class User(UserMixin, db.Model):
    # ...
    def can(self, permissions):
        return self.role is not None and \
            (self.role.permissions & permissions) == permissions

    def is_administrator(self):
        return self.can(Permission.ADMINISTER)

class AnonymousUser(AnonymousUserMixin):
    
    def can(self, permissions):
        return False
    def is_administrator(self):
        return False

login_manager.anonymous_user = AnonymousUser

User 模型中添加的 can() 方法在请求和赋予角色这两种权限之间进行位与操作。如果角色 中包含请求的所有权限位,则返回 True,表示允许用户执行此项操作。检查管理员权限的 功能经常用到,因此使用单独的方法 is_administrator() 实现。

出于一致性考虑,我们还定义了 AnonymousUser 类,并实现了 can() 方法和 is_administrator() 方法。这个对象继承自 Flask-Login 中的 AnonymousUserMixin 类,并将其设为用户未登录时 current_user 的值。这样程序不用先检查用户是否登录,就能自由调用 current_user.can() 和 current_user.is_administrator()。

如果你想让视图函数只对具有特定权限的用户开放,可以使用自定义的修饰器。示例 9-6 实现了两个修饰器,一个用来检查常规权限,一个专门用来检查管理员权限。

示例 9-6 app/decorators.py:检查用户权限的自定义修饰器

from functools import wraps
from flask import abort
from flask.ext.login import current_user

def permission_required(permission):

    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return decorated_function
    return decorator

def admin_required(f):
    return permission_required(Permission.ADMINISTER)(f)

这两个修饰器都使用了 Python 标准库中的 functools 包,如果用户不具有指定权限,则返 回 403 错误码,即 HTTP“禁止”错误。我们在第 3 章为 404 和 500 错误编写了自定义的 错误页面,所以现在也要添加一个 403 错误页面。

下面我们举两个例子演示如何使用这些修饰器。

from decorators import admin_required, permission_required
from .models import Permission

@main.route('/admin')
@login_required
@admin_required
def for_admins_only():
    return "For administrators!"

@main.route('/moderator')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def for_moderators_only():
    return "For comment moderators!"

在模板中可能也需要检查权限,所以 Permission 类为所有位定义了常量以便于获取。为了 避免每次调用 render_template() 时都多添加一个模板参数,可以使用上下文处理器。上 下文处理器能让变量在所有模板中全局可访问。

app/main/__init__.py:把 Permission 类加入模板上下文:

@main.app_context_processor
def inject_permissions():
   return dict(Permission=Permission)

测试这块不写进来 。暂时没必要

在你阅读下一章之前,最好重新创建或者更新开发数据库,如此一来,那些在实现角色和 权限之前创建的用户账户就被赋予了角色。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十章:用户资料

在本章,我们要实现 Flasky 的用户资料页面。所有社交网站都会给用户提供资料页面,其 中简要显示了用户在网站中的活动情况。用户可以把资料页面的 URL 分享给别人,以此 宣告自己在这个网站上。因此,这个页面的 URL 要简短易记。

资料信息

为了让用户的资料页面更吸引人,我们可以在其中添加一些关于用户的其他信息。

app/models.py:用户信息字段:

class User(UserMixin, db.Model):
    # ...
    name = db.Column(db.String(64))
    location = db.Column(db.String(64))
    about_me = db.Column(db.Text())
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)

新添加的字段保存用户的真实姓名、所在地、自我介绍、注册日期和最后访问日期。about_me 字段的类型是 db.Text()。db.String 和 db.Text 的区别在于后者不需要指定最大长度。

两个时间戳的默认值都是当前时间。注意,datetime.utcnow 后面没有 (),因为 db.Column() 的 default 参数可以接受函数作为默认值,所以每次需要生成默认值时,db.Column() 都会 调用指定的函数。member_since 字段只需要默认值即可。

last_seen 字段创建时的初始值也是当前时间,但用户每次访问网站后,这个值都会被刷新。

这样每次访问都需要一个数据库更新操作增加数据库的访问,会不会性能低下?我也不清楚 app/models.py:刷新用户的最后访问时间:

class User(UserMixin, db.Model):
# ...
def ping(self):
    self.last_seen = datetime.utcnow()
    db.session.add(self)

每次收到用户的请求时都要调用 ping() 方法。由于 auth 蓝本中的 before_app_request 处 理程序会在每次请求前运行,所以能很轻松地实现这个需求。

app/auth/views.py:更新已登录用户的访问时间:

@auth.before_app_request
def before_request():
if current_user.is_authenticated():
    current_user.ping()
    if not current_user.confirmed and request.endpoint[:5] != 'auth.':
        return redirect(url_for('auth.unconfirmed'))
用户资料页面

为每个用户都创建资料页面并没有什么难度。

app/main/views.py:资料页面的路由:

@main.route('/user/<username>')
def user(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        abort(404)
    return render_template('user.html', user=user)

这个路由在 main 蓝本中添加。对于名为 john 的用户,其资料页面的地址是 http://localhost:5000/user/john。这个视图函数会在数据库中搜索 URL 中指定的用户名,如果找到,则渲染模板 user. html,并把用户名作为参数传入模板。如果传入路由的用户名不存在,则返回 404 错误。user. html 模板应该渲染保存在用户对象中的信息。

app/templates/user.html:用户资料页面的模板:

{% block page_content %} 
<div class="page-header">
    <h1>{{ user.username }}</h1>
    {% if user.name or user.location %} 
    <p>
        {% if user.name %}{{ user.name }}{% endif %}
        {% if user.location %}
            From <a href="http://maps.google.com/?q={{ user.location }}">{ user.location }}</a>
        {% endif %} 
    </p>
    {% endif %}
    {% if current_user.is_administrator() %}
        <p><a href="mailto:{{ user.email }}">{{ user.email }}</a></p> 
    {% endif %}
    {% if user.about_me %}
    <p>{{ user.about_me }}</p>
    {% endif %} 
    <p>
    Member since {{ moment(user.member_since).format('L') }}.
             Last seen {{ moment(user.last_seen).fromNow() }}.
    </p>
</div>
{% endblock %}
在这个模板中,有几处实现细节需要说明一下:
  • name 和 location 字段在同一个 <p> 元素中渲染。只有至少定义了这两个字段中的一个时, <p> 元素才会创建。
  • 用户的location字段被渲染成指向谷歌地图的查询链接。
  • 如果登录用户是管理员,那么就显示用户的电子邮件地址,且渲染成mailto链接。

大多数用户都希望能很轻松地访问自己的资料页面,因此我们可以在导航条中添加一个链接。

app/templates/base.html:

{% if current_user.is_authenticated() %}
<li>
    <a href="{{ url_for('main.user', username=current_user.username) }}"> Profile</a>
</li>
{% endif %}

把资料页面的链接包含在条件语句中是非常必要的,因为未认证的用户也能看到导航条, 但我们不应该让他们看到资料页面的链接。

资料编辑器

用户资料的编辑分两种情况。最显而易见的情况是,用户要进入一个页面并在其中输入自 己的资料,而且这些内容显示在自己的资料页面上。还有一种不太明显但也同样重要的情 况,那就是要让管理员能够编辑任意用户的资料——不仅要能编辑用户的个人信息,还要 能编辑用户不能直接访问的 User 模型字段,例如用户角色。这两种编辑需求有本质上的区别,所以我们要创建两个不同的表单。

用户级别的资料编辑器

普通用户的资料编辑表单 app/main/forms.py:资料编辑表单:

class EditProfileForm(Form):
    name = StringField('Real name', validators=[Length(0, 64)])
    location = StringField('Location', validators=[Length(0, 64)])
    about_me = TextAreaField('About me')
    submit = SubmitField('Submit')

注意,这个表单中的所有字段都是可选的,因此长度验证函数允许长度为零。

app/main/views.py:资料编辑路由:

@main.route('/edit-profile', methods=['GET', 'POST']) 
@login_required
def edit_profile():
    form = EditProfileForm()
    if form.validate_on_submit():
        current_user.name = form.name.data
        current_user.location = form.location.data
        current_user.about_me = form.about_me.data
        db.session.add(current_user)
        flash('Your profile has been updated.')
        return redirect(url_for('.user', username=current_user.username))
    form.name.data = current_user.name
    form.location.data = current_user.location
    form.about_me.data = current_user.about_me
    return render_template('edit_profile.html', form=form)

在显示表单之前,这个视图函数为所有字段设定了初始值。对于所有给定字段,这一工作 都是通过把初始值赋值给 form.<field-name>.data 完成的。当 form.validate_on_submit() 返回 False 时,表单中的 3 个字段都使用 current_user 中保存的初始值。提交表单后,表 单字段的 data 属性中保存有更新后的值,因此可以将其赋值给用户对象中的各字段,然后 再把用户对象添加到数据库会话中。

为了让用户能轻易找到编辑页面,我们可以在资料页面中添加一个链接:

{% if user == current_user %}
<a class="btn btn-default" href="{{ url_for('.edit_profile') }}">Edit Profile </a>
{% endif %}

链接外层的条件语句能确保只有当用户查看自己的资料页面时才显示这个链接。

管理员级别的资料编辑器

管理员使用的资料编辑表单比普通用户的表单更加复杂。除了前面的 3 个资料信息字段之 外,管理员在表单中还要能编辑用户的电子邮件、用户名、确认状态和角色。

app/main/forms.py:管理员使用的资料编辑表单

class EditProfileAdminForm(Form):
    email = StringField('Email', validators=[Required(), Length(1, 64),Email()]) 
    username = StringField('Username', validators=[
        Required(), Length(1, 64), 
        Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
            'Usernames must have only letters, ',
            'numbers, dots or underscores')])
    confirmed = BooleanField('Confirmed')
    role = SelectField('Role', coerce=int)
    name = StringField('Real name', validators=[Length(0, 64)]) 
    location = StringField('Location', validators=[Length(0, 64)]) 
    about_me = TextAreaField('About me')
    submit = SubmitField('Submit')

    def __init__(self, user, *args, **kwargs): 
        super(EditProfileAdminForm, self).__init__(*args, **kwargs) 
        self.role.choices = [(role.id, role.name) \
                            for role in Role.query.order_by(Role.name).all()]
        self.user = user

    def validate_email(self, field):
        if field.data != self.user.email and \
                User.query.filter_by(email=field.data).first(): 
            raise ValidationError('Email already registered.')

    def validate_username(self, field):
        if field.data != self.user.username and \
                User.query.filter_by(username=field.data).first(): 
            raise ValidationError('Username already in use.')

WTForms 对 HTML 表单控件 <select> 进行 SelectField 包装,从而实现下拉列表,用来 在这个表单中选择用户角色。 SelectField 实例必须在其 choices 属性中设置各选项 。选 项必须是一个由元组组成的列表,各元组都包含两个元素:选项的标识符和显示在控件中 的文本字符串。choices 列表在表单的构造函数中设定,其值从 Role 模型中获取,使用一 个查询按照角色名的字母顺序排列所有角色。元组中的标识符是角色的 id,因为这是个整 数,所以在 SelectField 构造函数中添加 coerce=int 参数,从而把字段的值转换为整数, 而不使用默认的字符串。

email 和 username 字段的构造方式和认证表单中的一样,但处理验证时需要更加小心。验 证这两个字段时,首先要检查字段的值是否发生了变化,如果有变化,就要保证新值不 和其他用户的相应字段值重复;如果字段值没有变化,则应该跳过验证。为了实现这个逻 辑,表单构造函数接收用户对象作为参数,并将其保存在成员变量中,随后自定义的验证 方法要使用这个用户对象。

app/main/views.py:管理员的资料编辑路由

@login_required
@admin_required
def edit_profile_admin(id):
    user = User.query.get_or_404(id)
    form = EditProfileAdminForm(user=user) 
    if form.validate_on_submit():
        user.email = form.email.data
        user.username = form.username.data
        user.confirmed = form.confirmed.data
        user.role = Role.query.get(form.role.data)
        user.name = form.name.data
        user.location = form.location.data
        user.about_me = form.about_me.data
        db.session.add(user)
        flash('The profile has been updated.')
        return redirect(url_for('.user', username=user.username))
    form.email.data = user.email
    form.username.data = user.username
    form.confirmed.data = user.confirmed
    form.role.data = user.role_id
    form.name.data = user.name
    form.location.data = user.location
    form.about_me.data = user.about_me
    return render_template('edit_profile.html', form=form, user=user)

这个路由和较简单的、普通用户的编辑路由具有基本相同的结构。在这个视图函数中,用 户由 id 指定,因此可使用 Flask-SQLAlchemy 提供的 get_or_404() 函数,如果提供的 id 不正确,则会返回 404 错误。

我们还需要再探讨一下用于选择用户角色的 SelectField。设定这个字段的初始值时, role_id 被赋值给了 field.role.data,这么做的原因在于 choices 属性中设置的元组列表 使用数字标识符表示各选项。表单提交后,id 从字段的 data 属性中提取,并且查询时会 使用提取出来的 id 值加载角色对象。表单中声明 SelectField 时使用 coerce=int 参数, 其作用是保证这个字段的 data 属性值是整数。

为链接到这个页面,我们还需在用户资料页面中添加一个链接按钮,

app/templates/user.html:管理员使用的资料编辑链接:

{% if current_user.is_administrator() %}
<a class="btn btn-danger" href="{{ url_for('.edit_profile_admin', id=user.id) }}">
    Edit Profile [Admin]
</a>
{% endif %}

为了醒目,这个按钮使用了不同的 Bootstrap 样式进行渲染。这里使用的条件语句确保只当 登录用户为管理员时才显示按钮。

用户头像

可以使用email以外的值也行比如传唯一的用户名或手机号哈希加密过后 通过显示用户的头像,我们可以进一步改进资料页面的外观。在本节,你会学到如何添加 Gravatar(http://gravatar.com/)提供的用户头像。Gravatar 是一个行业领先的头像服务,能 把头像和电子邮件地址关联起来。用户先要到 http://gravatar.com 中注册账户,然后上传图 片。生成头像的 URL 时,要计算电子邮件地址的 MD5 散列值:

(venv) $ python
>>> import hashlib
>>> hashlib.md5('john@example.com'.encode('utf-8')).hexdigest()
'd4c74594d841139328695756648b6bd6'

生 成 的 头 像 URL 是 在 http://www.gravatar.com/avatar/https://secure.gravatar.com/avatar/ 之后加上这个 MD5 散列值。例如,你在浏览器的地址栏中输入 http://www.gravatar.com/ avatar/d4c74594d841139328695756648b6bd6,就会看到电子邮件地址 john@example.com 对 应的头像图片。如果这个电子邮件地址没有对应的头像,则会显示一个默认图片。头像 URL 的查询字符串中可以包含多个参数以配置头像图片的特征

Gravatar查询字符串参数:
  • s 图片大小,单位为像素
  • r 图片级别。可选值有 "g"、"pg"、"r" 和 "x"
  • d 没有注册 Gravatar 服务的用户使用的默认图片生成方式。可选值有:"404",返回 404 错误;默认图片的 URL;图片生成器 "mm"、"identicon"、"monsterid"、"wavatar"、"retro" 或 "blank"之一
  • fd 强制使用默认头像

我们可将构建 Gravatar URL 的方法添加到 User 模型中,

import hashlib
from flask import request

class User(UserMixin, db.Model):
    # ...
    def gravatar(self, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        hash = hashlib.md5(self.email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
            url=url, hash=hash, size=size, default=default, rating=rating)

这一实现会选择标准的或加密的Gravatar URL基以匹配用户的安全需求。头像的URL由 URL 基、用户电子邮件地址的 MD5 散列值和参数组成,而且各参数都设定了默认值。有 了上述实现,我们就可以在 Python shell 中轻易生成头像的 URL 了:

(venv) $ python manage.py shell
>>> u = User(email='john@example.com')
>>> u.gravatar()
'http://www.gravatar.com/avatar/d4c74594d84113932869575bd6?s=100&d=identicon&r=g'
>>> u.gravatar(size=256)
'http://www.gravatar.com/avatar/d4c74594d84113932869575bd6?s=256&d=identicon&r=g'

gravatar() 方法也可在 Jinja2 模板中调用。

app/tempaltes/user.html:资料页面中的头像:

...
<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256) }}">
...

使用类似方式,我们可在基模板的导航条上添加一个已登录用户头像的小型缩略图。为了 更好地调整页面中头像图片的显示格式,我们可使用一些自定义的 CSS 类。你可以在源码 仓库的 styles.css 文件中查看自定义的 CSS,styles.css 文件保存在程序静态文件的文件夹 中,而且要在 base.html 模板中引用。图 10-3 为显示了头像的用户资料页面。

生成头像时要生成 MD5 值,这是一项 CPU 密集型操作。如果要在某个页面中生成大量头 像,计算量会非常大。由于用户电子邮件地址的 MD5 散列值是不变的,因此可以将其缓 存在 User 模型中。若要把 MD5 散列值保存在数据库中,需要对 User 模型做些改动,

app/models.py:使用缓存的 MD5 散列值生成 Gravatar URL

class User(UserMixin, db.Model):
    # ...
    avatar_hash = db.Column(db.String(32))

    def __init__(self, **kwargs):
        # ...
        if self.email is not None and self.avatar_hash is None:
            self.avatar_hash = hashlib.md5(
                self.email.encode('utf-8')).hexdigest()

    def change_email(self, token):
        # ...
        self.email = new_email
        self.avatar_hash = hashlib.md5(
            self.email.encode('utf-8')).hexdigest()
        db.session.add(self)
        return True

    def gravatar(self, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        hash = self.avatar_hash or hashlib.md5(
            self.email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
            url=url, hash=hash, size=size, default=default, rating=rating)

模型初始化过程中会计算电子邮件的散列值,然后存入数据库,若用户更新了电子邮件 地址,则会重新计算散列值。gravatar() 方法会使用模型中保存的散列值;如果模型中没 有,就和之前一样计算电子邮件地址的散列值。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十一章:博客文章

在本章,我们要实现 Flasky 的主要功能,即允许用户阅读、撰写博客文章。本章你会学到 一些新技术:重用模板、分页显示长列表以及处理富文本。

提交和显示博客文章

为支持博客文章,我们需要创建一个新的数据库模型

app/models.py:文章模型:

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))

class User(UserMixin, db.Model):
    # ...
    posts = db.relationship('Post', backref='author', lazy='dynamic')

博客文章包含正文、时间戳以及和 User 模型之间的一对多关系。body 字段的定义类型是 db.Text,所以不限制长度。

在程序的首页要显示一个表单,以便让用户写博客。这个表单很简单,只包括一个多行文本 输入框,用于输入博客文章的内容,另外还有一个提交按钮,

app/main/forms.py:博客文章表单:

class PostForm(Form):
    body = TextAreaField("What's on your mind?", validators=[Required()])
    submit = SubmitField('Submit')

index() 视图函数处理这个表单并把以前发布的博客文章列表传给模板

app/main/views.py:处理博客文章的首页路由:

@main.route('/', methods=['GET', 'POST'])
def index():
    form = PostForm()
    if current_user.can(Permission.WRITE_ARTICLES) and \
            form.validate_on_submit():
        post = Post(body=form.body.data,
                author=current_user._get_current_object())
        db.session.add(post)
        return redirect(url_for('.index'))

    posts = Post.query.order_by(Post.timestamp.desc()).all()
    return render_template('index.html', form=form, posts=posts)

这个视图函数把表单和完整的博客文章列表传给模板。文章列表按照时间戳进行降序排 列。博客文章表单采取惯常处理方式,如果提交的数据能通过验证就创建一个新 Post 实 例。在发布新文章之前,要检查当前用户是否有写文章的权限。

注意,新文章对象的 author 属性值为表达式 current_user._get_current_object()。变量 current_user 由 Flask-Login 提供,和所有上下文变量一样,也是通过线程内的代理对象实 现。这个对象的表现类似用户对象,但实际上却是一个轻度包装,包含真正的用户对象。 数据库需要真正的用户对象,因此要调用 _get_current_object() 方法。

这个表单显示在 index.html 模板中欢迎消息的下方,其后是博客文章列表。在这个博客文 章列表中,我们首次尝试创建博客文章时间轴,按照时间顺序由新到旧列出了数据库中所 有的博客文章。

app/templates/index.html:显示博客文章的首页模板:

.. literalinclude:: code/code11-1.html
   :language: html

注意,如果用户所属角色没有 WRITE_ARTICLES 权限,则经 User.can() 方法检查后,不会显 示博客文章表单。博客文章列表通过 HTML 无序列表实现,并指定了一个 CSS 类,从而 让格式更精美。页面左侧会显示作者的小头像,头像和作者用户名都渲染成链接形式,可 链接到用户资料页面。所用的 CSS 样式都存储在程序 static 文件夹里的 style.css 文件中。 你可到 GitHub 仓库中查看这个文件。

在资料页中显示博客文章

我们可以将用户资料页改进一下,在上面显示该用户发布的博客文章列表。

app/main/views.py:获取博客文章的资料页路由:

@main.route('/user/<username>')
def user(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        abort(404)
    posts = user.posts.order_by(Post.timestamp.desc()).all()
    return render_template('user.html', user=user, posts=posts)

用户发布的博客文章列表通过 User.posts 关系获取,User.posts 返回的是查询对象,因此 可在其上调用过滤器,例如 order_by()。

和index.html模板一样,user.html模板也要使用一个HTML <ul>元素渲染博客文章。维护 两个完全相同的 HTML 片段副本可不是个好主意,遇到这种情况,Jinja2 提供的 include() 指令就非常有用。user.html 模板包含了其他文件中定义的列表,

app/templates/user.html:显示有博客文章的资料页模板:

...
<h3>Posts by {{ user.username }}</h3>
{% include '_posts.html' %}
...

为了完成这种新的模板组织方式,index.html 模板中的 <ul> 元素需要移到新模板 _posts. html 中,并替换成另一个 include() 指令。注意,_posts.html 模板名的下划线前缀不是必 须使用的,这只是一种习惯用法,以区分独立模板和局部模板。

分页显示长博客文章列表

随着网站的发展,博客文章的数量会不断增多,如果要在首页和资料页显示全部文章,浏 览速度会变慢且不符合实际需求。在 Web 浏览器中,内容多的网页需要花费更多的时间生 成、下载和渲染,所以网页内容变多会降低用户体验的质量。这一问题的解决方法是分页 显示数据,进行片段式渲染。

创建虚拟博客文章数据

不能生成中文数据? 若想实现博客文章分页,我们需要一个包含大量数据的测试数据库。手动添加数据库记录 浪费时间而且很麻烦,所以最好能使用自动化方案。有多个 Python 包可用于生成虚拟信 息,其中功能相对完善的是 ForgeryPy,可以使用 pip 进行安装:

(venv) $ pip install forgerypy

严格来说,ForgeryPy 并不是这个程序的依赖,因为它只在开发过程中使用。为了区分生 产环境的依赖和开发环境的依赖,我们可以把文件 requirements.txt 换成 requirements 文件 夹,它们分别保存不同环境中的依赖。在这个新建的文件夹中,我们可以创建一个 dev.txt 文件,列出开发过程中所需的依赖,再创建一个 prod.txt 文件,列出生产环境所需的依赖。 由于两个环境所需的依赖大部分是相同的,因此可以创建一个 common.txt 文件,在 dev.txt 和 prod.txt 中使用 -r 参数导入。

requirements/dev.txt:开发所需的依赖文件:

-r common.txt
ForgeryPy==0.1

app/models.py:生成虚拟用户和博客文章

class User(UserMixin, db.Model):
    # ...
    @staticmethod
    def generate_fake(count=100):
        from sqlalchemy.exc import IntegrityError
        from random import seed
        import forgery_py

        seed()
        for i in range(count):
            u = User(
                    email=forgery_py.internet.email_address(), 
                    username=forgery_py.internet.user_name(True), 
                    password=forgery_py.lorem_ipsum.word(), 
                    confirmed=True, name=forgery_py.name.full_name(), 
                    location=forgery_py.address.city(), 
                    about_me=forgery_py.lorem_ipsum.sentence(), 
                    member_since=forgery_py.date.date(True)
                )
            db.session.add(u)
            try:
                db.session.commit()
            except IntegrityError:
                db.session.rollback()


class Post(db.Model):
    # ...
    @staticmethod
    def generate_fake(count=100):
        from random import seed, randint
        import forgery_py
        seed()

        user_count = User.query.count()
        for i in range(count):
            u = User.query.offset(randint(0, user_count - 1)).first()
            p = Post(
                    body=forgery_py.lorem_ipsum.sentences(randint(1, 3)),
                    timestamp=forgery_py.date.date(True),
                    author=u
                )
            db.session.add(p)
            db.session.commit()

这些虚拟对象的属性由 ForgeryPy 的随机信息生成器生成,其中的名字、电子邮件地址、 句子等属性看起来就像真的一样。

用户的电子邮件地址和用户名必须是唯一的,但 ForgeryPy 随机生成这些信息,因 此有重复的风险。如果发生了这种不太可能出现的情况,提交数据库会话时会抛出 IntegrityError 异常。这个异常的处理方式是,在继续操作之前回滚会话。在循环中生成 重复内容时不会把用户写入数据库,因此生成的虚拟用户总数可能会比预期少。

随机生成文章时要为每篇文章随机指定一个用户。为此,我们使用 offset() 查询过滤器。 这个过滤器会跳过参数中指定的记录数量。通过设定一个随机的偏移值,再调用 first() 方法,就能每次都获得一个不同的随机用户。

使用新添加的方法,我们可以在 Python shell 中轻易生成大量虚拟用户和文章:

(venv) $ python manage.py shell
>>> User.generate_fake(100)
>>> Post.generate_fake(100)
在页面中渲染数据

app/main/views.py:分页显示博客文章列表:

@main.route('/', methods=['GET', 'POST'])
def index():
    #...
    page = request.args.get('page', 1, type=int)
    pagination = Post.query.order_by(
        Post.timestamp.desc()).paginate(
            page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],error_out=False)

    posts = pagination.items
    return render_template('index.html', form=form,posts=posts,
        pagination=pagination)

渲染的页数从请求的查询字符串(request.args)中获取,如果没有明确指定,则默认渲 染第一页。参数 type=int 保证参数无法转换成整数时,返回默认值。

为了显示某页中的记录,要把 all() 换成 Flask-SQLAlchemy 提供的 paginate() 方法。页 数是 paginate() 方法的第一个参数,也是唯一必需的参数。可选参数 per_page 用来指定 每页显示的记录数量;如果没有指定,则默认显示 20 个记录。另一个可选参数为 error_out,当其设为 True 时(默认值),如果请求的页数超出了范围,则会返回 404 错误;如果 设为 False,页数超出范围时会返回一个空列表。为了能够很便利地配置每页显示的记录 数量,参数 per_page 的值从程序的环境变量 FLASKY_POSTS_PER_PAGE 中读取。

这样修改之后,首页中的文章列表只会显示有限数量的文章。若想查看第 2 页中的文章, 要在浏览器地址栏中的 URL 后加上查询字符串 ?page=2。

添加分页导航

paginate() 方法的返回值是一个 Pagination 类对象,这个类在 Flask-SQLAlchemy 中定义。 这个对象包含很多属性,用于在模板中生成分页链接,因此将其作为参数传入了模板。

Flask-SQLAlchemy分页对象的属性:
  • items 当前页面中的记录
  • query 分页的源查询
  • page 当前页数
  • prev_num 上一页的页数
  • next_num 下一页的页数
  • has_next 如果有下一页,返回 True
  • has_prev 如果有上一页,返回 True
  • pages 查询得到的总页数
  • per_page 每页显示的记录数量
  • total 查询返回的记录总数

在Flask-SQLAlchemy对象上可调用的方法:

-iter_pages(left_edge=2,left_current=2,right_current=5,right_edge=2) :一个迭代器,返回一个在分页导航中显示的页数列表。这个列表的最左边显示left_edge页,当前页的左边显示 left_current页,当前页的右边显示right_current页 最右边显示right_edge页。例如,在一个100页的列表中,当前页为第50页,使用,默认配置,这个方法会返回以下页数:1、2、None、48、49、50、51、52、53、54、55、None、99、100。None 表示页数之间的间隔 - prev() 上一页的分页对象 - next() 下一页的分页对象

拥有这么强大的对象和 Bootstrap 中的分页 CSS 类,我们很轻易地就能在模板底部构建一 个分页导航。

app/templates/_macros.html:分页模板宏

此处用html代码 但是代码加入错误了当时。略过

这个宏创建了一个 Bootstrap 分页元素,即一个有特殊样式的无序列表,其中定义了下述页面链接:
  • “上一页”链接。如果当前页是第一页,则为这个链接加上 disabled 类。
  • 分页对象的iter_pages()迭代器返回的所有页面链接。这些页面被渲染成具有明确页数的链接,页数在 url_for() 的参数中指定。当前显示的页面使用 activeCSS 类高亮显 示。页数列表中的间隔使用省略号表示。
  • “下一页”链接。如果当前页是最后一页,则会禁用这个链接。

Jinja2 宏的参数列表中不用加入**kwargs 即可接收关键字参数。分页宏把接收到的所有关 键字参数都传给了生成分页链接的 url_for() 方法。这种方式也可在路由中使用,例如包 含一个动态部分的资料页。

pagination_widget 宏可放在 index.html 和 user.html 中的 _posts.html 模板后面。

app/templates/index.html:在博客文章列表下面添加分页导航:

{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}

{% include '_posts.html' %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.index') }}
</div>
{% endif %}
使用Markdown和Flask-PageDown支持富 文本文章

这个略过 ,使用flask-ueditor比这个pagedown好用, 到时候在另外做一篇文章再讲解

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十二章:关注者

社交 Web 程序允许用户之间相互联系。在程序中,这种关系称为关注者、好友、联系人、 联络人或伙伴。但不管使用哪个名字,其功能都是一样的,而且都要记录两个用户之间的 定向联系,在数据库查询中也要使用这种联系。

在本章,你将学到如何在 Flasky 中实现关注功能,让用户“关注”其他用户,并在首页只 显示所关注用户发布的博客文章列表。

再论数据库关系

我们在第 5 章介绍过,数据库使用关系建立记录之间的联系。其中,一对多关系是最常用的 关系类型,它把一个记录和一组相关的记录联系在一起。实现这种关系时,要在“多”这一 侧加入一个外键,指向“一”这一侧联接的记录。本书开发的示例程序现在包含两个一对多 关系:一个把用户角色和一组用户联系起来,另一个把用户和发布的博客文章联系起来。

大部分的其他关系类型都可以从一对多类型中衍生。多对一关系从“多”这一侧看,就是 一对多关系。一对一关系类型是简化版的一对多关系,限制“多”这一侧最多只能有一个 记录。唯一不能从一对多关系中简单演化出来的类型是多对多关系,这种关系的两侧都有 多个记录。下一节将详细介绍多对多关系。

多对多关系

这一章很重要数据库的多对多原理 博主经常搞错 一对多关系、多对一关系和一对一关系至少都有一侧是单个实体,所以记录之间的联系通 过外键实现,让外键指向这个实体。但是,你要如何实现两侧都是“多”的关系呢?

下面以一个典型的多对多关系为例,即一个记录学生和他们所选课程的数据库。很显然, 你不能在学生表中加入一个指向课程的外键,因为一个学生可以选择多个课程,一个外键 不够用。同样,你也不能在课程表中加入一个指向学生的外键,因为一个课程有多个学生 选择。两侧都需要一组外键。

这种问题的解决方法是添加第三张表,这个表称为关联表。现在,多对多关系可以分解成 原表和关联表之间的两个一对多关系。

查询多对多关系要分成两步。若想知道某位学生选择了哪些课程,你要先从学生和注册之 间的一对多关系开始,获取这位学生在 registrations 表中的所有记录,然后再按照多到 一的方向遍历课程和注册之间的一对多关系,找到这位学生在 registrations 表中各记录 所对应的课程。同样,若想找到选择了某门课程的所有学生,你要先从课程表中开始,获 取其在 registrations 表中的记录,再获取这些记录联接的学生。

通过遍历两个关系来获取查询结果的做法听起来有难度,不过像前例这种简单关系, SQLAlchemy 就可以完成大部分操作。

registrations = db.Table('registrations',
    db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
    db.Column('class_id', db.Integer, db.ForeignKey('classes.id'))
)


class Student(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String)
    classes = db.relationship('Class',
        secondary=registrations,
        backref=db.backref('students', lazy='dynamic'),
        lazy='dynamic')

class Class(db.Model):
    id = db.Column(db.Integer, primary_key = True)
    name = db.Column(db.String)

多对多关系仍使用定义一对多关系的 db.relationship() 方法进行定义,但在多对多关系中, 必须把 secondary 参数设为关联表。多对多关系可以在任何一个类中定义,backref 参数会处 理好关系的另一侧。关联表就是一个简单的表,不是模型,SQLAlchemy 会自动接管这个表。

classes 关系使用列表语义,这样处理多对多关系特别简单。假设学生是 s,课程是 c,学 生注册课程的代码为:

>>> s.classes.append(c)
>>> db.session.add(s)

Class 模型中的 students 关系由参数 db.backref() 定义。注意,这个关系中还指定了 lazy= 'dynamic'参数,所以关系两侧返回的查询都可接受额外的过滤器。

如果后来学生 s 决定不选课程 c 了,那么可使用下面的代码更新数据库:

>>> s.classes.remove(c)
自引用关系

多对多关系可用于实现用户之间的关注,但存在一个问题。在学生和课程的例子中,关联 表联接的是两个明确的实体。但是,表示用户关注其他用户时,只有用户一个实体,没有 第二个实体。

如果关系中的两侧都在同一个表中,这种关系称为自引用关系。在关注中,关系的左侧是 用户实体,可以称为“关注者”;关系的右侧也是用户实体,但这些是“被关注者”。从概 念上来看,自引用关系和普通关系没什么区别,只是不易理解。

高级多对多关系

使用前一节介绍的自引用多对多关系可在数据库中表示用户之间的关注,但却有个限制。 使用多对多关系时,往往需要存储所联两个实体之间的额外信息。对用户之间的关注来 说,可以存储用户关注另一个用户的日期,这样就能按照时间顺序列出所有关注者。这种 信息只能存储在关联表中,但是在之前实现的学生和课程之间的关系中,关联表完全是由 SQLAlchemy 掌控的内部表。

为了能在关系中处理自定义的数据,我们必须提升关联表的地位,使其变成程序可访问的 模型。

app/models/user.py:关注关联表的模型实现:

class Follow(db.Model):
    __tablename__ = 'follows'
    follower_id = db.Column(db.Integer,db.ForeignKey('users.id'),
        primary_key=True)
    followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),primary_key=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)

SQLAlchemy 不能直接使用这个关联表,因为如果这么做程序就无法访问其中的自定义字 段。相反地,要把这个多对多关系的左右两侧拆分成两个基本的一对多关系,而且要定义 成标准的关系。

app/models/user.py:使用两个一对多关系实现的多对多关系:

class User(UserMixin, db.Model):
    # ...
    followed = db.relationship('Follow',
        foreign_keys=[Follow.follower_id],
        backref=db.backref('follower', lazy='joined'),
        lazy='dynamic',
        cascade='all, delete-orphan')

    followers = db.relationship('Follow',
        foreign_keys=[Follow.followed_id],
        backref=db.backref('followed', lazy='joined'),
        lazy='dynamic',cascade='all, delete-orphan')

在这段代码中,followed 和 followers 关系都定义为单独的一对多关系。注意,为了 消除外键间的歧义,定义关系时必须使用可选参数 foreign_keys 指定的外键。而且, db.backref() 参数并不是指定这两个关系之间的引用关系,而是回引 Follow 模型。

回引中的 lazy 参数指定为 joined。这个 lazy 模式可以实现立即从联结查询中加载相关对 象。例如,如果某个用户关注了 100 个用户,调用 user.followed.all() 后会返回一个列 表,其中包含 100 个 Follow 实例,每一个实例的 follower 和 followed 回引属性都指向相 应的用户。设定为 lazy='joined' 模式,就可在一次数据库查询中完成这些操作。如果把 lazy 设为默认值 select,那么首次访问 follower 和 followed 属性时才会加载对应的用户, 而且每个属性都需要一个单独的查询,这就意味着获取全部被关注用户时需要增加 100 次 额外的数据库查询。

这两个关系中,User 一侧设定的 lazy 参数作用不一样。lazy 参数都在“一”这一侧设定, 返回的结果是“多”这一侧中的记录。上述代码使用的是 dynamic,因此关系属性不会直 接返回记录,而是返回查询对象,所以在执行查询之前还可以添加额外的过滤器。

cascade 参数配置在父对象上执行的操作对相关对象的影响。比如,层叠选项可设定为: 将用户添加到数据库会话后,要自动把所有关系的对象都添加到会话中。层叠选项的默认 值能满足大多数情况的需求,但对这个多对多关系来说却不合用。删除对象时,默认的层 叠行为是把对象联接的所有相关对象的外键设为空值。但在关联表中,删除记录后正确的 行为应该是把指向该记录的实体也删除,因为这样能有效销毁联接。这就是层叠选项值 delete-orphan 的作用。

cascade 参数的值是一组由逗号分隔的层叠选项,这看起来可能让人有 点困惑,但 all 表示除了 delete-orphan 之外的所有层叠选项。设为 all, delete-orphan 的意思是启用所有默认层叠选项,而且还要删除孤儿记录。

程序现在要处理两个一对多关系,以便实现多对多关系。由于这些操作经常需要重复执 行,所以最好在 User 模型中为所有可能的操作定义辅助方法。

app/models.py:关注关系的辅助方法

class User(db.Model):
    # ...
    def follow(self, user):
        if not self.is_following(user):
            f = Follow(follower=self, followed=user)
            db.session.add(f)

    def unfollow(self, user):
        f = self.followed.filter_by(followed_id=user.id).first() 
        if f:
            db.session.delete(f)

    def is_following(self, user): 
        return self.followed.filter_by(followed_id=user.id).first() is not None

    def is_followed_by(self, user): 
        return self.followers.filter_by(follower_id=user.id).first() is not None

follow() 方法手动把 Follow 实例插入关联表,从而把关注者和被关注者联接起来,并让程 序有机会设定自定义字段的值。联接在一起的两个用户被手动传入 Follow 类的构造器,创 建一个 Follow 新实例,然后像往常一样,把这个实例对象添加到数据库会话中。注意, 这里无需手动设定 timestamp 字段,因为定义字段时指定了默认值,即当前日期和时间。 unfollow() 方法使用 followed 关系找到联接用户和被关注用户的 Follow 实例。若要销毁这 两个用户之间的联接,只需删除这个 Follow 对象即可。is_following() 方法和 is_followed_by() 方法分别在左右两边的一对多关系中搜索指定用户,如果找到了就返回 True。

在资料页中显示关注者

如果用户查看一个尚未关注用户的资料页,页面中要显示一个“Follow”(关注)按钮,如 果查看已关注用户的资料页则显示“Unfollow”(取消关注)按钮。而且,页面中最好能显 示出关注者和被关注者的数量,再列出关注和被关注的用户列表,并在相应的用户资料页 中显示“Follows You”(关注了你)标志。

app/templates/user.html:在用户资料页上部添加关注信息

{% if current_user.can(Permission.FOLLOW) and user != current_user %} 
    {% if not current_user.is_following(user) %}
        <a href="{{ url_for('.follow', username=user.username) }}"class="btn btn-primary">Follow</a> 
    {% else %}
    <a href="{{ url_for('.unfollow', username=user.username) }}"class="btn btn-default">Unfollow</a>
    {% endif %} 
{% endif %}

    <a href="{{ url_for('.followers', username=user.username) }}">
        Followers: <span class="badge">{{ user.followers.count() }}</span>
    </a>

    <a href="{{ url_for('.followed_by', username=user.username) }}">Following: <span class="badge">{{ user.followed.count() }}</span>
    </a>

{% if current_user.is_authenticated() and user != current_user and user.is_following(current_user) %}
    | <span class="label label-default">Follows you</span> 
{% endif %}

这次修改模板用到了 4 个新端点。用户在其他用户的资料页中点击“Follow”(关注)按钮 后,执行的是/follow〈/ username〉路由。

app/main/views.py:“关注”路由和视图函数

@login_required
@permission_required(Permission.FOLLOW)
def follow(username):
    user = User.query.filter_by(username=username).first() 
    if user is None:
        flash('Invalid user.')
        return redirect(url_for('.index'))
    if current_user.is_following(user):
        flash('You are already following this user.')
        return redirect(url_for('.user', username=username)) 
        
    current_user.follow(user)
    flash('You are now following %s.' % username)

    return redirect(url_for('.user', username=username))

这个视图函数先加载请求的用户,确保用户存在且当前登录用户还没有关注这个用户,然后调用 User 模型中定义的辅助方法 follow(),用以联接两个用户。/unfollow/<username> 路由的实现方式类似。

用户在其他用户的资料页中点击关注者数量后,将调用 /followers/<username> 路由。

app/main/views.py:“关注者”路由和视图函数

@main.route('/followers/<username>')
def followers(username):
    user = User.query.filter_by(username=username).first() 
    if user is None:
        flash('Invalid user.')
        return redirect(url_for('.index'))
    page = request.args.get('page', 1, type=int)
    pagination = user.followers.paginate(
            page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
            error_out=False)
    follows = [{'user': item.follower, 'timestamp': item.timestamp} for item in pagination.items]

    return render_template('followers.html', 
            user=user, 
            title="Followers of",
            endpoint='.followers', 
            pagination=pagination,
            follows=follows)

这个函数加载并验证请求的用户,然后使用第 11 章中介绍的技术分页显示该用户的 followers 关系。由于查询关注者返回的是 Follow 实例列表,为了渲染方便,我们将其转 换成一个新列表,列表中的各元素都包含 user 和 timestamp 字段。

渲染关注者列表的模板可以写的通用一些,以便能用来渲染关注的用户列表和被关注的用 户列表。模板接收的参数包括用户对象、分页链接使用的端点、分页对象和查询结果列表。

followed_by 端点的实现过程几乎一样,唯一区别在于:用户列表从 user.followed 关系中 获取。传入模板的参数也要进行相应调整。

followers.html 模板使用两列表格实现,左边一列用于显示用户名和头像,右边一列用于显 示 Flask-Moment 时间戳。

使用数据库联结查询所关注用户的文章

程序首页目前按时间降序显示数据库中的所有文章。现在我们已经完成了关注功能,如果能让用户选择只查看所关注用户发布的博客文章就更好了。

若想显示所关注用户发布的所有文章,第一步显然先要获取这些用户,然后获取各用户的 文章,再按一定顺序排列,写入单独列表。可是这种方式的伸缩性不好,随着数据库不断 变大,生成这个列表的工作量也不断增长,而且分页等操作也无法高效率完成。获取博客 文章的高效方式是只用一次查询。

完成这个操作的数据库操作称为联结。联结操作用到两个或更多的数据表,在其中查找满 足指定条件的记录组合,再把记录组合插入一个临时表中,这个临时表就是联结查询的结 果。理解联结查询的最好方法是实例讲解。

下面略,查询的语句

在首页显示所关注用户的文章

现在,用户可以选择在首页显示所有用户的博客文章还是只显示所关注用户的文章了。

app/main/views.py:显示所有博客文章或只显示所关注用户的文章

@app.route('/', methods = ['GET', 'POST'])
def index():
    # ...
    show_followed = False
    if current_user.is_authenticated():
        show_followed = bool(request.cookies.get('show_followed', ''))
        if show_followed:
            query = current_user.followed_posts
        else:
            query = Post.query
        pagination = query.order_by(Post.timestamp.desc()).
            paginate(page, 
                per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
                error_out=False)
        posts = pagination.items
        return render_template('index.html', 
            form=form, posts=posts,
            show_followed=show_followed, 
            pagination=pagination)

决定显示所有博客文章还是只显示所关注用户文章的选项存储在 cookie 的 show_followed 字段中,如果其值为非空字符串,则表示只显示所关注用户的文章。cookie 以 request. cookies 字典的形式存储在请求对象中。这个 cookie 的值会转换成布尔值,根据得到的值 设定本地变量 query 的值。query 的值决定最终获取所有博客文章的查询,或是获取过滤 后的博客文章查询。显示所有用户的文章时,要使用顶级查询 Post.query;如果限制只 显示所关注用户的文章,要使用最近添加的 User.followed_posts 属性。然后将本地变量 query 中保存的查询进行分页,像往常一样将其传入模板。

show_followedcookie 在两个新路由中设定

app/main/views.py:查询所有文章还是所关注用户的文章

@main.route('/all')
@login_required
def show_all():
    resp = make_response(redirect(url_for('.index')))
    resp.set_cookie('show_followed', '', max_age=30*24*60*60)
    return resp

@main.route('/followed')
@login_required
def show_followed():
    resp = make_response(redirect(url_for('.index')))
    resp.set_cookie('show_followed', '1', max_age=30*24*60*60)
    return resp

指向这两个路由的链接添加在首页模板中。点击这两个链接后会为 show_followedcookie 设 定适当的值,然后重定向到首页。

cookie 只能在响应对象中设置,因此这两个路由不能依赖 Flask,要使用 make_response() 方法创建响应对象。

set_cookie() 函数的前两个参数分别是 cookie 名和值。可选的 max_age 参数设置 cookie 的 过期时间,单位为秒。如果不指定参数 max_age,浏览器关闭后 cookie 就会过期。在本例 中,过期时间为 30 天,所以即便用户几天不访问程序,浏览器也会记住设定的值。

如果你现在访问网站,切换到所关注用户文章列表,会发现自己的文章不在列表中。这是 肯定的,因为用户不能关注自己。

虽然查询能按设计正常执行,但用户查看好友文章时还是希望能看到自己的文章。这个问 题最简单的解决办法是,注册时把用户设为自己的关注者。

app/models.py:构建用户时把用户设为自己的关注者:

class User(UserMixin, db.Model):
    # ...
    def __init__(self, **kwargs):
        # ...
        self.follow(self)

可是,现在的数据库中可能已经创建了一些用户,而且都没有关注自己。如果数据库还比 较小,容易重新生成,那么可以删掉再重新创建。

app/models.py:把用户设为自己的关注者:

class User(UserMixin, db.Model):
    # ...
    @staticmethod
    def add_self_follows():
        for user in User.query.all():
            if not user.is_following(user):
                user.follow(user)
                db.session.add(user)
                db.session.commit()
    # ...

现在,可以通过在 shell 中运行这个函数来更新数据库:

   (venv) $ python manage.py shell
   >>> User.add_self_follows()

.. literalinclude:: code/code9-1.py
   :language: python

创建函数更新数据库这一技术经常用来更新已部署的程序,因为运行脚本更新比手动更新 数据库更少出错。

用户关注自己这一功能的实现让程序变得更实用,但也有一些副作用。因为用户的自关注 链接,用户资料页显示的关注者和被关注者的数量都增加了 1 个。为了显示准确,这些数 字要减去1,这一点在模板中很容易实现,直接渲染{{ user.followers.count() - 1 }}和 {{ user.followed.count() - 1 }} 即可。然后,还要调整关注用户和被关注用户的列表,不显示自己。这在模板中也容易实现,使用条件语句即可。最后,检查关注者数量的单元 测试也会受到自关注的影响,必须做出调整,计入自关注。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十三章:用户评论

允许用户交互是社交博客平台成功的关键。在本章,你将学到如何实现用户评论。

评论属于某篇博客文章,因此定义了一个从 posts 表到 comments 表的一对多关系。使用这个关系可以获取某篇特定博客文章的评论列表。

comments 表还和 users 表之间有一对多关系。通过这个关系可以获取用户发表的所有评 论,还能间接知道用户发表了多少篇评论。用户发表的评论数量可以显示在用户资料页 中。

app/models.py:Comment 模型

class Comment(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    body_html = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    disabled = db.Column(db.Boolean)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))

    @staticmethod
    def on_changed_body(target, value, oldvalue, initiator):
        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i','strong']
        target.body_html = bleach.linkify(bleach.clean(
            markdown(value, output_format='html'),
            tags=allowed_tags, strip=True))

db.event.listen(Comment.body, 'set', Comment.on_changed_body)

Comment 模型的属性几乎和 Post 模型一样,不过多了一个 disabled 字段。这是个布尔值字 段,协管员通过这个字段查禁不当评论。和博客文章一样,评论也定义了一个事件,在修 改 body 字段内容时触发,自动把 Markdown 文本转换成 HTML。转换过程和第 11 章中的 博客文章一样,不过评论相对较短,而且对 Markdown 中允许使用的 HTML 标签要求更严 格,要删除与段落相关的标签,只留下格式化字符的标签。

为了完成对数据库的修改,User 和 Post 模型还要建立与 comments 表的一对多关系

app/models/user.py:users 和 posts 表与 comments 表之间的一对多关系:

class User(db.Model):
    # ...
    comments = db.relationship('Comment', backref='author', lazy='dynamic')

class Post(db.Model):
    # ...
    comments = db.relationship('Comment', backref='post', lazy='dynamic')
提交和显示评论

在这个程序中,评论要显示在单篇博客文章页面中。这个页面在第 11 章添加固定链接时 已经创建。在这个页面中还要有一个提交评论的表单。用来输入评论的表单如示例 13-3 所 示。这个表单很简单,只有一个文本字段和一个提交按钮。

app/main/forms.py:评论输入表单:

class CommentForm(Form):
    body = StringField('', validators=[Required()])
    submit = SubmitField('Submit')

app/main/views.py:支持博客文章评论

@main.route('/post/<int:id>', methods=['GET', 'POST'])
def post(id):
    post = Post.query.get_or_404(id)
    form = CommentForm()
    if form.validate_on_submit():
        comment = Comment(body=form.body.data,
            post=post,author=current_user._get_current_object())
        db.session.add(comment)
        flash('Your comment has been published.')
        return redirect(url_for('.post', id=post.id, page=-1))
    page = request.args.get('page', 1, type=int)
    if page == -1:
        page = (post.comments.count() - 1) / \ 
            current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1
        pagination = post.comments.order_by(Comment.timestamp.asc()).paginate( page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'], error_out=False)
        comments = pagination.items
        return render_template('post.html', posts=[post], form=form,
                comments=comments, pagination=pagination)

这个视图函数实例化了一个评论表单,并将其转入 post.html 模板,以便渲染。提交表单 后,插入新评论的逻辑和处理博客文章的过程差不多。和 Post 模型一样,评论的 author 字段也不能直接设为 current_user,因为这个变量是上下文代理对象。真正的 User 对象要 使用表达式 current_user._get_current_object() 获取。

评论按照时间戳顺序排列,新评论显示在列表的底部。提交评论后,请求结果是一个重定 向,转回之前的 URL,但是在 url_for() 函数的参数中把 page 设为 -1,这是个特殊的页 数,用来请求评论的最后一页,所以刚提交的评论才会出现在页面中。程序从查询字符串 中获取页数,发现值为 -1 时,会计算评论的总量和总页数,得出真正要显示的页数。

文章的评论列表通过 post.comments 一对多关系获取,按照时间戳顺序进行排列,再使 用与博客文章相同的技术分页显示。评论列表对象和分页对象都传入了模板,以便渲染。 FLASKY_COMMENTS_PER_PAGE 配置变量也被加入 config.py 中,用来控制每页显示的评论数量。

评论的渲染过程在新模板 _comments.html 中进行,类似于 _posts.html,但使用的 CSS 类不 同。_comments.html 模板要引入 post.html 中,放在文章正文下方,后面再显示分页导航。 你可以在 GitHub 上的仓库中查看在这个程序里对模板所做的改动。

为了完善功能,我们还要在首页和资料页中加上指向评论页面的链接,

app/templates/_posts.html:链接到博客文章的评论:

<a href="{{ url_for('.post', id=post.id) }}#comments">
    <span class="label label-primary">
        {{ post.comments.count() }} Comments
    </span>
</a>

注意链接文本中显示评论数量的方法。评论数量可以使用 SQLAlchemy 提供的 count() 过滤器轻易地从 posts 和 comments 表的一对多关系中获取。

指向评论页的链接结构也值得一说。这个链接的地址是在文章的固定链接后面加上一个 #comments 后缀。这个后缀称为 URL 片段,用于指定加载页面后滚动条所在的初始位置。 Web 浏览器会寻找 id 等于 URL 片段的元素并滚动页面,让这个元素显示在窗口顶部。这 个初始位置被设为 post.html 模板中评论区的标题,即 <h4 id="comments">Comments<h4>。

除此之外,分页导航所用的宏也要做些改动。评论的分页导航链接也要加上 #comments 片 段,因此在 post.html 模板中调用宏时,传入片段参数。

管理评论

我们在第 9 章定义了几个用户角色,它们分别具有不同的权限。其中一个权限是 Permission.MODERATE_COMMENTS,拥有此权限的用户可以管理其他用户的评论。

为了管理评论,我们要在导航条中添加一个链接,具有权限的用户才能看到。这个链接在 base.html 模板中使用条件语句添加

app/templates/base.html:在导航条中加入管理评论链接:

...
{% if current_user.can(Permission.MODERATE_COMMENTS) %}
<li><a href="{{ url_for('main.moderate') }}">Moderate Comments</a></li>
{% endif %}
...

管理页面在同一个列表中显示全部文章的评论,最近发布的评论会显示在前面。每篇评 论的下方都会显示一个按钮,用来切换 disabled 属性的值。

app/main/views.py:管理评论的路由:

@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def moderate():
    page = request.args.get('page', 1, type=int)
    pagination = Comment.query.order_by(
        Comment.timestamp.desc()).paginate(
            page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
            error_out=False)
    comments = pagination.items
    return render_template('moderate.html', comments=comments,
        pagination=pagination, page=page)

这个函数很简单,它从数据库中读取一页评论,将其传入模板进行渲染。除了评论列表之 外,还把分页对象和当前页数传入了模板。

moderate.html 模板也很简单,如示例 13-8 所示,因为它依靠之前创建的子模板 _comments. html 渲染评论。

app/templates/moderate.html:评论管理页面的模板

{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - Comment Moderation{% endblock %}
{% block page_content %} 
<div class="page-header">
    <h1>Comment Moderation</h1>
</div>
{% set moderate = True %}
{% include '_comments.html' %} 
{% if pagination %}
    <div class="pagination">
        {{ macros.pagination_widget(pagination, '.moderate') }}
    </div>
{% endif %}
{% endblock %}

这个模板将渲染评论的工作交给 _comments.html 模板完成,但把控制权交给从属模板之 前,会使用 Jinja2 提供的 set 指令定义一个模板变量 moderate,并将其值设为 True。这个 变量用在 _comments.html 模板中,决定是否渲染评论管理功能。

_comments.html 模板中显示评论正文的部分要做两方面修改。对于普通用户(没设定 moderate 变量),不显示标记为有问题的评论。对于协管员(moderate 设为 True),不管评 论是否被标记为有问题,都要显示,而且在正文下方还要显示一个用来切换状态的按钮。

app/templates/_comments.html:渲染评论的正文

...
<div class="comment-body">
{% if comment.disabled %}
<p></p>
<i>This comment has been disabled by a moderator.</i></p>
{% endif %}
{% if moderate or not comment.disabled %}
    {% if comment.body_html %}
        {{ comment.body_html | safe }}
    {% else %}
        {{ comment.body }}
    {% endif %}
{% endif %}
</div>
{% if moderate %}
    <br>
    {% if comment.disabled %}
        <a class="btn btn-default btn-xs" href="{{ url_for('.moderate_enable',id=comment.id, page=page) }}">Enable</a> 
        {% else %}
        <a class="btn btn-danger btn-xs" href="{{ url_for('.moderate_disable',id=comment.id, page=page) }}">Disable</a>
    {% endif %} 
{% endif %}
...

做了上述改动之后,用户将看到一个关于有问题评论的简短提示。协管员既能看到这个提 示,也能看到评论的正文。在每篇评论的下方,协管员还能看到一个按钮,用来切换评论 的状态。点击按钮后会触发两个新路由中的一个,但具体触发哪一个取决于协管员要把评 论设为什么状态。

app/main/views.py:评论管理路由

@main.route('/moderate/enable/<int:id>')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def moderate_enable(id):
    comment = Comment.query.get_or_404(id)
    comment.disabled = False
    db.session.add(comment)
    return redirect(url_for('.moderate',
        page=request.args.get('page', 1, type=int)))

@main.route('/moderate/disable/<int:id>')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def moderate_disable(id):
    comment = Comment.query.get_or_404(id)
    comment.disabled = True
    db.session.add(comment)
    return redirect(url_for('.moderate',
        page=request.args.get('page', 1, type=int)))

上述启用路由和禁用路由先加载评论对象,把 disabled 字段设为正确的值,再把评论对象 写入数据库。最后,重定向到评论管理页面(如图 13-3 所示),如果查询字符串中指定了 page 参数,会将其传入重定向操作。_comments.html 模板中的按钮指定了 page 参数,重 定向后会返回之前的页面。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十四章:应用编程接口

最近几年,Web 程序有种趋势,那就是业务逻辑被越来越多地移到了客户端一侧,开创出 了一种称为富互联网应用(Rich Internet Application,RIA)的架构。在RIA中,服务器的 主要功能(有时是唯一功能)是为客户端提供数据存取服务。在这种模式中,服务器变成 了 Web 服务或应用编程接口(Application Programming Interface,API)。

RIA可采用多种协议与Web服务通信。远程过程调用(Remote Procedure Call,RPC)协议, 例如 XML-RPC,及由其衍生的简单对象访问协议(Simplified Object Access Protocol,SOAP), 在几年前比较受欢迎。最近,表现层状态转移(Representational State Transfer,REST)架构崭 露头角,成为 Web 程序的新宠,因为这种架构建立在大家熟识的万维网基础之上。

Flask 是开发 REST 架构 Web 服务的理想框架,因为 Flask 天生轻量。在本章,你将学到 如何使用 Flask 实现符合 REST 架构的 API。

REST简介

Roy Fielding在其博士论文(http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style. htm)中介绍了 Web 服务的 REST 架构方式,并列出了 6 个符合这一架构定义的特征。

客户端−服务器:
客户端和服务器之间必须有明确的界线。
无状态
客户端发出的请求中必须包含所有必要的信息。服务器不能在两次请求之间保存客户端的任何状态。
缓存
服务器发出的响应可以标记为可缓存或不可缓存,这样出于优化目的,客户端(或客户端和服务器之间的中间服务)可以使用缓存。
接口统一
客户端访问服务器资源时使用的协议必须一致,定义良好,且已经标准化。REST Web 服务最常使用的统一接口是 HTTP 协议。
系统分层
在客户端和服务器之间可以按需插入代理服务器、缓存或网关,以提高性能、稳定性和伸缩性。
按需代码
客户端可以选择从服务器上下载代码,在客户端的环境中执行。
资源就是一切

资源是 REST 架构方式的核心概念。在 REST 架构中,资源是程序中你要着重关注的事物。 例如,在博客程序中,用户、博客文章和评论都是资源。

每个资源都要使用唯一的URL表示。还是以博客程序为例,一篇博客文章可以使用URL / api/posts/12345 表示,其中 12345 是这篇文章的唯一标识符,使用文章在数据库中的主键 表示。URL 的格式或内容无关紧要,只要资源的 URL 只表示唯一的一个资源即可。

某一类资源的集合也要有一个 URL。博客文章集合的 URL 可以是 /api/posts/,评论集合的 URL 可以是 /api/comments/。

API 还可以为某一类资源的逻辑子集定义集合 URL。例如,编号为 12345 的博客文章,其 中的所有评论可以使用URL /api/posts/12345/comments/表示。表示资源集合的URL习惯 在末端加上一个斜线,代表一种“文件夹”结构。

注意,Flask 会特殊对待末端带有斜线的路由。如果客户端请求的 URL 的末 端没有斜线,而唯一匹配的路由末端有斜线,Flask 会自动响应一个重定向, 转向末端带斜线的 URL。反之则不会重定向。

请求方法

客户端程序在建立起的资源 URL 上发送请求,使用请求方法表示期望的操作。若要从博客 API 中获取现有博客文章的列表,客户端可以向 http://www.exam-ple.com/api/posts/ 发 送 GET 请求。若要插入一篇新博客文章,客户端可以向同一地址发送 POST 请求,而且请求 主体中要包含博客文章的内容。若要获取编号为 12345 的博客文章,客户端可以向 http:// www.example.com/api/posts/12345 发送 GET 请求。

请求和响应主体

在请求和响应的主体中,资源在客户端和服务器之间来回传送,但 REST 没有指定编码 资源的方式。请求和响应中的 Content-Type 首部用于指明主体中资源的编码方式。使用 HTTP 协议中的内容协商机制,可以找到一种客户端和服务器都支持的编码方式。

REST Web服务常用的两种编码方式是JavaScript对象表示法(JavaScript Object Notation, JSON)和可扩展标记语言(Extensible Markup Language,XML)。对基于Web的RIA来 说,JSON 更具吸引力,因为 JSON 和 JavaScript 联系紧密,而 JavaScript 是 Web 浏览器 使用的客户端脚本语言。继续以博客 API 为例,一篇博客文章对应的资源可以使用如下的 JSON 表示:

{
    "url": "http://www.example.com/api/posts/12345",
    "title": "Writing RESTful APIs in Python",
    "author": "http://www.example.com/api/users/2",
    "body": "... text of the article here ...",
    "comments": "http://www.example.com/api/posts/12345/comments"
}

注意,在这篇博客文章中,url、author 和 comments 字段都是完整的资源 URL。这是很重 要的表示方法,因为客户端可以通过这些 URL 发掘新资源。

在设计良好的REST API中,客户端只需知道几个顶级资源的URL,其他资源的URL则从响 应中包含的链接上发掘。这就好比浏览网络时,你在自己知道的网页中点击链接发掘新网页。

版本

在传统的以服务器为中心的 Web 程序中,服务器完全掌控程序。更新程序时,只需在服务 器上部署新版本就可更新所有的用户,因为运行在用户 Web 浏览器中的那部分程序也是从 服务器上下载的。

但升级 RIA 和 Web 服务要复杂得多,因为客户端程序和服务器上的程序是独立开发的, 有时甚至由不同的人进行开发。你可以考虑一下这种情况,即一个程序的REST Web服 务被很多客户端使用,其中包括 Web 浏览器和智能手机原生应用。服务器可以随时更新 Web 浏览器中的客户端,但无法强制更新智能手机中的应用,更新前先要获得机主的许 可。即便机主想进行更新,也不能保证新版应用上传到所有应用商店的时机都完全吻合新 服务器端版本的部署。

基于以上原因,Web 服务的容错能力要比一般的 Web 程序强,而且还要保证旧版客户端 能继续使用。这一问题的常见解决办法是使用版本区分 Web 服务所处理的 URL。例如, 首次发布的博客 Web 服务可以通过 /api/v1.0/posts/ 提供博客文章的集合。

在 URL 中加入 Web 服务的版本有助于条理化管理新旧功能,让服务器能为新客户端提供 新功能,同时继续支持旧版客户端。博客服务可能会修改博客文章使用的 JSON 格式,同 时通过 /api/v1.1/posts/ 提供修改后的博客文章,而客户端仍能通过 /api/v1.0/posts/ 获取旧的 JSON 格式。在一段时间内,服务器要同时处理 v1.1 和 v1.0 这两个版本的 URL。

提供多版本支持会增加服务器的维护负担,但在某些情况下,这是不破坏现有部署且能让 程序不断发展的唯一方式。

使用Flask提供REST Web服务

使用Flask创建REST Web服务很简单。使用熟悉的route()修饰器及其methods可选参 数可以声明服务所提供资源 URL 的路由。处理 JSON 数据同样简单,因为请求中包含的 JSON 数据可通过 request.json 这个 Python 字典获取,并且需要包含 JSON 的响应可以使 用 Flask 提供的辅助函数 jsonify() 从 Python 字典中生成。

以下几节将介绍如何扩展Flasky,创建一个REST Web服务,以便让客户端访问博客文章 及相关资源。  创建API蓝本 ------------------------------------------------------------------

REST API相关的路由是一个自成一体的程序子集,所以为了更好地组织代码,我们最好 把这些路由放到独立的蓝本中。

注意,API 包的名字中有一个版本号。如果需要创建一个向前兼容的 API 版本,可以添加 一个版本号不同的包,让程序同时支持两个版本的 API。

在这个 API 蓝本中,各资源分别在不同的模块中实现。蓝本中还包含处理认证、错误以及 提供自定义修饰器的模块。蓝本的构造文件如示例 14-2 所示。

app/api_1_0/__init__.py:API 蓝本的构造文件:

from flask import Blueprint
api = Blueprint('api', __name__)
from . import authentication, posts, users, comments, errors

app/_init_.py:注册 API 蓝本:

def create_app(config_name):
# ...
from .api_1_0 import api as api_1_0_blueprint
app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
# ...
错误处理

REST Web服务将请求的状态告知客户端时,会在响应中发送适当的HTTP状态码,并将 额外信息放入响应主体。

处理 404 和 500 状态码时会有点小麻烦,因为这两个错误是由 Flask 自己生成的,而且一 般会返回 HTML 响应,这很可能会让 API 客户端困惑。

为所有客户端生成适当响应的一种方法是,在错误处理程序中根据客户端请求的格式改写 响应,这种技术称为内容协商。示例 14-4 是改进后的 404 错误处理程序,它向 Web 服务 客户端发送 JSON 格式响应,除此之外都发送 HTML 格式响应。500 错误处理程序的写法 类似。

app/main/errors.py:使用 HTTP 内容协商处理错误:

@main.app_errorhandler(404)
def page_not_found(e):
    if request.accept_mimetypes.accept_json and \
            not request.accept_mimetypes.accept_html:
        response = jsonify({'error': 'not found'})
        response.status_code = 404
        return response
    return render_template('404.html'), 404

这个新版错误处理程序检查 Accept 请求首部(Werkzeug 将其解码为 request.accept_mimetypes),根据首部的值决定客户端期望接收的响应格式。浏览器一般不限制响应的格 式,所以只为接受 JSON 格式而不接受 HTML 格式的客户端生成 JSON 格式响应。

其他状态码都由 Web 服务生成,因此可在蓝本的 errors.py 模块作为辅助函数实现。示例 14-5 是 403 错误的处理程序,其他错误处理程序的写法类似。

app/api_1_0/errors.py:API 蓝本中 403 状态码的错误处理程序:

def forbidden(message):
    response = jsonify({'error': 'forbidden', 'message': message})
    response.status_code = 403
    return response
使用Flask-HTTP Auth认证用户

和普通的 Web 程序一样,Web 服务也需要保护信息,确保未经授权的用户无法访问。为 此,RIA 必须询问用户的登录密令,并将其传给服务器进行验证。

我们前面说过,REST Web服务的特征之一是无状态,即服务器在两次请求之间不能“记 住”客户端的任何信息。客户端必须在发出的请求中包含所有必要信息,因此所有请求都 必须包含用户密令。

程序当前的登录功能是在 Flask-Login 的帮助下实现的,可以把数据存储在用户会话中。默 认情况下,Flask 把会话保存在客户端 cookie 中,因此服务器没有保存任何用户相关信息, 都转交给客户端保存。这种实现方式看起来遵守了 REST 架构的无状态要求,但在 REST Web 服务中使用 cookie 有点不现实,因为 Web 浏览器之外的客户端很难提供对 cookie 的 支持。鉴于此,使用 cookie 并不是一个很好的设计选择。

REST 架构的无状态要求看起来似乎过于严格,但这并不是随意提出的要 求,无状态的服务器伸缩起来更加简单。如果服务器保存了客户端的相关信 息,就必须提供一个所有服务器都能访问的共享缓存,这样才能保证一直使 用同一台服务器处理特定客户端的请求。这样的需求很难实现。

因为 REST 架构基于 HTTP 协议,所以发送密令的最佳方式是使用 HTTP 认证,基本认证 和摘要认证都可以。在 HTTP 认证中,用户密令包含在请求的 Authorization 首部中。

HTTP认证协议很简单,可以直接实现,不过Flask-HTTPAuth扩展提供了一个便利的包 装,可以把协议的细节隐藏在修饰器之中,类似于 Flask-Login 提供的 login_required 修 饰器。

Flask-HTTPAuth使用pip安装:

(venv) $ pip install flask-httpauth

在将 HTTP 基本认证的扩展进行初始化之前,我们先要创建一个 HTTPBasicAuth 类对象。 和Flask-Login一样,Flask-HTTPAuth不对验证用户密令所需的步骤做任何假设,因此所需的信息在回调函数中提供。示例14-6展示了如何初始化Flask-HTTPAuth扩展,以及如 何在回调函数中验证密令。

app/api_1_0/authentication.py:初始化Flask-HTTPAuth

from flask.ext.httpauth import HTTPBasicAuth

auth = HTTPBasicAuth()

@auth.verify_password
def verify_password(email, password):
    if email == '':
        g.current_user = AnonymousUser()
        return True

    user = User.query.filter_by(email = email).first() 
    if not user:
        return False
    g.current_user = user
    return user.verify_password(password)

由于这种用户认证方法只在API蓝本中使用,所以Flask-HTTPAuth扩展只在蓝本包中初 始化,而不像其他扩展那样要在程序包中初始化。

电子邮件和密码使用 User 模型中现有的方法验证。如果登录密令正确,这个验证回调函数 就返回 True,否则返回 False。API 蓝本也支持匿名用户访问,此时客户端发送的电子邮 件字段必须为空。

验证回调函数把通过认证的用户保存在 Flask 的全局对象 g 中,如此一来,视图函数便能 进行访问。注意,匿名登录时,这个函数返回 True 并把 Flask-Login 提供的 AnonymousUser 类实例赋值给 g.current_user。

如果认证密令不正确,服务器向客户端返回401错误。默认情况下,Flask-HTTPAuth自 动生成这个状态码,但为了和 API 返回的其他错误保持一致,我们可以自定义这个错误响 应

app/api_1_0/authentication.py:Flask-HTTPAuth错误处理程序:

@auth.error_handler
def auth_error():
    return unauthorized('Invalid credentials')

为保护路由,可使用修饰器 auth.login_required:

@api.route('/posts/')
@auth.login_required
def get_posts():
    pass

不过,这个蓝本中的所有路由都要使用相同的方式进行保护,所以我们可以在 before_request 处理程序中使用一次 login_required 修饰器,应用到整个蓝本,

app/api_1_0/authentication.py:在 before_request 处理程序中进行认证

from .errors import forbidden_error
@api.before_request
@auth.login_required
def before_request():
    if not g.current_user.is_anonymous and \
        not g.current_user.confirmed:
    return forbidden('Unconfirmed account')

现在,API 蓝本中的所有路由都能进行自动认证。而且作为附加认证,before_request 处理程序还会拒绝已通过认证但没有确认账户的用户。

基于令牌的认证

每次请求时,客户端都要发送认证密令。为了避免总是发送敏感信息,我们可以提供一种 基于令牌的认证方案。

使用基于令牌的认证方案时,客户端要先把登录密令发送给一个特殊的 URL,从而生成 认证令牌。一旦客户端获得令牌,就可用令牌代替登录密令认证请求。出于安全考虑,令 牌有过期时间。令牌过期后,客户端必须重新发送登录密令以生成新令牌。令牌落入他人 之手所带来的安全隐患受限于令牌的短暂使用期限。为了生成和验证认证令牌,我们要在 User 模型中定义两个新方法。这两个新方法用到了 itsdangerous 包,

app/models.py:支持基于令牌的认证

class User(db.Model):
    # ...

    def generate_auth_token(self, expiration):
        s = Serializer(current_app.config['SECRET_KEY'],expires_in=expiration)
        return s.dumps({'id': self.id})

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(current_app.config['SECRET_KEY']) 
        try:
            data = s.loads(token)
        except:
            return None

        return User.query.get(data['id'])

generate_auth_token() 方法使用编码后的用户 id 字段值生成一个签名令牌,还指定了以秒 为单位的过期时间。verify_auth_token() 方法接受的参数是一个令牌,如果令牌可用就返 回对应的用户。verify_auth_token() 是静态方法,因为只有解码令牌后才能知道用户是谁。

为了能够认证包含令牌的请求,我们必须修改Flask-HTTPAuth提供的verify_password回 调,除了普通的密令之外,还要接受令牌。

app/api_1_0/authentication.py:支持令牌的改进验证回调

@auth.verify_password
def verify_password(email_or_token, password):
    if email_or_token == '':
        g.current_user = AnonymousUser()
        return True

    if password == '':
        g.current_user = User.verify_auth_token(email_or_token)
        g.token_used = True
        return g.current_user is not None

    user = User.query.filter_by(email=email_or_token).first() 
    if not user:
        return False
    g.current_user = user
    g.token_used = False
    return user.verify_password(password)

在这个新版本中,第一个认证参数可以是电子邮件地址或认证令牌。如果这个参数为空, 那就和之前一样,假定是匿名用户。如果密码为空,那就假定 email_or_token 参数提供的 是令牌,按照令牌的方式进行认证。如果两个参数都不为空,假定使用常规的邮件地址和 密码进行认证。在这种实现方式中,基于令牌的认证是可选的,由客户端决定是否使用。 为了让视图函数能区分这两种认证方法,我们添加了 g.token_used 变量。

把认证令牌发送给客户端的路由也要添加到 API 蓝本中,

app/api_1_0/authentication.py:生成认证令牌:

@api.route('/token')
def get_token():
    if g.current_user.is_anonymous() or g.token_used:
        return unauthorized('Invalid credentials')
    return jsonify({'token': g.current_user.generate_auth_token(expiration=3600), 'expiration': 3600})

由于这个路由也在蓝本中,所以添加到 before_request 处理程序上的认证机制也会用在这 个路由上。为了避免客户端使用旧令牌申请新令牌,要在视图函数中检查 g.token_used 变 量的值,如果使用令牌进行认证就拒绝请求。这个视图函数返回 JSON 格式的响应,其中 包含了过期时间为 1 小时的令牌。JSON 格式的响应也包含过期时间。

资源和JSON的序列化转换

开发 Web 程序时,经常需要在资源的内部表示和 JSON 之间进行转换。JSON 是 HTTP 请求和响应使用的传输格式。

app/models.py:把文章转换成 JSON 格式的序列化字典

class Post(db.Model):
    # ...
    def to_json(self):
        json_post = {
            'url': url_for('api.get_post', id=self.id, _external=True),
            'body': self.body,
            'body_html': self.body_html,
            'timestamp': self.timestamp,
            'author': url_for('api.get_user', id=self.author_id,
                    _external=True),
            'comments': url_for('api.get_post_comments', id=self.id,
                    _external=True)
            'comment_count': self.comments.count()
        }
        return json_post

url、author 和 comments 字段要分别返回各自资源的 URL,因此它们使用 url_for() 生 成,所调用的路由即将在 API 蓝本中定义。注意,所有 url_for() 方法都指定了参数 _ external=True,这么做是为了生成完整的 URL,而不是生成传统 Web 程序中经常使用的 相对 URL。

这段代码还说明表示资源时可以使用虚构的属性。comment_count 字段是博客文章的评论 数量,并不是模型的真实属性,它之所以包含在这个资源中是为了便于客户端使用。

User 模型的 to_json() 方法可以按照 Post 模型的方式定义,

app/models.py:把用户转换成 JSON 格式的序列化字典

class User(UserMixin, db.Model):
    # ...
    def to_json(self):
        json_user = {
            'url': url_for('api.get_post', id=self.id, _external=True),
            'username': self.username,
            'member_since': self.member_since,
            'last_seen': self.last_seen,
            'posts': url_for('api.get_user_posts', id=self.id, _external=True),
            'followed_posts': url_for('api.get_user_followed_posts',
                    id=self.id, _external=True),
            'post_count': self.posts.count()
        }
        return json_user

注意,为了保护隐私,这个方法中用户的某些属性没有加入响应,例如 email 和 role。这 段代码再次说明,提供给客户端的资源表示没必要和数据库模型的内部表示完全一致。

把 JSON 转换成模型时面临的问题是,客户端提供的数据可能无效、错误或者多余。

app/models.py:从 JSON 格式数据创建一篇博客文章:

from app.exceptions import ValidationError
class Post(db.Model):
    # ...
    @staticmethod
    def from_json(json_post):
        body = json_post.get('body')
        if body is None or body == '':
            raise ValidationError('post does not have a body')
        return Post(body=body)

如你所见,上述代码在实现过程中只选择使用 JSON 字典中的 body 属性,而把 body_html 属性忽略了,因为只要 body 属性的值发生变化,就会触发一个 SQLAlchemy 事件,自动 在服务器端渲染 Markdown。除非允许客户端倒填日期(这个程序并不提供此功能),否则 无需指定 timestamp 属性。由于客户端无权选择博客文章的作者,所以没有使用 author 字 段。author 字段唯一能使用的值是通过认证的用户。comments 和 comment_count 属性使用 数据库关系自动生成,因此其中没有创建模型所需的有用信息。最后,url 字段也被忽略 了,因为在这个实现中资源的 URL 由服务器指派,而不是客户端。

注意如何检查错误。如果没有 body 字段或者其值为空,from_json() 方法会抛出 ValidationError 异常。在这种情况下,抛出异常才是处理错误的正确方式,因为 from_json() 方法并没有掌握处理问题的足够信息,唯有把错误交给调用者,由上层代码处理这个错误。 ValidationError 类是 Python 中 ValueError 类的简单子类,

app/exceptions.py:ValidationError 异常:

class ValidationError(ValueError):
    pass

现在,程序需要向客户端提供适当的响应以处理这个异常。为了避免在视图函数中编写捕 获异常的代码,我们可创建一个全局异常处理程序。

app/api_1_0/errors.py:API 中 ValidationError 异常的处理程序:

@api.errorhandler(ValidationError)
def validation_error(e):
    return bad_request(e.args[0])

这里使用的 errorhandler 修饰器和注册 HTTP 状态码处理程序时使用的是同一个,只不过 此时接收的参数是 Exception 类,只要抛出了指定类的异常,就会调用被修饰的函数。注 意,这个修饰器从 API 蓝本中调用,所以只有当处理蓝本中的路由时抛出了异常才会调用 这个处理程序。

使用这个技术时,视图函数中得代码可以写得十分简洁明,而且无需检查错误:

@api.route('/posts/', methods=['POST'])
def new_post():
    post = Post.from_json(request.json)
    post.author = g.current_user
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json())
实现资源端点

现在我们需要实现用于处理不同资源的路由。GET 请求往往是最简单的,因为它们只返回 信息,无需修改信息。

app/api_1_0/posts.py:文章资源 GET 请求的处理程序:

@api.route('/posts/')
@auth.login_required
def get_posts():
    posts = Post.query.all()
    return jsonify({ 'posts': [post.to_json() for post in posts] })

@api.route('/posts/<int:id>')
@auth.login_required
def get_post(id):
    post = Post.query.get_or_404(id)
    return jsonify(post.to_json())

第一个路由处理获取文章集合的请求。这个函数使用列表推导生成所有文章的 JSON 版本。 第二个路由返回单篇博客文章,如果在数据库中没找到指定 id 对应的文章,则返回 404 错误。

404 错误的处理程序在程序层定义,如果客户端请求 JSON 格式,就要返回 JSON 格式响应。如果要根据 Web 服务定制响应内容,也可在 API 蓝本中重 新定义 404 错误处理程序。

博客文章资源的 POST 请求处理程序把一篇新博客文章插入数据库。

app/api_1_0/posts.py:文章资源 POST 请求的处理程序:

@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE_ARTICLES)
def new_post():
    post = Post.from_json(request.json)
    post.author = g.current_user
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json()), 201, \
        {'Location': url_for('api.get_post', id=post.id, _external=True)}

这个视图函数包含在 permission_required 修饰器(下面的示例中会定义)中,确保通过认证的用户有写博客文章的权限。得益于前面实现的错误处理程序,创建博客文章的过程 变得很直观。博客文章从 JSON 数据中创建,其作者就是通过认证的用户。这个模型写入 数据库之后,会返回 201 状态码,并把 Location 首部的值设为刚创建的这个资源的 URL。

注意,为便于客户端操作,响应的主体中包含了新建的资源。如此一来,客户端就无需在 创建资源后再立即发起一个 GET 请求以获取资源。

用来防止未授权用户创建新博客文章的 permission_required 修饰器和程序中使用的类似, 但会针对 API 蓝本进行自定义。

app/api_1_0/decorators.py:permission_required 修饰器:

def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not g.current_user.can(permission):
                return forbidden('Insufficient permissions')
            return f(*args, **kwargs)
        return decorated_function
    return decorator

博客文章 PUT 请求的处理程序用来更新现有资源

app/api_1_0/posts.py:文章资源 PUT 请求的处理程序:

@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE_ARTICLES)
def edit_post(id):
    post = Post.query.get_or_404(id)
    if g.current_user != post.author and \
        not g.current_user.can(Permission.ADMINISTER): return forbidden('Insufficient permissions')
    post.body = request.json.get('body', post.body)
    db.session.add(post)
    return jsonify(post.to_json())

本例中要进行的权限检查更为复杂。修饰器用来检查用户是否有写博客文章的权限,但为 了确保用户能编辑博客文章,这个函数还要保证用户是文章的作者或者是管理员。这个检 查直接添加到视图函数中。如果这种检查要应用于多个视图函数,为避免代码重复,最好 的方法是为其创建修饰器。

因为程序不允许删除文章,所以没必要实现 DELETE 请求方法的处理程序。

用户资源和评论资源的处理程序实现方式类似。表 14-3 列出了这个程序要实现的资源。你 可到 GitHub 仓库(https://github.com/miguelgrinberg/flasky)中获取完整的实现,以便学习 研究

分页大型资源集合

对大型资源集合来说,获取集合的 GET 请求消耗很大,而且难以管理。和 Web 程序一样, Web 服务也可以对集合进行分页。

class User(UserMixin, db.Model):
    # ...
    def to_json(self):
        json_user = {
            'url': url_for('api.get_post', id=self.id, _external=True),
            'username': self.username,
            'member_since': self.member_since,
            'last_seen': self.last_seen,
            'posts': url_for('api.get_user_posts', id=self.id, _external=True),
            'followed_posts': url_for('api.get_user_followed_posts',
                    id=self.id, _external=True),
            'post_count': self.posts.count()
        }
        return json_user

JSON 格式响应中的 posts 字段依旧包含各篇文章,但现在这只是完整集合的一部分。如 果资源有上一页和下一页,prev 和 next 字段分别表示上一页和下一页资源的 URL。count 是集合中博客文章的总数。

这种技术可应用于所有返回集合的路由。

使用HTTPie测试Web服务

测试 Web 服务时必须使用 HTTP 客户端。最常使用的两个在命令行中测试 Web 服务的客 户端是 curl 和 HTTPie。后者的命令行更简洁,可读性也更高。 HTTPie 使用 pip 安装:

(venv) $ pip install httpie

GET 请求可按照如下的方式发起:

(venv) $ http --json --auth <email>:<password> GET
> http://127.0.0.1:5000/api/v1.0/posts
HTTP/1.0 200 OK
Content-Length: 7018
Content-Type: application/json
Date: Sun, 22 Dec 2013 08:11:24 GMT
Server: Werkzeug/0.9.4 Python/2.7.3
{
"posts": [
    ...
    ],
"prev": null
"next": "http://127.0.0.1:5000/api/v1.0/posts/?page=2",
"count": 150
}

注意响应中的分页链接。因为这是第一页,所以没有上一页,不过返回了获取下一页的 URL 和总数。

匿名用户可发送空邮件地址和密码以发起相同的请求:

(venv) $ http --json --auth : GET http://127.0.0.1:5000/api/v1.0/posts/

下面这个命令发送 POST 请求以添加一篇新博客文章:

(venv) $ http --auth <email>:<password> --json POST \
> http://127.0.0.1:5000/api/v1.0/posts/ \
> "body=I'm adding a post from the *command line*."
HTTP/1.0 201 CREATED
Content-Length: 360
Content-Type: application/json
Date: Sun, 22 Dec 2013 08:30:27 GMT
Location: http://127.0.0.1:5000/api/v1.0/posts/111
Server: Werkzeug/0.9.4 Python/2.7.3

{
    "author": "http://127.0.0.1:5000/api/v1.0/users/1",
    "body": "I'm adding a post from the *command line*.",
    "body_html": "<p>I'm adding a post from the <em>command line</em>.</p>",
    "comments": "http://127.0.0.1:5000/api/v1.0/posts/111/comments",
    "comment_count": 0,
    "timestamp": "Sun, 22 Dec 2013 08:30:27 GMT",
    "url": "http://127.0.0.1:5000/api/v1.0/posts/111"
}

要想使用认证令牌,可向 /api/v1.0/token 发送请求:

(venv) $ http --auth <email>:<password> --json GET \
> http://127.0.0.1:5000/api/v1.0/token
HTTP/1.0 200 OK
Content-Length: 162
Content-Type: application/json
Date: Sat, 04 Jan 2014 08:38:47 GMT
Server: Werkzeug/0.9.4 Python/3.3.3
{
    "expiration": 3600,
    "token": "eyJpYXQiOjEzODg4MjQ3MjcsImV4cCI6MTM4ODgyODMyNywiYWxnIjoiSFMy..."
}

在接下来的 1 小时中,这个令牌可用于访问 API,请求时要和空密码一起发送:

(venv) $ http --json --auth eyJpYXQ...: GET http://127.0.0.1:5000/api/v1.0/posts/

令牌过期后,请求会返回 401 错误,表示需要获取新令牌。

祝贺你!我们在这一章结束了第二部分,至此,Flasky 的功能开发阶段就完全结束了。很 显然,下一步我们要部署 Flasky。在部署过程中,我们会遇到新的挑战,这就是第三部分 的主题

class User(UserMixin, db.Model):
    # ...
    avatar_hash = db.Column(db.String(32))

    def __init__(self, **kwargs):
        # ...
        if self.email is not None and self.avatar_hash is None:
            self.avatar_hash = hashlib.md5(
                self.email.encode('utf-8')).hexdigest()

    def change_email(self, token):
        # ...
        self.email = new_email
        self.avatar_hash = hashlib.md5(
            self.email.encode('utf-8')).hexdigest()
        db.session.add(self)
        return True

    def gravatar(self, size=100, default='identicon', rating='g'):
        if request.is_secure:
            url = 'https://secure.gravatar.com/avatar'
        else:
            url = 'http://www.gravatar.com/avatar'
        hash = self.avatar_hash or hashlib.md5(
            self.email.encode('utf-8')).hexdigest()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
            url=url, hash=hash, size=size, default=default, rating=rating)

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十五章:测试

这一部分略

第十六章:性能

这一部分略

第十七章:部署

这一部分略

第十八章:其他资源

这一部分略

Flask Web 开发实战入门、进阶与原理解析

第一章、初识Flask
第二章、Flask与HTTP
第三章、模板
第四章、表单
第五章、数据库
第六章、电子邮件
第七章、留言板
第八章、个人博客
第九章、图片社交网站
第十章、待办事项
第十一章、在线聊天室
第十二章、自动化测试
第十三章、性能优化
第十四章、部署上线
第十五章、Flask扩展开发
第十六章、Flask工作原理与机制解析

客户端开发(wxpython/qt)

wxPython in Action

安全相关书籍

安全相关书籍

python黑帽子(黑客与渗透测试编程)

黑客秘籍(渗透测试指南)

python绝技(运用py成为顶级黑客)

Metasploit渗透测试魔鬼训练营

xss跨站脚本攻击剖析与防御

wireshark数据包分析实战详解

C++黑客编程揭秘与防范第二版

逆向工程核心原理

加密与解密第三版

完全掌握加密解密实战超级手册

杀不死的秘密-反汇编揭秘,黑客免杀变种技术

filename:cant_kill_secret.rst

第一章、背景知识
1.1、免杀技术的发展
1.2、免杀技术的定义
1.3、杀毒软件查杀原理
1.3.1、特征码发
1.3.2、校验和法
1.3.3、行为检测法
1.3.4、软件模拟法
1.3.5、总结
1.4、常见杀毒软件及杀毒引擎特点
1.5、免杀技术的分类
1.5.1、内部免杀和外部免杀
1.5.2、特征码免杀和大范围免杀
1.5.3、文件免杀、内存免杀、和行为免杀
1.5.4、盲免技术

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第二章、搭建实验环境
2.1、免杀测试步骤及测试环境
2.1.1、免杀测试中遇到的问题
2.1.2、虚拟机的概念
2.1.3、VMware工作原理
2.1.4、系统还原技术
2.1.5、冰点还原工作原理
2.1.6、选用那种方式测试免杀效果
2.2、VMware的安装与使用
2.2.1、安装VMware workstation 6.5.2中文版
2.2.2、创意一个新的虚拟机
2.2.3、在VMware的虚拟机中安装ghost xp
2.2.4、安装VMware Tools及简单使用VMware
2.3、冰点还原
2.3.1、安装冰点还原
2.3.2、使用冰点还原
2.4、综合性的测试环境
2.5、常用免杀工具一览

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第三章、免杀技术前置知识
3.1、PE结构简单介绍
3.1.1、PE文件结构(简化)
3.1.2、初步理解内存地址
3.1.3、文件偏移地址和虚拟地址转换
3.2、DOS文件头和DOS块
3.2.1、DOS文件头
3.2.2、DOS头
3.3、PE文件头
3.3.1、FileHeader字段
3.3.2、OptionalHeader字段
3.4、区段表和区段
3.5、输出表和输入表
3.5.1、输出表
3.5.2、输入表
3.6、什么是加壳免杀
3.6.1、加壳免杀概念
3.6.2、壳程序的分类
3.7、壳程序的使用
3.7.1、ASPack加壳实例
3.7.2、UPX加壳实例
3.7.3、NSPack加壳实例
3.8实战加壳免杀
3.9、从PEID实战PE结构
3.9.1、使用PEID载入一个文件
3.9.2、入口点
3.9.3、EP段
3.9.4、文件偏移
3.9.5、首字节及汇编概念
3.9.6、查壳功能
3.9.7、查壳原理
3.9.8、PEID的设置

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第四章、免杀技术前置知识-汇编基础
4.1、免杀技术与汇编及反汇编的关系
4.1.1、机器语言
4.1.2、汇编语言
4.1.3、高级语言
4.1.4、反汇编
4.1.5、汇编与反汇编
4.2、寄存器和堆栈
4.2.1、寄存器
4.2.2、堆栈
4.3、内存单元与内存寻址
4.3.1、内存单元
4.3.2、内存地址
4.3.3、80386的寻址机制
4.3.4、大尾与小尾
4.4、JMP指令与EIP寄存器
4.4.1、jmp(JuMP)指令
4.4.2、EIP寄存器
4.5、常用传送指令
4.5.1、PUSH(PUSH)指令
4.5.2、POP指令
4.5.3、MOV指令
4.5.4、LEA指令
4.6、算术运算符I
4.6.1、ADD指令
4.6.2、SUB指令
4.7、标志寄存器
4.7.1、ZF指令
4.7.2、PF指令
4.7.3、SF指令
4.7.4、CF指令
4.8、算术运算符指令II
4.8.1、ADC指令
4.8.2、SBB指令
4.8.3、INC指令
4.8.4、DEC指令
4.8.5、CMP指令
4.9、逻辑运算符
4.9.1、AND指令
4.9.2、OR指令
4.9.3、XOR指令
4.9.4、TEST指令
4.10、程序转移指令
4.10.1、CALL指令
4.10.2、RETN/RETF指令
4.10.3、条件转移指令
4.10.4、LOOP指令
4.10.5、NOP指令
4.11、环境保存
4.11.1、变化的ESP寄存器
4.11.2、LEAVE指令
4.12、OD使用指南
4.12.1、将文件载入OD
4.12.2、反汇编代码窗口
4.12.3、程序是如何执行的
4.12.4、免杀过程中经常用到的OD的调试功能
4.12.5、单步跟踪和单步步入
4.12.6、断点和设置断点
4.12.7、编辑指令
4.12.8、表达式跟随
4.12.9、查找指令
4.12.10、重新设置EIP
4.12.11、复制到可执行文件
4.12.12、查看跳转方向
4.13、手工加花免杀
4.13.1、加花免杀的原理
4.13.2、一个典型的花指令
4.13.3、给上兴服务端加花
4.14、工具加花免杀实例
4.15、为什么艺编写花指令
4.15.1、堆栈平衡
4.15.2、pushad和popad
4.15.3、常用平衡指令
4.16、C32Asm使用指南
4.16.1、打开文件
4.16.2、编辑数据
4.16.3、地址跳转
4.16.4、数据查找
4.16.5、文本澳洲
4.16.6、保存文件
4.17、API调用

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第五章、手工脱壳
5.1、脱壳基础知识
5.1.1、脱壳步骤
5.1.2、OEP
5.1.3、脱壳的相处
5.2、单步跟踪法
5.2.1、使用单步跟踪法跟踪OEP的常见步骤
5.2.2、使用单步跟踪法脱UXP壳
5.3、ESP定律法
5.3.1、使用ESP定律跟踪OEP的常见步骤
5.3.2、ESP定律脱北斗壳
5.3.3、ESP定律原理
5.4、二次断点法
5.4.1、使用二次断点法跟踪OEP的常见步骤
5.4.2、使用二次断点法脱壳实例
5.5、末次异常法
5.5.1、使用末次异常法跟踪OEP的常见步骤
5.5.2、使用末次异常法脱tElock
5.6、模拟跟踪法
5.6.1、模拟跟踪法的常见步骤
5.6.2、使用模拟跟踪法脱FSG壳
5.7、SFX自动托脱壳法
5.7.1、使用SFX自动脱壳法脱壳的常见步骤
5.7.2、使用SFX自动脱壳法脱dxpack壳
5.8、出口标志法
5.8.1、使用出口标志脱壳法的常见步骤
5.8.2、使用出口标志法脱dxpack壳
5.9、使用脱壳脚本复制脱壳
5.10、使用脱壳工具脱壳
5.10.1、超级巡警脱壳工具的工作方法
5.10.2、使用超级巡警脱壳工具脱壳

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第六章、常用大范围免杀方法
6.1、利用加多个花指令的方法实现木马免杀
6.1.1、加多花免杀的原理
6.1.2、加多花免杀实例
6.2、利用壳外花指令实现木马免杀
6.3、利用FreeRes实现加多壳免杀
6.4、利用修改壳头实现木马免杀
6.4.1、ASPack 壳的修改
6.4.2、UPX壳的修改
6.5、利用移动PE头的方法实现木马免杀
6.6、利用SEH技术给木马加花
6.7、利用去头加花方法实现木马免杀
6.8、利用reloc改壳免杀木马
6.9、利用LordPE重建PE实现免杀
6.10、利用添加PE数字签名实现免杀
6.10.1、判断一个PE文件是否具有数字证书
6.10.2、获取PE文件内数据签名的起始位置
6.10.3、获取PE文件内数字签名的长度
6.10.4、拷贝数字签名pcmain.dll中
6.10.5、给pcmain.dll添加PE数字签名相关配置
6.10.6、利用工具快递给PE文件添加数字签名

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第七章、特征码定位
7.1、再谈特征码
7.1.1、特征码查杀两要素
7.1.2、复合特征码查杀
7.1.3、隐藏特征码
7.1.4、启发式扫描
7.1.5、狭义上的特征码与广义上的特征码
7.2、MyCCL定位原理
7.2.1、MyCCL定位符合特征码的原理
7.2.2、MyCCL对特征码的精确定位
7.2.3、隐藏式含特征码
7.3、MultiCCL定位原理
7.4、MyCCL定位文件特征码实例
7.4.1、粗略定位复位复合特征码
7.4.2、精确定位复合特征码
7.5、MultiCCL定位文件特征码实例
7.5.1、使用MultiCCL定位文件特征码
7.5.2、MultiCCL保护区域的设置
7.6、定位内存特征码
7.6.1、使用MyCCL定位内存特征码
7.6.2、使用MultiCCL定位内存特征码
7.7、启发式扫描与主动防御
7.7.1、启发式扫描
7.7.2、主动防御

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第八章、特征码修改方法
8.1、等值替换法修改特征码
8.2、修改ASCII特征码大小写
8.2.1、ASCII码和ANSI码
8.2.2、利用ASCII码特征码大小写转换免杀LCX
8.3、去除无用ASCII特征码
8.4、移动ASCII特征码
8.4.1、ASCII码数据如何发挥作用
8.4.2、免杀原始文件
8.4.3、移动ASCII特征码实例
8.5、颠倒代码顺序实现特征码修改
8.6、利用vmprotect 加密特征码
8.6.1、导出原始服务端中的SYS驱动文件
8.6.2、免杀SYS驱动文件
8.7、利用通过跳转法修改特征码
8.7.1、利用通过跳转法修改特征码的原理
8.7.2、利用通用跳转法修改特征码
8.8、移动输入表函数特征码
8.9、加空格免杀输入表文件名
8.9.1、什么是加空格免杀法
8.9.2、下面就来实际操作一下
8.10、修改输入表描述信息免杀
8.11、移动输出表函数特征码
8.11.1、认识输出表结构
8.11.2、移动输出表函数名
8.12、利用异或算法加密特征码
8.12.1、重温异或算法
8.12.2、异或算法在免杀上的运用
8.12.3、修改区段标志
8.12.4、加入新区段,女兵记录相关数据
8.12.5、实施加密
8.13、利用异或算法加密输出表函数特征码
8.14、通过修改干扰吗实现特征码免杀
8.14.1、修改第一部分干扰码
8.14.2、修改第二部分干扰码
8.14.3、修改第三部分干扰码
8.14.4、修改特征码
8.15、特征码交换
8.15.1、数据传送法修改特征码的原理
8.15.2、特征码交换的原理及应用
8.15.3、特征码交换实例
8.16、隐藏输入表

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第九章、对抗新型安全工具
9.1、利用Abetter突破卡巴斯基主动防御
9.2、突破卡巴斯基、瑞星、360安全卫士的安全监控
9.3、简单突破江民等杀毒软件主动防御
9.4、突破360安全卫士启动项监控1
9.4.1、突破360安全卫士启动项监控的原理
9.4.2、突破360安全卫士启动项监控项实例
9.5、突破360安全卫士启动项监控2

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十章、综合免杀实例
10.1、免杀PCShare过诺顿11
10.1.1、定位特征码
10.1.2、特征码修改
10.2、免杀PCShare过NOD32
10.2.1、PcMain.dll文件特征码的修改
10.2.2、PcInit.exe文件特征码的修改
10.3、免杀PCShare过瑞星
10.3.1、免杀PcMain.dll
10.3.2、免杀PcHide.sys
10.3.3、免杀PcInit.exe
10.4、免杀PCShare过卡巴斯基
10.4.1、免杀PcMain.dll
10.4.2、免杀PcInit.dll
10.5、免杀PCShare附加数据过常见杀毒软件
10.5.1、修改配置信息加密密钥
10.5.2、设置动态密钥加密
10.6、免杀PCShare过BitDefender 2010
10.6.1、免杀PcMain.dll
10.6.2、免杀PcInit.exe

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第十一章、简单脚本免杀
11.1、PHP后门的免杀
11.2、简单免杀ASP木马
11.3、拆分网页木马的特征码
11.4、利用HTML混淆器免杀网页木马
11.5、用Escape加密网页木马
11.6、最实用的ASP后门免杀方法
11.7、脚本后门及网页木马的免杀综述
11.7.1、加密法
11.7.2、特征码修改法
11.7.3、分割文件包含法
11.7.4、王牌"免杀法"

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
附录
汇编速查手册

ASM/C/C++书籍

ASM/C/C++书籍

C Primer Plus第六版

本书详细讲解了C语言的基本概念和编程技巧

全书共17章,第一、二章介绍C语言的预备知识。第3章~第15章详细讲解了C语言的相关知识,包括数据类型、格式化输入/输出、运算符、表达式、语句、循环、字符输入和输出、函数、数组和指针、字符和字符串函数、内存管理、文件输入输出、结构、位操作、第16章、第17章介绍C预处理器、C库和高级数据表示。

本书适用于C语言初学者,也可用于巩固C语言知识。

添加时间:2018-09-16

书籍每章节编写时间
章节标题名 时间
第一章、初识C语言 2018-09-16
第二章、C语言概述 2018-09-16
第三章、数据和C 2018-09-17
第四章、字符串和格式化输入/输出 2018-09-23
第五章、运算符、表达式和语句 2018-09-25
第六章、C控制语句 :循环 2018-09-28
第七章、C控制语句:分支和跳转 2018-09-29
第八章、字符输入/输出和输入验证 2018-10-09未完成,此书废话很多。
第九章、函数  
第十章、数组和指针  
第十一章、字符串和字符串函数  
第十二章、存储类型、连接和内存管理  
第十三章、文件输入和输出  
第十四章、结构和其他数据形式  
第十五章、位操作  
第十六章、C预处理器和C库  
第十七章、高级数据表示  
第一章:初识C语言

C是一门功能强大的专业化编程语言。

1.1、C语言的起源

1972年由贝尔实验室 在开发Unix系统时设计了C语言。

1.2、选择C语言的理由

1.2.1设计特性

C语言的设计理念让用户轻松完成自顶向下的规划、结构化编程和模块化设计。

1.2.2高效性

C语言具有通常是汇编才具有的微调控能力。

1.2.3可移植性

C是可移植的语言。

1.2.4强大而灵活

C语言功能强大且灵活。例如功能强大的的UNIX系统大部分是C语言编写的,其他语言(如:FORTRAN、Perl、Python)的许多编译器和解释器都是C语言编写的。C程序可以用于解决物理学和工程学的问题,甚至可以制作电影动画特效。

1.2.5面向程序员

C语言是为了满足程序员的需求而设计的,程序员利用C可以访问硬件、操控内存中的位。

1.2.6缺点

C语言使用指针,而设计指针的编程错误往往难以察觉。
1.3、C语言的应用范围

C++在C语言的基础上嫁接了面向对象编程工具。

C++几乎是C的超集,这意味着任何C程序多不多就是一个C++程序。学习C语言相当于学习了许多C++的知识

C语言已经成为嵌入式系统编程的流行语言。

1.4、计算机能做什么
没有说明,略
1.5、高级计算机语言和编译器

编译器是把高级语言程序翻译成计算机能理解的机器语言指令集的程序。

编译器还有一个优势,一般而言,不同cpu制造商使用的指令系统和编码格式不同,但是可以找到与特定cpu匹配的编译器,因此使用合适的编译器或编译器集,便可以把一种高级语言程序转换成供各种不同类型cpu使用的机器语言程序。

1.6、语言标准

C语言发展之初,并没有所谓的C标准。1978年布莱恩合著的《C语言程序设计》书中附录中就成为了实现C的指导标准。与大多数语言不同的是,C语言比其他语言更依赖库,因此需要一个标准,实际上,由于缺乏官方标准,Unix实现提供的库成为了标准库。

1.6.1、第一个ANSI/ISO C标准

1989年定义了C语言和C标准库。叫C89或C90

1.6.2、C99标准

1994年开始修订C标准,最终发布了C99标准。

1.6.3、C11标准

2011年发布了C11标准。
1.7、使用C语言的7个步骤

C是编译型语言,如果之前使用过编译型语言,就会很熟悉组件C程序的几个基本步骤,但是,如果以前使用的是解释性语言,就有必要学习如何编译。

  • 第一步:定于程序的目标
  • 第二步:设计程序
  • 第三步:编写程序
  • 第四步:编译
  • 第五步:运行程序
  • 第六步:测试和调试程序
  • 第七步:维护和修改代码
1.8、编程机制

用C语言编写程序时,编写的内容被存储在文本文件中,以.c为结尾。

1.8.1、目标代码文件、可执行文件和库

C编程的基本策略是,用程序把源代码文件转换成可执行文件,典型的C实现通过编译和链接两个步骤来完成这一过程。

1.8.2、Unix系统

1、Unix C 没有自己的编辑器,但是可以使用通用的Unix编辑器,编写好代码存储为.c结尾文件

2、在Unix系统上编译

以前Unix C编译器需要语言定义 cc命令,但是现在退出了舞台。但是Unix系统提供的C编译通常来自一些其他源,然后以cc作为编译器的别名,因此还是可以使用cc来表用编译器编译。

1.8.3、GUN编译器集合和LLVM项目

GUN项目始于1987年,是一个开发大量免费UNIX系统的软件集合、GUN编译器(也被成为GCC)集合是该项目的产品之一

LLVM项目成为cc的另一个替代品,它的clang编译器处理c代码,可以通过clang调用。

可以使用如下命令查看编译器其版本:

cc -v

1.8.4、Linux系统

在Linux中准备C程序与在Unix系统中几乎一样

1.8.5、PC的命令行编译器

C编译器不是标准Windows软件包的一部分,因此需要从别处获取并安装c编译器,免费的有: Cygwin、MinGW,这样可以在PC通过命令使用GCC编译器,都支持C99和C11最新的一些功能。

1.8.6、集成开发环境

可免费下载的IDE有ms vs 和pelles C

这些IDE都内置了用于编写C程序的编辑器,集成了各种菜单(编译程序,运行程序等)

使用getchar()等待用户的enter按键

1.8.7、Windows/Linux

不同通过Windows系统访问Linux文件,但是可以通过Linux系统访问Windows文档。

1.8.8、MAC OS中的C

如果下载了Xcode,还可以下载可选的命令行工具,这样就可以使用clang和gcc命令了。
本章小结

C是强大而简洁的编程语言,他之所以流行,在于自身提供大量的使用编程工具,能很好的控制硬件,且可移植。

C是编译型语言。C编译器和链接器是把C语言源代码转换成可执行的程序。

复习题
  • 对编程而言,可移植意味着什么?
  • 解释源代码文件、目标代码文件和可执行文件有什么区别?
  • 编程的7个主要步骤是什么?
  • 编译器的任务是什么?
  • 链接器的任务是什么?

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第二章 C语言概述
本章介绍以下内容:
■ 运算符:=
■ 函数:main()、printf()
■ 编写一个简单的C程序
■ 创建整形变量,赋值并显示
■ 换行字符
■ 写注释,创建包含多个函数的程序
■ 关键字
2.1 简单的C程序示例
// filename:first.c
#include <stdio.h>
int main(void){
    int num;
    num = 1;

    printf(" C  语言\n");
    printf(" C  语言  %d .\n",num);
    return 0;
}
2.2 示例解释

2.2.1 第一遍:快速概要:

#include <stdio.h>  <-包含另一个文件
int main(void){     <-函数名

2.2.2 第二遍:程序细节:

1.#include指令和头文件
#include这行是一条C预处理指令,包含供黏一起输入和输出函数 printf()信息
ANSI/ISO C 规定了C编译器必须提供哪些头文件,有些城西要包含stdio.h,而有些不用。

2.main()函数
int main()
int是main()函数的返回类型,这表明main()返回的值是整数。
旧的代码有可能是这样写main() C90可以接受这样的形式,但是C99/C11不允许这样写。
void main() 一些编译器允许 但是所有的标准都为任何这样的写法。

3。注释
/*  这是注释  */
C99增加了      //注释

4.花括号、函数和块
一般而言,所有的C函数都使用花括号标记函数体的开始和结束,这是规定不能省略。只有花括号能起这种作用,圆括号和方括号都不行。

5声明
略

6赋值
num = 1;

7printf()函数
显示

8return
返回
2.3 简单的程序结构

上面的代码就是。

2.4 提高程序可读性的技巧

注意空格 注释说明,有意义的命名

2.5 进一步使用C

//fathm_ft.c 
#include <stdio.h>
int main(void){
    int feet, fathoms;

    fathoms = 2;
    feet = 6*fathoms;

    printf("there %d feet in %d \n",feet,fathoms );
    printf("yes %d\n",6*fathoms );

    return 0;
}

2.5.1、程序说明:

程序在开始出有一条注释,给出了文件名和程序的目的。

2.5.2、多条声明:

int feet,fathome;

2.5.3、乘法

feet =  6*fathoms;

2.5.4、打印多个值:

printf("there %d feet in %d \n",feet,fathoms );
2.6 多个函数

把自己的函数加入程序中:

//two_func.c 一个文件中包含两个函数

#include <stdio.h>
void butler(void);  //snsi/iso c函数原型

int main(void){
    printf("main_start\n");
    butler()
    printf("main_end\n");

    return 0;
}

void butler(void){
    printf("butler\n");
}

butler()函数在程序中出现了3次,第一次是函数原型,告知编译器在程序中要使用该函数。 第2次以函数调用的形式出现在main(),最后一次出现在函数定义中,函数定义即是函数本身的源代码。

C90标准新增了函数原型,旧的编译器可能无法识别。函数原型是一种声明形式,告知编译器正在使用某函数,因此函数原型也被成为函数声明。函数原型还指明了函数的属性。

2.7 调试程序

2.7.1 语法错误

如果不遵循C语言的规则就会发生语法错误

2.7.2 语义错误

编译器无法检测语义错误

2.7.3 程序状态

通过逐步跟踪程序的执行步骤,并记录每个变量,便可件事程序的状态。
2.8 关键字和保留标识符

关键字是C语言的词汇,他们对C而言比较特殊,不能用他们来做标识符、变量

auto extern short while
break float signed _Alignas
case for sizeof _Alignof
char goto static _Atomic
const if struct _Bool
continue inline switch _Complex
default int typedef _Generic
do long union _Imaginary
double register unsigned _Noreturn
else restrict void _Static_assert
enum return volatile _Thread_local
2.9 关键概念

编程是一件富有挑战性的事情

编译器不会在概念性问题上帮助你

本章小结

C语言是一个或多个C函数组成。每个C程序必须包含一个main()函数,这是C程序要调用的第一个函数。

简单的函数有函数头和后面的一堆花括号组成

大部分语句由分号结尾。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第三章 数据和C

本章介绍以下内容:

关键字:int、short、long、unsigned、char、float、double、_Bool、_Comple、_Imaginary
运算符:sizeof()
函数:scanf()
整数类型和浮点数类型的区别
如何书写整形和浮点型常数,如何声明这些类型的变量
如何使用printf()和scanf()函数读写不同类型的值
3.1示例程序
//filename: platinum.c

#include <stdio.h>
int main(void){
    float weight; 
    float value;

    printf("weight\n");
    //用户给输入
    scanf("%f",&weight);
    value = 1700.00 * weight ;

    printf("weight: %.2f",value);

    getchar()

    return 0;
}

注意这里双引号和单引号的却别,博主在敲代码就搞成了单引号报错了。

还有注意一下里面的符号&

3.1.1 程序中的新元素:

■ 注意,代码中使用了新的变量声明, 本利使用了浮点型类型float 的变量
■ 为了打印新类型的变量, 在printf()中使用%f来处处理浮点值, %.2f中的.2用于控制小数点后两位
■ scanf()函数用于读取键盘的输入
    %f要读取用户输入的浮点数
    &weight告诉scanf()把输入的值赋给weight变量,
    &符号表明找到weight变量的地点。
3.2 变量与常量数据
weight就是一个变量
weight*1700   其中1700就是常量

3.3 数据:数据类型关键字

不仅变量和常量不同,不同的数据类型之间也有差异。一些数据类型表示数字,一些数据类型表示字母。

C通过识别一些基本的数据类型来区分和使用这些不同的数据类型

最初K&R给出的关键字 C90标准添加的关键字 C99标准添加的关键字

表3.1 C语言的数据类型关键字
最初K&R给出的关键字 C90标准添加的关键字 C99标准添加的关键字
int signed _Bool
long void _Complex
short   _Imaginary
unsigned    
char    
float    
double    

在C语言中,用int关键字来表示基本的整型类型。后3个关键字(long、short、unsigned)和C90新增的signed用于提供基本整数类型的变式。_Bool表示布尔值true和false,_Complex和_Imaginary表示复数和虚数。

3.3.1 整数和浮点数

对我们而言,整数和浮点数的区别是他们的书写方式不同。对计算机而言,他们的区别是存储的方式不同。

3.3.2 整数

整数是没有小数部分的数。

3.3.3 浮点数

2.75、3.16E7、7.00、2e-8都是浮点数,显然浮点数有多种形式, 3.16E7 = 3.16*10的7次方

1.整数没有小数部分,浮点数有小数部分 2.浮点数可以表示的范围比整数大 3.对于一些算术运算,浮点数损失的精度更多。 4.任何区间内都存在无穷多个实数,所以浮点数通常指是实际值的近似值。 5.过去,浮点运算比整数运算慢,现在许多CPU都包含浮点处理器,缩小了速度上的差距

3.4 C语言基本数据类型

本节将介绍C语言的基本数据类型,包括声明变量、如何表示字面值常量以及典型的用法。一些老式的C语言编译器无法支持这里提到的所有类型,请注意。

3.4.1 int类型

int类型是有符号整形,即int类型的值必须是整数,可以是正整数、负整数、零,其取值范围依计算机而异。

  1. 声明int变量:

    int erns;
    int hogs, cows, goats;
    
    cows = 12;
    
  2. 初始化变量

初始化变量就是为变量赋一个初始值:

int hogs =21;
int cows =32; goats =14;
  1. int类型常量

C语言把大多数整形常量视为int类型,但是非常大的整数除外

  1. 打印int值:

    printf("%d",num)
    
  2. 八进制和十六进制

0x或0X开头为十六进制,0开头为八进制

  1. 显示八进制和十六进制

十进制使用%d, 八进制使用%o, 十六进制使用 %x:

int x = 100;
printf("dec = %d; octal=%o; hex= %x;")
printf("dec = %d; octal=%#o; hex= %#x;")
>>dec=100;octal=144;hex=64;
>>dec=100;octal=0144;hex=0x64;

注意,如果要在八进制和十六进制值前显示0和0x前缀,要分别在转换说明前加入#

3.4.2 其他整数类型

C语言提供了3个附属关键字修饰基本整数类型:short、long、unsigned:

*short占用空间比int少
*long占用空间比int多
*long long 占用比long 多,适用更大的数值场景
*unsigned 只用于非负值场景
*在任何有符号类型前面添加关键字signed,可强调使用有符号类型的意图

1.声明其他整数类型:

long int estine;
long johns;
short int erns;
short ribs;
unsigned int s_count;
unsigned play;
unsigned long head;
unsigned short yesvotes;
long long ago;

2.使用多种整数类型的原因

。。还是继续使用int吧 方便

3.long常量和long long常量

在支持long long类型的系统中,也可以使用ll或LL后缀来表示,如3LL ,另外u或U表示 unsigned long long 如5ull、10LLU、6LLU

4.打印long、short long long和unsigned类型:

%u:unsigned
%hd:short
%hd:long
%lld:long long

3.4.3 使用字符char类型

C语言把1字节定义为char类型占用的位(bit)数

1.声明:

char latan;

2.初始化:

latan ='A';  //正确
latan =A;  //错误
latan =“A”;  //错误

注意只能是单引号

3.非打印字符

单引号只适用于字符、数字和标点符号,浏览ASCII表会发现,有些字符打印不出来,C语言提供了3种表示这些字符:

1.使用ASCII码:
    char deep = 7;

2.使用特殊的符号序列表示一些特殊字符。
3.?书上没看到

4.打印字符:

%c 指明待打印的字符

5.有符号还是无符号

有些编译器把char实现为有符号类型,意味着范围是-128~127,有的实现为无符号类型范围是0~255,可查阅对应的编译手册,确定如何实现char类型。

根据C90标准 C语言允许在关键字char前面使用signed或unsigned,这样无论编译器默认的char是什么类型,signed char表示有符号类型,unsigned char表示无符号类型。

3.4.4 _Bool类型

用于表示布尔值,true和false 占用1位存储空间

3.4.5 可移植类型: stdint.h和inttypes.h

C语言提供了许多有用的整数类型,但是 某些类型名在不同系统中的功能不一样,C99新增了两个头文件stdint.h和inttype.h,以确保C语言的类型在各系统中功能相同。

int32_t me32; //me32Shi一个32位有符号整型变量

这里需要注意一下不同的头文件对应不同的类型变量类型。

3.4.6 float、double和long double

浮点型 双精度浮点型 long double 精度最高。

3.4.7 复数和虚数类型

许多科学和工程计算都要用到复数和虚数。

3.4.8其他类型

C语言还有一些从基本类型衍生的其他类型,包括数组、指针、结构、和联合。前面接触的&符号就是指针的创建

3.4.9 类型大小

sizeof是C语言的内置运算符,以字节为单位给出指定类型的大小。

使用%zd做转换 不支持C99和C11可使用%u、%lu代替

3.5 使用数据类型

在使用变量之前必须先声明,并选择有意义的变量名。初始化变量应使用与变量类型匹配的常数类型。

3.6 参数和陷阱

注意双引号和单引号区别
3.7 转义序列示例

转义概念在计算机使用电传打字机作为输出设备试就有了,但是他们不一定能与现代图形接口兼容。

3.8关键概念

计算机在内存中用数值编码来表示字符。

3.9 本章小结

C有多重数据类型,基本数据类型分为两大类,整数类型和浮点数类型。

浮点类型有3种 long 、double 、long double

整数可以表示为十进制、八进制、十六进制

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第四章 字符串和格式化输入/输出

注解

本章介绍以下内容:
  • 函数 strlen()
  • 关键字:const
  • 字符串
  • 如何创建、存储字符串
  • 如何使用strlen()函数获取字符串的长度
  • 用C预处理器指令#define和ANSIC的const修饰符创建符号常量

本章重点介绍输入和输出。与程序交互和使用字符串可以编写个性化的程序,本章将详细介绍C语言的两个输入/输出函数printf()和scanf()。 最后简要介绍一个重要的工具-C预处理指令,并学习如何定义、使用符号常量。

4.1 前导程序

程序清单4.1talkback.c程序

// file:talkback.c
#include <stdio.h>
#include <string.h>
#define DENSITY 62.4

int main(){
    float weight,volume;
    int size,letters;
    char name[40];

    printf("you name\n");
    scanf("%s",name);
    print("%s nameis ",name);
    scanf("%f",&weight);
    size = sizeof name;

    letters = strlen(name);

    volume = weight/DENSITY;

    printf("well %s ,you volume is %2.2f cuble \n",name,volume );

    return 0;
}
改程序包含了一下新特性:
  • 使用数组储存字符串
  • 使用%s转换说明来处理字符串的输入和输出,注意在scanf()中,name,没有&前缀,而weight有。
  • 用C预处理器把字符常量DENSITY定义为62.4
  • 用C函数strlen()获取字符串的长度。

写到这里的时候有个小插曲,编译的时候提示:

WARNING: Could not lex literal_block as "c". Highlighting skipped.

经检查上面的4-1的代码发生了错误,然后发现是双引号和单引号的区别 , 这个还能检查语法错误呢。不错。

4.2 字符串简介

字符串是一个或多个字符的序列

4.2.1 char类型数组和null字符

C语言没有专门用于存储字符串变量类型,字符串都被存储在char类型的数组中,数组由连续的存储单元组成,字符串中的字符被存储在响铃的存储单元,每个单元存储一个字符。

字符串末尾字符0 ,这是空字符,C语言用来标记字符串的结束。空字符不是数字0 ,他是非打印字符。

4.2.2 使用字符串
程序清单4.2 praise1.c
//praise1.c  使用不同类型的字符串

#include <stdio.h>
#define PRAISE 'you str an exterraodinaty bering'
int main(void){
    char name[40];
    printf("wath you name");
    scanf("%s",name);
    print("hello %s, .%s \n",name,PRAISE);
    return 0;
}

C语言还有其他输入函数,如fgets() 用于读取一般字符串。后面会详细介绍。

4.2.3 strlen() 函数

用于获取字符串长度

//praise2.c  使用不同类型的字符串
//如果编译器识别不了%zd  尝试换成%u或者%lu
#include <stdio.h>
#include <string.h>
#define PRAISE 'you str an exterraodinaty bering'
int main(void){
    char name[40];
    printf("wath you name");
    scanf("%s",name);
    print("hello %s, .%s \n",name,PRAISE);
    print("your %zd letters ",strlen(name));
    print("%zd",sizeof PRAISE);
    return 0;
}

如果使用ANSI C之前的编译器 必须移除这一行:

#include <string.h>

string.h 头文件包含多个与字符串相关的函数原型,包括strlen()。

另外值得注意的一点,之前使用sizeof 使用了圆括号 本例中没有使用,但还是建议所有情况下都写上圆括号。

4.3 常量和预处理器

定义常量:

#define NAME value

这样就是定义了预处理的常量了

4.3.1 const关键字

C90标准新增const关键字,用于限定一个变量只读:

const int MONTHS = 12;

const用起来比#define更灵活。

4.3.2 明示常量

C头文件limits.h 和float.h 分别提供了整数类型和浮点数类型大小限制相关的详细信息,

4.4 printf() 和scanf()

输入输出函数

4.4.1 printf() 函数
表4.3转换说明及其打印的输出结果
转换说明 输出
%a 浮点数、十六进制和p计数法 C99、C11
%A 浮点数、十六进制和p计数法 C99、C11
%c 单个字符
%d 有符号十进制整数
%e 浮点数、e计数法
%E 浮点数、e计数法
%f 浮点数、十进制计数法
%g 根据值的不同,自动选择%f或%e,%e格式用于指数小于-4或者大于等于精度时
%G 根据值的不同,自动选择%f或%E,%E格式用于指数小于-4或者大于等于精度时
%i 有符号十进制整数 与%d相同
%o 无符号八进制整数
%p 指针
%s 字符串
%u 无符号十进制整数
%x 无符号十六进制整数,使用十九禁止数0f
%X 无符号十六进制整数,使用十六进制数0F
%% 打印一个百分号
4.4.2 使用printf()

4.4.3 printf() 的转换说明修饰符

4.4.4 转换说明的意义

转换说明应该与待打印的值类型相匹配。通常都有多种选择,例如打印int类型可以使用%d、%x、%o ,对应的打印double的值可以使用% f、%e、%g

输出较长的字符串的时候,可以使用 反斜杠换行。

4.4.5 使用scanf()
注意:
  • 如果使用scanf()读取基本变量的值,在变量名前面加上一个&
  • 如果使用scanf()把字符串读入字符数组中,不要使用&
4.5关键概念

C语言用char类型表示单个字符,用字符串表示字符序列。

在程序中,最好用#define定义数值常量,用const关键字声明的变量为只读。

4.6 本章小结

字符串是一系列被视为一个处理单元的字符,在C语言中,字符串是以空字符结尾的一系列字符

strlen()函数说获取字符串长度

C预处理器为预处理器指令查找源代码,并在开始编译程序之前处理他们。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第五章 运算符、表达式和语句

注解

本章介绍如下内容:
  • 关键字 while、typedef
  • 运算符 =、-、*、/、%、++、--、类型名
  • C语言的各种运算符,包括用于普通数学运算的运算符
  • 运算符优先级以及语句、表达式含义
  • while循环
  • 复合语句、自动类型转换和强制类型转换
  • 如何编写带参数的函数
5.1 循环简介

程序清单 5.1 shoesl.c 程序

// shoesl.c 
#include <stdio.h>
#define ADJUST 7.31

int main(void){
    const double SCALE = 0.33
    double shoe, foot;

    shoe = 9.0;

    printf("%10.1f %12.5f inches \n",shoe,foot);
    return 0;
}

该程序使用了#define指令创建符号常量和用const限定符创建在程序运行中不可更改的变量。

循环程序:

while(shoe<18.5){
    shoe++;
}
5.2基本运算符

C用运算符表示算术运算。

5.2.1 赋值运算符

hmw = 2002;

5.2.2 加法运算符: +
4+20 ;
5.2.3 减法运算符 -

a = 50-5;

5.2.4 符号运算符 +和-
a = -12;
b = +12;
5.2.5 乘法运算符

a = 2*5;

5.2.6 除法运算符

a = 5.00/2.00

5.2.7 运算符优先级

跟数学里的一样没必要讲

5.2.8 优先级和求值顺序

5.3 其他运算符
5.3.1 sizeof 运算符和 size_t 类型

sizeof运算符以字节为单位返回运算对象的大小或类型。如果运算符对象是类型则必须用个括号括起来。

C语言规定,sizeof返回size_t类型的值。这是一个无符号整数类型。C有一个typedef机制,允许你程序员为现有类型创建别名:

typedef double real;
这样real就是double的别名
real deal;
5.3.2 求模运算符

略 %符号 和数学里的一样 取余数

5.3.3 递增运算符++
i++;
5.3.4 递减运算符
i--;
5.3.5 优先级

递增运算符和递减运算符都是很高的结合优先级。只有括号比它们高。

5.3.6 不要自作聪明

如果一次用太多递增运算符,自己都会糊涂。

5.4 表达式

5.5 类型转换
5.5.1 强制类型转换
nice = (int)1.5+(int)1.7
nice=2
5.6 带参数的函数
//filename: pound.c

#include <stdio.h>
void pound(int n);

int main(void){
    int time = 5;
    char ch = '!';  //ASCII码为33;
    float f = 6.0f;

    pound(time);
    pound(ch);      //pound((int)ch)
    pound(f);       //pound((int)f)

    return 0;
}

void pound(int n){
    while(n-- > 0)
        printf("#");
    printf("\n");
}
5.7 示例程序

5.8 关键概念

C通过运算符提供多种操作。每个运算符的特性包括运算对象的数量、优先级和结合律。当两个运算符共享一个运算对象时,优先级和结合律决定了;额先进性哪像运算。每个C表达式都有一个值。如果不了解运算符和优先级,写出的表达式可能不合法或者表达式的值与预期不符。

5.9 本章小结

C语言有许多运算符,本章讨论了赋值运算符和算术运算符。

表达式由运算符和运算对象组成。每个表达式都有一个值,包括赋值表达式和比较表达式。运算符优先级规则决定了表达式中各项的求值顺序。

5.10 复习题

5.11编程练习

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第六章 C控制语句:循环
本章介绍以下内容:
  • 关键字:for、while、do while
  • 运算符: <、>、>=、<=、!=、==、+=、*=、-=、/=、%=、
  • 函数:fabs()
  • C语言有三种循环:for、 while 、do while
  • 使用关系运算符构建控制循环的表达式
  • 其他运算符
  • 循环常用的数组
  • 编写有返回值的函数
对于计算机科学而言,一门语言应该提供以下3种的程序流:
  • 执行语句序列
  • 如果满足某些条件就重复执行语句序列(循环);
  • 通过测试选择执行哪一个语句序列(分支)
6.1 再探while循环
#include <stdio.h>

int main(void){
    long num;
    long sum = 0L;
    int status;

    status = scanf("%ld",&num);
    while(status ==1){
        status = scanf("%ld",&num);
    }

    return 0;

}
6.1.1 程序注释

6.1.2 C语言读取循环

实际业务复杂,不肯可能这么写!!!略!!! 这个数也真是的 感觉凑页数。

6.2 循环
while (expression)
statement
6.2.1 终止while循环
index = 10;
while(--index <5)
    printf("index")

当index小于5就退出循环。。

书本凑页数

6.2.2 何时终止循环
#include <stdio.h>
int main(void){
    int n = 0 ;
    while(n<7){
        n++;
    }
    return 0;
}
6.2.3 while 入口条件循环

6。2.4 语法要点

6.3 用关系运算符和表达式比较大小

略 数学知识 符号运算优先级

6.4 不确定循环和计数循环

6.5 for循环
#include <stdio.h>
int main(void){
    const int NUMBER = 22;
    int count;
    for(count=1;count<=NUMBER;count++){
        printf("ok \n");
    }
    return 0;
}
6.6 其他运算符: +=、-=、*=、/=、%=

6.7 逗号运算符
#include <stdio.h>
int main(void){
    const int FIRST_OZ = 46;
    const int NEXT_OZ = 20;
    for(ounces = 1,cost=FIRST_OZ;OUNCES<=16;ounces++,cost+=NEXT_OZ){
        printf("ok\n");
    }
}
6.8 出口条件循环 do while
do
    statement
while(expression)

先执行一遍循环体在做判断

6.9 如何选择循环

6.10 嵌套循环

内容略,实际不要嵌套太多层循环。

6.11 数组简介

在很多程序中,数组很重要。数组可以作为一种存储多个相关项的便利方式,

数组是按顺序存储一系列类型相同的值。

float detes[20]

声明detes是一个内含20个元素的数组,每个元素都可以存储float类型的值。

这里简单介绍 后面再详细介绍。

6.12 使用函数返回值的循环示例

略,之前已经介绍了如何函数返回值

6.13 关键概念

6.14 本章小结

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第七章:C控制语句 分支和跳转
本章介绍以下内容
  • 关键字:if、 else、switch、continue、break、case、default、goto
  • 运算符: &&、||、?、
  • 函数:getchar()、putchar()、ctype.h
  • 如何使用if和if else语句,如何嵌套他们
  • 在更复杂的测试表达式中用逻辑运算符组合关系表达式
  • C的条件运算符
  • switch语句
  • break、continue、和goto语句
  • 使用C的字符I/O函数:getchar()和putchar()
  • ctype.h头文件提供的字符解析函数
7.1 if语句
#include <stdio.h>

int main(void){
    if(1<2){
        printf("ok\n");
    }else{
        printf("no\n");
    }
}
7.2 if else语句

如上

7.2.1 L另外一个示例 介绍getchar()和putchar()

目前为止,学过的大多数程序都要求输入数值。以下将介绍和scanf()、printf()不同的一对函数:

getchar()函数不到任何参数,它从输入队列中返回下一个字符:

ch = getchar()

打印出来:

putchar(ch)

反正是比printf()和scanf()实用。

7.2.2 ctype.h系列的字符函数
//ctype.c //替换输入的字符

#include <stdio.h>
int main(void){
    char ch;
    while((ch=getchar())!= '\n'){
        if(isalpha()){          //如果是一个空字符
            putchar(ch+1);       //显示该字符的下一个字符
        }else{
            putchar(ch);
        }
    }
    putchar(ch);        //显示换行符
    return 0;
}

表7.1和7.2列出ctype.h头文件的一些函数。

表7.1 ctype.h头文件中字符测试函数
函数名 如果是下列参数时,返回值为真
isalnum() 字符数字(字母或数字)
isalpha() 字母
isblank() 标准的空白字符
iscntrl() 控制字符,如Ctrl+B
isdigit() 数字
isgraph() 除空格之外可打印的字符
islower() 小写字母
isprint() 可打印字符
ispunct() 标点符号
isspace() 空白字符,和上面有区别的
isupper() 是否大写字母
isxidgit() 十六进制字符
表7.2 ctype.h头文件中字符映射函数
函数名 行为
tolower() 如果参数是大写字符,返回小写字符。
toupper() 返回大写字符。
7.2.3 多重选择 else if
if(1>2){
    printf("ok");
}else if(2>3){
    printf("ok");
}else{
    printf("no");
}
7.2.4 else与if 配对

7.2.5 多层嵌套的if语句

7.3 逻辑运算符
表7.3 逻辑运算符
逻辑运算符 含义
&&
||

使用:

5>2 && 4>7   为假
5>2 || 4>7   为真
!(4>7)   为真
7.3.1 备选拼写:iso646.h头文件

定义iso646.h头文件后,可以使用and or not 代替 &&、||、!符号

7.3.2 优先级

7.3.3 求值顺序

7.3.4 范围

7.4 一个统计单词的程序

7.5 条件运算符 ?:

又称三目运算符 三元运算符:

x = (y<0) ? -y :y;
相当于:
if(y<0)
    x = -y
else
    x = y
7.6 循环辅助: continue和break
7.6.1 continue语句

x大于5就跳下一次执行:

while(x<10){
    if(x>5){
        continue;
    }
}
7.6.2 break

x大于5就跳出循环:

while(x<10){
    if(x>5){
        break;
    }
}
7.7 多重选择 switch和break

当ch为a进入case ‘a’执行,然后break退出:

switch(ch){
    case 'a':
        printf("a\n");
        break;
    case 'b':
        printf("b\n");
        break;
}
7.7.1 switch语句

7.7.2 只读每行的首字母

7.7.3 多重标签
switch(ch){
    case 'a':
    case 'A':
        printf("a\n");
        break;
    case 'b':
        printf("b\n");
        break;
}
7.7.4 switch和if else

7.8 goto语句

早期使用,现在已经废弃 略

7.9 关键概念

7.10 本章小结

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第八章、字符输入/输出和输入验证

注解

本章介绍以下内容:
  • 更详细地介绍输入、输出以及缓冲输入和无缓冲输入的区别
  • 如果通过键盘模拟文件结尾条件
  • 如何使用重定向把程序和文件相连接
  • 创建更友好的用户界面

最初,输入输出函数不是C定义的一部分,C把开发这些函数的任务留给编译器的实现者来完成。

8.1、单字符IO:getchar()和putchar()

在第七章中提到过,getchar()和putchar()每次只处理一个字符。,这种方法和合适计算机,而且这是绝大多数文本处理程序所用的核心方法。

#include <stdio.h>
int main(void){
    char ch;
    while((ch = getchar())!='#')
        putchar(ch);
}

自从ANSI C标准发布以后,C就把stdio.h头文件与使用getchar()和putchar()相关联。这就是为什么程序中要包含这个头文件的原因

8.2、缓冲区

缓冲区分为两类:完全缓冲和行缓冲,完全缓冲输入指的是当缓冲区被填满时才刷新缓冲区,通常出现在文件输入中。缓冲区的大小取决于系统,行缓冲指的是在出现换行符时刷新缓冲区。键盘输入通常是行缓冲,所以在按下Enter键后才刷新缓冲区。

8.3、结束键盘输入
8.3.1、文件、流和键盘输入

8.3.2、文件结尾

无论操作系统实际使用何种方法检测文件结尾,在C语言中用getchar()读取文件检测到文件结尾是将返回一个特殊的值。即EOF。scanf()函数检测到文件结尾时也返回EOF,通常EOF定义在stdio.h文件中:

#define EOF(-1)

为什么是-1呢,因为getchar()函数的返回值通常都介于0-127之间这些值对应标准字符集,但是如果系统能识别扩展字符,该函数的返回值可能在0-255之间,无卵那种情况,-1都对应不带任何字符,所以该值用于标记文件结尾。

绝大部分系统都有办法通过键盘模拟文件结尾条件:

while((ch = getchar()) != EOF)
注意一下几点:
  • 不用定义EOF,因为stdio.h中已经定义过了。
  • 不用担心EOF的实际值,
  • 变量ch的类型从char变为int,因为char类型的变量只能表示0-255的无符号整数,但是EOF的值是-1
  • 由于getchar()函数的返回值类型是int,如果把getchar()的返回值赋给char类型的变量,一些编译器就会警告可能丢失数据。
  • 使用该程序进行键盘输入,要设法输入EOF字符,不能只输入EOF,也不能只输入-1

未完 待更新2018-10-09

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png

C++ Primer Plus第六版

汇编语言-王爽第二版

汇编语言是很多相关课程的重要基础。概括的说,如果从事计算机科学方面的工作的话,汇编语言的基础是必不可缺的.汇编语言是人和机器沟通的最直接的方式,它描述了机器最终要执行的指令序列。汇编语言是和具有微处理器相联系的,每一种微处理器的汇编语言都不一样,只能通过一种常用的、结构简介的微处理器的汇编语言来进行学习,从而达到学习汇编的两个根本目的:充分获得底层编程的体验,深刻理解机器运行程序的机理。

书籍每章节编写时间
章节标题名 时间
第一章、基础知识 2018-10-10
第二章、寄存器 2018-10-11
第三章、寄存器(内存访问) 2018-10-12
第四章、第一个程序  
第五章、[BX]和loop指令  
第六章、包含多个段的程序  
第七章、更灵活定位内存地址的方法  
第八章、处理数据的两个基本问题  
第九章、转移指令的原理  
第十章、CALL和RET指令  
第十一章、标志寄存器  
第十二章、内中断  
第十三章、int指令  
第十四章、端口  
第十五章、外中断  
第十六章、直接定址表  
第十七章、使用BIOS进行键盘输入和磁盘读写  
第一章、基础知识

汇编语言是直接在硬件上工作的编程语言,我们首先要了解硬件的结构,才能有效的应用汇编语言对其编程。

1.1、机器语言

机器语言是机器指令的集合,机器指令展开来讲就是一台机器可以正确执行的命令。电子计算机的机器指令是一列二进制数字。

1.2、汇编语言的产生

汇编语言的主体是汇编指令,汇编指令和机器指令的差别在于指令的表示方法上, 汇编指令是机器指令便于记忆的书写格式。

程序员用汇编指令编写源程序,然后需要有一个能够将汇编指令转换成机器指令的翻译程序,这样的程序我们称其为编译器。程序员用汇编语言写出源程序,再用汇编编译器将其变异为机器码,由计算机最终执行。

1.3、汇编语言的组成
汇编语言发展至今,有一下3类指令组成::
  • 汇编指令:机器码的助记符,有对应的机器码
  • 伪指令:没有对应的机器码。由编译器执行,计算机并不执行
  • 其他符号:如+、-、*、/,由编译器识别,没有对应的机器码。
1.4、储存器

CPU是计算机的核心部件,它控制整个计算机的运作并进行运算,要想让一个CPU工作,必须向他提供指令和数据。指令和数据在存储器中存放,也就是我们平常说的内存

1.5、指令和数据

指令和数据是应用上的概念,在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。

1.6、存储单元

微机存储器的容量是以字节为最小单位来计算的,对应拥有128个存储单元的存储器,它的容量是128个字节。

对于大容量的存储器一般还用以下单位来计量容量:

8bit = 1Byte  8个二进制 =1B
B=Byte
1KB=1024B
1MB=1024KB
1GB=1024MB
1TB=1024GB
1PB=1024TB
1.7、CPU对储存器的读写

存储器被划分成多个存储单元,存储党员从零开始顺序编号。这些编号可以看做存储单元在存储器中的地址。

CPU要从内存中读数据,首先要制定存储单元的地址,也就是要确定他读取哪一个存储单元中的数据。

另外CPU在读写数据时要指明要对哪一个器件进行操作、进行哪种操作。

CPU要想进行数据的读写,必须和外部器件进行下面3类信息的交互:
  • 存储单元的地址(地址信息);
  • 器件的选择,读或写的命令(控制信息)
  • 读或写的数据(数据信息)
1.8、地址总线

CPU是通过地址总线来制定存储器单元的,课件地址总线上能传送多少个不同的信息,。CPU就可以对多少个存储单元进行寻址。假设有10根地址总线,那么就可以传送2的10次方的数据也就是1023,最小0

一个CPU有N根地址线,那么就可以说这个CPU的地址总线的宽度为N,就可以寻找2的N次方个内存单元,。

1.9、数据总线

CPU与内存或其他器件之间的数据传送石通过数据总线来进行的。数据总线的宽度决定了CPU和外界的数据传送速度。8根数据总线一次可传一个8位二进制数据。即2的8次方

1.10、控制总线

CPU对外部器件的控制是通过控制总线来进行的,这里控制总线是总称,控制总线是一些不同控制线的集合。有多少根控制总线,就意味着CPU提供了对外部器件的多少种控制。

1.11、内存地址空间(概述)

一个CPU的地址总线宽度为10,那么可以寻址1024个内存单元,这1024个可寻址到的内存单元就结构成这个CPU的内存地址空间。

1.12、主板

1.13、接口卡

1.14、各类储存器的芯片

1.15、内存地址空间

第二章、寄存器

CPU中的主要部件是寄存器,寄存器是CPU中程序员可以用指令读写的部件,程序员通过改变各种寄存器中的内容来实现对CPU的控制。

不同的CPU,寄存器的个数、结构是不同的,8086CPU有14个寄存器,每个寄存器有一个名称。这些寄存器是:AX BX CX DC SI DI SP BP IP CS SS DS ES PSW,以下用到哪些寄存器在介绍。

2.1、通用寄存器

8086CPU的所有寄存器都是16位的,可以存放2个字节。 AX、BX、CX、DX这4个寄存器通常用来存放一般性数据,称为通用寄存器。

8086CPU的上一代寄存器是8位的,为了保证兼容,使原来基于上代CPU编写的程序可以运行,把这4个寄存器分为可独立使用的两个8位寄存器来使用:
  • AX分为AH、AL
  • BX分为BH、BL
  • CX分为CH、CL
  • DX分为DH、DL

高8位AH,低8位位AL

2.2、字在寄存器中的存储
处于对兼容性的考虑,8086CPU可以一次性处理一下两种尺寸的数据:
  • 字节:记为byte,一个字节由8个bit组成,可以存在8位寄存器中。
  • 字:记为word,一个字由两个字节组成,这两个字节分别称为这个字的高位字节和低位字节。
2.3、几条汇编指令
汇编指令举例
汇编指令 控制CPU完成的操作 用高级语言的语法描述
mov ax,78 将18送入寄存器AX中 AX=78
mov ah,78 将78送入寄存器AH中 ah=78
add ax,8 将寄存器中的ax中的数值加上8 ax = ax+8
mov ax,bx 将寄存器bx中的数据传入寄存器ax AX=BX
add ax,bx 将ax和bx中的数值相加,结果存在ax中 ax = ax+bx
2.4、物理地址

所有的内存单元构成的存储空间是一个一维的线性空间,每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址。

CPU通过地址总线送入存储器的,必须是一个内存单元的物理地址。在CPU向地址总线上发出物理地址之前,必须要在内部先形成这个物理地址。不同的CPU可以有不同的形成物理地址的方式。

2.5、16位结构CPU
概括的讲,16位结构描述了一个CPU具有下面几个方面的结构特性:
  • 运算器一次最多可以处理16位的数据
  • 寄存器的最大宽度为16位
  • 寄存器和运算器之间的通路位16位

8086是16位结构的CPU,这就是说,在8086内部,能够一次性处理、传输、暂时存储的信息最大长度是16位。

2.6、8086CPU给出的物理地址方法

8086CPU有20位的地址总线,可以传送20位地址,达到1mb寻址能力,8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位。

8086CPU采用了一种在内部使用两个16位地址合成的方法来形成一个20位的物理地址。一个称为段地址,一个称为偏移地址。

所以 物理地址=段地址*16+偏移地址

2.7、段地址*16+偏移地址=物理地址的本质含义

因为是16位的数据所以*16就是左移动一位+偏移地址就是一个20位的物理地址了

2.8、段的概念

内存中并没有分段,段的划分来自CPU。由于8086CPU用段地址+偏移地址=物理地址 的方式给出内存单元的物理地址,使得我们可以用分段的方式来管理内存。

将若干地址连续的内存单元看做一个段,用段地址*16定位段的起始地址 用偏移地址定位段中的内存单元。有两段需要注意:段地址*16必然是16的倍数,所以一个段地址的起始地址也一定是16的倍数。偏移地址位16位,16位的寻址能力为64kb 所以一个段的长度最大为64kb。

2.9、段寄存器

8086CPU在访问内存时要由相关部件提供内存单元的段地址和偏移地址合成物理地址。段地址在8086的段寄存器中存放,8086CPU由4个段寄存器: CS DS SS DS 。本章介绍CS。

2.10、CS和IP

CS和IP是8086CPU中最关键的寄存器。它们指示了CPU当前要读取指令的地址, CS为代码段寄存器,IP为指令指针寄存器。

任意时刻,CPU将CS:IP指向的内容当做指令执行。

2.11、修改CS、IP的指令

在CPU中,程序员能够用指令读写的部件只有寄存器,程序员可以通过改变寄存器中的内容实现对CPU的控制。CPU从何处执行指令有CS、IP中的内容决定的,程序员可以改变CS、IP中的内容来控制CPU执行的目标指令。

若想同时修改CS、IP的内容,可以用jmp 段地址:偏移地址 的指令完成,如:

jmp 2AE3:3 执行后 CS=2AE3H,IP=0003H CPU将从2AE33H处读取指令。
2.12、代码段

第三章、寄存器(内存访问)
3.1、内存中的字的存储

CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元时字节单元,则一个字要用两个地址连续的内存单元来存放。这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。

3.2、DS和[address]

CPU要读写一个内存单元的时候,必须先给出这个内存单元的地址,在8086CPU中,内存地址由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的段地址。比如我们要读取10000H单元的内容,可以用如下程序段进行:

mov bx,1000H
mov ds,bx
mov al,[0]
上面的3条执行将10000H(1000:0)中的数据读到al中

前面我们使用mov指令,可以完成两种传送:1、将数据直接送入寄存器,2、将一个寄存器中的内容送入另一个寄存器。

[0]表示内存单元的偏移地址。

3.3、字的传送

前面我们用mov指令在寄存器和内存之间进行字节型数据的传送,因为8086CPU是16位结构,有16根数据线,所以一次性传送16位的数据,也就是说可以一次性传送一个字。只要在mov指令中给出16位的寄存器就可以进行16位数据传送了:

mov bx,1000h
mov ds,bx
mov ax,[0]
mov [0],cx
3.4、mov、add、sub指令
mov指令可以有以下几种形式
mov 寄存器,数据 比如: mov ax,8
mov 寄存器,寄存器 比如: mov ax,bx
mov 寄存器,内存单元 比如: mov ax,[0]
mov 内存单元,寄存器 比如: mov [0],ax
mov 寄存器,寄存器 比如: 段寄存器 ds,ax
3.5、数据段

如何访问数据段中的数据呢?将一段内存当做数据段,是我们在编程时的一种安排。

3.6、栈

栈有两个基本的操作:入栈和出栈。入栈是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈,需要出栈时,又最先被从栈中去取出。栈的这种操作规则被称为:后进先出。

3.7、CPU提供的栈机制

8086CPU提供入栈和出栈指令,最基本的两个是push入栈和pop出栈。比如,push ax 表示将寄存器ax中的数据传入栈中,pop ax表示从栈顶取出数据送入ax。8086CPU的入栈和出栈都是以字位单位进行的。

8086CPU中有两个段寄存器SS和SP,栈顶的段地址存放在SS中偏移地址存放在SP中。 任意时刻,SS:SP指向栈顶元素

3.8、栈顶超界问题

栈顶超界是危险的,因为我们既然将一段空间安排为栈,那么在栈空间之外的空间里很有可能存放具有其他用途的数据、代码等。但是由于我们在入栈出栈时的不小心,将会引发一连串的错误。

我们在编程的时候要自己操心栈顶超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的超界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的超界。

3.9、push、pop指令

前面我们一直在使用push ax 和pop ax, push 和pop 指令是可以在寄存器和内存之间传送数据的。

push 寄存器
pop 寄存器
push 段寄存器
pop 段寄存器
push 内存单元
pop 内存单元
3.10、栈段

将一段内存当做栈段,仅仅是我们在编程时的一种安排,cpu并不会由于这种安排,就在执行push pop 等栈操作指令时自动地将我们定义的栈段当做栈空间来访问。

第四章、第一个程序
4.1、一个源程序从写出到执行的过程
4.2、源程序
4.3、编辑源程序
4.4、编译
4.5、连接
4.6、以简化的方式编译和连接
4.7、1.exe的执行
4.8、谁将可执行文件中的程序装载进入内存并使它运行
4.9、程序执行过程的跟踪
第五章、[BX]和loop指令
5.1、[BX]
5.2、loop
5.3、在Debug中跟踪用loop指令实现的循环程序
5.4、Debug和汇编编译器masm对指令的不同处理
5.5、loop和[bx]的联合应用
5.6、段前缀
5.7、一段安全的空间
5.8、段前缀的使用
第六章、包含多个段的程序
6.1、在代码段中使用数据
6.2、在代码段中使用栈
6.3、将数据、代码、栈放入不同的段
第七章、更累灵活的定位内存地址的方法
7.1、and和or指令
7.2、关于ASCII码
7.3、以字符形式给出数据
7.4、大小写转换问题
7.5、[bx+idata]
7.6、用[bx+idata]的方式进行数组的处理
7.7、SI和DI
7.8、[bx+si]和[bx+di]
7.9、[bx+si+idata]和[bx+di+idata]
7.10、不同的寻址方式的灵活应用
第八章、数据处理的两个基本问题
8.1、bx、si、di和bp
8.2、机器指令处理的数据在什么地方
8.3、汇编语言中的数据位置的表达
8.4、寻址方式
8.5、指令要处理的数据有多长
8.6、寻址方式的综合应用
8.7、div指令
8.8、伪指令dd
8.9、dup
第九章、转移指令的原理
9.1、操作符offset
9.2、jmp指令
9.3、依据位移进行转移的jmp指令
9.4、转移的目的地址在指令中的jmp指令
9.5、转移地址在寄存器中的jmp指令
9.6、转移地址在内存中的jmp指令
9.7、jcxz指令
9.8、loop指令
9.9、根据位移进行转移的意义
9.10、编译器对转移位移超界的程序
第十章、CALL和RET指令
10.ret和retf
10.2、call指令
10.3、依据位移进行转移的call指令
10.4、转移的目的地址在指令中call指令
10.5、转移地址在寄存器中call指令
10.6、转移地址在内存中的call指令
10.7、call和ret的配合使用
10.8、mul指令
10.9、模块化程序设计
10.10、参数和结果的传递问题
10.11、批量数据的传递
10.12、寄存器冲突的问题
第十一章、标志寄存器
11.1、ZF标志
11.2、PF标志
11.3、SF标志
11.4、CF标志
11.5、OF标志
11.6、abc指令
11.7、sbb指令
11.8、cmp指令
11.9、检测比较结果的跳转转移指令
11.10、DF标志和串传送指令
11.11、push和popf
11.12、标志寄存器在Debug中的表示
第十二章、内中断
12.1、内中断的产生
12.2、中断处理程序
12.3、中断向量表
12.4、中断过程
12.5、中断处理程序和ire指令
12.6、除法错误中断的处理
12.7、编程处理0号中断
12.8、安装
12.9、do0
12.10、设置中断向量
12.11、单步中断
12.12、响应中断的特殊情况
第十三章、int指令
13.1、int指令
13.2、编写供应程序调用的中断例程
13.3、对int、inet和栈的深入理解
13.4、BIOS和DOS所提供的中断例程
13.5、BIOS和DOS中断例程的安装过程
13.6、BIOS中断例程应用
13.7、DOS中断例程应用
第十四章、端口
14.1、端口的读写
14.2、CMOS RAM芯片
14.3、shl和shr指令
14.4、CMOS RAM中存储的时间信息
第十五章、外中断
15.1、接口芯片和端口
15.2、外中断信息
15.3、PC机键盘的处理过程
15.4、编写int9中断例程
15.5、安装新的int9中断例程
第十六章、直接定址表
16.1、描述了单元长度的标号
16.2、在其他段中使用数据标号
16.3、直接定址表
16.4、程序入口对应的直接地址表
第十七章、使用BIOS进行键盘输入和磁盘读写
17.1、int9中断例程对键盘输入的处理
17.2、使用int16h中断例程读取键盘缓冲区
17.3、字符串的输入
17.4、应用int13h中断例程对磁盘进行读写

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png

windows下32位汇编语言程序设计-罗云彬

汇编语言基于x86处理器

书籍目录及编写时间
章节标题名 时间
第一章、基础概念 2018-10-14 后面有许多未补充的。
第二章、x86处理器架构  
第三章、汇编语言基础  
第四章、数据传送、寻址和算术运算  
第五章、过程  
第六章、条件处理  
第七章、整数运算  
第八章、高级过程  
第九章、字符串和数组  
第十章、结构和宏  
第十一章、MS-Windows编程  
第十二章、浮点数处理与指令编码  
第十三章、高级语言接口  
第一章、基本概念

本章将建立汇编语言编程的一些核心概念。比如,汇编语言是如何适应各种语言和应用程序的。本章还将介绍虚拟机概念,它在理解软件与硬件层之间的关系时非常重要。本章还用大量的篇幅说明二进制和十六进制的数制系统,展示如何执行转换和基本的算术运算。本章的最后将介绍基础逻辑操作(AND、OR和NOT),后续章节将证明这些操作是很重要的。

1.1、欢迎来到汇编语言的世界

本书主要介绍与运行 Microsoft Windows Intel和AMD处理器相兼容的微处理器编程。

配合本书应使用Microsoft宏汇编器(称为MASM)的最新版本。Microsoft Studio的大多数版本(专业版,旗舰版,精简版)都包含MASM。

在运行 Microsoft Windows 的X86系统中,有一些其他有名的汇编器包括:TASM、NASM、MAsm32。GAS(GNU汇编器)和NASM是两种基于 Linux的汇编器。在这些汇编器中,NASM的语法与MASM的最相似。

汇编语言是最古老的编程语言,在所有的语言中,它与原生机器语言最为接近。它能直接访问计算机硬件,要求用户了解计算机架构和操作系统。

本书有助于学习计算机体系结构、机器语言和底层编程的基本原理。读者可以学到足够的汇编语言,来测试其掌握的当今使用最广泛的微处理器系列的知识。

如果读者计划成为C或C++开发者,就需要理解内存、地址和指令是如何在底层工作的。在高级语言层次上,很多编程错误不容易被识别。因此,程序员经常会发现需要“深入”到程序内部,才能找出程序不工作的原因。

1.1.1、读者可能会问的问题

需要怎样的背景知识? 在阅读本书之前,读者至少使用过一种结构化高级语言进行编如ava、C、 Python或C++需要了解如何使用正F语句、数组和函数来解决编程问题。

什么是汇编器和链接器? 汇编器是一种工具程序,用于将汇编语言源程序转换为机器语言。连接器也是一种工具程序员,他把汇编器生成的单个文件组合为一个可执行文件。还有一个相关的工具,称为调试器( bugger)使程序员可以在程序运行时,单步执行程序并检查寄存器和内存状态。

要哪些硬件和软件? 一台运行32位或64位 Microsoft Windows系统的计算机,并已安装了近期版本的 Microsoft Visual Studio。

MASM能创建那些类型的程序?
  • 32位保护模式(32-Bite-Protected-Mode):32位保护模式程序运行于所有的32位和64位版本的 Microsoft Windows系统。它们通常比实模式程序更容易编写和理解。从现在开始,将其简称为32位模式。
  • 64位模式(64-Bit-Mode)64位程序运行与所有的64位版本的Microsoft Windows 系统。
  • 16位实地址模式(116-Bit6- Real-Address Mode)16位程序运行于32位版本Windows 和嵌入式系统,由于64位Windows不支持这类程序,本书只在14-17章讨论这种模式。这些章节是电子版的。可以从出版社网站获取。
能学到什么?本书将使读者更好地了解数据表示、调试编程和硬件控制。读者将学到:
  • x86处理器应用的计算机体系结构的基本原理。
  • 基本布尔逻辑,以及它是如何应用于编程和计算机硬件的。
  • 使用保护模式和虚模式时,x86处理器如何管理内存。
  • 高级语言编译器(如C++)如何将其语句转换为汇编语言和原生机器代码。
  • 高级语言如何在机器级实现算术表达式、循环和逻辑结构。
  • 数据表示,包括有符号和无符号整数、实数以及字符数据。
  • 如何在机器级调试程序。使用C和C++语言时,它们生成的是原生机器代码,这个技术显得至关重要。
  • 应用程序如何通过中断处理程序和系统调用与计算机操作系统进行通信。
  • 如何连接汇编语言代码与C++程序。
  • 如何创建汇编语言应用程序。

汇编语言与机器语言有什么关系? 机器语言( machine language)是一种数字语言,专门设计成能被计算机处理器(CPU)理解。所有x86处理器都理解共同的机器语言。江编语言( assembly language)包含用短助记符如ADD、MOV、SUB和CALL书写的语句。汇编语言与机器语言是一对一( one-to-one)的关系:每一条汇编语言指令对应一条机器语言指令。

C++和Java与汇编语言有什么关系? 高级语言如 Python、C++和Java与汇编语言和机器语言的关系是一对多(one-to-many)。比如,C++的一条语句就会扩展为多条汇编指令或机器指令。大多数人无法阅读原始机器代码,因此,本书探讨的是与之最接近的汇编语言。例如、下面的C++代码进行了两个算术操作,并将结果赋给一个变量。假设X和Y是整数

int Y;
int X = (Y+4)*3;

与之等价的汇编语言程序如下所示。这种转换需要多条语句,因为每条汇编语句只对应条机器指令:

mov eax, Y  ;Y送入EAX寄存器
add eax, 4  ;EAX存器内容加4
mov ebx, 3  ;3送入EBX寄存器
imul ebx    ;EAX与EBX相乘
mov X,eax   ;EAX的值送入X

(寄存器( register)是CPU中被命名的存储位置,用于保存操作的中间结果。)这个例子的重点不是说明C++与汇编语言哪个更好,而是展示它们的关系。

汇编语言可移植吗? 一种语言,如果它的源程序能够在各种各样的计算机系统中进行编译和运行,那么这种语言被称为是可移植的( portable)。例如,一个C++程序,除非需要特别引用某种操作系统的库函数,否则它就几乎可以在任何一台计算机上编译和运行。Java语言的一大特点就是,其编译好的程序几乎能在所有计算机系统中运行。

汇编语言不是可移植的,因为它是为特定处理器系列设计的。目前广泛使用的有多种不同的汇编语言,每一种都基于ー个处理器系列。对于ー些广为人知的处理器系列如Mola[468x00、x86、 SUN Sparc、Vax和IBM-370,汇编语言指令会直接与该计算机体系结构相匹配,或者在执行时用一种被称为微代码解释器( microcode interpreter)的处理器内置程序来进行转换。

为什么要学习汇编语言? 如果对学习汇编语言还心存疑虑,考虑一下这些观点
  • 如果是学习计算机工程,那么很可能会被要求写嵌入式( embedded)程序。嵌入式程序是指一些存放在专用设备中小容量存储器内的短程序,这些专用设备包括:电话、汽车燃油和点火系统、空调控制系统、安全系统、数据采集仪器、显卡、声卡、硬盘驱动器、调制解调器和打印机。由于汇编语言占用内存少,因此它是编写嵌入式程序的理想工具。
  • 处理仿真和硬件监控的实时应用程序要求精确定时和响应。高级语言不会让程序员对编译器生成的机器代码进行精确控制。汇编语言则允许程序员精确指定程序的可执行代码。
  • 电脑游戏要求软件在减少代码大小和加快执行速度方面进行高度优化。就针对一个目标系统编写能够充分利用其硬件特性的代码而言,游戏程序员都是专家。他们经常选择汇编语言作为工具,因为汇编语言允许直接访问计算机硬件,所以,为了提高速度可以对代码进行手工优化。
  • 汇编语言有助于形成对计算机硬件、操作系统和应用程序之间交互的全面理解。使用汇编语言,可以运用并检验从计算机体系结构和操作系统课程中获得的理论知识。
  • 一些高级语言对其数据表示进行了抽象,这使得它们在执行底层任务时显得有些不方便,如位控制。在这种情况下,程序员常常会调用使用汇编语言编写的子程序来完成他们的任务。
  • 硬件制造商为其销售的设备创建设备驱动程序。设备驱动程序(device driver)是一种程序,它把通用操作系统指令转换为对硬件细节的具体引用。比如,打印机制造商就为他们销售的每一种型号都创建了一种不同的 MS-Windows设备驱动程序。通常,这些设备驱动程序包含了大量的汇编语言代码。

汇编语言有规则吗? 大多数汇编语言规则都是以目标处理器及其机器语言的物理局限性为基础的。比如,CPU要求两个指令操作数的大小相同。与C++或Java相比,汇编语言的规则较少,因为,前者是用语法规则来减少意外的逻辑错误,而这是以限制底层数据访问为代价的。汇编语言程序员可以很容易地绕过高级语言的限制性特征。例如,Java就不允许访问特定的内存地址。程序员可以使用JNI(java Native Interface)类来调用C函数绕过这个限制,可结果程序不容易维护。反之,汇编语言可以访问所有的内存地址。但这种自由的代价也很高:汇编语言程序员需要花费大量的时间进行调试。

1.1.2、汇编语言的应用

早期在编程时,大多数应用程序部分或全部用汇编语言编写。它们不得不适应小内存,并尽可能在慢速处理器上有效运行。随着内存容量越来越大,以及处理器速度急速提高,程序变得越来越复杂。程序员也转向高级语言如C、 FORTRAN COBOL,这些语言具有很多结构化能力。最近, Python、C++、c#和Java等面向对象语言已经能够编写含数百万行代码的复杂程序了。

很少能看到完全用汇编语言编写的大型应用程序,因为它们需要花费大量的时间进行编写和维护。不过,汇编语言可以用于优化应用程序的部分代码来提升速度,或用于访问计算机硬件。表1-1比较了汇编语言和高级语言对各种应用类型的适应性。

汇编语言与高级语言的比较
应用类型 高级语言 汇编语言
商业或科学应用程序,为单一的中型或大型平台编写 规范结构使其易于组织和维护大量代码 最小规范结构,因此必须由具有 不同程度经验的程序员来维护结构。这导致对已有代码的维护困难
硬件设备驱动程序 语言不一定提供对硬件的直接访问。 对硬件的访问直接且简单。当程序较短且文档良好时易于维护
为多个平台(不同的操作系统)编写的商业或科学应用程序 通常可移植。在每个目标操作系统上,源程序只做少量修改就能重新编译 需要为每个平台单独重新编写代码,每个汇编器都使用不同的语法,维护困难
需要直接访问硬件的嵌人式系统和电脑游戏 可能生成很大的可执行文件,以至于超出设备的内存容量 理想,因为可执行代码小,运行速度快

C和C++语言具有一个独特的特性,能够在高级结构和底层细节之间进行平衡。直接访问硬件是可能的,但是完全不可移植。大多数C和C++编译器都允许在其代码中嵌人汇编语句,以提供对硬件细节的访问。

1.1.3、本节回顾
  • 汇编器和链接器是如何一起工作的?
  • 学习汇编语言如何能提高你对操作系统的理解?
  • 比较高级语言和机器语言时,一对多关系是什么意思?
  • 解释编程语言中的可移植性概念。
  • x86处理器的汇编语言与Vax或Motorola8x00等机器的汇编语言是一样的吗?
  • 举一个嵌入式系统应用程序的例子。
  • 什么是设备驱动程序?
  • 汇编语言和C/C++语言中的指针变量类型检查,哪一个更强(更严格)?
  • 给出两种应用类型,与高级语言相比,它们更适合使用汇编语言。
  • 编写程序来直接访问打印机端口时,为什么高级语言不是理想工具?
  • 为什么汇编语言不常用于编写大型应用程序?
  • 挑战:参考本章前面给出的例子,将下述C++表达式转换为汇编语言: X = (Y*4) +3
1.2、虚拟机的概念

虚拟机概念是一种说明计算机硬件和软件关系的有效方法。在安德鲁·塔嫩鲍姆中可以找到对这个模型广为人知的解释。要说明这个概念,先从计算机的最基本功能开始,即执行程序。

计算机通常可以执行用其原生机器语言编写的程序。这种语言中的每一条指令都简单到可以用相对少量的电子电路来执行。了简便,称这种语言为LO。

由于LO极其详细,并且只由数字组成,因此,程序员用其编写程序就非常困难。如果能够构造一种较易使用的新语言L1,那么就可以用L1编写程序。有两种实现方法:

  • 解释(Interpretation): 运行L1程序时,它的每一条指令都由一个用LO语言编写的程序进行译码和执行。L1程序可以立即开始运行,但是在执行之前,必须对每条指令进行译码。
  • 翻译(Translation): 由一个专门设计的L程序将整个L1程序转换为L0程序。然后,得到的L0程序就可以直接在计算机硬件上执行。
1.3、数据表示

汇编语言程序员处理的是物理级数据,因此他们必须善于检查内存和寄存器。

1.3.1、二进制整数

计算机以电子电荷集合的形式在内存中保存指令和数据。用数字来表示这些内容就需要系统能够适应开关的概念。二进制数用2个数字作基础,其中每一个二进制数字不是0就是1.位自右向左,从0开始顺序增量编号。左边的位称为最高有效位-MSB,右边的位称为最低有效位-LSB

二进制整数可以是有符号的,也可以是无符号的。有符号整数又分为正数和负数。

1.3.2、二进制加法

两个二进制数相加时,是位对位处理的,从最低的以为(右边)开始,依序将每一个对位进行加法运算。

1.3.3、整数存储大小

在x86计算机中,所有数据存储的基本单位都是字节(byte)一个字节由8位,其他的存储单位还有字word、双子DWORD、四字qword,一个字=2个字节 byte

1.3.4、十六进制整数

大的二进制读起来很麻烦,因此十六进制数字就提供同了简单的方式来表示二进制数据。十六进制整数中的1个数字就表示了4位二进制,两个十六进制数字就能表示一个字节。一个十六进制数字表示的范围是十进制数0到15.所以字母A到F代表十进制数10-15

1.3.5、十六进制加法

调试工具程序 通常用十六进制表示内存地址。为了定位一个新地址常常需要将两个地址相加。十六进制加法与十进制加法是一样的,只需要更换基数就可以了。

1.3.6、有符号二进制整数

有符号二进制整数有正数和负数。在x86处理器中,msb表示的是符号位:0表示正数。1表示负数

这里补码概念不做多解释,需要阅读其他书籍学习。

1.3.7、二进制减法

如果采用与十进制减法相同的方法,那么从一个较大的二进制数中减去一个较小的无符号二进制数就很容易了。

1.3.8、字符存储
计算机使用的是字符集,将字符映射为整数。
  • ANSI字符集:美国国家标准协会定了了8位字符集来表示多大256个字符。前128个字符对应标准美国键盘上的字母和符号。后128位字符表示特殊字符。window早期版本使用ANSI字符集。
  • Unicode标准:当前,计算机必须能表示计算机软件中世界中各种各样的语言,因此Unicode被创建出来,用于提供一种定义文字和符号的通用方法,他定了数字代码,定义的对象为文字、符号以及所有主要语言中使用的标点符号。代码特点转换可现实字符的格式有三种: UTF-8用于html,与ascii有相同的字节数值 UTF-16用于结余使用内存与高校访问字符相互平衡的环境中。 UTF-32用于不考虑空间,但需要固定宽度字符的环境中,每个字符都有32位的编码。
  • ASCII字符串 有一个或多个字符的序列被称为字符串。更具体的说,一个ASCII字符串是保存在内存中的,包含了ASCII代码的连续字节。有对应的ASCII表
1.3.9、本节回顾

1.4、布尔表达式

与或非 not and or

1.4.1、布尔函数真值表

1.4.2、本节回顾

1.5、本章小结

暂略

1.6、关键术语

1.7、复习题和练习

1.7.1、简答题

1.7.2、算法基础

第二章、x86处理器架构
2.1、一般概念
2.1.1、一般微机设计
2.1.2、指令执行周期
2.1.3、读取内存
2.1.4、加载并执行程序
2.1.5、本节回顾
2.2、32位x86处理器
2.2.1、操作模式
2.2.2、基本执行环境
2.2.3、x86内存管理
2.2.4、本节回顾
2.3、64位x86-64处理器
2.3.1、64位操作模式
2.3.2、基本x86位执行环境
2.4、典型x86计算机组件
2.4.1、主板
2.4.2、内存
2.4.3、本节回顾
2.5、输入输出系统
2.5.1、I/O访问层次
2.5.2、本节回顾
2.6、本章小结
2.7、关键术语
2.8、复习题
第三章、汇编语言基础
3.1、基本语言元素
3.1.1、第一个汇编语言程序
3.1.2、整数常量
3.1.3、整数常量表达式
3.1.4、实数常量
3.1.5、字符常量
3.1.6、字符串常量
3.1.7、保留字
3.1.8、标识符
3.1.9、伪指令
3.1.10、指令
3.1.11、本节回顾
3.2、示例:整数加减法
3.2.1、AddTow程序
3.2.2、运行和调试AddTow程序
3.2.3、程序模板
3.2.4、本节回顾
3.3、汇编、连接和运行程序
3.3.1、汇编-连接-执行周期
3.3.2、列表文件
3.3.3、本节回顾
3.4、定义数据
3.4.1、内部数据类型
3.4.2、数据定义语句
3.4.3、向AddTow程序添加一个变量
3.4.4、定义BYTE和SBYTE数据
3.4.5、定义WORD和SWORD数据
3.4.6、定义DWORD和SDWORD数据
3.4.7、定义QWORD数据
3.4.8、定义压缩BCD(TBYTE)数据
3.4.9、定义浮点类型
3.4.10、变量加法程序
3.4.11、小端排序
3.4.12、声明未初始化数据
3.4.13、本节回顾
3.5、符号常量
3.5.1、等号伪指令
3.5.2、计算数组和字符串大小
3.5.3、EQU指令
3.5.4、TEXTEQU指令
3.5.5、本节回顾
3.6、64位编程
3.7、本章小结
3.8、关键术语
3.8.1、术语
3.8.2、指令、运算符和伪指令
3.9、复习题和练习题
3.9.1、简答题
3.9.2、算法基础
3.10、编程练习
第四章、数据传送、寻址和算术运算
4.1、数据传送指令
4.1.1、引言
4.1.2、操作数类型
4.1.3、直接内存操作数
4.1.4、MOV指令
4.1.5、整数的全零/符号扩展
4.1.6、LAHF和SAHF指令
4.1.7、XCHG指令
4.1.8、直接-偏移量操作数
4.1.9、示例程序(Moves)
4.1.10、本节回顾
4.2、加法和减法
4.2.1、INC和DEC指令
4.2.2、ADD指令
4.2.3、SUB指令
4.2.4、NEG指令
4.2.5、执行算术表达式
4.2.6、加减法影响的标志位
4.2.7、示例程序(AddSubTest)
4.2.8、本节回顾
4.3、与数据相关的运算符和伪指令
4.3.1、OFFSET运算符
4.3.2、ALIGN伪指令
4.3.3、PTR运算符
4.3.4、TYPE运算符
4.3.5、LENGTHOF运算符
4.3.6、SIZEOF运算符
4.3.7、LABEL伪指令
4.3.8本节回顾
4.4、间接寻址
4.4.1、间接操作数
4.4.2、数组
4.4.3、变址操作数
4.4.5、本节回顾
4.5、JMP和LOOP指令
4.5.1、JMP指令
4.5.2、LOOP指令
4.5.3、在Visual Studio调试器中显示数组
4.5.4、整数数组求和
4.5.5、复制字符串
4.5.6、本节回顾
4.6、64位编程
4.6.1、MOV指令
4.6.2、64位的 SumArray程序
4.6.3、加法和减法
4.6.4、本节回顾
4.7、本章小结
4.8、关键术语
4.8.1、术语
4.8.2、指令、运算符和伪指令
4.9、复习题和练习
4.9.1、简答题
4.9.2、算法基础
4.10、编程练习
第五章、过程
5.1、堆栈操作
5.1.1、运行时堆栈(32位模式)
5.1.2、PUSH和POP指令
5.1.3、本节回顾
5.2、定义并使用过程
5.2.1、PROC伪指令
5.2.2、CALL和RET指令
5.2.3、过程调用嵌套
5.2.4、向过程传递寄存器参数
5.2.5、示例:整数数组求和
5.2.6、保存和恢复寄存器
5.2.7、本节回顾
5.3、链接到外部库
5.3.1、背景知识
5.3.2、本节回顾
5.4、Irvine32链接库
5.4.1、创建库的动机
5.4.2、概述
5.4.3、过程详细说明
5.4.4、库测试程序
5.4.5、本节回顾
5.5、64位汇编编程
5.5.1、Irvine64链接库
5.5.2、调用64位子程序
5.5.3、x64调用规范
5.5.4、调用过程示例
5.6、本章小结
5.7、关键术语
5.7.1、术语
5.7.2、指令、运算符和伪指令
5.8、复习题和练习
5.8.1、简答题
5.8.2、算法基础
5.9、编程练习
第六章、条件处理
6.1、条件分支
6.2、布尔和比较指令
6.2.1、CPU状态标志
6.2.2、AND指令
6.2.3、OR指令
6.2.4、位映射集
6.2.5、XOR指令
6.2.6、NOT指令
6.2.7、TEST指令
6.2.8、CMP指令
6.2.9、置位和清除单个CPU标志位
6.2.10、64位模式下的布尔指令
6.2.11、本节回顾
6.3、条件跳转
6.3.1、条件结构
6.3.2、Jcond指令
6.3.3、条件跳转指令类型
6.3.4、条件跳转应用
6.3.5、本节回顾
6.4、条件循环指令
6.4.1、LOOPZ和LOOPE指令
6.4.2、LOOPNZ和LOOPNE指令
6.4.3、本节回顾
6.5、条件结构
6.5.1、块结构的IF语句
6.5.2、复合表达式
6.5.3、WHILE循环
6.5.4、表驱动选择
6.5.5、本节回顾
6.6、应用:有限状态机
6.6.1、验证输入字符串
6.6.2、验证有符号整数
6.6.3、本节回顾
6.7、条件控制流伪指令
6.7.1、新建IF语句
6.7.2、有符号数和无符号数的比较
6.7.3、复合表达式
6.7.4、用 .REPEAT和.WhILE创建循环
6.8、本章小结
6.9、关键术语
6.9.1、术语
6.9.2、指令、运算符和伪指令
6.10、复习题和练习
6.10.1、简答题
6.10.2、算法基础
6.11、编程练习
6.11.1、测试代码的建议
6.11.2、习题
第七章、整数运算
7.1、移位和循环移位指令
7.1.1、逻辑移位和算术移位
7.1.2、SHL指令
7.1.3、SHR指令
7.1.4、SAl和SAR指令
7.1.5、ROL指令
7.1.6、ROR指令
7.1.7、RCl和RCR指令
7.1.8、有符号数溢出
7.1.9、SHLD/SHRD指令
7.1.10、本节回顾
7.2、移位和循环移位的应用
7.2.1、多个双字的移位
7.2.2、二进制乘法
7.2.3、显示二进制位
7.2.4、提取文件日期字段
7.2.5、本节回顾
7.3、乘法和除法指
7.3.1、MUL指令
7.3.2、IMUL指令
7.3.3、测量程序执行时间
7.3.4、diV指令
7.3.5、有符号数除法
7.3.6、实现算术表达式
7.3.7、本节回顾
7.4、扩展加减法
7.4.1、ADC指令
7.4.2、扩展加法示例
7.4.3、SBB指令
7.4.4、本节回顾
7.5、ASCII和非压缩十进制运算
7.5.1、AAA指令
7.5.2、AAS指令
7.5.3、AAM指令
7.5.4、AAD指令
7.5.5、本节回顾
7.6、压缩十进制运算
7.6.1、DAA指令
7.6.2、DAS指令
7.6.3、本节回顾
7.7、本章小结
7.8、关键术语
7.8.1、术语
7.8.2、指令、运算符和伪指令
7.9、复习题和练习
7.9.1、简答题
7.9.2、算法基础
7.10、编程练习
第八章、高级过程
8.1、引言
8.2、堆栈帧
8.2.1、堆栈参数
8.2.2、寄存器参数的缺点
8.2.3、访问堆栈参数
8.2.4、32位调用规范
8.2.5、局部变量
8.2.6、引用参数
8.2.7、LEA指令
8.2.8、ENTER和LEAVE指令
8.2.9、LOCAL伪指令
8.2.10、Microsoft X64调用规范
8.2.11、本节回顾
8.3、递归
8.3.1、递归求和
8.3.2、计算阶乘
8.3.3、本节回顾
8.4、INVOKE、Addr、PROC和PROTO
8.4.1、INVOKE伪指令
8.4.2、ADDR运算符
8.4.3、PROC伪指令
8.4.4、PROTO伪指令
8.4.5、参数类别
8.4.6、示例:交换两个整数
8.4.7、调试提示
8.4.8、WriteStackFrame过程
8.4.9、本节回顾
8.5、新建多模块程序
8.5.1、隐藏和导出过程名
8.5.2、调用外部过程
8.5.3、跨模块使用变量和标号
8.5.4、示例: ArraySum程序
8.5.5、用 Extern新建模块
8.5.6、用INVOKE和PROTO新建模块
8.5.7、本节回顾
8.6、参数的高级用法(可选主题)
8.6.1、受USES运算符影响的堆栈
8.6.2、向堆栈传递8位和16位参数
8.6.3、传递64位参数
8.6.4、非双字局部变量
8.7、Java字节码(可选主题)
8.7.1、Java虚拟机
8.7.2、指令集
8.7.3、Java反汇编示例
8.7.4、示例:条件分支
8.8、本章小结
8.9、关键术语
8.9.1、术语
8.9.2、指令、运算符和伪指令
8.10、复习题和练习
8.10.1、简答题
8.10.2、算法基础
8.11、编程练习
第九章、字符串和数组
9.1、引言
9.2、字符串基本指令
9.2.1、MOVSB、MOVSW和、MOVSD
9.2.2、CMPSB、CMPSW和CMPSD
9.2.3、SCASB、SCASW和SCASD
9.2.4、STOSB、STOSW和STOSD
9.2.5、LODSB、LODSW和LODSD
9.2.6、本节回顾
9.3、部分字符串过程
9.3.1、Str_compare过程
9.3.2、Str_length过程
9.3.3、Str_copy过程
9.3.4、Str_trim过程
9.3.5、Str_ucase过程
9.3.6、字符串库演示程序
9.3.7、Irivne64库中的字符串过程
9.3.8、本节回顾
9.4、二维数组
9.4.1、行列顺序
9.4.2、基址-变址操作数
9.4.3、基址-变址-偏移量操作数
9.4.4、64位模式下的基址-变址操作数
9.4.5、本节回顾
9.5、整数数组的检索和排序
9.5.1、冒泡排序
9.5.2、对半查找
9.5.3、本节回顾
9.6、Java字节码:字符串处理(可选主题)
9.7、本章小结
9.8、关键术语和指
9.9、复习题和练习
9.9.1、简答题
9.9.2、算法基础
9.10、编程练习
第十章、结构和宏
10.1、结构
10.1.1、定义结构
10.1.2、声明结构变量
10.1.3、引用结构变量
10.1.4、示例:显示系统时间
10.1.5、结构包含结构
10.1.6、示例:醉汉行走
10.1.7、声明和使用联合
10.1.8、本节回顾
10.2、宏
10.2.1、概述
10.2.2、定义宏
10.2.3、调用宏
10.2.4、其他宏特性
10.2.5、使用本书的宏库(仅32位模式)
10.2.6、示例程序:封装器
10.2.7、本节回顾
10.3、条件汇编伪指令
10.3.1、检查缺失的参数
10.3.2、默认参数初始值设定
10.3.3、布尔表达式
10.3.4、IF、else和 ENDIF伪指令
10.3.5、IFIDN和IFIDNI伪指令
10.3.6、示例:矩阵行求和
10.3.7、特殊运算符
10.3.8、宏函数
10.3.9、本节回顾
10.4、定义重复语句块
10.4.1、WHILE伪指令
10.4.2、REPEAT伪指令
10.4.3、FOR伪指令
10.4.4、FORC伪指令
10.4.5、示例:链表
10.4.6、本节回顾
10.5、本章小结
10.6、关键术语
10.6.1、术语
10.6.2、运算符和伪指令
10.7、复习题和练习
10.7.1、简答题
10.7.2、算法基础
10.8、编程练习
第十一章、MS-Windows编程
11.1、Win32控制台编程
11.1.1、背景知识
11.1.2、Win32控制台函数
11.1.3、显示消息框
11.1.4、控制台输入
11.1.5、控制台输出
11.1.6、读写文件
11.1.7、Irvine32链接库的文件I/O
11.1.8、测试文件I/O过程
11.1.9、控制台窗口操作
11.1.10、控制光标
11.1.11、控制文本颜色
11.1.12、时间与日期函数
11.1.13、使用64位WindowsAPI
11.1.14、本节回顾
11.2、编写图形化的Windows应用程序
11.2.1、必要的结构
11.2.2、MessageBox函数
11.2.3、WinMain过程
11.2.4、WinProc过程
11.2.5、ErrorHandler过程
11.2.6、程序清单
11.2.7、本节回顾
11.3、动态内存分配
11.3.1、HeapTest程序
11.3.2、本节回顾
11.4、x86存储管理
11.4.1、线性地址
11.4.2、页转换
11.4.3、本节回顾
11.5、本章小结
11.6、关键术语
11.7、复习题和练习
11.7.1、简答题
11.7.2、算法基础
11.8、编程练习
第十二章、浮点数处理与指令编码
12.1、浮点数二进制表示
12.1.1、IEEE二进制浮点数表示
12.1.2、阶码
12.1.3、规格化二进制浮点数
12.1.4、新建IEEE表示
12.1.5、十进制小数转换为二进制实数
12.1.6、本节回顾
12.2、浮点单元
12.2.1、FPU寄存器栈
12.2.2、舍入
12.2.3、浮点数异常
12.2.4、浮点数指令集
12.2.5、算术运算指令
12.2.6、比较浮点数值
12.2.7、读写浮点数值
12.2.8、异常同步
12.2.9、代码示例
12.2.10、混合模式运算
12.2.11、屏蔽与未屏蔽异常
12.2.12、本节回顾
12.3、x86指令编码
12.3.1、指令格式
12.3.2、单字节指令
12.3.3、立即数送寄存器
12.3.4、寄存器模式指令
12.3.5、处理器操作数大小前缀
12.3.6、内存模式指令
12.3.7、本节回顾
12.4、本章小结
12.5、关键术语
12.6、复习题和练习
12.6.1、简答题
12.6.2、算法基础
12.7、编程练习
第十三章、高级语言接口
13.1、引言
13.1.1、通用规范
13.1.2、.MODEL伪指令
13.1.3、检查编译器生成的代码
13.1.4、本节回顾
13.2、内嵌汇编代码
13.2.1、Visual++中的_asm伪指令
13.2.2、文件加密示例
13.2.3、本节回顾
13.3、32位汇编程序与C/C++的链接
13.3.1、IndexOf示例
13.3.2、调用C和C++函数
13.3.3、乘法表示例
13.3.4、调用C库函数
13.3.5、目录表程序
13.3.6、本节回顾
13.4、本章小结
13.5、关键术语
13.6、复习题
13.7、编程练习

现代x86汇编语言程序设计

算法相关

算法相关书籍

算法导论(厚,且公式多)

大话数据结构

算法精解C语言描述

数据库相关

数据库相关书籍

高性能的MySQL

译者序在互联网行业, MySQLLinux+ Apache MySQL+PHp)甚至已经成为专有名词, 也是很多中小网站建站的首选技术架构。我所在的公司淘宝网,在2003年非典肆虐期间创立时, 选择的就是LAMP架构, 当时 MySQLMySQL4.0(当时用的还是 MyISAM引擎)的很多点在高并发大压力下暴露了出来, 于是技术上开始改用商业的 Oracle Oracle加小型机和高端存储的数据库架构支撑了淘宝网业务的爆炸式增长, 数据库也从最初的两三个库增长到十几个库,并且每个库的硬件已经逐步升级到顶配,“天花板”很明显地摆在了眼前。 于是在2008年,基于PC服务器的 MySQLMySQL的稳定版本已经升级到5.0, 并且5.1也已经在开发中性能和特性相对于2003年的时候已经有了非常大的提升。 淘宝网的数据库架构也逐渐从垂直拆分走向水平拆分,在大规模水平集群的架构设计中, 开源的 MySQL受到的关注度越来越高,并且一年多来的实践也证明了 TMySQL InnoDB在高压力下的可用性。 于是从2009年开始,后来颇受外界关注的所谓“去IOE”开始实施,经过三年多的架构改造, 2012年整个淘宝网的核心交易系统已经全部运行在基于PC服务器的 MySQL数据库集群中, 全部实例数超过2000个。2012年的“双11大促中, MySQL单库经受了最高达6.5万的QPS, 某个拥有32个节点的核心集群的总QPS则稳定在86万以上,并且在整个大促(包括之前三年的“双11”大促)期间, 数据库未发生过任何影响大促的重大故障。当然,这个结果,也得益于淘宝网整个应用架构的设计, 以及这几年来革命性的闪存设备的迅猛发展。

2008年,淘宝DBA团队准备从 Oracle MySQL的时候,团队中的大多数人对 MySQL MySQL的讨论也不多见, 网上能找到的大多数中文资料基本上关注的还是如何安装如何配置主备复制等。

我们写这本书不仅仅是为了满足 MySQL MySQL数据库管理员的需要。 我们假定读者已经有了一定的 MySQL基础。我们还假定读者对于系统管理、 网络和类Unix的操作系统都有一些了解。

本书的第二版为读者提供了大量的信息,但没有一本书是可以涵盖一个主题的所有方面的。 在第二版和第三版之间的这段时间里,我们记录了数以千计有趣的问题,其中有些是我们解决的, 也有一些是我们观察到其他人解决的。当我们在规划第三版的时候发现,如果要把这些主题完全覆盖, 可能三千页到五千页的篇幅都还不够,这样本书的完成就遥遥无期了。 在反思这个问题后,我们意识到第二版强调的广泛的覆盖度事实上有其自身的限制, 从某种意义上来说也没有引导读者如何按照 MySQL的方式来思考问题。 所以第三版和第二版的关注点有很大的不同。我们虽然还是会包含很多的信息, 并且会强调同样的诸如可靠性和正确性的目标,但我们也会在本书中尝试更深入的讨论:我们会指出 MySQL MySQL做了什么。 我们会使用更多的演示和案例学习来将上述原则落地。通过这样的方式, 我们希望能够尝试回到下面这样的问题:“给出 MySQL的内部结构和操作,对于实际应用能带来什么帮助? 为什么能有这样的帮助?如何让 MySQL适合(或者不适合)特定的需求?” 最后,我们希望关于内部原理的知识能够帮助大家解决本书没有覆盖到的一些情况。 我们更希望读者能培养发现新问题的洞察力,能学习和实践合理的方式来设计、维护和诊断基于 MySQL的系统。

第1章、MySQL架构与历史

和其他数据库系统相比, MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥好的作用,但同时也会带来一点选择上的困难。 MySQL并不完美,却足够灵活,能够适应高要求的环境,例如Web类应用。同时, MySQL既可以嵌入到应用程序中,也可以支持数据仓库、内容索引和部署软件、高可用的冗余系统、在线事务处理系统(OLTP)等各种应用类型。

为了充分发挥 MySQLMySQL的灵活性体现在很多方面。例如,你可以通过配置使它在不同的硬件上都运行得很好,也可以支持多种不同的数据类型。但是,MySQL最重要、最与众不同的特性是它的存储引擎架构,这种架构的设计将查询处理(Query ProcessingServer Task)和数据的存储/提取相分离。这种处理和存储分离的设计可以在使用时根据性能、特性,以及其他需求来选择数据存储的方式。

本章概要地描述了 MySQL的服务器架构、各种存储引擎之间的主要区别,以及这些区别的重要性。另外也会回顾一下 MySQL的历史背景和基准测试,并试图通过简化细节和演示案例来讨论 MySQL的原理。这些讨论无论是对数据库一无所知的新手,还是熟知其他数据库的专家,都不无裨益。

1、MySQL逻辑架构

如果能在头脑中构建出一幅 MySQL各组件之间如何协同工作的架构图,就会有助于深入理解 MySQL MySQL的逻辑架构图。

最上层的服务并不是 MySQL所独有的,大多数基于网络的客户端/服务器的工具或者服务都有类似的架构。比如连接处理、授权认证、安全等等。

第二层架构是 MySQL MySQL的核心服务功能都在这一层,包括查询解析、分析、优化、缓存以及所有的内置函数(例如,日期、时间、数学和加密函数),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。

第三层包含了存储引擎。存储引擎负责 MySQLLinux下的各种文件系统一样,每个存储引擎都有它的优势和劣势。服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎API包含几十个底层函数,用于执行诸如“开始一个事务”或者“根据主键提取一行记录”等操作。但存储引擎不会去解析SQL生1,不同存储引擎之间也不会相互通信,而只是简单地响应上层服务器的请求。

1.1.1、连接管理与安全性

每个客户端连接都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行。服务器会负责缓存线程,因此不需要为每一个新建的连接创建或者销毁线程2

当客户端(应用)连接到 MySQL服务器时,服务器需要对其进行认证。认证基于用户名原始主机信息和密码。如果使用了安全套接字(SSL)的方式连接,还可以使用X.509证书认证。一旦客户端连接成功,服务器会继续验证该客户端是否具有执行某个特定查询的权限(例如,是否允许客户端对worl数据库的 Country表执行 SELECT语句)。

1.1.2、优化与执行

My SQL会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询、决定表的读取顺序,以及选择合适的索引等。用户可以通过特殊的关键字提示(hint)优化器,影响它的决策过程。也可以请求优化器解释(explain)优化过程的各个因素,使用户可以知道服务器是如何进行优化决策的,并提供一个参考基准,便于用户重构查询和 schema、修改相关配置,使应用尽可能高效运行。第6章我们将讨论更多优化器的细节。

优化器并不关心表使用的是什么存储引擎,但存储引擎对于优化查询是有影响的。优化器会请求存储引擎提供容量或某个具体操作的开销信息,以及表数据的统计信息等。例如,某些存储引擎的某种索引,可能对一些特定的查询有优化。关于索引与的优化,请参见第4章和第5章。

对于 SELECT语句,如果能够在其中找到对应的查询,服务器就不必再执行查询解析、优化和执行的整个过程,而是直接返回查询缓存中的结果集。第7章详细讨论了相关内容。

1.2、并发控制

无论何时,只要有多个查询需要在同一时刻修改数据,都会产生并发控制的问题。本章的目的是讨论 MySQL在两个层面的并发控制:服务器层与存储引擎层。并发控制是一个内容庞大的话题,有大量的理论文献对其进行过详细的论述。本章只简要地讨论 MySQL如何控制并发读写,因此读者需要有相关的知识来理解本章接下来的内容。

以Unix系统的 email box为例,典型的mbox文件格式是非常简单的。一个mbox邮箱中的所有邮件都串行在一起,彼此首尾相连。这种格式对于读取和分析邮件信息非常友好,同时投递邮件也很容易,只要在文件末尾附加新的邮件内容即可。

但如果两个进程在同一时刻对同一个邮箱投递邮件,会发生什么情况?显然,邮箱的数据会被破坏,两封邮件的内容会交叉地附加在邮箱文件的末尾。设计良好的邮箱投递系统会通过锁(lock)来防止数据损坏。如果客户试图投递邮件,而邮箱已经被其他客户锁住,那就必须等待,直到锁释放才能进行投递。这种锁的方案在实际应用环境中虽然工作良好,但并不支持并发处理。因为在任意一个时刻,只有一个进程可以修改邮箱的数据,这在大容量的邮箱系统中是个问题

1.2.1、读写锁

从邮箱中读取数据没有这样的麻烦,即使同一时刻多个用户并发读取也不会有什么问题。因为读取不会修改数据,所以不会出错。但如果某个客户正在读取邮箱,同时另外一个用户试图删除编号为25的邮件,会产生什么结果?结论是不确定,读的客户可能会报错退出,也可能读取到不一致的邮箱数据。所以,为安全起见,即使是读取邮箱也需要特别注意。

如果把上述的邮箱当成数据库中的一张表,把邮件当成表中的一行记录,就很容易看出,同样的问题依然存在。从很多方面来说,邮箱就是一张简单的数据库表。修改数据库表中的记录,和删除或者修改邮箱中的邮件信息,十分类似。

解决这类经典问题的方法就是并发控制,其实非常简单。在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁(shared lockexclusive lock),也叫读锁(read lock)和写锁(write lock).

这里先不讨论锁的具体实现,描述一下锁的概念如下:读锁是共享的,或者说是相互不阻塞的。多个客户在同一时刻可以同时读取同一个资源,而互不干扰。写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,这是出于安全策略的考虑,只有这样,才能确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。

在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时, MySQL MySQL锁的内部管理都是透明的。

1.2.2、锁粒度

一种提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。更理想的方式是只对会修改的数据片进行精确的锁定。任何时候,在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可。

问题是加锁也需要消耗资源。锁的各种操作,包括获得锁、检查锁是否已经解除、释放锁等,都会增加系统的开销。如果系统花费大量的时间来管理锁,而不是存取数据,那么系统的性能可能会因此受到影响。

所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然也会影响到性能。大多数商业数据库系统没有提供更多的选择,一般都是在表上施加行级锁(row level lock),并以各种复杂的方式来实现,以便在锁比较多的情况下尽可能地提供更好的性能。

而 MySQL则提供了多种选择。每种MySL存储引擎都可以实现自己的锁策略和锁粒度。在存储引擎的设计中,锁管理是个非常重要的决定。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时却会失去对另外一些应用场景的良好支持。好在 MysQL支持多个存储引擎的架构所以不需要单一的通用解决方案。下面将介绍两种最重要的锁策略。

表锁(table lock)

表锁是 MySQL中最基本的锁策略,并且是开销最小的策略。表锁非常类似于前文描述的邮箱加锁机制:它会锁定整张表。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。

在特定的场景中,表锁也可能有良好的性能。例如, READ LOCAL表锁支持某些类型的并发写操作。另外,写锁也比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面(写锁可以插入到锁队列中读锁的前面,反之读锁则不能插入到写锁的前面)。

尽管存储引擎可以管理自己的锁,MySQL本身还是会使用各种有效的表锁来实现不同的目的。例如,服务器会为诸如 ALTER TABLE之类的语句使用表锁,而忽略存储引擎的锁机制。

行级锁(row lock)

行级锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。众所周知,在 InnoDB XtraDB,以及其他一些存储引擎中实现了行级锁。行级锁只在存储引擎层实现,而i MySQL服务器层(如有必要,请回顾前文的逻辑架构图)没有实现。服务器层完全不了解存储引擎中的锁实现。在本章的后续内容以及全书中,所有的存储引擎都以自己的方式显现了锁机制。

1.3、事务

1.3事务 在理解事务的概念之前,接触数据库系统的其他高级特性还言之过早。事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。

本节的内容并非专属于 MySQL,如果读者已经熟悉了事务的ACID的概念,可以直接跳转到1.3.4节。

银行应用是解释事务必要性的一个经典例子假设一个银行的数据库有两张表:支票(checkingsavings)表。现在要从用户Jane的支票账户转移200美元到她的储蓄账户,那么需要至少三个步骤:

  1. 检查支票账户的余额高于200美元。
  2. 从支票账户余额中减去200美元。
  3. 在储蓄账户余额中增加200美元。

上述三个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。

可以用 START TRANSACTION COMMIT提交事务将修改的数据持久保留,要么使用 ROLLBACK撤销所有的修改。

原子性(atomicity)
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性
一致性(consistency)
数据库总是从一个一致性的状态转换到另外一个一致性的状态。在前面的例子中,一致性确保了,即使在执行第三、四条语句之间时系统崩溃,支票账户中也不会损失200美元,最终因为事务最终没有提交,所以事务中所做的修改也不会保存到数据库中。
隔离性(isolation)
通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有被减去200美元。后面我们讨论隔离级别(Isolation level)的时候,会发现为什么我们要说“通常来说”是不可见的。
持久性(durability)
一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证的策略(如果数据库本身就能做到真正的持久性,那么备份又怎么能增加持久性呢?)。在后面的一些章节中,我们会继续讨论 MySQL中持久性的真正含义。

事务的ACID特性可以确保银行不会弄丢你的钱。而在应用逻辑中,要实现这一点非常难,甚至可以说是不可能完成的任务。一个兼容ACID的数据库系统,需要做很多复杂但可能用户并没有觉察到的工作,才能确保ACID的实现。

就像锁粒度的升级会增加系统开销一样,这种事务处理过程中额外的安全性,也会需要数据库系统做更多的额外工作。一个实现了CID的数据库,相比没有实现ACID的数据库,通常会需要更强的CPU处理能力、更大的内存和更多的磁盘空间。正如本章不断重复的,这也正是 MySQL的存储引擎架构可以发挥优势的地方。用户可以根据业务是否需要事务处理,来选择合适的存储引擎对于一些不需要事务的查询类应用,选择一个非事务型的存储引擎,可以获得更高的性能。即使存储引擎不支持事务,也可以通过 LOCK TABLES语句为应用提供一定程度的保护,这些选择用户都可以自主决定。

1.3.1、隔离级别

隔离性其实比想象的要复杂。在SQL标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。

每种存储引擎实现的隔离级别不尽相同。如果熟悉其他的数据库产品,可能会发现某些特性和你期望的会有些不一样(但本节不打算讨论更详细的内容)读者可以0根据所选择的存储引擎,查阅相关的手册。

下面简单地介绍一下四种隔离级别。

READ UNCOMMITTED(未提交读)
在 READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read)这个级别会导致很多问题,从性能上来说, READ UNCOMMITTED不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。
READ COMMITTED(提交读)
大多数数据库系统的默认隔离级别都是 READ COMMITTED MySQL不是)read COMMITTED i满足前面提到的隔离性的简单定义:一个事务开始时,只能“看见已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatableread),因为两次执行同样的查询,可能会得到不一样的结果。
REPEATABLE READ(可重复读)
REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row InnoDB制(MVCC, Multiversion Concurrency Control)解决了幻读的问题。本章稍后会做和X进一步的讨论。aDB存储引擎通过多版本并发控可重复读是MySQL的默认事务隔离级别。
SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。简单来说, SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致8大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
1.3.2、死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。例如,设想下面两个事务同时处理 StockPrice表:

如果凑巧,两个事务都执行了第一条 UPDATE句,更新了一行数据,同时也锁定了该行数据,接着每个事务都尝试去执行第二条 UPDATE语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷入死循环。除非有外部因素介入才可能解除死锁。

为了解决这种问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,比如 n InnoDB存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方式,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。 InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。

锁的行为和顺序是和存储引擎相关的。以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。死锁的产生有双重原因:有些是因为真正的数据冲突,这种情况通常很难避免,但有些则完全是由于存储引擎的实现方式导致的。

1.3.3、事务日志

事务日志可以帮助提高事务的效率。

1.3.4、MySQL中的事务

MySQL提供了两种事务性的存储引擎。

1.4、多版本并发控制
1.5、MySQL的存储引擎
1.5.1、InnoDB存储引擎
1.5.2、MyISAM存储引擎
1.5.3、MySQL内建的其他存储引擎
1.5.4、第三方存储引擎
1.5.5、选择合适的引擎
1.5.6、转换表的引擎
1.6、MySQL时间线(Timeline)
1.7、MySQL的开发模式
1.8、总结
第2章、MySQL基准测试
2.1、为什么需要基准测试
2.2、基准测试的策略

2.2.1、测试何种指标

2.3、基准测试方法
2.3.1、设计和规划基准测试
2.3.2、基准测试应该运行多长时间
2.3.3、获取系统性能和状态
2.3.4、获得准确的测试结果
2.3.5、运行基准测试并分析结果
2.3.6、绘图的重要性
2.4、基准测试工具
2.4.1、集成式测试工具
2.4.2、单组件式测试工具
2.5、基准测试案例
2.5.1 http_load
2.5.2 MySQL基准测试套件
2.5.3 sysbench
2.5.4数据库测试套件中的dbt2TPC-C测试
2.5.5 Percona TPCC-MySQL测试工具
2.6总结
第3章、服务器性能剖析
3.1、性能优化简介
3.1.1、通过性能剖析进行优化
3.1.2、理解性能剖析
3.2、对应用程序进行性能剖析
3.2.1、测量PHP应用程序
3.3、剖析MySQL查询
3.3.1、剖析服务器负载
3.3.2、剖析单条查询
3.3.3、使用性能剖析
3.4、诊断间歇性问题
3.4.1、单条查询问题还是服务器问题
3.4.2、捕获诊断数据
3.4.3、一个诊断案例
3.5、其他剖析工具
3.5.1、使用 USERSTATISTICS表
3.5.2、使用 strace
3.6、总结
第4章、Schema与数据类型优化
4.1、选择优化的数据类型
4.1.1、整数类型
4.1.2、实数类型
4.1.3、字符串类型
4.1.4、日期和时间类型
4.1.5、位数据类型
4.1.6、选择标识符(identifier)
4.1.7、特殊类型数据
4.2、MySQL schema设计中的陷阱
4.3、范式和反范式
4.3.1、范式的优点和缺点
4.3.2、反范式的优点和缺点
4.3.3、混用范式化和反范式化
4.4、缓存表和汇总表
4.4.1、物化视图
4.4.2、计数器
4.5、加快 AlTER TABLE操作的速度
4.5.1、只修改frm文件
4.5.2、快速创建 MyISAM索引
4.6、总结
第5章、创建高性能的索引
5.1、索引基础
5.1.1、索引的类型
5.2、索引的优点
5.3、高性能的索引策略
5.3.1、独立的列
5.3.2、前缀索引和索引选择
5.3.3、多列索引
5.3.4、选择合适的索引列顺序
5.3.5、聚簇索引
5.3.6、覆盖索引
5.3.7、使用索引扫描来做排序
5.3.8、压缩(前缀压缩)索引
5.3.9、冗余和重复索引
5.3.10、未使用的索引
5.3.11、索引和锁
5.4、索引案例学习
5.4.1、支持多种过滤条件
5.4.2、避免多个范围条件
5.4.3、优化排序
5.5、维护索引和表
5.5.1、找到并修复损坏的表
5.5.2、更新索引统计信息
5.5.3、减少索引和数据的碎片
5.6、总结
第6章、查询性能优化
6.1、为什么查询速度会慢
6.2.1、是否向数据库请求了不需要的数据
6.2.2、MySQL是否在扫描额外的记录
6.3、重构查询的方式
6.3.1、一个复杂查询还是多个简单查询
6.3.2、切分查询
6.3.3、分解关联查询
6.4、查询执行的基础
6.4.1、MySQL客户端/服务器通信协议
6.4.2、查询缓存
6.4.3、查询优化处理
6.4.4、查询执行引擎
6.4.5、返回结果给客端
6.5、MySQL查询优化器的局限性
6.5.1、关联子查
6.5.2、UNION的限制
6.5.3、索引合并优化
6.5.4、等值传递
6.5.5、并行执行
6.5.6、哈希关联
6.5.7、松散索引扫描
6.5.8、最大值和最小值优化
6.5.9、在同一个表上查询和更新
6.6、查询优化器的提示(hint)
6.7、优化特定类型的查询
6.7.1、优化 t COUNTO查询
6.7.2、优化关联查询
6.7.3、优化子查询
6.7.4、优化 GROUP BY DISTINCT
6.7.5、优化 LIMIT分页
6.7.6、优化 SQLCALC FOUND ROWS
6.7.7、优化 UNION查询
6.7.8、静态查询分析
6.7.9、使用用户自定义变量
6.8、案例学习
6.8.1、使用 MySQL构建一个队列
6.8.2、计算两点之间的距离
6.8.3、使用用户自定义函数
6.9、总结
第7章、MySQL高级特性
7.1、分区
7.1.1、分区表的原理
7.1.2、分区表的类型
7.1.3、如何使用分区表
7.1.4、什么情况下会出问题
7.1.5、查询优化
7.1.6、合并表
7.2、视
7.2.1、可更新视图
7.2.2、视图对性能的影响
7.2.3、视图的限制
7.3、外键约
7.4、在 MySQL内部存储代码
7.4.1、存储过程和函数
7.4.2、触发器
7.4.3、事件
7.4.4、在存储程序中保留注释
7.5、游标
7.6、绑定变量
7.6.1、绑定变量的优化
7.6.2、SQL接口的绑定变量
7.6.3、绑定变量的限制
7.7、用户自定义函数
7.8、插件
7.9、字符集和校对
7.9.1、MySQL如何使用字符集
7.9.2、选择字符集和校对规则
7.9.3、字符集和校对规则如何影响查询
7.10、全文索
7.10.1、自然语言的全文索引
7.10.2、布尔全文索引
7.10.3、5.1中全文索引的变化
7.10.4、全文索引的限制和替代方案
7.10.5、全文索引的配置和优
7.11、分布式(XA)事
7.11.1、内部XA事务
7.11.2、外部XA事务
7.12、查询缓存
7.12.1、 MySQL如何判断缓存命中
7.12.2、查询缓存如何使用内存
7.12.3、什么情况下查询缓存能发挥作用
7.12.4、如何配置和维护查询缓存
7.12.5、InnoDB和查询缓存
7.12.6、通用查询缓存优化
7.12.7、查询缓存的替代方案
7.13、总结
第8章、优化服务器设置
8.1、MySQL配置的工作原理

8.1.1、语法、作用域和动态性 8.1.2、设置变量的副作用 8.1.3、入门 8.1.4、通过基准测试迭代优化 8.2、什么不该做 ------------------------------------------------------------------ 8.3、创建 MySQL配置文件 ------------------------------------------------------------------ 8.3.1、检查 MySQL服务器状态变量 8.4、配置内存使用 ------------------------------------------------------------------ 8.4.1、MySQL可以使用多少内存 8.4.2、每个连接需要的内存 8.4.3、为操作系统保留内存 8.4.4、为缓存分配内存 8.4.5、InnoDBBuffer Pool) 8.4.6、MyISAMKey Caches 8.4.7、线程缓存… 8.4.8、表缓存(Table Cache) 8.4.9、InnoDBData Dictionary 8.5、配置 MySQL的I/O行为 ------------------------------------------------------------------ 8.5.1、InnoDB/o配置 8.5.2、MyISAM的I/O配置 8.6、配置 MySQL并发 ------------------------------------------------------------------ 8.6.1、InnoDB并发配置 8.6.2、MyISAM并发配置 8.7、基于工作负载的配置 ------------------------------------------------------------------ 8.7.1、优化BLOB和TEXT的场景 8.7.2、优化排序(Filesorts) 8.8、完成基本配置 ------------------------------------------------------------------ 8.9、安全和稳定的设置 ------------------------------------------------------------------ 8.10、高级 InnoDB设置 ------------------------------------------------------------------ 8.11、总结 ------------------------------------------------------------------

第9章、操作系统和硬件优化
9.1、什么限制了 MySQL的性能
9.2、如何为 MySQL选择CPU
9.2.1、哪个更好:更快的CPU还是更多的CPU
9.2.2、CPU架构
9.2.3、扩展到多个CPU和核心
9.3、平衡内存和磁盘资源
9.3.1、随机1/O和顺序I/O
9.3.2、缓存,读和写
9.3.3、工作集是什么
9.3.4、找到有效的内存/磁盘比例
9.3.5、选择硬盘
9.4、固态存储
9.4.1、闪存概述
9.4.2、闪存技术
9.4.3、闪存的基准测试
9.4.4、固态硬盘驱动器(SSD)
9.4.5、PCle存储设备
9.4.6、其他类型的固态存储
9.4.7、什么时候应该使用存
9.4.8、使用 Flashcache
9.4.9、优化固态存储上的 MySQL
9.5、为备库选择硬件
9.6、RAiD性能优化
9.6.1、RAiD的故障转移、恢复和镜像
9.6.2、平衡硬件RAID和软件RAID
9.6.3、RAiD配置和缓存
9.7、san和NA
9.7.1、SAN基准测试
9.7.2、使用基于NFS或SMB的SAN
9.7.3、MySQL在SAN上的性能
9.7.4、应该用SAN吗
9.8、使用多磁盘卷
9.9、网络配置
9.10、选择操作系统
9.11、选择文件系统
9.12、选择磁盘队列调度策略
9.13、线程
9.14、内存交换区
9.15、操作系统状态
9.15.1、如何阅读 vmstat的输出
9.15.2、如何阅读 iostat的输出
9.15.3、其他有用的工具
9.15.5、I/O密集型的机器
9.15.6、发生内存交换的机器
9.15.7、空闲的机器
9.16、总结
第10章、复制
10.1、复制概述

10.1.1、复制解决的问题 10.1.2、复制如何工作 10.2、配置复制 ------------------------------------------------------------------ 10.2.1、创建复制账号 10.2.2、配置主库和备库 10.2.3、启动复制 10.2.4、从另一个服务器开始复制 10.2.5、推荐的复制配置 10.3、复制的原理 ------------------------------------------------------------------ 10.3.1、基于语句的复制 10.3.2、基于行的复制 10.3.3、基于行或基于语句:哪种更优 10.3.4、复制文件 10.3.5、发送复制事件到其他备库 10.3.6、复制过滤器 10.4、复制拓扑 ------------------------------------------------------------------ 10.4.1、一主库多备库 10.4.2、主动-主动模式下的主-主复制 10.4.3、主动-被动模式下的主-主复制 10.4.4、拥有备库的主-主结构 10.4.5、环形复制 10.4.6、主库、分发主库以及备库 10.4.7、树或金字塔形 10.4.8、定制的复制方案 10.5、复制和容量规划 ------------------------------------------------------------------ 10.5.1、为什么复制无法扩展写操作 10.5.2、备库什么时候开始延迟 10.5.3、规划余容量 10.6、复制管理和维护 ------------------------------------------------------------------ 10.6.1、监控复制 10.6.2、测量备库延迟 10.6.3、确定主备是否一致 10.6.4、从主库重新同步备 10.6.5、改变主库 10.6.6、在一个主-主配置中交换角色 10.7、复制的问题和解决方案 ------------------------------------------------------------------ 10.7.1、数据损坏或丢失的错误 10.7.2、使用非事务型 10.7.3、混合事务型和非事务型表 10.7.4、不确定语句 10.7.5、主库和备库使用不同的存储引擎 10.7.6、备库发生数据 10.7.7、不唯一的服务器ID 10.7.8、未定义的服务器ID 10.7.9、对未复制数据的依赖性 10.7.10、丢失的临时表 10.7.11、不复制所有的更新 10.7.12、InnoDB加锁读引起的锁争 10.7.13、在主-主复制结构中写入两台主库 10.7.14、过大的复制延迟 10.7.15、来自主库的过大的包 10.7.16、受限制的复制带宽 10.7.17、磁盘空间不足 10.7.18、复制的局限性 10.8、复制有多快 ------------------------------------------------------------------ 10.9、MySQL复制的高级特性 ------------------------------------------------------------------ 10.10、其他复制技术 ------------------------------------------------------------------ 10.11、总结 ------------------------------------------------------------------

第11章、可扩展的 MySQL
11.1、什么是可扩展性…

11.1、正式的可扩展性定义 11.2、扩展E MySQL ------------------------------------------------------------------ 11.2.1、规划可扩展性 11.2.2、为扩展赢得时间 11.2.3、上扩展 11.2.4、向外扩展… 11.2.5、通过多实例扩展 11.2.6、通过集群扩展 11.2.7、内扩展… 11.3、负载均衡 ------------------------------------------------------------------ 11.3.1、直接连接 11.3.2、引入中间件 11.3.3、一主多备间的负载均衡 11.4、总结 ------------------------------------------------------------------

第12章、高可用性
12.1、什么是高可用性
12.2、导致宕机的原因
12.3、如何实现高可用性

12.3.1、提升平均失效时间(MTBF) 12.3.2、降低平均恢复时间(MTTR)

12.4、避免单点失效

12.4.1、共享存储或磁盘复制 12.4.2、MySQL同步复制 12.4.3、基于复制的冗余 12.5、故障转移和故障恢复 ------------------------------------------------------------------ 12.5.1、提升备库或切换角色 12.5.2、虚拟IP地址或IP接管 12.5.3、中间件解决方案 12.5.4、在应用中处理故障转移 12.6、总结 ------------------------------------------------------------------

第13章、云端的 MySQL
13.1、云的优点、缺点和相关误解
13.2、MySQL在云端的经济价值
13.3、云中的 MySQL的可扩展性和高可用…
13.4、四种基础资源
13.5、MySQL在云主机上的性能

13.5.1、在云端的 MySQL基准测试 13.6、MySQLDBaas) ------------------------------------------------------------------ 13.6.1、Amazon RDS.. 13.6.2、其他 DBaaS解决方案

13.7、总结
第14章、应用层优化
14.1、常见问题
14.2、Web服务器问题

14.2.1、寻找最优并发度 14.3、缓存 ------------------------------------------------------------------ 14.3.1、应用层以下的缓存 14.3.2、应用层缓存 14.3.3、缓存控制策略 14.3.4、缓存对象分层 14.3.5、预生成内容 14.3.6、作为基础组件的缓存 14.3.7、使用 HandlerSocket memcached 14.4、拓展 MySQL ------------------------------------------------------------------ 14.5、MySQL的替代品 ------------------------------------------------------------------ 14.6、总结 ------------------------------------------------------------------

第15章、备份与恢复
15.1、为什么要备份…
15.2、定义恢复需求
15.3、设计 mySQL备份方案

15,3.1、在线备份还是离线备份 15.3.2、逻辑备份还是物理备份 15.3.3、备份什么 15.3.4、存引擎和一致性 15.4、管理和备份二进制日志 ------------------------------------------------------------------ 15.4.1、二进制日志格式 … 15.4.2、安全地清除老的二进制日志… 15.5、备份数据………… ------------------------------------------------------------------ 15.5.1、生成逻辑备份 15.5.2、文件系统快照 15.6、从备份中恢复…… ------------------------------------------------------------------ 15.6.1、恢复物理备份 15.6.2、还原逻辑备份 15.6.3、基于时间点的恢复…… 15.6.4、更高级的恢复技术 15.6.5、InnoDB崩溃恢复 15.7、备份和恢复工 ------------------------------------------------------------------ 15.7.1、MySQL Enterprise Backup. 15.7.2、Percona Xtra Backup. 15.7.3、mylvmbackup 15.7.4、 Zmanda Recovery Manager. 15.7.5、mydumper 15.7.6、 mysqldump. 15.8、备份脚本化 ------------------------------------------------------------------ 15.9、总结 ------------------------------------------------------------------

第16章、 MySQLF用户工具
16.1、接口工具
16.2、命令行工具集
16.3、SQL实用集
16.4、监测工

16.4.1、开源的监控工具 6.4.2、商业监控系统 16.4.3、Innotop的命令行监控 16.5、总结 ------------------------------------------------------------------

MongoDB权威指南(第二版)

start_time:2018-09-19

书籍每章节编写时间
章节标题名 时间
第一章、MongoDB简介 2018-09-19
第二章、MongoDB基础知识 2018-09-20
第三章、创建、更新和删除文档 2018-09-20
第四章、查询 2018-09-20
第五章、索引 2018-09-20
第六章、特殊的索引和集合 2018-09-22
第七章、聚合 2018-09-29
第八章、应用程序设计  
第九章、创建副本集  
第十章、副本集的组成  
第十一章、从应用程序连接副本集  
第十二章、管理  
第十三章、分片  
第十四章、配置分片  
第十五章、选择片键  
第十六章、分片管理  
第十七章、了解应用的动态  
第十八章、数据管理  
第十九章、持久性  
第二十章、启动和停止MongoDB  
第二十一章、监控MongoDB  
第二十二章、备份  
第二十三章、部署MongoDB  
第一章 MongoDB简介

注解

MongoDB是一款强大、灵活,且易扩展的通用型数据库。它能扩展出非常多的功能,如二级索引、范围查询、排序、聚合、以及地理空间索引、本书涵盖了MongoDB的主要涉及特点。

1.1 易于使用

MongoDB是一个面向文档的数据库,而不是关系型数据库。不采用关系模型主要是为了获得更好的扩展性。

与关系型数据库相比,面向文档的数据库不再有"行"的概念。取而代之的是更为灵活的“文档”模型。通过在文档中嵌入文档和数组,面向文档的方法能够仅使用一条记录来表现复杂的层次关系,这与使用现代面向对象语言的开发者对数据的看法一致。

另外,不再有预定义模式:文档的键值不在是固定的类型和大小。由于没有固定的模式,根据需要添加或删除字段变得更容易了。

1.2 易于扩展

MongoDB的设计采用横向扩展。面向文档的数据模型使它能很容易地在多台服务器之间进行数据分割。MongoDB能自动处理跨集群的数据和负载,自动重新分配文档,以及将用户请求路由到正确的机器上。这样,开发者能够集中精力编写应用程序,而不需要考虑如何扩展的问题。如果一个集群需要大量的容量,只需要向群集添加新服务器,MongoDB会自动将现有数据向新服务器传送。

1.3 丰富的功能

MongoDB作为一款通用的数据库,除了能够创建、读取、更新和删除数据之外,还提供一系列不断扩展的独特功能。

  • 索引:

    MongoDB支持通用二级索引,允许多种快速查询,且提供唯一索引、复合索引、地理空间索引、以及全文索引。
    
  • 聚合:

    MongoDB支持“聚合管道”,用户能够通过简单的片段创建和杂的聚合,并通过数据库自动优化。
    
  • 特殊的集合类型:

    MongoDB支持存在时间有限的集合。适用于那些将在某个时刻过期的数据,如会话。类似的,MongoDB也支持固定大小的集合,用于保存近期数据,如日志。
    
  • 文件存储:

    MongoDB支持一中非常易用的协议,用于存储大文件和文件元数据。
    

MongoDB并不具备一些在关系型数据库中很普遍的功能,如连接和复杂的多行事物。省略这些功能是出于架构上的考虑(为了得到更好的扩展性),因为在分部署系统中这两个功能难以高效的实现。

1.4 卓越的性能

MongoDB的一个主要目标是提供卓越的性能。这很大程度上觉得了MongoDB的设计。MongoDB能对文档进行自动填充。也能预分配数据文件以利用额外的空间来换取稳定的性能。MongoDB把尽可能多的内存作为缓存,试图为每次查询自动选择正确的索引。

MongoDB非常强大并试图保留关系型数据库的很多特性,但它并不追求具备关系型数据库的所有功能。只要有可能,数据库服务器就会将处理器和逻辑交给客户端,这种精简方式的设计是MongoDB能够实现如果高效性能的原因之一。

1.5 小结

掌握MongoDB最好的方式是创建一个易扩展、灵活、快速的功能完备的数据存储。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第二章 MongoDB基础知识

MongoDB非常强大但很容易上手。

  • 文档是MongoDB中数据的基本单元。非常类似于关系型数据库管理系统中的行,但更具表现力。
  • 类似的,集合可以看作是一个拥有动态模型的表。
  • MongoDB的一个实例可以拥有多个相互独立的数据库,每一个数据库都拥有自己的集合。
  • MongoDB到了一个简单单功能强大的JavaScript shell 可用于管理 MongoDB 的实例或数据操作。
2.1 文档

文档是MongoDB的核心概念。文档就是键值对的一个有序集。

文档的键是字符串,除了少数例外情况,键可以使用任意utf-8字符

  • 键不能包含“\0”空字符。这个字符用于表示键的结尾。(一个斜杠+0)
  • .和$具有特殊意义,只能在特定环境中使用。通常,这两个字符是被保留的,如果使用不当,驱动程序会有提示。

MongoDB不但区分类型,还区分大小写,例如,下面两个文档是不同的:

{'foo':3}
{'foo':"3"}

下面两个文档也是不同的:

{'foo':3}
{'Foo':3}

还有一个非常重要的事项需要注意,MongoDB的文档不能有重复的键。例如,下面的文档是非法的:

{'greeting':'hello world','greeting':'hello MongoDB'}

文档中的键值对是有序的: {'x':1,'y':2}{'y':2,'x':1} 是不同的,通常字段顺序并不重要,无须让数据库默认依赖特定的字段顺序。在某些情况下,字段顺序变得非常重要。

2.2 集合

集合就是一组文档。如果将MongoDB中的一个文档比喻成关系型数据库中的一行,那么集合就相当于一张表。

2.2.1 动态模型

集合是动态模式的,这意味着一个集合里面的文档可以是各式各样的。例如下面两个文档可以存储在一个集合里面:

{'greeting':'hello world'}
{'foo':5}

需要注意的是,上面的文档不光值类型不同(一个是字符串,一个是整数),他们的键也不同。因为集合里面可以放置任何文档,随之而来的一个问题是:还有必要使用多个集合吗? 这的确值得思考:既然没有必要区分不同类型的文档的模式,为什么还需要多个集合呢?

  • 如果把各种各样的文档不加区分第放在同一个集合中,无论对开发者还是对管理者来说都是噩梦。开发者要么确保每次查询只返回特性类型的文档,要么让执行查询的应用程序来处理所有不同类型的文档。
  • 在一个集合里查询特定类型的文档在速度上也很不划算,分开查询多个集合要快得多。
  • 把同种类型的文档放在一个集合中,数据会更加集中。
  • 创建索引时,需要使用文档的附加结构。索引是按照集合来定义的。
2.2.2 命名

集合使用名称进行命名,集合名可以满足下列条件任何utf-8字符串

  • 集合名不能是空字符串""
  • 集合不能包含"\0"字符(空字符),这个字符表示集合名的结束。
  • 集合名不能以"system."开头,这是为系统保留的前缀。
  • 用户创建的集合不能再集合中包含保留关键字符"$",因为某些系统中生成的集合中包含"$",很多驱动程序确实支持在集合中包含该字符。除非你要访问这种系统创建的集合,否则不应该在该集合中包含$。

子集合

组织集合的惯例是使用"."分隔不同命名空间的子集合。例如一个具有博客功能的应用可能包含两个集合。分别是 blog.postsblog.authors 。这是为了使组织结构更清晰。

在MongoDB中,使用子集合来组织数据非常高效,值得推荐。

2.3 数据库

在MongoDB中,多个文档组成集合,多个集合组成数据库,一个MongoDB实例可以承载多个数据库,每个数据库用友0个或多个集合。每个数据库都有独立的权限,即使是在磁盘上,不同的数据库也放置再通的文件中。

数据库通过名称标识,这点与集合类似。数据库名可以是满足一下条件的任意utf-8字符串:

- 不能是空字符串
- 不得包含有/、\\、.、"、<、>、.....   基本上只能使用ASCII中的字母和数字。
- 数据库区分大小写,即便是在不区分大小写的文件系统中也是如此。简单起见,数据库名应该全部小写。
- 数据库名最多为64字节。

要记住一点。数据库最终会变成文件系统里的文件,而数据库名就是相对应的文件名。

另外,有一些数据库名是保留的。可以直接访问这些有特殊语义的数据库。

  • admin
  • local
  • config

把数据库名添加到集合前面,得到结合的完全限定名,即命名空间。例如使用cms数据库中的blog.posts集合。这个集合的命名空间就是cms.blog.posts。命名空间的长度不得超过121字节。且在实际红应小于100字节。

2.4 启动MongoDB

通常,MongoDB作为网络服务器来运行,客户端可以连接该服务器进行操作。下载MongoDB并解压,运行mongod命令来启动数据库服务器:

$ mongod

mongod在没有参数的情况下会使用默认数据目录/data/db.如果数据目录不存在或者不可读写,服务器会启动失败。

启动时,服务器会打印版本和系统信息,然后等待连接.默认情况下,MongoDB会监听27017端口。

mongod还会启动一个非常基本的http服务器,监听数字比主端口高1000的端口,也就是28017端口,可以访问127.0.0.1:28017端口获取数据库管理信息。

2.5 MongoDB shell简介

MongoDB 自带JavaScript shell 可以在shell 中使用命令与MongoDB实例交互。 shell非常有用,通过他可以执行管理操作。检查运行实例,亦或做其他操作。

2.5.1 运行shell

运行mongo启动shell:

$ mongo

启动时,shell 将自动连接MongoDB服务器,须确保mongod已启动。

shell 是一个功能完备的JavaScript解释器,可运行任意JavaScript程序。

2.5.2 MongoDB客户端

如果想查看db当前指向那个库可以使用db命令:

db

选择数据库:

>use foobar
>db
foobar
2.5.3基本操作

1.创建

insert函数可将一个文档添加到集合中。

post = {'title':'blog title'}
db.blog.insert(post)

查看:

db.blog.find()

2.读取

读取一个记录:

db.blog.findOne()

3.更新

使用update修改博客文章,update至少接受两个参数,第一个是限定条件,第二个是新的文档。

首先修改变量post,增加comment键:

post.comment = []

然后执行update操作:

db.blog.update({'title':'update title'},post)

db.blog.find()

4.删除

使用remove方法可将数据中永久删除,如果没有任何参数,它会将集合内所有文档删除。它可以接受一个作为限定条件的文旦作为参数

db.blog.remove({'title':'update title'})
2.6 数据类型
2.6.1 基本数据类型

在概念上, MongoDB的文档与JavaScript中的对象相似,因而可认为它类似于json。

MongoDB在保留json基本键值特性的基础上,添加了一些其他数据类型:
  • null 用于表示空值或不存在的字段。 {'x':null}
  • 布尔值, true 、false {'x':true}
  • 数值 {'x':3.14}、{'x':NumberInt("3")}、{'x':NumberLong('3')}
  • 字符串 {'x':'foobar'}
  • 日期 {'x':new Date()}
  • 正则表达式 {'x':/foobar/i}
  • 数组 {'x':['a','b',3]}
  • 内嵌文档 {'x':{1,2,3}}
  • 对象id {'x':ObjectId()}
  • 二进制数据 !二进制不能直接在shell中使用
  • 代码 查询和文档中可以包含任意JavaScript代码
2.6.2 日期

创建日期对象时,应使用new Date(),而非Date()。

2.6.3 数组

数组是一组值,它既能做有序对象,也能做无序对象:

{'titings':['pie',123]}
2.6.4 内嵌文档
{'name':'job','address':{'city':'nanning','state':1}}
2.6.5 _id和ObjectId()

MongoDB文档必须有一个_id键,这个键可以是任意类型,默认是ObjectId对象,在一个集合里面,每个文档都有唯一的"_id",确保集合里面每个文档都能唯一标识。

id生成方式:
0 1 2 3     4 5 6     7 8    9 10 11
时间戳       机器       PID    计数器

如果插入文档没有id键,系统会自动创建一个。

2.7 使用MongoDB shell

shell 可以连接到任意MongoDB实例。在启动shell时指定机器名和端口,就可以连接到一套不同的机器:

$ mongo some-host:30000/myDB

通过--nodb参数启动shell,启动时将不会连接任何数据库:

$  mongo --nodb

启动之后,在需要时运行new Mongo(hostname)命令就可以连接到想要的mongo了:

conn = new Mongo('some-host:30000')
>connection to some-host:30000
db = conn.getDB('myDB')
>dbDB
2.7.1 shell 小贴士

可以使用help查看帮助:

>help()
2.7.2 使用shell执行脚本
$ mongo script.js

如果希望使用指定的主机/端口上的mongo运行脚本,需要先指定地址,再跟上脚本文件:

$ mongo --quiet server-1:30000/foo script.js
2.7.3 创建.mongorc.js文件

如果某些脚本会被频繁加载,可以将它们添加到mongorc.js文件中,这个文件会在启动shell时自动运行。

例如,我们希望启动成功时让shell显示一句欢迎语,为此,我们在用户主目录下创建一个名为.mongorc.js的文件:

//mongorc.js

var comliment = ['attractive','hello','world']
var index = Math.floor(Math.random()*3)
print('hello world')

为了实用,可以使用这个脚本创建一些自己需要的全局变量,或者是为一个太长的名称创建一个简短的别名,也可以重写内置函数。.mongorc.js最常见的用途之一是移除那些危险的shell辅助函数。可以在这里集中重写这些方法:

var no = function(){
    print('not on my watch');
}
//禁止删除数据库
db.dropDatabase = DB.prototype.dropDatabase = no;
//禁止删除集合
DBCollection.prototype.drop = no;
//禁止删除索引
DBCollection.prototype.dropIndex = no;

如果在启动shell时指定--norc参数,就可以禁止加载.mongorc.js。

2.7.4 定制shell 提示

2.7.5 编辑复合变量

2.7.6 集合命名注意事项

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第三章 创建、更新和删除文档
本章介绍数据库移入/移出数据的基本操作,具体包含如下操作:
  • 向集合添加新文档
  • 从集合里删除文档
  • 更新现有文档
  • 为这些操作选择合适的安全级别和速度
3.1 插入并保存文档
db.foo.insert({'bar':'baz'})

这些操作会给文档自动增加一个"_id"键,然后保存到MongoDB中。

3.1.1 批量插入

只接受一个文档数组作为参数:

db.foo.batchinsert([{'_id':0},{'_id':1}])

目前最大的消息长度为48MB,大于的会拆分成多个插入请求。

3.1.2 插入校验

插入数据时,MongoDB只对数据进行最基本的检查:检查文档的基本结构,如果没有"_id"字段就会增加一个。由于MongoDB只进行最基本的检查,所以插入非法数据很容易。

3.2 删除文档
db.foo.remove()
>db.mailing.list.remove({'option':true})

删除速度

删除文档通常很快,但是要清空整个集合,使用drop直接删除集合更快。

3.3 更新文档

文档存入数据库以后,就可以使用update方法来更新它了。update有两个参数,一个是查询文档,另一个是修改文档

3.3.1 文档替换

最简单的更新就是用一个新文档完全替换匹配的文档,这适用进行大规模模式的迁移情况。

{
    '_id':ObjectId('xxxxx'),
    'name':'job',
    'friends':32,
    'enemies':2
}

我们希望将friends 和 enemies 字段移到 relationships子文档中:

var joe = db.users.findOne({'name':'job'});
joe.relationships = {'friends':joe.friends,'enemies':joe.enemies};
>joe.username = joe.name
delete joe.friends;
delete joe.enemies
delete joe.name
db.users.update({'name':'joe'},joe);

一个常见的错误是查询条件匹配到了多个文档,然后更新时由于第二个参数的存在就产生重复的ID。数据库会抛出错误,任何文档都不会更新。

使用 _id 作为查询条件比使用随机字段速度更快。

3.3.2 使用修改器

通常文档只会有一部分要更新,可以使用原子性的更新修改器,指定对文档中的某些字段进行更新,更新修改器是特殊的键,用来指定复杂的更新操作,比如修改,增加或者删除键。还可以操作数组或者内置文档。

#自增1
db.site.update({'url':'localhost'},{"$inc":{'views':1}})

db.users.update({'name','joe'},{'$set':{'title':'set title'}})
"$push"会向已有的数组末尾插入一个元素。
"$each"子操作符,可以通过一次"$each"操作添加多个值
db.stock.ticker.update({'_id':xxxx},{
    {'$push':{'hourly':{"$each":[1,2,3,4,5]}}}
})

如果希望数组最大长度是固定的,那么可以将"$slice"和"$push"组合在一起使用,这样就可以确保数组不会超出设定好的长度:

db.movies.find({'grnre':'hroor'},
{
"$push":{'top10':{
'$each':['nightmare','saw'],"$slice":-10
}}
})

这个例子会限制数组只包含最后加入的10个元素,"$slice"必须是负整数。

还有 "$sort","$ne","$addToSet"详细需要用了在查询。

对应删除元素:

"$pop","$pull"

定位符操作: "$"

移动文档是非常慢的

3.3.3 upsert

upsert是一种特殊的更新。要是没有找到符合更新条件的文档,他会以这个条件和更新文档为基础创建一个新的文档。如果找到文档则更新。supert非常方便,不必预置集合。

3.3.4 更新多个文档

默认情况下,更新只对符合条件的第一个文档执行操作,要是有多个文档符合条件,只有第一个文档被更新,需要更新所有文档可以将update的第四个参数设置为true。

3.3.5 返回被更新的文档

3.4 写入安全机制

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第四章 查询

本章将详细介绍查询。主要会涵盖一下几个方面:

  • 使用find或findOne函数和查询文档对数据库执行查询。
  • 使用$条件查询返回查询、数据集包含查询、不等式查询、以及其他查询;
  • 查询将会返回一个数据库游标,游标只会在你需要时才将需要的文档批量返回;
  • 还有很多针对游标执行的元操作,包括忽略一定数量的结果。或者限定返回结果的数量,以及结果排序。
4.1 find简介

MongoDB中使用find来进行查询。查询就是返回集合中文档的子集。子集的返回从0个文档到整个集合。

空的查询文档会匹配集合的全部内容:

db.users.find()

以上会返回users的所有结果。

查询年龄为27的用户:

db.users.find({'age':27})

查询用户名为joe,年龄27的用户:

db.users.find({'username':'joe',{'age':27}})
4.1.1 执行需要返回的键

有时并不需要将文档的所有键/值都返回,这种情况,可以通过find的第二个参数来指定想要的键:

db.users.find({},{'username':1,'email':1})

!这个参数后面的1暂未指定意思,等待实际操作。

如果要剔除不需要的键:

db.users.find({},{'age':0})
4.1.2 限制

查询使用上有些限制。传递给数据库的查询文档的值必须是常量。也就是不能引用文档中其他键的值。

4.2 查询条件
4.2.1 查询条件

"$lt"、"$lte"、"$gt"、"$gte"分别对应<、<=、>、>=

例如查询年龄大于18 小于30的结果:

db.users.find({'age':{'$gte':18,"$lte":30}})

查询时间大于2017年的:

start_time = new Date('01/01/2017')
db.users.find({'register_time':{'$lt':start_time}})

"$ne"符号为不相等,查询年龄不为18的用户:

db.users.find({'age':{"$ne":18}})
4.2.2 or查询 in查询
//查询年龄为18,19的用户
db.users.find({'age':{'$in':[18,19]}})
//查询年龄不为18,19的用户
db.users.find({'age':{'$nin':[18,19]}})
//查询年龄为18或者用户名为joe的用户
db.users.find({'$or':[{'age':18},{'username':'joe'}]})
4.2.3 $not

$not是元条件句,即可以在任何条件之上。 比如用$mod取模,会将查询的值除以第一个给定的值,若余数等于第二个至则匹配成功:

db.users.find({'id_num':{"$mod":[5,1]}})

返回 1,6,11,16。。。的用户

db.users.find({'id_num':{"$not":{"$mod":[5,1]}}})

返回 2,3,4,5,7,8,9...的用户

4.2.4条件语句

都是前面的查询 注意一下元操作符是在外层文档中, 如$or $and

4.3 特性类型的查询
4.3.1 null
db.users.find('name':null)

如果仅想匹配键值为null的文档,既要检查该键的值是否null,还要通过 $exists条件判断键值已存在:

db.users.find({'name':{'$in':[null],'$exists':true}})
4.3.2 正则表达式
db.users.find({'name':/joe/i})

系统接受正则表达式标志i,但不是一定要有。

db.users.find({'name':/joey?/i})

MongoDB使用perl兼容的正则表达式库来匹配正则表达式。

4.3.3 查询数组
db.users.find('name':['zhangsan','lisi','wangwu'])

如果需要多个元素来匹配数组,那就要使用$all了。

想要查询数组特定位置的元素,需要使用key.index语法指定下标:

db.foot.find({'title.2':'paht'})

$size可以用来查询特定长度的数组。

4.3.4 查询内嵌文档

有两种方法可以查询内嵌文档:查询整个文档,或者只针对其键/值进行查询。

例如如下文档:

{
    'name':{
        'first':'joe',
        'last':'scc'
    },
    'age':45
}

db.users.find({'name':{'first':'joe'}})
4.4 $where 查询

键值对是一种表达能力非常好的查询方式,但是依然有些需求他无法表达。其他方法都无法查询是 $where就可以登场了。

$where操作比较麻烦 详细以后在添加。

4.5 游标

数据库使用游标返回find的执行结果。

4.5.1 limit、skip和sort

最常用的查询选项就是限制返回结果的数量、忽略一定数量的结果以及排序:

db.c.find().limit(3)
db.c.find().skip(3)
#name升序,age降序
db.c.find().sort(name:1,age:-1)

这三个方法组合使用对于分页非常有用。

详细书本也不够详细。需要后面使用在添加。

4.5.3 高级查询选项

有两种类型的查询:简单查询和封装查询

4.5.4 获取一致结果

4.5.5 游标生命周期

4.6 数据库命令

有一种非常特殊的查询类型叫作数据库命令。前面已经介绍过文档的创建、更新删除以及查询、这些都是数据库命令的范畴。包括管理性的任务,统计集合内的文档数量以及执行聚合等。

本章节有很多理论上的东西。实际操作在实际修改添加。这里没有一一添加编写进来。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第五章 索引
索引可以用来优化查询,而且在某些特定类型的查询中,索引是必不可少的。
  • 什么是索引,为什么要用索引。
  • 如果选择需要建立索引的字段
  • 如何强制使用索引,如果评估索引的效率
  • 创建索引和删除索引

为集合选择合适的索引是提高性能的关键。

5.1 索引简介

数据库索引与书籍的索引类似。

不使用索引的查询成为全表扫描,也就是说,服务器必须查找完一本完整的书才能找到查询结果。

使用索引是有代价的,对于添加的每一个索引,每次写操作(插入、更新、删除)都将耗时更加多的时间。因此MongoDB限制每个集合上最多只能有64个索引。

5.1.1 复合索引简介

索引的值是按一定顺序排列的,因此使用索引键对文档进行排序非常快。然后,只要在首先使用索引键进行排序时才有用。例如,在下面排序中,username上的索引没有什么作用:

db.users.find().sort({'age':1,"username":1})

为了优化这个排序,需要在age 和username上建立索引:

db.users.ensureIndex({'age':1,"username":1})

这样就建立了一个复合索引,如果查询中多有个排序方向或者查询条件中有多个键,这个索引就非常有用。

5.1.2 使用复合索引

在多个键上建立的索引就是符合索引。复合索引比单键索引要复杂一些,但是也更强大。

1.选择键的方向

到目前为止,我们的所有索引都是升序的,但是,如果需要在两个或者多个查询条件上进行排序,可能需要要让索引键的方向不同。

为了在不同方向上优化这个复合排序,需要使用与方向相匹配的索引。

只有基于多个查询条件进行排序时,索引方向菜市比较重要的。

2.使用覆盖索引

如果你的查询只需要朝赵索引中包含的字段,那就根本没必要获取实际的文档,当一个索引包含用户请求的所有字段,可以认为这个索引副高了本次查询。在实际中,应该优先使用覆盖索引,而不是去获取实际的文档。

3.隐式索引

5.1.3 $操作符如何使用索引

有一些查询完全无法使用索引,也有一些查询能够比其他查询更搞笑的i使用索引。

1.低效率的操作符

有一些查询完全无法使用索引,比如 $where 查询和检查一个键是否存在的查询。也有其他一些操作不能高效的使用索引。

2.范围

复合索引使MongoDB能够高效地执行用友多个语句的查询。设计基于多个字段的索引时,应该将会用于精确匹配的字段放在索引的前面,将用于范围的字段放在最后。

3.or查询

通常来说,执行两次查询再将结果合并的效率不如单次查询高,因此应该尽量使用 $in 而不是使用 $or
5.1.4 索引对象和数组

MongoDB允许深入文档内部,对嵌套字段和数组建立索引。

5.1.5 索引基数

基数就是集合中某个字段用友不同的数量。

一般来说,应该在基数比较高端键上简历索引,或者至少应该吧基数较高的键放在复合索引前面。

5.2 使用explain() 和 hint()

从上面的内容可以看出,explain()能够提供大量与查询相关的信息。对于速度比较慢的查询来说,这是最重要的诊断工具之一。

最常见的explain()输出有两种类型:所用索引的查询和没有使用索引的查询。

查询优化器

MongoDB的查询优化器与其他数据库稍有不同,基本来说如果以索引能够精确匹配一个查询,那么查询优化器就会使用这个索引,不然的话,可能会有几个索引都适合你的查询。MongoDB会从这些可能的索引子集中为每次查询计划选择一个,这些查询计划是并行执行的。

这个查询计划会被缓存,这个查询接下来都会使用它。直到集合数据发生了比较大的变动。如果在最初的计划评估之后集合发生了比较大的数据变动,查询优化器就会重新挑选可行的查询计划。

5.3 何时不应该使用索引

提取较小的子数据集时,索引非常高效,也有一些查询不适用索引会更快。

表5.1 影响索引效率的属性
索引通常适用的情况 全表扫描通常适用的情况
集合较大 集合较小
文档较大 文档较小
选择性查询 非选择性查询

适用 $natural 排序有一个副作用:返回的结果是按照磁盘上的顺序排列的,对于一个活跃的集合来说,这是没有意义的:随着文档提交的增加或者缩小,文档会在磁盘上进行移动,新的文档会被写入到这些文档留下的空白位置,但是对于只需要进行插入的工作来说,如果要得到罪行的或者最早的文档,使用 $natural就非常有用了。

5.4 索引类型

创建索引时可以指定一些选项,使用不同选项简历索引会有不同的行为。

5.4.1 唯一索引

唯一索引可以确保集合的每一个文档的指定键都有唯一的值:

db.users.ensureIndex({'username':1},{'unique':true})

有些情况下,一个值可能无法被索引,索引储桶的大小是有限制的。如果某个索引条码超出了他的限制,那么这个条目就不会包含在索引里。

1.复合唯一索引

创建复合唯一索引时,单个键的值可以相同,但所有键的组合值必须是唯一的。

2.去除重复

在极少数情况下,可能希望直接删除重复的值。创建索引时使用dropDups选项,如果遇到重复的值,第一个会被保留,之后的重复文档都会被删除。

"dropDups"会强制性简历唯一索引,但是这个方式太粗暴了:你无法控制哪些文档被保留哪些文档被删除,对于比较重要的数据,千万不要使用"dropDups".

5.4.2 稀疏索引

唯一索引会吧null看做值,所以无法将多个缺少唯一索引中的键的文档插入到集合中。然而,在有些情况下,你可以希望唯一索引只对包含相应键的文档生效。如果有一个可能存在也可能不存在的字段,但是他存在是必须是唯一的,这时就可以将unique和sparse选项组合在一起使用。

使用sparse选项就可以创建稀疏索引。

稀疏索引不比是唯一的,只要去掉unique选项,就可以创建一个非唯一的稀疏索引。

5.5 索引管理

对于一个集合,每个索引只需要创建一次,如果重复创建相同的索引,是没有任何作用的。

所有的数据库索引信息都存储在system.indexes集合中,这是一个保留集合,不能再其中插入或者删除文档。只能通过ensureIndex或者dropIndexes对其进行操作。

5.5.1 标识索引

集合中的每一个索引都有一个名称,用于唯一标识这个索引,也可以用于服务器端来删除或者操作索引。

索引名称的长度是有限制的,所以新建复杂索引时可能需要已定义索引名称,调用getLastError就可以指定索引是否成功创建,或者失败的原因。

5.5.2 修改索引

可以使用dropIndex命令删除不在需要的索引

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第六章 特殊的索引和集合
本章介绍MongoDB中一些特殊的集合和索引类型,包括:
  • 用于类队列数据的固定集合
  • 用于缓存的TTL索引
  • 用于简单字符串搜索的全文本索引;
  • 用于二维平面和球体空间的地理空间索引;
  • 用于存储大文件的GridFS
6.1 固定集合

MongoDB中的普通集合是动态 而且可以自动增长可以容纳更多的数据。MongoDB还有另一种不同类型的集合,叫作固定集合。说道固定大小的集合,有一个很有趣的问题,像一个已经满了的固定集合中插入数据会怎么样,答案是,固定集合的行类似于循环,如果已经没有空间了,最老的文档会被删除以释放空间。新插入的文档会 占据这块空间。也就是说,当固定空间被占满时,如果再插入新文档,固定集合会自动将最老的文档从集合中删除。

6.1.1创建固定集合

可以使用create命令创建固定集合。在shell中,可以使用createColletion函数:

db.createCollection('my_collection',{'capped':ture,'size':100000})

db.createCollection('my_collection2',{'capped':true,'size':10000,'max':100})

固定集合创建之后,就不能改变了,如果需要修改固定集合点属性,只能将它删除之后再重建,因此在创建大的固定集合之前应该仔细想清楚它的大小。

6.1.2 自然排序

对固定集合可以进行分一种特殊的排序,成为自然排序。自然排序返回结果集中文档的顺序就是文档在磁盘上的顺序。

对大多数集合来说,自然排序的意义不大,因为文档的位置经常变动。但是,固定集合中的文档是按照文档被插入的顺序保存的。自然顺序就是文档的插入顺序。因此,自然排序得到的文档就是旧到新排序的。当然也可以按照新到旧的顺序排序。

db.my_collection.find().sort({'$natural':-1})
6.1.3 循环游标

循环游标是一种特殊的游标。当循环游标的结果集被取光后。游标不会被关闭。循环游标的灵感来自tail - f命令,会尽可能久地持续提取输出结果。由于循环游标在结果集取光之后不会被关闭,因此,当有新文档插入到集合中时,循环游标会继续取到结果。由于普通集合并不维护文档的插入顺序,所有云鬟游标只能用在固定集合上。

6.1.4 有没_id索引的集合

默认情况下,每个集合都有一个_id索引,但是,如果在调用createCollection创建集合时指定autoIndexId选项为false,创建集合时就不会自动在 _id 上床架索引。实践中不建议这么使用,但是对于只有插入操作的集合来说,这确实可以带来速度上的稍许提升。

6.2 TTL索引

如果需要更加灵活的老化移出系统,。可以使用TTL索引,这种索引允许为每一个文档设计一个超时时间,一个文档到达预设置的老化程度之后就会被删除。这种类型的索引对于缓存问题非常有用。

在ensureIndex中指定恶心poreAfatterSecs选项中就可以创意一个TTL索引:

db.foo.exsureIndex({'lastUpdated':1},{'expireAfterSecs':60*24*24})

这样就在lastUpdated字段上建议了一个TTL索引,如果一个文档的lastUpdated字段存在并且他的值是日期类型,当服务器时间比文档的lastUpdated字段是时间晚expireAfterSecs秒时,文档就会把删除。

MongoDB每分钟对TTL索引进行一次清理,所以不应该依赖以秒为单位的时间保证suoy8in的存活状态。可以使用collMod命令修改expireAfterSecs的值:

db.runCommand({'collMod':'someapp.cache','expireAfterSecs':3600})

在一个给定的集合上可以有多个TTL索引。TTL索引不能是复合索引,但是可以像普通索引一样来优化排序和查询。

6.3 全文索引

MongoDB有一个忑书类型的索引用于在文档中搜索文本。前面几章都是在使用精确匹配和正则表达式来查询字符串,但是这些技术有一些限制,使用正则表达式搜索大块文本的速度非常慢,而且无法处理语言的理解问题。使用全文本索引可以非常快的进行文本搜索。就如同内置了多种语言分词机制的支持一样。

创建任何一种索引的开销都比较大。而创建全文本索引的成本更高。在一个系统频繁的集合上创建全文本索引可能会导致MongoDB过载,所以应该是离线状态下创建全文本索引,或者是在对性能没有要求时。创建全文本索引时要特别小心谨慎没存可能会不够用。

全文本索引也会导致比普通索引更严重的性能问题。因为所有字符串都需要被分解、分词,并且保存到一些地方,因此可能会发现拥有全文本索引的集合的写入性能比其他集合要差。全文本索引也会降低分片时的数据迁移速度,将数据迁移到其他分片时,所有文本都需要重新进行索引。

db.adminCommand({'setParameter':1,"textSearchEnabled":true})
6.3.1 搜索语法

默认情况下,MongoDB会使用or连接查询中的每个词, ‘ask OR hn’ 。这是执行会文本查询最有效的方式,但是也可以进行短语的精确匹配,以及使用not 为了精确查询 ‘ask hn’ 可以用 双引号将查询内容括起来:

db.runCommand({text:"hn",earqch:"\"ask hn\""})
6.3.2 优化全文索引

有几种方式可以优化全文搜索。如果能够使用某些查询条件将搜索结果的范围变小,可以创建一个由其他查询条件前缀和全文本字段组成的复合索引:

db.blog.ensureIndex({"data":1,"post":"test"})

这就是局部的全文本索引,MongoDB会基于上面例子中的date先将搜索范围分散为多个比较小的树,这样,对于特定日期的文档进行全文本查询就会快很多。

6.3.3 在其他语言中搜索

当一个文档被插入之后,MongoDB会查找索引字段,对字符串进行分词,将其减小为一个基本单元,然后不同语言的分词机制是不同的,所以必须制定索引或者文档使用的语言,文本类型的索引允许制定 default_language选项,它默认是enlish。

6.4 地理空间索引

MongoDB支持集中类型的地理空间索引,其中最常用的是2dsphere索引 和2d索引

2dsphere云心使用 GeoJSON格式指定点,线和多边形,点可以用形如[longitude,latitude] 两个元素的数组表示

6.4.1 地理空间查询的类型

可以使用多种不同类型的里空间查询:交集、包含、以及接近。查询时,需要讲希望查找地方内容指定为形如{'$geometry':genJsonDesc}的GeoJSON对象。

6.4.2 符合地理空间索引

如果有其他类型的索引,可以将地理空间索引与其他字段组合在一起使用,以便对更复杂的查询进行优化。

6.4.3 2d索引

对于非球面地图 可以使用2d索引代替 2dsphere

db.byrule.ensureIndex({'title':'2d'})

2d 索引用于扁平表面,而不是球体表面,2d索引不应该用在球体表面上,否则几点附近会出现大量的扭曲变形。

6.5 使用GridFS存储文件
GridFS是MongoDB的一种存储机制,用来存储大型二进制文件。
  • 使用GridFS能够简化你的栈。如果已经在使用MongoDB,那么可以使用GridFSB来代替独立的文件存储工具。
  • GridFS会自动平衡已有的复制或者为MongoDB设置的自动分片。所以对文件存储做故障转移或者横向扩展会更容易。
  • 当用于存储用户上传的文件时,GridFS可以比较从容地解决其他一些文件系统可能会遇到的问题。例如,在GridFS文件系统中,如果在同一个目录下存储大量的文件,没有任何问题。
  • 在GridFS中,文件存储的集中度会比较高,因为在MongoDB是以2GB为单位来分配数据文件的。
GridFS也有一些缺点:
  • GridFS的性能比较低,从MongoDB中访问文件,不如直接从文件系统中访问文件速度快。
  • 如果要修改GridFS上的文档,只能先将已有文档删除,然后在新建整个文档重新保存,MongoDB将文件作为多个文档进行存储,所以他无法再同一时间对文件中的所有快加锁。

通常来说,如果有一些不常改变戴氏经常需要联系访问的大文件,那么使用GridFS在合适不过了。

6.5.1 GridFS入门
使用GridFS最简单的方式是使用mongofiles工具,可以用它在GridFS中上传文件,下载文件,查看文件列表,搜索文件,以及删除文件。
6.5.2 在MongoDB启动程序中使用GridFS

所有客户端驱动程序提供了GridFS API。例如 pymongo 执行与上面直接使用mongofiles一样的操作:

from pymongo import Collection
import gridfs
db.Connection().test
fs = gridfs.GridFS(db)
file_id fs.put('hello',filename='foo.txt')
fs.list()
fs.get(file_id).read()
6.5.3 揭开GridFS的面纱

GridFS是一种轻量级的文件存储规范,用于存储MongoDB中的普通文档。MongoDB服务器几乎不会对GridFS请求做特殊处理,所有处理都由客户端的驱动程序和工具负责。

GridFS背后的理念是:可以将大文件分割为多个比较大的快,将每个快作为独立的文档进行存储,由于MongoDB支持在文档中存储二进制数据,所以可以将快存储的开销降到非法低。除了将文档的每一个快单独存储之外,还有一个文档用于将这些快组织在一起并存储该快的元信息。

很有书本上一些文字性的东西没有列出来不能代替全书内容。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第七章 聚合
如果你有数据存储在MongoDB中,你想做的可能就不仅仅是将数据提取出来这么简单了。你可能希望对数据进行分析并加以利用。本章介绍MongoDB提供的聚合工具:
  • 聚合框架
  • MapReduce
  • 几个简单聚合命令:count、distinct、group
7.1 聚合框架

使用聚合框架可以对集合中的文档进行变换和组合。基本上,可以用多个构件创建一个管道,用于对一连串的文档进行处理,这些构建包括筛选、投射、分组、排序、限制、跳过。

例如,有一个保存杂志的文章集合,你可能希望找出发表文章最多的那个作者,假设每篇文章被保存为MongoDB中的一个文档,可以按照如下步骤创建管道:
  • 1、将每个文章文档中的作则投射出来
  • 2、将作者按照名字排序,统计每个名字出现的次数。
  • 3、将作者名字出现的次数的降序顺序
  • 4、返回结果限制为前5个

这里的每一步都对应聚合框架中的一个操作符:

#1、
{"$project":{"author":1}}
    这样可以将"author"投射出来。

#2、
{"$group":{"_id":"%author","count":{"$sum":1}}}

#3、
{"$sort":{"count":1}}

#4、
{"$limit":5}

再MongoDB中实际运行时,要将这些操作分别传给aggregate()函数

7.2 管道操作符

每个操作符都会接受一连串的文档,对这些文档做一些类型转换,最后将转换后的文档作为结果传递给下一个操作符。

不同的管道操作符可以按任意顺序组合在一起,而且可以被重复任意次。

7.2.1 $match

$match 用于对文档集合进行筛选,之后就可以在筛选得到的文档子集上做聚合。

7.2.2 $project

使用$project可以从文档中提取字段。可以重命名字段,还可以在这些字段上济宁一些有意思的操作。

7.2.3 $group

$group操作符可以将文档依据特定字段的的不同值进行分组。

7.2.4 $unwind

拆分(unwind) 可以将数组中的每一个值拆分为单独的文档。

7.2.5 $sort

可以根据任意字段(或者多个字段)进行排序,在普通查询中的语法相同。

7.2.6 $limit

$limit会接受一个数字n,返回结果集中的前n个文档。

7.2.8 使用管道

应该尽量在管道的开始阶段就将尽可能多的文档和字段过滤掉。管道如果不是直接从原先的集合中使用数据,那就无法再筛选和排序中使用索引。

MongoDB不允许单一的聚合操作暂用过多的系统内存:如果MongoDB发现某个聚合操作占用20%以上的内存,这个操作就会直接输出错误。

7.3 MapReduce

用MapReduce开解决这个问题有点大材小用了。不过还是一种了解其机制的不错的方式。

7.3.2 示例2:网页分类

假设有个网站,人们可以提交其他网页的链接,比如reddit。提交者可以给这个链接添加标签,表明主题,比如politice、geek或者icanhascheezburger。可以用MapReduce找出哪个主题最为热门。热门与否由最近的投票决定。

7.3.3 MongoDB和MapReduce

7.4 聚合命令

MongoDB为在集合上执行基本的聚合任务提供了一些命令。然而,复杂的group操作可能仍然需要JavaScript、count和distinct操作可以被简化为普通命令,不需要使用聚合框架。

7.4.1 count

count是最简单的聚合工具,用于返回集合中的文档数量。

也可以给count传递一个查询文档,Mongo会计算查询结果的数量。

7.4.2 distinct

distinct用来找出给定键的所有不同值,使用时必须指定集合和键。

7.4.3 group

使用group可以执行更复杂的聚合,先选定分组所依据的键,然后MongoDB就会将集合依据选定的键的不同值分成若干组。然后可以对每一个分组内的文档进行聚合,得到一个结果文档。

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第八章 应用程序设计
本章介绍如何设计应用程序,以便更好地使用MongoDB,内容包括:
  • 内嵌数据和引用数据之间的权衡
  • 优化技巧
  • 数据一致性
  • 模式迁移
  • 不合适使用MongoDB作为数据存储的场景
8.1 范式化和反范式化

数据表示的方式有很多种,其中最重要的问题之一就是在大多程度上对数据进行范式化,范式化是将数据分散到多个不同的集合,不同集合之间可以相互引用数据。虽然很多外地可以应用某一块数据,但是这块数据只存储在一个集合中,所以如果要修改这块数据,只需修改保存在这块数据的那一个文档就行了。但是,MongoDB没有提供连接join工具,所以在不同集合之间执行连接查询需要进行多次查询。

反范式化与范式化相反:将每个文档所需的数据都嵌入在文档内部。没文档都拥有自己的数据副本,而不是所有文档共同引用同一个数据副本。这意味着,如果信息发生了改变,那么所有相关文档都需要进行更新,但是执行查询时,只需要一次查询,就可以得到所有数据。

8.1.1 数据表示的例子

内容略

内嵌数据与引用数据的比较
更合适内嵌 更合适引用
子文档较小 子文档较大
数据不会定期改变 数据经常改变
最终数据一致即可 中间阶段的数据必须一致
文档数据小幅增加 文档数据大幅增加
数据通常需要执行二次查询才能获得 数据通常不包含在结果中
快速读取 快速写入
8.1.2 基数

一个集合中包含的对其他集合的引用数量叫作基数,常见的关系有一对一、一对多、多对多。

8.1.3 还有、粉丝、以及其他的麻烦事项

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
第九章 复制副本集
第十章 副本集的组成
第十一章 从应用程序连接副本集
第十二章 管理
第十三章 分片
第十四章 配置分片
第十五章 选择片键
第十六章 分片管理
第十七章 了解应用的动态
第十八章 数据管理
第十九章 持久性
第二十章 启动和停止MongoDB
第二十一章 监控MongoDB
第二十二章 备份
第二十三章 部署MongoDB

人工智能/机器学习/深度学习/数据挖掘相关书籍

人工智能相关书籍

人工智能/机器学习/深度学习/数据挖掘

贝叶斯思维

深度学习

深度学习核心技术与实现

机器学习实战

机器学习基础
kNN近邻算法

是分类数据最简单有效的算法。缺点:训练数据集很大的时候必须使用大量存储空间,且必须对数据集中的没一个数据进行距离计算,非常耗时,且无法给出任意数据的基础结构信息。

决策树

决策树分类器就像带有终止块的流程图,终止块表示分类结果。是一种贪婪算法,他要在给定的时间内做出最佳选择,但并不关心能否达到全局最优。 缺点:当类别太多时,错误可能就会增加的比较快。

朴素贝叶斯

贝叶斯提供了一种利于已知值来估计未知概率的有效方案

Logistics回归

用一条直线对数据点进行拟合,这个拟合过程就称作回归。 根据现有数据对分类边界线建立回归公式,以此进行分类。

支持向量机SVM

svm是一种监督式学习的方法,可广泛地应用于统计分类以及回归分析,泛化错误率较低。

AdaBoost元算法

AdaBoost算法十分强大,它能够快速处理其他分类器很难处理的数据集

回归

与分类一样,回归也有预测目标值的过程。回归预测连续型变量,而回归预测离散型变量。回归是统计学中最有力的工具之一。

树回归CART

数据集中经常包含一些复杂的相互关系,是的输入数据和目标变量之间呈现非线性关系。一般采用树结构来对这种数据建模,相应的,若叶节点使用的模型是分段常数则称为树回归,若叶节点使用的模型是线性回归方程则为模型树。

k-均值聚类算法
Apriori算法
FP-growth算法
PCA简化数据
SVD简化数据
大数据与MapReduce
其他:神经网络RNN

TensorFlow实战Google深度学习框架

python神经网络编程

深度强化学习

数据挖掘与知识发现

数学书

数学书

初中数学/高中数学/高数/高数以上

初一数学

初二数学

初三数学

高中必修1

第一章 集合与函数概念
一、集合有关概念

1、集合的含义:某些指定的对象集在一起就成为一个集合,其中每一个对象叫元素。

2、集合的中元素的三个特性:

1.元素的确定性; 2.元素的互异性; 3.元素的无序性

说明:

(1)对于一个给定的集合,集合中的元素是确定的,任何一个对象或者是或者不是这个给定的集合的元素。

(2)任何一个给定的集合中,任何两个元素都是不同的对象,相同的对象归入一个集合时,仅算一个元素。

(3)集合中的元素是平等的,没有先后顺序,因此判定两个集合是否一样,仅需比较它们的元素是否一样,不需考查排列顺序是否一样。

(4)集合元素的三个特性使集合本身具有了确定性和整体性。

3、集合的表示:{ … } 如{我校的篮球队员},{太平洋,大西洋,印度洋,北冰洋}

  1. 用拉丁字母表示集合:A={我校的篮球队员},B={1,2,3,4,5}

2.集合的表示方法:列举法与描述法。

非负整数集(即自然数集)记作:N

正整数集 N*或 N+ 整数集Z 有理数集Q 实数集R

关于“属于”的概念

集合的元素通常用小写的拉丁字母表示,如:a是集合A的元素,就说a属于集合A 记作 a∈A ,相反,a不属于集合A 记作 aA

列举法:把集合中的元素一一列举出来,然后用一个大括号括上。

描述法:将集合中的元素的公共属性描述出来,写在大括号内表示集合的方法。用确定的条件表示某些对象是否属于这个集合的方法。

①语言描述法:例:{不是直角三角形的三角形}

②数学式子描述法:例:不等式x-3>2的解集是{x?R| x-3>2}或{x| x-3>2}

4、集合的分类:

(1).有限集 含有有限个元素的集合

(2).无限集 含有无限个元素的集合

(3).空集 不含任何元素的集合 例:{x|x2=-5}

二、集合间的基本关系

1.“包含”关系—子集

注意: 有两种可能(1)A是B的一部分,;(2)A与B是同一集合。

反之: 集合A不包含于集合B,或集合B不包含集合A,记作A B或B A

2.“相等”关系(5≥5,且5≤5,则5=5)

实例:设 A={x|x2-1=0} B={-1,1} “元素相同”

结论:对于两个集合A与B,如果集合A的任何一个元素都是集合B的元素,同时,集合B的任何一个元素都是集合A的元素,我们就说集合A等于集合B,即:A=B

任何一个集合是它本身的子集。AA

②真子集:如果AB,且B A那就说集合A是集合B的真子集,记作A B(或B A)

③如果 AB, BC ,那么 AC

④如果AB 同时 BA 那么A=B

  1. 不含任何元素的集合叫做空集,记为Φ

规定: 空集是任何集合的子集, 空集是任何非空集合的真子集。

三、集合的运算

1.交集的定义:一般地,由所有属于A且属于B的元素所组成的集合,叫做A,B的交集.

记作A∩B(读作”A交B”),即A∩B={x|x∈A,且x∈B}.

2、并集的定义:一般地,由所有属于集合A或属于集合B的元素所组成的集合,叫做A,B的并集。记作:A∪B(读作”A并B”),即A∪B={x|x∈A,或x∈B}.

3、交集与并集的性质:A∩A = A, A∩φ= φ, A∩B = B∩A,A∪A = A,

A∪φ= A ,A∪B = B∪A.

4、全集与补集

(1)补集:设S是一个集合,A是S的一个子集(即 ),由S中所有不属于A的元素组成的集合,叫做S中子集A的补集(或余集)

(2)全集:如果集合S含有我们所要研究的各个集合的全部元素,这个集合就可以看作一个全集。通常用U来表示。

四、函数的有关概念

1.函数的概念:设A、B是非空的数集,如果按照某个确定的对应关系f,使对于集合A中的任意一个数x,在集合B中都有唯一确定的数f(x)和它对应,那么就称f:A→B为从集合A到集合B的一个函数.记作: y=f(x),x∈A.其中,x叫做自变量,x的取值范围A叫做函数的定义域;与x的值相对应的y值叫做函数值,函数值的集合{f(x)| x∈A }叫做函数的值域.

注意:如果只给出解析式y=f(x),而没有指明它的定义域,则函数的定义域即是指能使这个式子有意义的实数的集合;函数的定义域、值域要写成集合或区间的形式.

定义域补充

能使函数式有意义的实数x的集合称为函数的定义域,求函数的定义域时列不等式组的主要依据是:(1)分式的分母不等于零; (2)偶次方根的被开方数不小于零; (3)对数式的真数必须大于零;(4)指数、对数式的底必须大于零且不等于1. (5)如果函数是由一些基本函数通过四则运算结合而成的.那么,它的定义域是使各部分都有意义的x的值组成的集合.(6)指数为零底不可以等于零 (6)实际问题中的函数的定义域还要保证实际问题有意义.

(又注意:求出不等式组的解集即为函数的定义域。)

构成函数的三要素:定义域、对应关系和值域

注意:(1)构成函数三个要素是定义域、对应关系和值域.由于值域是由定义域和对应关系决定的,所以,如果两个函数的定义域和对应关系完全一致,即称这两个函数相等(或为同一函数)(2)两个函数相等当且仅当它们的定义域和对应关系完全一致,而与表示自变量和函数值的字母无关。相同函数的判断方法:①表达式相同;②定义域一致 (两点必须同时具备) (见课本21页相关例2)

值域补充

(1)、函数的值域取决于定义域和对应法则,不论采取什么方法求函数的值域都应先考虑其定义域. (2).应熟悉掌握一次函数、二次函数、指数、对数函数及各三角函数的值域,它是求解复杂函数值域的基础。

  1. 函数图象知识归纳

(1)定义:在平面直角坐标系中,以函数 y=f(x) , (x∈A)中的x为横坐标,函数值y为纵坐标的点P(x,y)的集合C,叫做函数 y=f(x),(x ∈A)的图象.

集合C上每一点的坐标(x,y)均满足函数关系y=f(x),反过来,以满足y=f(x)的每一组有序实数对x、y为坐标的点(x,y),均在C上 . 即记为C={ P(x,y) | y= f(x) , x∈A },图象C一般的是一条光滑的连续曲线(或直线),也可能是由与任意平行与Y轴的直线最多只有一个交点的若干条曲线或离散点组成。

  1. 画法

A、描点法:根据函数解析式和定义域,求出x,y的一些对应值并列表,以(x,y)为坐标在坐标系内描出相应的点P(x, y),最后用平滑的曲线将这些点连接起来.

B、图象变换法(请参考必修4三角函数)

常用变换方法有三种,即平移变换、伸缩变换和对称变换

(3)作用:

1、直观的看出函数的性质;2、利用数形结合的方法分析解题的思路。提高解题的速度。发现解题中的错误。

4.了解区间的概念

(1)区间的分类:开区间、闭区间、半开半闭区间;(2)无穷区间;(3)区间的数轴表示.

5.什么叫做映射

一般地,设A、B是两个非空的集合,如果按某一个确定的对应法则f,使对于集合A中的任意一个元素x,在集合B中都有唯一确定的元素y与之对应, 那么就称对应f:A→ B为从集合A到集合B的一个映射。记作“f:A→ B”

给定一个集合A到B的映射,如果a∈A,b∈B.且元素a和元素b对应,那么,我们把元素b叫做元素a的象,元素a叫做元素b的原象

说明:函数是一种特殊的映射,映射是一种特殊的对应,①集合A、B及对应法则f是确定的;②对应法则有“方向性”,即强调从集合A到集合B的对应,它与从B到A的对应关系一般是不同的;③对于映射f:A→B来说,则应满足:(Ⅰ)集合A中的每一个元素,在集合B中都有象,并且象是唯一的;(Ⅱ)集合A中不同的元素,在集合B中对应的象可以是同一个;(Ⅲ)不要求集合B中的每一个元素在集合A中都有原象。

常用的函数表示法及各自的优点:

1 函数图象既可以是连续的曲线,也可以是直线、折线、离散的点等等,注意判断一个图形是否是函数图象的依据;2 解析法:必须注明函数的定义域;3 图象法:描点法作图要注意:确定函数的定义域;化简函数的解析式;观察函数的特征;4 列表法:选取的自变量要有代表性,应能反映定义域的特征.

解析法:便于算出函数值。列表法:便于查出函数值。图象法:便于量出函数值.

补充一:分段函数 (参见课本P24-25)

在定义域的不同部分上有不同的解析表达式的函数。在不同的范围里求函数值时必须把自变量代入相应的表达式。分段函数的解析式不能写成几个不同的方程,而就写函数值几种不同的表达式并用一个左大括号括起来,并分别注明各部分的自变量的取值情况.(1)分段函数是一个函数,不要把它误认为是几个函数;(2)分段函数的定义域是各段定义域的并集,值域是各段值域的并集.

补充二:复合函数

如果y=f(u),(u∈M),u=g(x),(x∈A),则 y=f[g(x)]=F(x),(x∈A) 称为f、g 的复合函数。

例如: y=2sinx y=2cos(2x+1)

7.函数单调性

(1).增函数

设函数y=f(x)的定义域为I,如果对于定义域I内的某个区间D内的任意两个自变量a,b,当a<b时,都有f(a)<f(b),那么就说f(x)在区间D上是增函数。区间D称为y=f(x)的单调增区间(睇清楚课本单调区间的概念)

如果对于区间D上的任意两个自变量的值a,b,当a<b 时,都有f(a)>f(b),那么就说f(x)在这个区间上是减函数.区间D称为y=f(x)的单调减区间.

注意:1 函数的单调性是在定义域内的某个区间上的性质,是函数的局部性质;

2 必须是对于区间D内的任意两个自变量a,b;当a<b时,总有f(a)<f(b) 。

(2) 图象的特点

如果函数y=f(x)在某个区间是增函数或减函数,那么说函数y=f(x)在这一区间上具有(严格的)单调性,在单调区间上增函数的图象从左到右是上升的,减 函数的图象从左到右是下降的.

(3).函数单调区间与单调性的判定方法

  1. 定义法:任取a,b∈D,且a<b;2 作差f(a)-f(b);3 变形(通常是因式分解和配方);4 定号(即判断差f(a)-f(b)的正负);5 下结论(指出函数f(x)在给定的区间D上的单调性).

(B)图象法(从图象上看升降)_

(C)复合函数的单调性

复合函数f[g(x)]的单调性与构成它的函数u=g(x),y=f(u)的单调性密切相关

注意:1、函数的单调区间只能是其定义域的子区间 ,不能把单调性相同的区间和在一起写成其并集. 2、还记得我们在选修里学习简单易行的导数法判定单调性吗?

8.函数的奇偶性

(1)偶函数

一般地,对于函数f(x)的定义域内的任意一个x,都有f(-x)=f(x),那么f(x)就叫做偶函数.

(2).奇函数

一般地,对于函数f(x)的定义域内的任意一个x,都有f(-x)=—f(x),那么f(x)就叫做奇函数.

注意:1、 函数是奇函数或是偶函数称为函数的奇偶性,函数的奇偶性是函数的整体性质;函数可能没有奇偶性,也可能既是奇函数又是偶函数。

2、 由函数的奇偶性定义可知,函数具有奇偶性的一个必要条件是,对于定义域内的任意一个x,则-x也一定是定义域内的一个自变量(即定义域关于原点对称).

3、具有奇偶性的函数的图象的特征

偶函数的图象关于y轴对称;奇函数的图象关于原点对称.

总结:利用定义判断函数奇偶性的格式步骤:1 首先确定函数的定义域,并判断其定义域是否关于原点对称;2 确定f(-x)与f(x)的关系;3 作出相应结论:若f(-x) = f(x) 或 f(-x)-f(x) = 0,则f(x)是偶函数;若f(-x) =-f(x) 或 f(-x)+f(x) = 0,则f(x)是奇函数.

注意:函数定义域关于原点对称是函数具有奇偶性的必要条件.首先看函数的定义域是否关于原点对称,若不对称则函数是非奇非偶函数.若对称,(1)再根据定义判定; (2)有时判定f(-x)=±f(x)比较困难,可考虑根据是否有f(-x)±f(x)=0或f(x)/f(-x)=±1来判定; (3)利用定理,或借助函数的图象判定 .

9、函数的解析表达式

(1).函数的解析式是函数的一种表示方法,要求两个变量之间的函数关系时,一是要求出它们之间的对应法则,二是要求出函数的定义域.

(2).求函数的解析式的主要方法有:待定系数法、换元法、消参法等,如果已知函数解析式的构造时,可用待定系数法;已知复合函数f[g(x)]的表达式时,可用换元法,这时要注意元的取值范围;当已知表达式较简单时,也可用凑配法;若已知抽象函数表达式,则常用解方程组消参的方法求出f(x)

10.函数最大(小)值(定义见课本p36页)

(1)、 利用二次函数的性质(配方法)求函数的最大(小)值. (2)、 利用图象求函数的最大(小)值 (3)、 利用函数单调性的判断函数的最大(小)值:如果函数y=f(x)在区间[a,b]上单调递增,在区间[b,c]上单调递减则函数y=f(x)在x=b处有最大值f(b);如果函数y=f(x)在区间[a,b]上单调递减,在区间[b,c]上单调递增则函数y=f(x)在x=b处有最小值f(b);

高中必修2

高中必修3

高中必修4

高中必修5

笔记

笔记

使用技巧

sublime Text3 的使用相关
设置

使用Sublime Text前永久设置使用4个空格缩进和取消更新:

#设置方法是点击首选项--"设置-用户":
"tab_size": 4,
"translate_tabs_to_spaces": true,
"expand_tabs_on_save": true,
"update_check": false,
快捷键
选择类:
  • Ctrl+D: 选中光标所占的文本,继续操作则会选中下一个相同的文本。
  • Alt+F3: 选中文本按下快捷键,即可一次性选择全部的相同文本进行同时编辑。举个栗子:快速选中并更改所有相同的变量名、函数名等。
  • Ctrl+L: 选中整行,继续操作则继续选择下一行,效果和 Shift+↓ 效果一样。
  • Ctrl+Shift+L: 先选中多行,再按下快捷键,会在每行行尾插入光标,即可同时编辑这些行。
  • Ctrl+Shift+M: 选择括号内的内容(继续选择父括号)。举个栗子:快速选中删除函数中的代码,重写函数体代码或重写括号内里的内容。
  • Ctrl+M: 光标移动至括号内结束或开始的位置。
  • Ctrl+Enter: 在下一行插入新行。举个栗子:即使光标不在行尾,也能快速向下插入一行。
  • Ctrl+Shift+Enter: 在上一行插入新行。举个栗子:即使光标不在行首,也能快速向上插入一行。
  • Ctrl+Shift+[: 选中代码,按下快捷键,折叠代码。
  • Ctrl+Shift+]: 选中代码,按下快捷键,展开代码。
  • Ctrl+K+0: 展开所有折叠代码。
  • Ctrl+←: 向左单位性地移动光标,快速移动光标。
  • Ctrl+→: 向右单位性地移动光标,快速移动光标。
  • shift+↑: 向上选中多行。
  • shift+↓: 向下选中多行。
  • Shift+←: 向左选中文本。
  • Shift+→: 向右选中文本。
  • Ctrl+Shift+←: 向左单位性地选中文本。
  • Ctrl+Shift+→: 向右单位性地选中文本。
  • Ctrl+Shift+↑: 将光标所在行和上一行代码互换(将光标所在行插入到上一行之前)。
  • Ctrl+Shift+↓: 将光标所在行和下一行代码互换(将光标所在行插入到下一行之后)。
  • Ctrl+Alt+↑: 向上添加多行光标,可同时编辑多行。
  • Ctrl+Alt+↓: 向下添加多行光标,可同时编辑多行。
编辑类:
  • Ctrl+J: 合并选中的多行代码为一行。举个栗子:将多行格式的CSS属性合并为一行。
  • Ctrl+Shift+D: 复制光标所在整行,插入到下一行。
  • Tab: 向右缩进。
  • Shift+Tab: 向左缩进。
  • Ctrl+K+K: 从光标处开始删除代码至行尾。
  • Ctrl+Shift+K: 删除整行。
  • Ctrl+/: 注释单行。
  • Ctrl+Shift+/: 注释多行。
  • Ctrl+K+U: 转换大写。
  • Ctrl+K+L: 转换小写。
  • Ctrl+Z: 撤销。
  • Ctrl+Y: 恢复撤销。
  • Ctrl+U: 软撤销,感觉和 Gtrl+Z 一样。
  • Ctrl+F2: 设置书签
  • Ctrl+T: 左右字母互换。
  • F6: 单词检测拼写
搜索类:
  • Ctrl+F: 打开底部搜索框,查找关键字。
  • Ctrl+shift+F: 在文件夹内查找,与普通编辑器不同的地方是sublime允许添加多个文件夹进行查找,略高端,未研究。
  • Ctrl+P: 打开搜索框。举个栗子:1、输入当前项目中的文件名,快速搜索文件,2、输入@和关键字,查找文件中函数名,3、输入:和数字,跳转到文件中该行代码,4、输入#和关键字,查找变量名。
  • Ctrl+G: 打开搜索框,自动带:,输入数字跳转到该行代码。举个栗子:在页面代码比较长的文件中快速定位。
  • Ctrl+R: 打开搜索框,自动带@,输入关键字,查找文件中的函数名。举个栗子:在函数较多的页面快速查找某个函数。
  • Ctrl+: 打开搜索框,自动带#,输入关键字,查找文件中的变量名、属性名等。
  • Ctrl+Shift+P: 打开命令框。场景栗子:打开命名框,输入关键字,调用sublime text或插件的功能,例如使用package安装插件。
  • Esc: 退出光标多行选择,退出搜索框,命令框等。
显示类:
  • Ctrl+Tab: 按文件浏览过的顺序,切换当前窗口的标签页。
  • Ctrl+PageDown: 向左切换当前窗口的标签页。
  • Ctrl+PageUp: 向右切换当前窗口的标签页。
  • Alt+Shift+1: 窗口分屏,恢复默认1屏(非小键盘的数字)
  • Alt+Shift+2: 左右分屏-2列
  • Alt+Shift+3: 左右分屏-3列
  • Alt+Shift+4: 左右分屏-4列
  • Alt+Shift+5: 等分4屏
  • Alt+Shift+8: 垂直分屏-2屏
  • Alt+Shift+9: 垂直分屏-3屏
  • Ctrl+K+B: 开启/关闭侧边栏。
  • F11: 全屏模式
  • Shift+F11: 免打扰模式

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
解决Inno Setup制作安装包无法创建桌面快捷方式的问题

编译前修改如下代码:

[Tasks];
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkablealone
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checkablealone

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png
定时任务框架APScheduler学习详解
APScheduler简介

APScheduler基于Quartz的一个Python定时任务框架,实现了Quartz的所有功能,使用起来十分方便。提供了基于日期、固定时间间隔以及crontab类型的任务,并且可以持久化任务。基于这些功能,我们可以很方便的实现一个python定时任务系统。

安装
pip install apscheduler
APScheduler有四种组成部分:

触发器(trigger)包含调度逻辑,每一个作业有它自己的触发器,用于决定接下来哪一个作业会运行。除了他们自己初始配置意外,触发器完全是无状态的。

作业存储(job store)存储被调度的作业,默认的作业存储是简单地把作业保存在内存中,其他的作业存储是将作业保存在数据库中。一个作业的数据讲在保存在持久化作业存储时被序列化,并在加载时被反序列化。调度器不能分享同一个作业存储。

执行器(executor)处理作业的运行,他们通常通过在作业中提交制定的可调用对象到一个线程或者进城池来进行。当作业完成时,执行器将会通知调度器。

调度器(scheduler)是其他的组成部分。你通常在应用只有一个调度器,应用的开发者通常不会直接处理作业存储、调度器和触发器,相反,调度器提供了处理这些的合适的接口。配置作业存储和执行器可以在调度器中完成,例如添加、修改和移除作业。 通过配置executor、jobstore、trigger,使用线程池(ThreadPoolExecutor默认值20)或进程池(ProcessPoolExecutor 默认值5)并且默认最多3个(max_instances)任务实例同时运行,实现对job的增删改查等调度控制

简单应用:

import time
from apscheduler.schedulers.blocking import BlockingScheduler

def my_job():
    print time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))

sched = BlockingScheduler()
sched.add_job(my_job, 'interval', seconds=5)
sched.start()
#上面的例子表示每隔5s执行一次my_job函数,输出当前时间信息
添加作业

上面是通过add_job()来添加作业,另外还有一种方式是通过scheduled_job()修饰器来修饰函数

import time
from apscheduler.schedulers.blocking import BlockingScheduler

sched = BlockingScheduler()

@sched.scheduled_job('interval', seconds=5)
def my_job():
    print time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))

sched.start()
移除作业
job = scheduler.add_job(myfunc, 'interval', minutes=2)
job.remove()
#如果有多个任务序列的话可以给每个任务设置ID号,可以根据ID号选择清除对象,且remove放到start前才有效
sched.add_job(myfunc, 'interval', minutes=2, id='my_job_id')
sched.remove_job('my_job_id')
暂停和恢复作业
#暂停作业
apsched.job.Job.pause()
apsched.schedulers.base.BaseScheduler.pause_job()
#恢复作业
apsched.job.Job.resume()
apsched.schedulers.base.BaseScheduler.resume_job()
获得job列表

获得调度作业的列表,可以使用get_jobs()来完成,它会返回所有的job实例。或者使用print_jobs()来输出所有格式化的作业列表。也可以利用get_job(任务ID)获取指定任务的作业列表

job = sched.add_job(my_job, 'interval', seconds=2 ,id='123')
print sched.get_job(job_id='123')
print sched.get_jobs()
关闭调度器

默认情况下调度器会等待所有正在运行的作业完成后,关闭所有的调度器和作业存储。如果你不想等待,可以将wait选项设置为False。

sched.shutdown()
sched.shutdown(wait=False)
作业运行的控制(trigger)

add_job的第二个参数是trigger,它管理着作业的调度方式。它可以为date, interval或者cron。对于不同的trigger,对应的参数也相同。

(1)cron定时调度(某一定时时刻执行)

(int|str) 表示参数既可以是int类型,也可以是str类型
(datetime | str) 表示参数既可以是datetime类型,也可以是str类型

year (int|str) – 4-digit year -(表示四位数的年份,如2008年)
month (int|str) – month (1-12) -(表示取值范围为1-12月)
day (int|str) – day of the (1-31) -(表示取值范围为1-31日)
week (int|str) – ISO week (1-53) -(格里历2006年12月31日可以写成2006年-W52-7(扩展形式)或2006W527(紧凑形式))
day_of_week (int|str) – number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun) - (表示一周中的第几天,既可以用0-6表示也可以用其英语缩写表示)
hour (int|str) – hour (0-23) - (表示取值范围为0-23时)
minute (int|str) – minute (0-59) - (表示取值范围为0-59分)
second (int|str) – second (0-59) - (表示取值范围为0-59秒)
start_date (datetime|str) – earliest possible date/time to trigger on (inclusive) - (表示开始时间)
end_date (datetime|str) – latest possible date/time to trigger on (inclusive) - (表示结束时间)
timezone (datetime.tzinfo|str) – time zone to use for the date/time calculations (defaults to scheduler timezone) -(表示时区取值)

例子:

#表示2017年3月22日17时19分07秒执行该程序
sched.add_job(my_job, 'cron', year=2017,month = 03,day = 22,hour = 17,minute = 19,second = 07)

#表示任务在6,7,8,11,12月份的第三个星期五的00:00,01:00,02:00,03:00 执行该程序
sched.add_job(my_job, 'cron', month='6-8,11-12', day='3rd fri', hour='0-3')

#表示从星期一到星期五5:30(AM)直到2014-05-30 00:00:00
sched.add_job(my_job(), 'cron', day_of_week='mon-fri', hour=5, minute=30,end_date='2014-05-30')
# 添加任务作业,args()中最后一个参数后面要有一个逗号,本任务设置在每天凌晨1:00:00执行
scheduler.add_job(task, 'cron', hour='1', minute='0', second='0', args=("hello",))


#表示每5秒执行该程序一次,相当于interval 间隔调度中seconds = 5
sched.add_job(my_job, 'cron',second = '*/5')

(2)interval 间隔调度(每隔多久执行)

weeks (int) – number of weeks to wait
days (int) – number of days to wait
hours (int) – number of hours to wait
minutes (int) – number of minutes to wait
seconds (int) – number of seconds to wait
start_date (datetime|str) – starting point for the interval calculation
end_date (datetime|str) – latest possible date/time to trigger on
timezone (datetime.tzinfo|str) – time zone to use for the date/time calculations

例子:

#表示每隔3天17时19分07秒执行一次任务
sched.add_job(my_job, 'interval',days  = 03,hours = 17,minutes = 19,seconds = 07)

(3)date 定时调度(作业只会执行一次)

run_date (datetime|str) – the date/time to run the job at  -(任务开始的时间)
timezone (datetime.tzinfo|str) – time zone for run_date if it doesn’t have one already

例子:

# The job will be executed on November 6th, 2009
sched.add_job(my_job, 'date', run_date=date(2009, 11, 6), args=['text'])
# The job will be executed on November 6th, 2009 at 16:30:05
sched.add_job(my_job, 'date', run_date=datetime(2009, 11, 6, 16, 30, 5), args=['text'])

原文:http://www.cnblogs.com/luxiaojun/p/6567132.html

https://blog.csdn.net/caiguoxiong0101/article/details/50364236

疯狂的程序员摘要

  • 人生最大的痛苦莫过于没人理解你。绝影没法跟他们讲“技术”,因为他们根本就不懂“技术”。比如你跟猪讲《普通物理学》,要是猪能成功计算出杀猪刀进入身体时力量有多大,压强有多大,能够通过给定的猪皮的厚度和强度计算出自己应该以多大速度向后缓冲才能成功让杀猪刀无法穿透猪皮,那么你就可以跟土匪和王江讲什么是汇编语言,为什么要学汇编语言了
  • ~
  • 所以你自己跑得多起劲的时候要知道女人的心思是和你不一样的。你要买一个东西到处找,急得要死。对她们来说,她们只想去逛,逛她们喜欢的东西,也许那东西她们根本就没想过要买,光是逛一下看一下就能让她们很满足。男人呢?总是要拿到手里才会满足。所以男人逛街目的性是很强,女人逛街就没目的。
  • ~
  • 所以现在的女人面临的最大的敌人是啥?不是别的女人。你要是自己够体贴够理解男人说实话鬼才原意去外面找女人。――不但浪费钱,还容易把自己搞得众叛亲离。她们最大的敌人是电脑。搞 IT的就不说了,想起码有 80%的女人很想砸电脑。搞其它的呢?要是男人迷上了游戏,迷上了上网怎么办。所以女人们,现在就得学:有一天,我们必须和游戏一起争夺男人,该怎么做?
  • ~
  • 燕儿说:“不清楚,好像是 ASP方面的。”绝影说:“ASP我不会,什么 ASP啊,Java啊,做网页之类的这些我都不会,一点都不会。所以以后有这样的消息就不用去关注了。”几次过后,燕儿跟绝影生气了。她问:“学 ASP难吗?”绝影说:“不难,很简单,两个月就好了。那玩艺很高层的。”“那你为什么不学呢?你天天坐在电脑面前写程序,学了这么久了也写了这么久了。可是你写的程序有什么用呢?有人买吗?还不如去学 ASP呢。”那时候的确是这样,很多公司刚开始有了网络的意识,开始做公司网页,论坛啊,社区啊应运而生,整个 IT届确实很需要做 ASP,做 Java的程序员。但是绝影不会盲目跟风,他觉得走汇编这条道理也是经过深思熟虑了的。他跟燕儿说:“我学的汇编,这是很地层和基础性的东西,是学的很慢,但高层的东西老是会变,比如 ASP,一下升级到 ASP.NET好多东西就又要学。汇编就不一样了,基础性的东西,除非微软把操作系统都全换了,把 API全换了,否则永远都不用怎么变的。”燕儿听不懂这些,她反而更讨厌他,她觉得她这是在为自己找借口。
  • ~
  • 绝影深信自己是对的,他相信有一天他会很快乐的用汇编工作,并且他的工作能带给他不菲的收入。但是他不能给燕儿说,人有时候就是这样明明知道事情肯定是这样,但是不能跟别人说,成果出来了你跟别人说,那是你的成功,成果没出来你就跟别人说,那是你狂想,反而被别人笑话。
  • ~
  • 现在的社会就是,谁吸引了眼球,谁就吸引了 Money。
  • ~
  • 事情往往是这样,当你不是黑客的时候,总说:“我是个黑客。”但当你真正成为黑客,你往往会说:“我不是黑客。”能进别人系统偷东西,那就是小偷;能进别人系统又不偷东西,那就是黑客。
  • ~
  • 程序员是值得尊敬的,程序员的双手是魔术师的双手。他们把枯燥无味的代码变成了丰富多彩的软件
  • ~
  • 所以记性不好的人还是尽量不要去跟女人借钱,宁愿把吉他当了也不要去借女人钱。
  • ~
  • 他深深地懂得一个道理:天下几乎所有的男人都会有自己的老婆,但不是所有的男人都能拥有自己的事业。要有事业,一定要花比找老婆更多的时间和心血。
  • ~
  • 你想你英语学得再好能比得上美国一个种田的农民吗?你随便到加拿大美国去看下,一个四五岁的小娃娃都哇啦哇啦满嘴流利的英语。
  • ~
  • 一个女人,如果在一群男人面前不拘小节,那说明她为人放荡邋遢,但是如果她只在一个男人面前不拘小节,那只能说明她对这个男人完全信任,换句话说:在她心里,她就是他的人
  • ~
  • 招十个不能做事的学生,工资 300,一个月下来做的工作为 0,支出工资 3000,还不如花 3000招一个能做事的人,只要这个月他做了东西,就赚了
  • ~
  • 理想就如同美女,生活就如同大便。
  • ~
  • 他想让她知道,男人不向你发火,并不代表他怕你,也不代表他无理,这是男人的风度
  • ~
  • 现在的女人啊,就是不懂男人的心,总觉得身边男性朋友多的是,打个电话随叫随到,为什么还偏偏喜欢跟女的交朋友,特别是美女,还故作深沉,什么红颜知己。
  • ~
  • 女人啊,不仅不懂男人,有时候连女人都不懂。男人在男人面前,始终都是强者,谈的都是征服宇宙的事情,就算不强,也要把自己伪装成强者,就算征服不了宇宙,至少也要征服世界。很多事情,很多话,特别是心里话,都是不能跟男人说的,于是只好找女性朋友来倾诉
  • ~
  • 你喜欢写程序吗?干这个工作和别的不一样,很大程度上在于你的兴趣。你对技术感兴趣,你才会抛开奖金啊工资啊这些东西,你才会不惜一切代价和时间去钻研它,解决它,你才不会在工作和工资上感到不平衡,这样,你才会很快进步起来
  • ~
  • 力的作用是相互的,你打别人有多疼,自己的手就有多疼。你要是明白了这个道理就应该知道,与其大家都疼,还不如最开始就不要下手打
  • ~
  • 老婆如衣服,兄弟如手足,衣服断尚可续,手足断安可续?所以,说实话,在你最困难的时候,兄弟才是最可靠的,兄弟才永远不会抛弃你
  • ~
  • 正如美女都不在大街上逛一样,高手根本不在群里混。美女去哪里了?多半在私家车里,高手去哪里了?多半在写程序。
  • ~
  • 记住,女人都是假的,狗才永远不会背叛你,狗是唯一爱你比爱自己还多的东西。
  • ~
  • 我觉得疯狂没什么不好,十九世纪初,当时的科学理论认为凡是比空气重的东西都不可能长时间飞行,所以他们认为莱特兄弟是疯子。可最后疯子赢了,正因为有他们这些疯子,今天我们才能坐飞机,才能放卫星。疯子都是不要命的,怕死的怕不怕死的,不怕死的怕不要命的,所以疯子的力量大啊!在我看来,程序员只有一种――疯狂的程序员。
  • ~
  • 人就是如此,哪怕你某一方面牛上天了又如何?你还不是有不懂的地方。比尔•盖茨不会拍戏,张艺谋不会唱歌,周杰伦不会写程序。
  • ~
  • 现在,最重要的是,我们要好好研究一下如何才能把技术变成钱,否则,我们就永远只是IT界挖沙的民工。”

励志话语

便利店缺收银,酒店少清洁工,厨房缺配菜小二,却没有大公司少我这样一个高不成低不就的少年。

如果让我选择创业项目,我肯定不会轻易卖衣服、做餐饮。选择一年不开张,开张吃一年的项目,或许是一个明智的选择。

人才,那就是在恶劣环境下,把事情摆平的人,与学历无关,与道德无关

人的言行举止,是思想的综合表达。

步行街四肢发达的中年男人,每天收集矿泉水瓶子卖钱糊口,这是思想的匮乏导致贫穷,眼里盯着矿泉水瓶子,这世界便只有瓶子值钱了。

写字楼里,澳大利亚不知名大学镀金的海归女,张口闭口偶尔冒出几句英语,竟也显得洋气,看不上前台工作,几经面试混个外资企业行政助理月入8000元。步行街的中年男人比海归女差吗?眼光不同,思想不同,选择不同而已。

Web site techniques web_site_techniques

网站的使用技巧一

静态文件掉存储 在知乎上看到 使用 七牛云存储有效防止流量问题 和速度问题

各种api接口,比如天气短信等身份证等等对接 聚合

加入广告 如果你开发了自己的移动应用,想添加广告来赚钱,推荐芒果广告。它是一个综合的移动广告平台,聚合了百度广告、多盟广告、易传媒等等广大广告商,可以自己定制广告显示的内容及样式,可以选择各种广告的投放比例。广告点击量、展现量详细报告、收入分析详细数据应有尽有。推荐一下。 同知乎看到芒果广告

reStructuredText标记规范(转)

注解

本文档为技术规范细节,而非教程或入门。如果你第一次接触 reStructuredText,请先阅读 ReStructuredText介绍ReStructuredText快速开始 用户手册。

reStructuredText 是使用简单直观结构来展示文档结构的纯文本。这些结构无论是原生状态还是处理后的形式都相当易于阅读。这篇文档本身就是一个reStructuredText的例子(原生的,如果你阅读的是文本文件;或处理后的,如果你阅读的是一个HTML文档)。reStructuredText解析器是 Docutils 的一个组件。

简单的隐式标记用于展示特殊的结果,如段落标题、无序列表和强调。使用的标记尽可能小且不显眼。reStructuredText基本语法中不常用的结构和扩展可能会更显眼或使用显式标记。

reStructuredText适用于任何长度的文档,从非常小的(如行内程序文档片段 -- Python文档字符串)到非常大的(本文档)。

第一节通过例子展示了reStructuredText标记的语法快速概览。完整的规范可以在 语法细节 章节看到。

文本块 (其中的标记不被处理)用于展示本文档的示例,其以纯文本的形式来说明标记。

语法快速概述

reStructuredText文档由正文或块级元素组成,可以使用章节的方式来组织。 章节 表现为标题风格(下划线和可选的上划线)。章节包含正文元素及/或子标题。某些正文元素包含其他元素,如列表包含列表项,列表项包含段落及其他正文元素。其他的,如段落包含文本和 行内标记 元素。

这是 正文元素 的例子:

  • 段落 (和 行内标记 ):

    段落包含文本,还可能包含行内标记: *强调* 、 **特别强调** 、 `解释文本` 、 ``行内文本`` 、 独立超链接(http://www.python.org)、扩展超链接(Python_)、内部交叉引用(example_)、脚注引用([1]_)、引文引用([CIT2002]_)、替代引用(|example|)和 _`行内内部目标`.
    
    段落使用空行分隔,左对齐。
    
  • 五种类型的列表:

    1. 无序列表:

      - 这是无序列表
      
      - 序号可以是"* + -"
      
    2. 有序列表:

      1. 这是一个有序列表
      
      2. 序号可以是任何数字、字母或罗马数字
      
    3. 定义列表:

      是什么
            定义列表关联一个术语到一个定义
      如何
            术语是一个一行句子,定义是一个或多个段落或正文元素,使用缩进联系到术语
      
    4. 字段列表:

      :是什么: 字段列表映射字段名到字段体,如数据库记录。它们通常是扩展语法的一部分
      :怎么样: 字段标记是一个冒号,字段名,另一个冒号
      
    5. 选项列表 ,用于列出命令行选项:

      -a            命令行选项"a"
      -b file       选项可以有参数和描述
      --long        选项也可以长
      --input=file  长选项也可以有参数
      /v            DOS/VMS风格的选项也行
      

      选项与描述之间需要至少两个空格

  • 文本块:

    文本块可以是缩进或段落最后的行前缀块(表现为两个冒号"::")::
    
        if literal_block:
          text = 'is left as-is'
          spaces_and_linebreaks = 'are preserved'
          markup_processing = None
    
  • 引用块:

    引用快包括缩进的正文元素:
    
      Tis theory, that is mine, is mine.
    
      -- Anne Elk (Miss)
    
  • 测试文档块::
    >>> print 'Python专用用例;以">>>"'
    Python专用用例;以">>>"
    >>> print '(cut and pasted from interactive Python sessions)'
    (cut and pasted from interactive Python sessions)
    
  • 表格 有两种语法:

    1. 网格表格 :完整的,但复杂、冗长:

      +------------------------+------------+----------+
      | Header row, column 1   | Header 2   | Header 3 |
      +========================+============+==========+
      | body row 1, column 1   | column 2   | column 3 |
      +------------------------+------------+----------+
      | body row 2             | Cells may span        |
      +------------------------+-----------------------+
      
    2. 简单表格 :简单且紧凑,但有限制:

      ====================  ==========  ==========
      Header row, column 1  Header 2    Header 3
      ====================  ==========  ==========
      body row 1, column 1  column 2    column 3
      body row 2            Cells may span columns
      ====================  ======================
      
  • 显式标记块 都是以一个显式块标记,两个点和一个空格:

    • 脚注:

      .. [1] 个脚注包含正文元素、最少3个空格的一致缩进
      
    • 引文:

      .. [CIT2002] 似脚注,除了标签是文本
      
    • 超链接目标:

      .. _Python: http://www.python.org
      
      .. _example:
      
      上面的"_example"指向这一段
      
    • 指令:

      .. image:: mylogo.png
      
    • 替代定义:

      .. |symbol here| image:: symbol.png
      
    • 注释:

      .. 注释以两个点和一个空格开始。可以接除了脚注/引文、超谅解、指令或替代定义之外的任何东西。
      
语法细节

下面的描述列出了"文档树元素"(文档树元素名称、XML DTD通用标识符)所对应的语法结构。想查看元素层次结构的细节,请阅读 Docutils文档树Docutils通用DTD XML文档类型定义。

空格

议使用空格进行 缩进 ,但tab也可以使用。tab会转换为空格。tab会停在每个第八列。

其他空白字符(form feeds [chr(12)] and vertical tabs [chr(11)])会在处理前转为单个空格。

空行

空行用于分隔段落和其他元素。除了在文本块(所有的空格被保留)中之外,多个连续的空行相当于一个空行。当标记使元素分离不明确时,空行会被忽略。文档的第一行会被当做其之前有一个空行,文档的最后一行会被当做其之后有一个空行。

缩进

缩进是用来表示引用块、定义(在定义列表项中)和本地嵌套内容的唯一重要标示:

  • 列表项内容(列表项多行内容和一个列表项中多个正文元素包括嵌套列表)
  • 文本块的内容
  • 显式标记块的内容

任何文本的缩进少于当前级别,会结束当前级别的缩进

因为所有的缩进都是重要的标志,因此缩进的级别应当一致。例如,缩进是引用块的唯一标记:

这是一个顶级段落。

该段落属于一级引用块。

一级引用块的第二段。

一个引用块内的多级缩进会导致复杂的结构:

这是一个顶级段落。

该段落属于一级引用块。

该段落属于二级引用块

另一个顶级段落

这一段属于二级引用块。

这一段属于一级引用块。上面的二级引用块在这个一级引用块里面。

当一个段落或其他结构有不止一行文本,行应该左对齐:

这是一个段落。段落各行
左对齐。

    这个段落有问题。行
没有左对齐。除了潜在的误解,还会
  由解析器生成警告和/或错误信息。

几种结构以同一个标记开始,结构体必须以缩进与标记联系。对于使用简单标记的结构(无序列表有序列表脚注引文超链接目标指令注释 ),正文的缩进级别由文本第一行的位置决定,与标记在同一位置。举例,无序列表体必须必子弹字符缩进至少2列:

- 这是无序列表项目的段落的第一行。
  所有行必须与这一行对齐。 [1]_

      这个缩进段落解释为一个引用块

因为其没有充分缩进,
这个段落不属于列表项。

.. [1] 这里是脚注。第二行与
   注标签对齐。".."标记
   用于决定缩进。

对于使用复杂标记( 字段列表选项列表 )的结构,标记可能包含任意文本,标记后的第一行的缩进决定了正文的左边。举例,字段列表可能有非常长的标记(包含字段名):

:Hello: 这个字段有一个很短的名字,因此
        对齐到第一行就行了。

:Number-of-African-swallows-required-to-carry-a-coconut: 这个
    很难将字段体对齐到第一行左边。甚至可能与标记不在同一行开
    始字段体。
转义机制

7位ASCII普遍适用,是有限的。不管用什么字符作标记,它们都会在文本中具有多重意义。因此,标记字符在文本中有时会出现,而不被认为是标记。任何严谨的标记系统都需要一个转义机制来重写标记字符的默认含义。我们使用与其他常用领域相同的转义字符,反斜杠。

反斜杠可以将任何非空白字符转义为字符。转义的字符表示字符本身,并阻止其在标记中扮演任何角色。反斜杠会在输出时去除。反斜杠文本用两个反斜杠表示(第一个反斜杠转义第二个,阻止其变被解释为转义角色)。

反斜杠转义空白字符会被从本文档中删除。在字符级 行内标记 中是允许的。

在两种上下文中反斜杠没有特殊含义:文本块和行内文本。在这些上下文中,单个的反斜杠表示反斜杠文本,无须重复。

注意:reSturcturedText规范和解析器不处理文本输入的表示或提取的问题(文本以如何和以何种形式到达解析器)。反斜杠与其他字符可能在特定的上下文中作为转义字符,其必须被合适的处理。例如,Python在字符串中使用反斜杠来转义特定字符,而不是其他的。在Python文档字符串中出现反斜杠最简单的处理方法就是使用原生文档字符串:

r"""This is a raw docstring.  Backslashes (\) are not touched."""
引用名称

简单引用名称是由字母和内部连字符、下划线、点、冒号和加号组成的单个单词,不能有空白或其他字符。脚注标签(脚注脚注引用 )、引文标签(引文引文引用 )、解释文本 角色以及某些 超链接引用 使用简单引用名称语法。

引用名称使用标点符号或短语(2个或更多空格分隔的单词),被称为“短语引用”。短语引用由在反引号封闭的短语表示,并将反引号文本作为引用名称:

想要学习 `我最喜欢的编程语言`_ ?

.. _我最喜欢的编程语言: http://www.python.org

简单引用名称也可以可选的使用反引号。

引用名称是空白中立的且不区分大小写。在内部解析引用名称时:

  • 空白会被归一(一个或多个空格、横向或纵向的tabs、新行、换行会被解释为一个空格)
  • 大小写会被归一(所有字母被转为小写)

举例,如下 超链接引用 是等价的:

- `A HYPERLINK`_
- `a    hyperlink`_
- `A
  Hyperlink`_

超链接脚注引文 对于引用名称共享相同的命名空间。引文的标签(简单引用名称)和手动编号脚注(数字)会进入相同的数据库作为其他超链接名称。这意味着一个可以被脚注引用([1]_)指向的脚注(定义为".. [1]")也可以被纯超链接引用 (1)指向。当然,每个类型的引用(超链接、脚注、引文)可能会以不同的方式处理和渲染。应该注意避免引用名称混淆。

文档结构
文档

文档树元素:文档

解析过的reStructuredText文档的顶级元素是"文档"元素。在初始化解析之后,文档元素是一个文档片段的简单容器,包含 正文元素过渡章节 ,但不包括文档标题或其他目录元素。调用解析器的代码可以选择运行一个或多个可选的post-parse transforms ,将文档片段重新组织为一个带有标题和其他可能的元数据的完整文档(作者、日期等等。详见 目录字段 )。

具体来说,没有办法在reStructuredText中显式的表示文档的标题和子标题。作为替代,一个长的顶级章节标题(见下面的 章节 )可以作为文档标题。类似的,紧跟在"文档标题"之后的长的二级章节标题,可以作为文档的子标题。其他所有章节会提升一到两级。详见:文档标题转换

章节

文档树元素:章节、标题

章节通过其标题识别,在标题文本下使用下划线进行标记或下划线和匹配的上划线。下划线/上划线是单个重复的标点字符,从左边第一列开始最少到与文档标题右边对齐。具体来说,一个下划线/上划线字符可以是任何非字母打印7位ASCII字符 [1] 。当使用上划线时,上划线的长度与使用的字符必须与下划线相同。可以有任意数字级别的章节标题,但某些输出格式可能有限制(HTML只有6级标题)。

[1]

下面是有效的章节标题装饰字符:

! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~

有一些字符比其他字符更适用,建议使用它们:

= - ` : . ' " ~ ^ _ * + #

相比强加一个固定数字和顺序的章节标题装饰风格,其执行的顺序是碰到每个标题的先后顺序。碰到的第一种类型是最外层标题(如HTML H1),第二种类型则成为子标题,第三种将成为子子标题,以此类推。

下面是章节标题样式的例子:

========
章节标题
========

--------
章节标题
--------

章节标题
========

章节标题
--------

章节标题
````````

章节标题
''''''''

章节标题
........

章节标题
~~~~~~~~

章节标题
********

章节标题
++++++++

章节标题
^^^^^^^^

当一个标题同时有上下划线,标题文本可以插入,类似上述前两个例子。这只是为了美观而非必要的。只有下划线的标题文本 可以插入。

标题后的空行是可选的。到下一个标题的所有文本块或更高级别会包含在章节中(或子章节,等等)。

所有章节标题样式不需要使用,也不需要使用任何特定的段落标题样式。然而,一个文档使用的章节标题必须是一致的:一旦建立了标题样式的层次结构,章节必须使用该层次结构。

每个章节标题会自动生成指向章节的超链接。超链接的文本(即引用名称)与章节标题一致。详见 隐式超链接目标

章节可以包含 正文元素过渡 和嵌套的章节。

过渡

文档元素:过渡

取代小标题,段落之间的额外空间或类型装饰符可用来标记文本分隔或主 题或重点的改变。

(The Chicago Manual of Style, 14th edition, section 1.80)

过渡常见于小说,作为一个跨越一行或多行的间隙,有或没有类似于一行星号的类型装饰符。过渡分隔其他正文元素。过渡不应开始或结束一个章节或文档,两个过渡也不应该直接相邻。

过渡标记的语法是一排至少4个重复的标点符号。该语法与章节标题下划线一样。过渡标记前后需要空行:

段落

----------

段落

不像章节标题下划线,章节标题不需要体系结构。建议使用同一种风格。

处理系统可以以任何其希望的方式在输出中渲染过渡。如,HTML中的<hr>输出是一种明显的选择。

正文元素
段落

文档树元素:段落

段落包含没有任何标记指向其他正文元素的左对齐文本块。使用空行分隔段落及其他正文元素。段落可以包含 行内标记

语法图:

+------------------------------+
| 段落                         |
|                              |
+------------------------------+

+------------------------------+
| 段落                         |
|                              |
+------------------------------+
无序列表

文档树元素:无序列表、列表项

以一个 "*", "+", "-"开头,后面根一个空格的文本块是一个无序列表项。列表项正文必须与bullet缩进左对齐。文本紧接在bullet分隔符之后。例如:

- 这是第一个无序列表项。上面的空行是必须的。两个列表项
  之间的空行(如这一段下面的)是可选的。

- 这是列表第二项的第一个段落

  这是列表第二项的第二个段落。
  这一段上面的空行是必须的。段落的左边与上一个段落对其
  所有的缩进与无序符号对齐。

  - 这是一个子列表。无序符号与上一行的左边对其。
    子列表是一个新的列表,因此要求上下都有空行。

- 这是主列表的第三项

这个段落不是列表的一部分。

下面是一些 错误 的无序列表格式的例子:

- 第一行没问题
列表项与段落之间需要空行(警告)

- 下面一行看似一个新的子列表,但实际上不是:
  - 这是一个连续的段落而非子列表(因为没有空行)
    这一行缩进也不对。
  - 可能会生成警告。

语法图:

+------+-----------------------+
| "- " | list item             |
+------| (body elements)+      |
       +-----------------------+
有序列表

文档树元素:有序列表、列表项

有序列表与无序列表类似,但是用序号而非圆点。序号包含有序成员和格式,之后跟着空格。以下有序序列可以识别:

  • 任意数字:1 2 3 ... (无上限)
  • 大写字母:A B C ... Z
  • 小写字母:a b c ... z
  • 大写罗马数字:I II III IV ... MMMMCMXCIX(4999)
  • 小写罗马数字:i ii iii iv ... mmmmcmxcix(4999)

另外,自动编号符"#"可以用于自动编号列表。自动编号列表可以以显示的编号开始设置序列。完整的自动有序列表使用以1开始的任意数字(自动有序列表为 Docutils 0.3.8新增)

以下格式可以识别:

  • 以点为后缀:"1." "A." "a." "I." "i."
  • 以括号包围:"(1)" "(A)" "(a)" "(I)" "(i)"
  • 以右括号为后缀:"1)" "A)" "a)" "I)" "i)"

解析一个有序列表时碰到下列情况,会开始一个新列表:

  • 碰到与当前列表序号的类型和格式不一致的序号(如,"1."和"a."分属两个列表)
  • 序号不在序列内有序(如,"1"、"3"产生连个独立的列表)

建议使用1 ("1", "A", "a", "I", or "i")作为第一个列表项的序号。当然以其他的数字开始也会被识别,但输出格式可能不支持。任何不以传统的1开始的列表都会生成一个一级[info]系统信息。

使用罗马数字的列表必须以"I/i"或一个多字符值如"II"或"XV"开始。任何其他单字符罗马数字("V", "X", "L", "C", "D", "M")会被解释为一个字母而非罗马数字。 同样,使用字母开始的列表不能使用"I/i",因为其会被识别为罗马数字1。

有序列表项的第二行会被验证。这会阻止原始段落被解释为列表项。例如,下面的文本会被解释为原始的段落:

A. Einstein was a really
smart dude.

但段落仅包含一行必然含糊不清。这段文本被解析为一个有序列表:

A. Einstein was a really smart dude.

如果一个单行段落以序号("A.", "1.", "(b)", "I)", 等等)开始,第一个字符需要转义,以便其被解析为一个段落:

\A. Einstein was a really smart dude.

嵌套的有序列表的例子:

1. Item 1 initial text.

   a) Item 1a.
   b) Item 1b.

2. a) Item 2a.
   b) Item 2b.

语法图:

+-------+----------------------+
| "1. " | list item            |
+-------| (body elements)+     |
        +----------------------+
定义列表

文档树元素:定义列表、定义列表项、术语、分类器、定义

每个定义列表项包含一个术语、可选的分类器和一个定义i。术语是一个简单的一行单词或句子。可选的分类器与术语在同一行,跟在它后面。每个分类器跟在一个行内":"(空格冒号空格)之后。定义是一个块通,过缩进与术语联系,可以包含多个段落和其他正文元素。术语与定义块之间不允许有空格(这区分了定义列表与 引用块 )。定义列表第一行之前和最后一行之后需要空行,中间的列表项是否空行是可选的。例如:

术语 1
    定义 1.

术语 2
    定义 2, 段落 1.

    定义 2, 段落 2.

术语 3 : 分类器
    定义 3.

术语 4 : 分类器 1 : 分类器 2
    定义 4.

行内标记在术语行被解析,在分类器分隔符(":")被识别之前。分隔符仅在出现在任何行内标记之外时被识别。

定义列表可用于多种用途,包括:

  • 作为一个字典或术语表。术语是单词本身,分类细可用于根据用途分类术语(动词、名词等等),定义跟在后面。
  • 用于描述程序变量。术语是变量名,分类器用于区分变量类型(字符串、整形等等),定义描述变量在程序中的用法。定义列表的该用途支持分类器语法 Grouch ,一种描述和执行Python对象约束的系统。

语法图:

+----------------------------+
| term [ " : " classifier ]* |
+--+-------------------------+--+
   | definition                 |
   | (body elements)+           |
   +----------------------------+
字段列表

文档树元素: 字段列表、字段、字段名、字段正文

字段列表作为扩展语法的一部分被使用,如 指令 的选项或等待进一步处理的类数据库记录。它们也被用于两列类列表结构类似于数据库记录(标签和数据对)。reStructuredText应用可以在特定上下文中识别字段名和变形字段或字段正文。例如,阅读下面的 目录字段指令 中的 "图片"和"" 指令 .

字段列表会映射字段名到字段正文,仿照 RFC822 头。一个字段名可以包含任何字符,但字段名中的冒号(":")必须使用反斜杠转义。行内标记被解析为字段名。在进一步处理或传输时,字段名大小写敏感。字段名. 字段名与一个单独的冒号前后缀一起构成字段标记。字段表及之后跟空格和字段正文。字段正文可以包含多个正文元素,缩进到字段标记处。字段名标记之后的第一行决定字段正文的缩进。如:

:Date: 2001-08-16
:Version: 1
:Authors: - Me
          - Myself
          - I
:Indentation: 因为字段标记可能很长,字段正文的第二行
   及随后的行不必与第一行对齐,但必须缩进到字段名标记
   处,且它们应当互相对齐。
:Parameter i: integer

一个多单词字段名中的单个词的解释是应用程序。该应用程序可以为该字段名指定一个语法。例如,第二个单词及其后面的单词可以被视为“参数”,引用短语可以被视为一个单一的参数,并可能会增加直接支持“键=值”的语法。

除了潜在的可能导致误解的标准 RFC822 标题不能用于这种构造是因为它们模糊不清。以一个单词后面跟一个冒号开始一行是一种通用的书写文本。然而,在定义良好的上下文如当一个字段列表总是在文档的开头(PEPS和电子邮件)时,标准RFC822头可以使用。

语法图(简化):

+--------------------+----------------------+
| ":" field name ":" | field body           |
+-------+------------+                      |
        | (body elements)+                  |
        +-----------------------------------+
目录字段

文档树元素: 文档信息、作者、多个作者、组织、 联系方式、版本、状态、日期、版权、字段、主题

当一个字段列表是文档的第一个非注释元素时(只在文档标题之后,如果有),它可以从字段转换为文档目录数据。这个目录数据对应一本书的封面,如标题页和版权页。

特定的注册过的字段名(见下表)会被识别并转换为对应的文档树元素,大部分会变为"docinfo"元素的子元素。对于这些字段,没有顺序要求,但它们会被重新组织以适应文档的结构。 除非另有说明,每一个目录元素的字段正文只能包含一个段落。字段正文会被 RCS关键字 检查和清理。任何不能识别的字段会被作为通用字段保留在docinfo元素中。

注册过的目录字段名和它们对应的文档树元素如下:

  • 字段名 "Author": 作者元素
  • "Authors": 作者.
  • "Organization": 组织.
  • "Contact": 联系方式.
  • "Address": 地址.
  • "Version": 版本.
  • "Status": 状态.
  • "Date": 日期.
  • "Copyright": 版权.
  • "Dedication": 主题.
  • "Abstract": 主题.

"Authors"字段可以包含: 一个包含作者列表(冒号或逗号分隔)的段落;或一个无序列表,其每个元素包含一个单独的段落每作者。首先检查";",因此"Doe, Jane; Doe, John"是可以的。如果单个饼子包含逗号,使用分号结束它: ":Authors: Doe, Jane;"。

"Address"字段用于多行邮件地址。新行和空格会被保留。

"Dedication"和"Abstract"字段可以包含任意正文元素。每种一个。它们会称为紧跟在docinfo元素之后的使用"Dedication"或"Abstract"标题(或语言相等)的主题元素。

这个字段名到元素的映射可以替换为其他语言。详见 文档信息转换 实现文档。

未注册/通用字段可以包含一个或多个段落或任意正文元素。

RCS关键字

被解析器识别的 目录字段 通常会检查并清理 RCS [2] 关键字 [3] 。RCS关键字会作为"$keyword$"进入源文件,一旦存储为 RCS 或 CVS [4] ,它们会扩展为"$keyword: expansion text $"。例如,一个"Status"字段会被转换为一个"status"元素:

:Status: $keyword: expansion text $
[2]修订控制系统(Revision Control System)。
[3]RCS关键字处理可以关闭(未实现)。
[4]并发版本系统(Concurrent Versions System)。CVS使用与RCS相同的关键字。

处理后,"status"元素的文本会变为简单的"扩展文本"。美元分隔符和开头的RCS关键字名会被去除。

RCS关键字仅处理目录上下文(文档标题,如果有,之后的文档中第一个非注释结构)中的字段列表。

选项列表

文档树元素: 选项列表、选项列表项、选项组、选项、选项字符串、选项参数、描述

选项列表是一个包含命令行参数和描述的两列列表,用于记录程序的选项。例如:

-a         输出全部
-b         都输出(该描述有点
           长)
-c arg     只输出参数
--long     整天输出

-p         这个选项的描述有两段
           这是第一段

           这是第二段。选项间的空行可能被
           忽略(像上面一样)或左对齐(像这里一样)

--very-long-option  一个VMS风格的选项。注意调整
                    必须的两个空格

--an-even-longer-option
           表述也可以从另一行开始

-2, --two  这个选项有两个变量

-f FILE, --file=FILE  这两个选项是同义词;
                      都有参数。

/V         一个VMS/DOS风格的选项

reStructuredText能够识别几种类型的选项:

  • POSIX短选项,由连字符和选项字符组成
  • POSIX长选项,由两个连字符和一个选项单词组成;某些系统 使用一个连字符。
  • 老式GNU风格"plus"选项,由一个plus和选项字符组成("plus" 选项已经被废弃了,不鼓励使用它们)。
  • DOS/VMS选项,由一个斜杠和一个选项字符或单词组成。

请注意:DOS或Windows软件可能使用POSIX风格和DOS/VMS风格的选项。 这些和其他变体有时可能会混合使用。选择上面的名字只是为了方便。

POSIX长/短选项的语法基于Python的 getopt.py 模块所提供的语法, 其实现一个类似于 GNU libc getopt_long() 函数但有某些约束的 选项解析器。有许多不同的选项系统,reStructuredText并非全部都 支持。

尽管POSIX长选项和DOS/VMS选项单词可能允许在使用命令行时被操作 系统或应用程序截取,但reStructuredText并不展示或支持这种方式。 应提供完整的选项单词。

选项可以跟在一个参数占位符之后,其角色和语法应该被解释为描述 文本。使用空格或等号作为选项与选项参数占位符之间的分隔符;短 选项(只有"-"或"+"前缀)可能会省略分隔符。选项参数有两种形式:

  • 字母([a-zA-Z])开头,其后紧跟字母、数字、下划线和连字符 ([a-zA-Z0-9_-])。
  • 以尖括号(<)开始,以反尖括号(>)结束;中间可以是除此 之外的任何字符。

多选项"同义词"可以列出并共享同一个描述。以逗号空格分隔。

选项和描述之间至少需要两个空格描述可以包含多个正文元素。选项 标记分隔符后的第一行缩进为描述。与其他类型的列表类似,第一个 列表项之后和最后一个列表项之后需要一个空行,中间的空行可选。

语法图(简化):

+----------------------------+-------------+
| option [" " argument] "  " | description |
+-------+--------------------+             |
        | (body elements)+                 |
        +----------------------------------+
文本块

文档树元素: 文本块

一个包含两个冒号("::")的段落表示接下来的文本由文本块组成。文本块 必须缩进或引用(看下面)。文本块内的任何标记都不会被处理。它会被 留下,通常适用等快字体渲染:

这是一个典型的段落,后面跟着一个缩进的文本块。

::

    for a in [5,4,3,2,1]:   # this is program code, shown as-is
        print a
    print "it's..."
    # a literal block continues until the indentation ends

这段文本恢复了缩进,其在文本块之外,因此会被当做普通的段落。

只包含"::"的段落会在输出时完全移除;不会保留空段落。

为了方便,段落结尾处的"::"可以被识别。 如果后面紧跟空格,输出时两个冒号都会被移除。当文本之后紧跟"::", 其中 一个 冒号违背保留(如,"::"会变为":")。

换句话说,这些全部是等价的(请注意段落之后的冒号):

  1. 扩展形式:

    段落:
    
    ::
    
        文本块
    
  2. 部分最小化形式:

    段落: ::
    
        文本块
    
  3. 完全最小化形式:

    段落::
    
        文本块
    

所有的空白(包括折行,但不包括对于缩进文本块最低限度的缩进)会被保留。 前后各需要一个空行,但这些空行不被认为是文本块的一部分。

缩进文本块

缩进文本块通过缩进关联到包围的文本(每行以空白开头)。缩进文本块的每 一行的最低限度的缩进会被移除。该文本块不需要是连续的,缩进文本的章 节之间允许空行。该文本块以缩进的结束而结束。

语法图:

+------------------------------+
| paragraph                    |
| (ends with "::")             |
+------------------------------+
   +---------------------------+
   | indented literal block    |
   +---------------------------+
引用文本块

引用文本块是非缩进的连续文本块,其每一行以相同的非字母可打印7位ASCII 字符 [5] 开始。引用文本快由空行结束。引用文本快会在处理过的文档中保 存。

[5]

以下是所有有效缩进字符:

! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~

注意:这与有效的 章节 标题装饰相同。

语法图:

+------------------------------+
| paragraph                    |
| (ends with "::")             |
+------------------------------+
+------------------------------+
| ">" per-line-quoted          |
| ">" contiguous literal block |
+------------------------------+
行块

文档树元素: 行块、行(Docutils 0.3.5新增)

行块对于地址块很有用。诗(诗歌、歌词)和无装饰列表等行结构有重要 意义。行块是一组由竖线("|")前缀开头的行。每个竖线前缀表示一个新 行,因此折行会被保留。初始缩进对于嵌套结构也很重要。支持行内标 记。连续行辈包装为一个长的行,他们以一个空格代替竖线开始,左边 必须对其,但不需要与上面的文字的左边对其。行块以空行结束。

这个例子展示了连续行:

| Lend us a couple of bob till Thursday.
| I'm absolutely skint.
| But I'm expecting a postal order and I can pay you back
  as soon as it comes.
| Love, Ewan.

这个例子展示了嵌套的行块,通过初始缩进表示新行:

Take it away, Eric the Orchestra Leader!

    | A one, two, a one two three four
    |
    | Half a bee, philosophically,
    |     must, *ipso facto*, half not be.
    | But half the bee has got to be,
    |     *vis a vis* its entity.  D'you see?
    |
    | But can a bee be said to be
    |     or not to be an entire bee,
    |         when half the bee is not a bee,
    |             due to some ancient injury?
    |
    | Singing...

语法图:

+------+-----------------------+
| "| " | line                  |
+------| continuation line     |
       +-----------------------+
引用块

文档树元素: 引用块、属性

一个以缩进与前面的文本关联的文本块,前面没有标记表示其为文被快或其他内容的,是引用块。里面的所有标记会被连续处理(对于正文元素和行内标记):

这是一个原始段落,介绍引用快。

    "It is my business to know things.  That is my trade."

    -- Sherlock Holmes

引用块可能以一个属性结束: 以"--"或"---"开始的文本块。如果属性包含多行,第二行及随后的行必须对其。

如果以属性结束,可能连续出现多个引用快。

非缩进段落

引用块 1.

—属性 1

引用块 2.

空注释 用于显式的结束前面可能会被当做一个引用块的结构:

* 列表项

..

    引用块 3.

空注释也可以用来分隔引用快:

    引用块 4.

..

    引用块 5.

前后均需要空行,但空行不是引用块的一部分。

语法图:

+------------------------------+
| (current level of            |
| indentation)                 |
+------------------------------+
   +---------------------------+
   | block quote               |
   | (body elements)+          |
   |                           |
   | -- attribution text       |
   |    (optional)             |
   +---------------------------+
测试文档块

文档树元素: 测试文档块

测试文档块是交互式Python会话剪切粘贴到文档字符串。它们是通过例子来做 使用说明,并通过Python标准库中的 测试文档模块 一个优雅且强大的测试环境。

测试文档块是以Python交互式解释器的主要提示符 ">>> " 开头的文本块, 并以空行结束。测试为本快会被当做文本块的特殊例子,不需要使用文本块 语法。如果都提供了,文本块语法优先于测试文本块语法:

这是一个原始段落。

>>> print '这是一个测试文本块'
这是一个测试文本块

以下是一个文本块::

    >>> 这里不会被识别为测试文本块。但它仍 *会* 被测试文档模块
    识别。

测试文档块不需要缩进。

表格

文档树元素: 表格、表格组、行、表头、表正文、行、入口

ReStructuredText提供两种语法来处理表格单元: 网格表格简单表格

类似于其他正文元素,表格前后都需要空行。表格应当与前面的文本块左对齐。 如果缩进,表格会被当做引用块的一部分。

一旦隔离,每个表格单元会被当做一个小型文档;顶部和底部的单元格分界线 作为分隔空行。每个单元格包含0个或多个正文元素。单元格的内容可以包含左 和/或右边距,其会在处理时删除。

网格表格

网格表格通过类网格"ASCII art"提供一个完整的表格表示。网格表格允许任意 单元格内容(正文元素),及跨行和列。但网格表格难以生成,特别是对于简单 数据集合来说。 Emacs表格模式 是一个Emacs中允许简单编辑网格表格的 工具。查看 简单表格 以获取一个简单(但有限制)的表示。

网格表格通过字符"-"、"="、"|"和"+"被描述为一个视觉网格。连字符("-")被 用于行行(行分隔符)。等号("=")可以用作分隔可选的标题行与表格正文(不被 Emacs表格模式 支持)。竖线 ("|")用于竖行(列分隔符)。加号用于横行与竖行的交叉。例如

+------------------------+------------+----------+----------+
| Header row, column 1   | Header 2   | Header 3 | Header 4 |
| (header rows optional) |            |          |          |
+========================+============+==========+==========+
| body row 1, column 1   | column 2   | column 3 | column 4 |
+------------------------+------------+----------+----------+
| body row 2             | Cells may span columns.          |
+------------------------+------------+---------------------+
| body row 3             | Cells may  | - Table cells       |
+------------------------+ span rows. | - contain           |
| body row 4             |            | - body elements.    |
+------------------------+------------+---------------------+

必须小心避免不需要的一起活动。例如,下面的表格第2行包含一个横跨三列的 单元格,从第二列到第四列

+--------------+----------+-----------+-----------+
| row 1, col 1 | column 2 | column 3  | column 4  |
+--------------+----------+-----------+-----------+
| row 2        |                                  |
+--------------+----------+-----------+-----------+
| row 3        |          |           |           |
+--------------+----------+-----------+-----------+

如果在单元格文本中使用了竖线,它会起到非缩进效果(如果与列分界线对其):

+--------------+----------+-----------+-----------+
| row 1, col 1 | column 2 | column 3  | column 4  |
+--------------+----------+-----------+-----------+
| row 2        | Use the command ``ls | more``.   |
+--------------+----------+-----------+-----------+
| row 3        |          |           |           |
+--------------+----------+-----------+-----------+

有几种解决办法。所有都是只需要将连续的单元格分开。一个可行的办法 是变换文本,在前面添加额外的空格:

+--------------+----------+-----------+-----------+
| row 1, col 1 | column 2 | column 3  | column 4  |
+--------------+----------+-----------+-----------+
| row 2        |  Use the command ``ls | more``.  |
+--------------+----------+-----------+-----------+
| row 3        |          |           |           |
+--------------+----------+-----------+-----------+

另一个可行的办法是在其中添加额外的行:

+--------------+----------+-----------+-----------+
| row 1, col 1 | column 2 | column 3  | column 4  |
+--------------+----------+-----------+-----------+
| row 2        | Use the command ``ls | more``.   |
|              |                                  |
+--------------+----------+-----------+-----------+
| row 3        |          |           |           |
+--------------+----------+-----------+-----------+
简单表格

简单表格为简单数据集合提供一个简洁、容易的输入但有限的行导向的 表格表示方式。单元格的内容是典型的单个段落,但任意的征文元素可 以表现在大部分单元格中。简单表格允许跨行的行(除了第一列之外的 所有行)和跨列,但不允许跨行。参见上面的 网格表格 以获取完整的 表格表现形式。

简单表格被描述为使用由等号"="组成的横向边框和连字符"-"组成。等号 ("=")用于表格的顶部和底部边框,也用于分隔可选的标题行。连字符 ("-")通过下划线合并列,用于在一个单行中展示列,可以可选的用于显式 和/或可视的分隔行。

一个简单表格以由等号组成的顶部边框和以空格(建议两个或以上)作为 每列的分界线开始。无论何种跨度,顶边 必须 完全描述整个表格列。 表格必须有至少两列(以便将其与章节标题区分开)。顶边之后可以是标 题行,且最后一个可选标题行以'='作为下划线及以空格作为列分界线。标 题行分隔符下不可以有空行;其会被解释为表格的底边。表格的底边分界线 由'='下划线组成,也以空格分隔列边界。例如,下面是一个正向表格,一个 三列表格,包含一个标题行和4个正文行:

=====  =====  =======
  A      B    A and B
=====  =====  =======
False  False  False
True   False  False
False  True   False
True   True   True
=====  =====  =======

下划线'-'可以用于展示列跨度。列跨度下华夏必须完整(必须覆盖所有列)并与 已建立的列边界对其。包含列跨度下划线的文本行不能包含任何其他文本。一个 列跨度下划线仅对其紧邻的上一行起效。例如,下面是一个在标题中包含列跨度 的表格:

=====  =====  ======
   Inputs     Output
------------  ------
  A      B    A or B
=====  =====  ======
False  False  False
True   False  True
False  True   True
True   True   True
=====  =====  ======

每一行文本必须在列边界处包含空格,处理被列跨度合并的单元格。每行文本 开启一个新行,除非第一列有一个空行。如果是那样,该行文本被解析为连续 行。因此,新行( 连续行)的第一列单元格单元格 必须 包含某些文本; 空单元格会导致误解(但看看下面的tip)。同时,该机制限制第一列单元格为单 行文本。如果不能接受这些限制,请使用 网格表格

小技巧

要在第一列没有文本需要处理输出的简单表格中开启一个新行,使用下列一种:

  • 一个空注释(".."),其会在输出时忽略掉(详见 注释 )
  • 一个反斜杠转义("\")后面跟一个空格(见上面的 转义机制 )

下划线'-'也可用于可视的分隔行,即使没有跨列。这对行里有许多行的长表格特 别有用。

简单表格内允许空行。它们的表现取决于上下文。行 之间 的空行会被忽略。 多行行 的空行能够分隔单元格中的段落或其他正文元素。

最右边的列是无限的;文本可以超出表格边界(表格边框表示)。但建议输入足够 长的边界来包含整个文本。

下面的例子展示了连续行(第二行包含2航文本,第三行包含四行文本)、一个空行分 隔段落(第三行第二列)、文本扩展到超出表格右边和第一列中没有文本需要处理 输出的新行(第四行):

=====  =====
col 1  col 2
=====  =====
1      Second column of row 1.
2      Second column of row 2.
       Second line of paragraph.
3      - Second column of row 3.

       - Second item in bullet
         list (row 3, column 2).
\      Row 4; column 1 will be empty.
=====  =====
显式标记块

显示标记快是一个文本块:

  • 其第一行以".."之后紧跟空格(显式标记开始)开始
  • 其第二行和接下来的行(如果有)以缩进与第一行关联
  • 以非缩进行结束

显式标记块与无序列表项相似,使用".."作为无序符号。文本紧跟在显式标记 开始分隔符缩进的正文块。最常用缩进总是会被从第二行及其后的行的正文块 中删除。因此,如果第一个结构满足只有一行,且第一和第二个结构的缩进应 该不一样,第一个结构应该不与显式标记开始的地方同一行。

显式标记块和其他元素之间需要空行,但显式标记块之间的空行是可选。

显式标记语法用于脚注、引文、超链接、指令、替代定义和注释。

脚注

文档树元素: 脚注、标签

每个脚注由一个以(".. ")开头的显式标记、一个左方括号、一个 脚注标签、一个右方括号和一个空格组成。脚注标签可以是:

脚注内容(正文元素)必须包含缩进(至少3个空格)并且左对齐。脚注的第一个正文元素一般与脚注标签在同一行中。但如果第一个元素适合单独成行,且缩进与其他元素不同,那么第一个元素必须在脚注标签下一行开始,否则无法检测到缩进的区别。

脚注可以在文档的任何位置,而非仅在末尾。在哪里及怎样处理后输出取决于处理下系统。

这是一个手动编号脚注:

.. [1] 这是正文元素

每个脚注自动生成一个指向自己的超链接目标。超链接目标名字的文本与脚注标签相同。自动编号脚注 生成一个数字标签及引用名。详见 隐式超链接目标

语法图:

+-------+-------------------------+
| ".. " | "[" label "]" footnote  |
+-------+                         |
        | (body elements)+        |
        +-------------------------+
自动编号脚注

一个数字符号("#")可以用作脚注标签的第一个字符以便自动编号脚注或脚注引用。

第一个需要自动编号的脚注的标签为"1",第二个为"2",依次类推(如果没有手动编号脚注出现;详见 混合手动和自动编号脚注自动编号脚注 )。一个标签为"1"的脚注会生成一个名为"1"的隐式超链接目标,就像该标签被显式的指定了。

脚注在使用自动编号的同时还可一个显式的指定一个标签: [#label]。这些标签称为自动编号标签。自动编号标签做两件事:

  • 在脚注上,它生成一个超链接目标,其名字为自动编号标签(不包括"#")

  • 它允许一个自动编号脚注被多次引用,就像是一个超链接引用。例如:

    如果 [#note]_ 是第一个脚注引用,它会表示为"[1]"。我们可以将其作为[#note]_ 再次指向它并在次看到"[1]"。我们也可以将其作为note_ (一个原
    始内部超链接引用)再次指向它
    
    .. [#note] 这是标签为"note"的脚注。
    

编号由脚注的顺序决定,而非引用的顺序。对于没有自动编号标签 ([#]_)的脚注引用,脚注和脚注引用必须必须以相同的编号关 联,但无需使用lock-step替代。如:

[#]_ 是指向脚注1的引用,[#]_ 是指向脚注2的引用

.. [#] 这是脚注 1.
.. [#] 这是脚注 2.
.. [#] 这是脚注 3.

[#]_ 这是指向脚注3的引用

如果脚注包含自动编号引用或多个引用在相近的位置生成,则必须 特别小心。脚注和引用会被按照其在文档中生成的顺序记录,这与 人们阅读的顺序不一定相同。

自动符号脚注

一个星号("*")可以用于需要自动符号生成脚注标签。星号可以是标 签中的单个字符。例如:

只是一个符号脚注引用: [*]_ 。

.. [*] 这是脚注。

符号会被转变为标签指向对应的脚注和脚注引用。引用的数量必须 与脚注的数量相等。一个符号脚注不可以被多次引用。

标准Docutils系统使用如下符号和脚注标记 [6]:

  • 型号 ("*")
  • dagger (HTML character entity "&dagger;", Unicode U+02020)
  • double dagger ("&Dagger;"/U+02021)
  • 章节标记 ("&sect;"/U+000A7)
  • 段落标记 ("&para;"/U+000B6)
  • 数字符号 ("#")
  • 黑桃 ("&spades;"/U+02660)
  • 红心 ("&hearts;"/U+02665)
  • 方片 ("&diams;"/U+02666)
  • 梅花 ("&clubs;"/U+02663)
[6]这个列表受到了"Note Reference Marks"符号列表(The Chicago Manual of Style, 14th edition, section 12.51.)的影响。

如果需要多余10个符号,相同的符号会被重用、双用或三用,依此类推("**"等等)。

注解

当使用自动符号脚注时,选择输出的编码很重要。许多符号 在特定的普通文本(如使用Latin-1编码的)中无法被编码。建议使 用UTF-8作为输出编码。对于HTML和XML输出,可以使用 "xmlcharref替代" 输出编码错误处理程序.

混合手动和自动编号脚注

手动和自动脚注编号可能在同一个文档中使用,因此结果有时会不符合 预期。手动编号优先级较高。只有未使用的脚注编号会分配给自动编号 脚注。下面的例子可以展示这点:

[2]_ will be "2" (manually numbered),
[#]_ will be "3" (anonymous auto-numbered), and
[#label]_ will be "1" (labeled auto-numbered).

.. [2] 这个脚注是手动标签,因此数字被固定了。

.. [#label] 这个自动编号标签会是"1"
   它是第一个自动编号脚注,且没有其他标签为"1"的脚注存在。
   脚注的顺序用于决定数字,而非脚注引用。

.. [#] 这个脚注的标签为"3"。它是第二个自动编号脚注,但脚注
   标签"2"已经被占用了。
引文

引文被展示位非数字标签的脚注,如``[note]``或``[GVR2001]``。引文 标签是简单的 引用名称 (大小写不敏感的单个单词,包含由连字符连 接的字母、下划线和点,不包括空格)。引文会被独立于脚注进行渲染, 如:

这是一个引文引用: [CIT2002]_.

.. [CIT2002] 这是引文。除标签文本外,它类似于脚注。
指令

文档树元素: 取决于指令.

指令是reStructuredText的扩展机制,一种添加支持新结构而不用添加新的 语法(指令支持额外的本地语法)的方法。所有的标准指令(那些已经在 reStructuredText解析器中实现和注册过的)在`指令`_ 文档中都有描述,它 们是特定域的,在处理文档时,可能需要特定操作以使其生效。

例如,这是 图片 如何被定位:

.. image:: mylogo.jpeg

一个 figure (带一个标题的图片)这样定位:

.. figure:: larch.png

   The larch.

一个 admonition (注意、小心,等等)包含其他正文元素:

.. note:: 这是一个段落

   - 这是一个无序列表。

指令由以开始后跟指令类型、两个冒号、空格(一起被称为指令标记)的显式标记展示。指令类型是大小写不敏感的单个单词(字母+单个连字符、冒号、点 不包括空格)。指令类型后使用两个冒号是因为:

  • 两个冒号更有特色,且不太会被用于普通文本

  • 两个冒号可以避免与普通的注释文本冲突:

    .. Danger: modify at your own risk!
    
  • 如果reStructuredText的某种实现不能识别一个指令(如,指令处理器未安装 ),会生成一个3级(error)系统信息,且整个指令块(包括指令本身)会被包含 为一个文本块。因为"::"是一个自然选择。

指令块由指令标记后的指令所在的第一行所包含的任何文本和任何紧跟的缩进文本组成。指令块的解释由指令代码完成。指令块有三个逻辑部分:

  1. 指令参数
  2. 指令选项
  3. 指令内容

个别指令可以采用这些部件的任何组合。指令参数可以是文件系统路径、URL、 标题文本,等等。指令选项使用 字段列表 表示。字段名和内容由指令指 定。参数和选项必须组成一个在指令第一、二行开始的连续的块。空行表示 指令内容块开始了。如果参数和/或选项被指令所使用,必须用一个空行分将他 们与指令内容分隔开。 "figure"指令使用所有这三个部分:

.. figure:: larch.png
   :scale: 50

   The larch.

简单指令可以不需要内容。如果一个指令不使用内容块而后面跟着任何缩进的 文本,会产生一个错误。如果一个引用块后立即是一个指令,使用空注释(见 注释 )分隔它们。

在指令内容块或随后的文本块中,指令和解释文本所作的任何动作都取决于指令。详见 指令

指令是对其内容的处理,它可以被转换成一些可能与原文无关的东西。它也可能被用来作为编译指令、修改解析器的行为,如实验替代语法。目前没有解析器支持此功能。如果发现一个对编译器指令是合理的需求,它们可能会支持。

指令不会生成"指令"元素,它们只是一个"解析器结构",在reStructuredText 以外没有任何意义。解析器会将可以识别的指令变形为文档元素。未知的指令 会触发3级系统信息(错误)。

语法图:

+-------+-------------------------------+
| ".. " | directive type "::" directive |
+-------+ block                         |
        |                               |
        +-------------------------------+
替代定义

文档树元素: 替代定义

替代定义由一个以(".. ")开始后面跟着竖线、替代文本、竖线、空格和 定义块的显式标记。替代文本不能以空格开始或结束。一个替代定义块 包含一个嵌套的行内兼容指令(没有开头的".. "),如" 图片 "或"替代"。举例,:

The |biohazard| symbol must be used on containers used to
dispose of medical waste.

.. |biohazard| image:: biohazard.png

替代定义块直接或间接包含一个子替代引用会发生一个错误。

替代引用 会在行内被处理过的定义对应的内容所替代。匹配是大小写敏感但可以宽容的,如果没有发现匹配,会尝试大小写不敏感的短语。

替代指令允许在行内文本共享强大而灵活的块级 指令 。它们是一种在文本内包含任意复杂行内结构并将细节保存在文本之外的方法。等价于SGML/XML的命 名实例或编程语言的宏。

没有替代机制,无论何时需要具体应用新行内结构,都必须改变语法。 通过与现有的指令语法结合,任何行内结构都可以使用而无需新的语法(除非 可能是一个新指令)。

语法图:

+-------+-----------------------------------------------------+
| ".. " | "|" substitution text "| " directive type "::" data |
+-------+ directive block                                     |
        |                                                     |
        +-----------------------------------------------------+

下面是替代机制的一些例子。请注意,大部分嵌入指令只能在例子中使用,其 尚未被实现。

对象

替代引用可以用于关联含糊的文本到一个唯一的对象识别符

例如,许多网站可能希望实现一个行内"用户"指令:

|Michael| and |Jon| are our widget-wranglers.

.. |Michael| user:: mjones
.. |Jon|     user:: jhl

根据网站的需求,这些可能用于索引文件供以后检索、以各种方式链接文本(邮件,网页,鼠标悬停JavaScript的简介和联系信息,等)或自定义文字显示(包括内联文本,包括旁边的用户名文本,链接图标图像使文字加粗或不同的颜色,等等)。

同样的目的可用于在需要经常引用具有独特标识符但具有模糊的通用名称的的一个特定类型对象的文档中。电影、唱片、书籍、照片、法庭案件和法律都是可能的。例如:

|The Transparent Society| offers a fascinating alternate view
on privacy issues.

.. |The Transparent Society| book:: isbn=0738201448

模块名或类名不明确和/或解释文本不能使用的上下文中的类或函数,是 另一种可能:

4XSLT has the convenience method |runString|, so you don't
have to mess with DOM objects if all you want is the
transformed output.

.. |runString| function:: module=xml.xslt class=Processor
图片

图片是替代引用的一种普遍用法:

West led the |H| 3, covered by dummy's |H| Q, East's |H| K,
and trumped in hand with the |S| 2.

.. |H| image:: /images/heart.png
   :height: 11
   :width: 11
.. |S| image:: /images/spade.png
   :height: 11
   :width: 11

* |Red light| means stop.
* |Green light| means go.
* |Yellow light| means go really fast.

.. |Red light|    image:: red_light.png
.. |Green light|  image:: green_light.png
.. |Yellow light| image:: yellow_light.png

|-><-| is the official symbol of POEE_.

.. |-><-| image:: discord.png
.. _POEE: http://www.poee.org/

"图片"指令已经被实现了。

风格 [7]

替代引用可以用于将行内文本关联到一种扩展定义的表示风格:

Even |the text in Texas| is big.

.. |the text in Texas| style:: big

在某些特定输出上下文(HTML输出的CSS类名、LaTeX风格名,等等)中风格名有有意义的,其会被另一种输出格式(如纯文本)忽略。

[7]有可能有足够的必要的“风格”机制,以保证简单的语法,如扩 展到解释的文本角色的语法。简单的文本样式的替换机制是繁 琐的。
模板

行内标记可能会稍后被一个模板引擎处理。如,一个 Zope 作者可能这么写:

Welcome back, |name|!

.. |name| tal:: 替代 user/getUserName

处理后,这个ZPT的输出结果可能是:

Welcome back,
<span tal:替代="user/getUserName">name</span>!

然后Zope在你某个实际用户的会话中将这个传递给某些类似于"Welcome back, David!"的东西。

替换文本

替代机制可以用于简单的宏替代。替换文本在一个或多个文档中重复多次,特别是在以后可能需要更改时,这可能是适当的。一个简短的例子是不可避免:

|RST|_ is a little annoying to type over and over, especially
when writing about |RST| itself, and spelling out the
bicapitalized word |RST| every time isn't really necessary for
|RST| source readability.

.. |RST| 替代:: reStructuredText
.. _RST: http://docutils.sourceforge.net/rst.html

注意:第一个替代引用的最后的下划线。它表示引用对应的超链接目标。

替代适用于当替换文本不能用其他行内结构表示或非常长的时候:

But still, that's nothing compared to a name like
|j2ee-cas|__.

.. |j2ee-cas| 替代::
   the Java `TM`:super: 2 Platform, Enterprise Edition Client
   Access Services
__ http://developer.java.sun.com/developer/earlyAccess/
   j2eecas/

"替代"指令已被实现.

注释

文档树元素: 注释

任意缩进文本可以跟在显示标记开始且会被处理为一个注释元素的后面。注释块文本不会再做处理。一个注释包含一个单独的"text blob"。取决于输出格 式,注释可能被处理后的输出移除。 对于注释唯一的限制是,它们与其他任何显式标记机构使用不同的语法: 替代定义、指令、脚注、引文或超链接目标。为了确保其他任何显式标记结构 都能被识别,在行中只使用"..":

.. This is a 注释
..
   _so: is this!
..
   [and] this!
..
   this:: too!
..
   |even| this:: !

一个显示标记开始,后面跟着空行且没有其他东西(除了空白)是一个 "empty 注释"。它用于结束一个前面的结构且 需要跟任何缩 进文本。需要一个块引用跟在一个列表或任何缩进结构之后,在它们之间 插入一个空注释即可。

语法图:

+-------+----------------------+
| ".. " | 注释                 |
+-------+                    |
        |                      |
        +----------------------+
行内标记

在reStructuredText中,行内标记是提供给文本块中的单词或句子的。在书写 的文本中使用相同的空白和标点符号用于分隔单词,就是行内标记语法结构。 含有行内标记的文本不能以空白开始或结束。任意 字符级行内标记 都能 被支持,但并不鼓励。行内标记不能嵌套。

有9个行内标记结构。5个结构使用相同的开始字符和结束字符来表示标记:

三种结构使用不同的开始与结构字符:

独立超链接 能被隐式的识别,且不适用额外的标记。

行内标记识别规则

行内标记开始、结束字符只要在所有条件都满足的情况下才会被识别:

  1. 行内标记开始字符必须开始一个文本块或前面紧接着
    • 空白
    • ASCII字符中的一个 - : / ' " < ( [ {
    • 一个使用 Unicode category Pd (Dash)、Po (Other)、 Ps (Open)、Pi (Initial quote)或`Pf` (Final quote) [8] 的非ASCII标点符号。
  2. 行内标记开始字符必须紧跟在非空白之后
  3. 行内标记结束字符必须之后必须是非空白字符
  4. 行内标记结束字符必须结束一个文本块或后面紧接
    • 空白
    • ASCII字符中的一个 - . , : ; ! ? \ / ' " ) ] } >
    • 一个使用 Unicode category Pd (Dash)、Po (Other)、 Ps (Open)、Pi (Initial quote)或`Pf` (Final quote) [8] 的非ASCII标点符号。
  5. 如果一个行内标记开始字符之前是一个ASCII字符 ' " < ( [ { ,或 一个使用Unicode字符category Ps, Pi, or Pf`的字符,其之后必 须是对应的 [#corresponding_quotes]_ 结束字符 `' " ) ] } >`` 或categories Pe, Pf, or Pi.
  6. 行内开始、结束字符之间必须至少有个一个字符
  7. 一个没有转义的反斜杠在开始或结束字符之前会终止标记识别,除非 是 行内文本 结束字符。详见 转义机制
[8](1, 2) Pi (Punctuation, Initial quote) characters are "usually closing, sometimes opening". Pf (Punctuation, Final quote) characters are "usually closing, sometimes opening".
[9]对于引文,对应字符可以是任 何 quotation marks in international usage

行内识别规则计划允许90%的非标记使用"*"、"`"、"_"、和"|"而无需转义。 例如,下面的属于没有一个会被识别为包含行内标记的字符:

  • 2*x a**b O(N**2) e**(x*y) f(x)*f(y) a|b file*.* (breaks 1)
  • 2 * x a ** b (* BOM32_* ` `` _ __ | (breaks 2)
  • "*" '|' (*) [*] {*} <*> (breaks 5)
  • || (breaks 6)
  • __init__ __init__()

下列行内标记的例子不需要转义:

  • 2 * x *a **b *.txt (breaks 3)
  • 2*x a**b O(N**2) e**(x*y) f(x)*f(y) a*(1+2) (breaks 4)

其中一些可能别描述为使用 行内文本 ,特鄙视如果它们表现为代码段。 这是一个判断调用。

识别顺序

行内标记分隔符被用于多个结构,因为为了避免混淆,必须有特定的识别 顺序。行内标记识别顺序如下:

字符级行内标记

可以使用反斜扛转义,在单词内制造独立字符(见 转义机制 )。反斜杠转义 可以用在行内标记之后的任何文本上:

Python ``list``\s use square bracket syntax.

反斜杠会在处理文档后消失。单词"list"会作为行内文本呈现,且字母"s"会 紧跟在它后面作为普通文本,中间无需空格。

行内标记之前的任意文本可以使用反斜杠空格:

Possible in *re*\ ``Structured``\ *Text*, though not encouraged.

反斜杠和空格分隔"re"、"Structured"和"Text",并会在文档处理后消失。

警告

不建议在字符级行内标记使用反斜杠转义。这种用法是丑陋的,对未经处 理的文档的可读性是有害的。请只在确实需要的地方使用该功能。

斜体

文档树元素: 斜体

开始字符 ,结束字符 = "*".

以单个星号封闭的文本是斜体:

This is *emphasized text*.
粗体

文档树元素: strong.

开始字符 = 结束字符 = "**".

被双星号封闭的文本是粗体:

这是 **粗体文本**.

强调文本通常以粗体显示。

解释文本

文档树元素: 取决于显式或隐式角色和处理

开始字符 = 结束字符 = "`".

文史文本是这样一种文本,它意味着被关联、索引、链接、概括或不同的处理,但文本本身会被典型保留。解释文本由单反引号字符封闭:

This is `interpreted text`.

解释文本的"role"决定了文本如何被解释。角色可能会被隐式的推断(像上面,使用了 "默认角色")或显式的表示,使用一个角色标记。 角色标记由一个冒号、角色名、另一个冒号组成。角色名是一个由字母加可能存在的连字符、下划线、加号、冒号、点组成的单个单词,不能有空格或其他字符。 角色标记是解释文本的前缀或后缀,取决于怎么读更合适,由作者决定:

:role:`interpreted text`

`interpreted text`:role:

解释文本允许扩展有效的行内描述标记结构。对于 斜体 , 粗体 , 行内文本超链接引用 ,我们可以添加"标题引用"、"索引入口"、"缩写"、"类"、"红色"、"闪烁"或任何你想要的东西。 只有预制的角色能够被识别,未知角色会生成错误。标准角色的核心集合在引用解析器中实现了。详见 reStructuredText解释文本角色角色 指令可以用于定义自定义解释文本角色。另外,程序可能支持特定 的角色。

行内文本

文档树元素: 文本.

开始字符 = 结束字符 = "``".

文本被双反引号封闭会被作为行内文本:

该文本是 ``行内文本`` 的一个例子。

行内文本可以包含任何字符除了与结束字符响铃的反引号根据上述识别规则)。没有标记的解释包括转义字符的解释)会在行内文本内完成。

在行内文本中,折行 不会 被保留。尽管reStructuredText解析器会在输出时保留空格,处理过的文本的最后表示取决于输出格式,因此不能放心的保留空白。如果折行和/或其他空白的表现是重要的,则应该使用 文本块

行内文本为简短的代码片段很有用。例如:

正则表达式 ``[+-]?(\d+(\.\d*)?|\.\d+)`` 匹配浮点数(没有指数)。
行内内部目标

文档树元素: target.

开始字符 = "_`",结束字符 = "`".

行内内部目标等价于显式 内部超链接目标 ,但可能呈现在运行的文本恶逆。该语法以一个下华夏和一个反引号开始,后面跟一个超链接名或短语,以一个反引号结束。行内内部目标不可以匿名。

例如,下面的段落包含一个名为"Norwegian Blue"的超链接目标:

Oh yes, the _`Norwegian Blue`.  What's, um, what's wrong with it?

参见 隐式超链接目标 以解决引用名重复的问题。

脚注引用

文档树元素: 脚注引用

开始字符 = "[",结束字符 = "]_".

每个脚注引用包含一个方括号标签后面跟一个下划线。脚注标签是以下之一:

例如:

Please RTFM [1]_.

.. [1] Read The Fine Manual
引文引用

文档树元素: 引文引用

开始字符 = "[",结束字符 = "]_".

每个引文引用由一个方括号标签后面跟一个下划线组成。引用标签是简单的 引用名称 (大小写不明感的单个单词,由字母加内部连字符、下划线、点组成,不能有空白)。

例如:

Here is a citation reference: [CIT2002]_.

引文

替代引用

文档树元素: 替代引用、引用

开始字符 = "|",结束字符 = "|" (可选的接"_"或"__").

竖线用于阔气替代引用文本。一个替代引用也可以是一个超链接引用,通过添加一个"_"(命名)或"__" (匿名)前缀,替代文本用于引用文本的命名的情况。

处理系统使用对应的处理后的内容替换替代引用中的 替代定义 。替代定义生成行内兼容的元素。

举例:

This is a simple |substitution reference|.  It will be 替代d by
the processing system.

This is a combination |substitution and hyperlink reference|_.  In
addition to being 替代d, the 替代ment text or element will
refer to the "substitution and hyperlink reference" target.
单位

(Docutils 0.3.10. 新增)

所有的单位由一个标准(非科学)符号正浮点数和一个单位组成,可能由一个或多个空格分隔。

只支持参考手册中显式的提到的单位。

长度单位

reStructuredText解析器支持下列长度单位:

  • em (ems, 元素字体的高度)
  • ex (x-height, 字母"x"的高度)
  • px (像素,关联到相对于画布的分辨率)
  • in (inches; 1in=2.54cm)
  • cm (厘米; 1cm=10mm)
  • mm (毫米)
  • pt (点; 1pt=1/72in)
  • pc (活字; 1pc=12pt)

该集合对应 CSS长度单位.

(列表和解释取自 http://www.htmlhelp.com/reference/css/unit.html#length)

以下是所有有效的长度值: "1.5em", "20 mm", ".5in".

不带单位的长度值会被自动添加(如,px with html4css1, pt with latex2e)。详见 用户文档

百分数单位

百分数值有一个百分数符("%")作为单位。百分数值与其他值关联,取决于其所处的上下文。

错误处理

文档树元素: 系统信息、problematic

标记错误根据 PEP 258 规范处理。

Python相关

flask-mongoengine扩展

time:2018-09-25

flask的扩展,提供了与MongoEngine的集成,详细查看MongoEngine的文档 http://docs.mongoengine.org/

安装flask-mongoengine:

pip install flask-mongoengine

config配置:

MONGODB_DB = ''
MONGODB_HOST = ''
MONGODB_PORT = '27017'
MONGODB_USERNAME = ''
MONGODB_PASSWORD = ''

默认情况下,flask-mongoengine会在实例化扩展时打开连接,但您可以将其配置为仅在第一次访问数据库时打开连接:

MONGODB_CONNECT=False
自定义查询
  • get_or_404 :与get类似,如果查询到没有对象返回abort(404)
  • first_or_404: 同上。
  • paginate:数据集分页传递两个参数,page和per_page。
  • paginate_field :在 查询中一个文档进行分页,参数:field_name,doc_id,page,per_page。

例子:

# 404 if object doesn't exist
def view_todo(todo_id):
    todo = Todo.objects.get_or_404(_id=todo_id)
..

# Paginate through todo
def view_todos(page=1):
    paginated_todos = Todo.objects.paginate(page=page, per_page=10)

# Paginate through tags of todo
def view_todo_tags(todo_id, page=1):
    todo = Todo.objects.get_or_404(_id=todo_id)
    paginated_tags = todo.paginate_field('tags', page, per_page=10)

分页对象的属性包括:iter_pages,next,prev,has_next,has_prev,next_num,prev_num。

template:

{# Display a page of todos #}
<ul>
{% for todo in paginated_todos.items %}
    <li>{{ todo.title }}</li>
{% endfor %}
</ul>

{# Macro for creating navigation links #}
{% macro render_navigation(pagination, endpoint) %}
<div class=pagination>
{% for page in pagination.iter_pages() %}
    {% if page %}
        {% if page != pagination.page %}
            <a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
        {% else %}
        <strong>{{ page }}</strong>
    {% endif %}
    {% else %}
        <span class=ellipsis>…</span>
    {% endif %}
{% endfor %}
</div>
{% endmacro %}

{{ render_navigation(paginated_todos, 'view_todos') }}
与WTForms的集成

flask-mongoengine自动从MongoEngine模型生成WTForms:

from flask_mongoengine.wtf import model_form

class User(db.Document):
    email = db.StringField(required=True)
    first_name = db.StringField(max_length=50)
    last_name = db.StringField(max_length=50)

class Content(db.EmbeddedDocument):
    text = db.StringField()
    lang = db.StringField(max_length=3)

class Post(db.Document):
    title = db.StringField(max_length=120, required=True, validators=[validators.InputRequired(message=u'Missing title.'),])
    author = db.ReferenceField(User)
    tags = db.ListField(db.StringField(max_length=30))
    content = db.EmbeddedDocumentField(Content)

PostForm = model_form(Post)

def add_post(request):
    form = PostForm(request.POST)
    if request.method == 'POST' and form.validate():
        # do something
        redirect('done')
    return render_template('add_post.html', form=form)

如果不是隐式转换,则允许提示用户参数:

PostForm = model_form(Post, field_args={'title': {'textarea': True}})

支持的参数:

choices:
  • multiple to use a SelectMultipleField
  • radio to use a RadioField
StringField:
  • password to use a PasswordField
  • textarea to use a TextAreaField

默认情况下,没有设置max_length时,StringField才会转换为TextAreaField。

支持的字段:
  • StringField
  • BinaryField
  • URLField
  • EmailField
  • IntField
  • FloatField
  • DecimalField
  • BooleanField
  • DateTimeField
  • ListField(使用wtforms.fields.FieldList)
  • SortedListField(重复ListField)
  • EmbeddedDocumentField(使用wtforms.fields.FormField并生成内联表单)
  • ReferenceField(使用带有从QuerySet或Document加载的选项的wtforms.fields.SelectFieldBase)
  • DictField
目前不支持的类型:
  • ObjectIdField
  • GeoLocationField
  • GenericReferenceField
Session 会话

要使用MongoEngine作为会话存储,只需配置会话接口:

from flask_mongoengine import MongoEngine, MongoEngineSessionInterface

app = Flask(__name__)
db = MongoEngine(app)
app.session_interface = MongoEngineSessionInterface(db)

Flask-DebugToolbar:

from flask import Flask
from flask_debugtoolbar import DebugToolbarExtension

app = Flask(__name__)
db = MongoEngine(app)
toolbar = DebugToolbarExtension(app)

#config.py:
DEBUG_TB_PANELS = 'flask_mongoengine.panels.MongoDebugPanel'
rocket_chat聊天室框架的学习使用

filename:rocket_chat.rst

官网https://rocket.chat

github: https://github.com/RocketChat/Rocket.Chat

#按照官网第一步,Quick start:
git clone https://github.com/RocketChat/Rocket.Chat.git
#昨晚这个过程很久下载了很久,找了很多加速方案都不行
cd Rocket.Chat

meteor npm start

#提示没有meteor

#安装 meteor  macos and linux:
#相关访问https://www.meteor.com/install
curl https://install.meteor.com/ | sh

#然后去node.js官网安装node.js,安装步骤略。

#运行 meteor npm start  报错:

fs.js:904:18: ENOENT: no such file or directory, scandir
'packages/rocketchat-katex/node_modules/katex/dist/fonts/'
at Object.fs.readdirSync (fs.js:904:18)
at package.js:24:29


=> Your application has errors. Waiting for file change.

#搜索说:更新版本,发现还是不行,仔细一看 是缺少目录
update --release 1.3.1


#在Rocket.Chat/packages/rocketchat-katex目录下创建对应的目录,注意权限。
packages/rocketchat-katex/node_modules/katex/dist/fonts/

继续报错:
npm ERR! Failed at the Rocket.Chat@0.70.0-develop start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
npm WARN Local package.json exists, but node_modules missing, did you mean to install?

#根据错误提示:
npm install package

mac上删除卸载node:

sudo npm uninstall npm -g
sudo rm -rf /usr/local/lib/node /usr/local/lib/node_modules /var/db/receipts/org.nodejs.*
sudo rm -rf /usr/local/include/node /Users/$USER/.npm
sudo rm /usr/local/bin/node
sudo rm /usr/local/share/man/man1/node.1
sudo rm /usr/local/lib/dtrace/node.d

2018-09-29重新clone一遍,未发现错误,但是卡在了node-pre-gyp install --fallback-to-build --library=static_library很久

google:

npm install -g node-gyp

出现错误;

meteor npm install

也提示了一堆错误;但是都是依赖包版本问题 使用npm install xxx@^x.x.x 之后 在运行 meteor npm start 等了一段时间编译提示:

Note:you are using a pure-JavaScript implementation of bcrypt. While this implementation will work correctly, it is known to be approximately three times slower than the native implementation. In order to use the native implementation instead, run meteor npm install --save bcrypt in the root directory of your application. Updating process.env.MAIL_URL I20180929-11:26:29.269(8)? Starting Email Intercepter... I20180929-11:26:37.295(8)? Exception in callback of async function: Error: Cannot find module '../build/Release/sharp.node'

重新来一次meteor npm start 还是不行 ,根据错误:

sudo npm install -g node-gyp

Failed at the sharp@0.20.8 install script.

meteor npm install --save bcrypt

Exception in callback of async function: Error: Cannot find module '/Volumes/mydata/www/flask_code/cookiecutter-flask/Rocket.Chat/node_modules/grpc/src/node/extension_binary/node-v57-darwin-x64-unknown/grpc_node.node'

Please make sure you are using a supported platform and node version. If you would like to compile fibers on this machine please make sure you have setup your build environment-- Windows + OS X instructions here: https://github.com/nodejs/node-gyp Ubuntu users please run: sudo apt-get install g++ build-essential Alpine users please run: sudo apk add python make g++ sh: nodejs: command not found

要安装xcode。。。

还是等假期更新系统到10.13再来吧。

2018-10-08使用mac10.10在走一次
#这里下载很久,速度慢又重新下  好几才ok
git clone https://github.com/RocketChat/Rocket.Chat.git
cd Rocket.Chat
#这里没有直接meteor npm start,
meteor npm install --save babel-runtime
#居然没有报错,继续  这里提示 需要python2.7  使用命令:meteor npm config set python python2.7,我默认的就是python2.7  所以略过
meteor npm install --save bcrypt
#也没有报错。  只是3个警告依赖不正确,
meteor npm start
#报错:Your application has errors. Waiting for file change.
#如图
_images/1538980616451.jpg

仔细看是目录问题 手动创建对应的目录:

#再次运行
meteor npm start
#报错,如图
_images/1538981173842.jpg

日志文件

_images/1538981302558.jpg

搜索到使用命令:

npm install --no-cach
再次运行
meteor npm start
#登录N久之后   OK
_images/1538982540948.jpg

提示正常运行,但打开http://localhost:3000/却无法访问

仔细想了想 原来是docker之前设置的了端口映射 ,在docker的虚拟机中关闭端口映射就好了

_images/1537937903837.jpg _images/1537938271293.jpg
新建flask项目的一些步骤

项目环境:mac10.13 python365

pip install cookiecutter
cookiecutter https://github.com/sloria/cookiecutter-flask.git
pip install -r requirements/dev.txt
#注意webpack,不用就删掉对应的扩展删除略

#还要注意一下database.py的封装,外键不能为空的选项可能会报错。

#创建虚拟环境
pip install virtualenv
python3 -m virtualenv venv
source venv/bin/activate


#2018-11-15和之前一样的bug
#报错ModuleNotFoundError: No module named 'MySQLdb'
pip install mysqlclient
#Python: MySQLdb and “Library not loaded: libmysqlclient.16.dylib”
#Library not loaded: libssl.1.0.0.dylib
#_mysql.cpython-36m-darwin.so  Reason: image not found

export DYLD_LIBRARY_PATH=/usr/local/mysql/lib/
#设置环境变量DYLD_LIBRARY_PATH,直接在.env下添加
Flask-Security官方中文翻译
添加安全机制,包括:
  • 会话认证
  • 角色管理
  • 哈希密码
  • 基于http身份认证
  • 基于令牌身份认证
  • 基于令牌账号激活[可选]
  • 基于令牌的密码重置[可选]
  • 用户注册[可选]
  • 登陆跟踪[可选]
  • JSON/Ajax支持
通过集成flask的插件可以实现这些功能:
  • Flask-Login
  • Flask-Mail
  • Flask-Principal
  • Flask-WTF
  • itsdangerous
  • passlib
Flask-Security支持以下用于数据持久性的Flask扩展:
  • Flask-SQLAlchemy
  • Flask-MongoEngine
  • Flask-Peewee
  • PonyORM
配置
核心配置
SECURITY_BLUEPRINT_NAME 指定flask-security蓝图名称
SECURITY_CLI_USERS_NAME 指定管理用户的命令名称。通过设置禁用false,默认为users
SECURITY_CLI_ROLES_NAME 指定命令管理角色名称。通过设置禁用false,默认为roles
SECURITY_URL_PREFIX 指定Flask-Security蓝图的URL前缀。默认为 None。
SECURITY_SUBDOMAIN 指定Flask-Security蓝图的子域。默认为 None。
SECURITY_FLASH_MESSAGES 指定在安全过程中是否刷新消息。默认为True。
SECURITY_I18N_DOMAIN 指定用于翻译的域的名称。默认为flask_security。
SECURITY_PASSWORD_HASH 指定散列密码时要使用的密码哈希算法。生产系统推荐值bcrypt、sha512_crypt或pbkdf2_sha512。默认为 bcrypt。
SECURITY_PASSWORD_SALT 指定HMAC盐。仅当密码哈希类型设置为纯文本以外的其他内容时才使用此选项。默认为None。
SECURITY_PASSWORD_SINGLE_HASH 指定仅对密码进行一次哈希处理。默认密码经过两次哈希处理
SECURITY_HASHING_SCHEMES 用于创建和验证令牌的算法列表。默认为sha256_crypt。
SECURITY_DEPRECATED_HASHING_SCHEMES 用于创建和验证令牌的已弃用算法列表。默认为hex_md5。
SECURITY_PASSWORD_HASH_OPTIONS 指定要传递给散列方法的其他选项。
SECURITY_EMAIL_SENDER 指定发送电子邮件的电子邮件地址。MAIL_DEFAULT_SENDER如果另外使用Flask-Mail,
SECURITY_TOKEN_AUTHENTICATION_KEY 指定使用令牌身份验证时要读取的查询字符串参数。默认为auth_token。
SECURITY_TOKEN_AUTHENTICATION_HEADER 指定使用令牌身份验证时要读取的HTTP标头。默认为 Authentication-Token
SECURITY_TOKEN_MAX_AGE 指定身份验证令牌到期之前的秒数。默认为None,表示令牌永不过期。
SECURITY_DEFAULT_HTTP_AUTH_REALM 使用基本HTTP身份验证时指定默认身份验证领域。默认为Login Required
URL和视图
SECURITY_LOGIN_URL 指定登录URL。默认为/login。
SECURITY_LOGOUT_URL 指定注销URL。默认为 /logout。
SECURITY_REGISTER_URL 指定注册URL。默认为 /register。
SECURITY_RESET_URL 指定密码重置URL。默认为 /reset。
SECURITY_CHANGE_URL 指定密码更改URL。默认为 /change。
SECURITY_CONFIRM_URL 指定电子邮件确认URL。默认为/confirm。
SECURITY_POST_LOGIN_VIEW 指定用户登录后重定向到的默认视图。此值可以设置为URL或端点名称。默认为/。
SECURITY_POST_LOGOUT_VIEW 指定用户注销后重定向到的默认视图。此值可以设置为URL或端点名称。默认为/。
SECURITY_CONFIRM_ERROR_VIEW 如果发生确认错误,则指定要重定向到的视图。此值可以设置为URL或端点名称。如果此值为 None,
SECURITY_POST_REGISTER_VIEW 指定在用户成功注册后重定向到的视图。此值可以设置为URL或端点名称。
SECURITY_POST_CONFIRM_VIEW 指定用户成功确认其电子邮件后要重定向到的视图。此值可以设置为URL或端点名称。
SECURITY_POST_RESET_VIEW 指定用户成功重置密码后要重定向到的视图。此值可以设置为URL或端点名称。
SECURITY_POST_CHANGE_VIEW 指定用户成功更改密码后要重定向到的视图。此值可以设置为URL或端点名称。
SECURITY_UNAUTHORIZED_VIEW 如果用户尝试访问他们无权访问的URL /端点,则指定要重定向到的视图。
模板路径
SECURITY_FORGOT_PASSWORD_TEMPLATE 指定忘记密码页面的模板路径。默认为 security/forgot_password.html。
SECURITY_LOGIN_USER_TEMPLATE 指定用户登录页面的模板路径。默认为 security/login_user.html。
SECURITY_REGISTER_USER_TEMPLATE 指定用户注册页面的模板路径。默认为 security/register_user.html。
SECURITY_RESET_PASSWORD_TEMPLATE 指定重置密码页面的模板路径。默认为 security/reset_password.html。
SECURITY_CHANGE_PASSWORD_TEMPLATE 指定更改密码页面的模板路径。默认为 security/change_password.html。
SECURITY_SEND_CONFIRMATION_TEMPLATE 指定重新发送确认说明页面的模板路径。默认为 security/send_confirmation.html。
SECURITY_SEND_LOGIN_TEMPLATE 指定无密码登录的发送登录说明页面模板的路径。默认为 security/send_login.html。
功能标志
SECURITY_CONFIRMABLE 指定在注册新帐户时是否要求用户确认其电子邮件地址。如果此值为True,Flask-Security会创建一个端点来处理确认和请求以重新发送确认指令。此端点的URL由SECURITY_CONFIRM_URL配置选项指定。默认为False。
SECURITY_REGISTERABLE 指定Flask-Security是否应创建用户注册端点。此端点的URL由SECURITY_REGISTER_URL 配置选项指定。默认为False。
SECURITY_RECOVERABLE 指定Flask-Security是否应创建密码重置/恢复端点。此端点的URL由SECURITY_RESET_URL配置选项指定。默认为False。
SECURITY_TRACKABLE 指定Flask-Security是否应跟踪基本用户登录统计信息。如果设置为True,请确保您的模型具有必需的字段/属性。如果您使用代理,请务必使用ProxyFix。默认为 False
SECURITY_PASSWORDLESS 指定Flask-Security是否应启用无密码登录功能。如果设置为True,则用户无需输入密码进行登录,但会收到带有登录链接的电子邮件。此功能是实验性的,应谨慎使用。默认为False。
SECURITY_CHANGEABLE 指定Flask-Security是否应启用更改密码端点。此端点的URL由SECURITY_CHANGE_URL配置选项指定。默认为False。
Email
SECURITY_EMAIL_SUBJECT_REGISTER 设置确认电子邮件的主题。默认为Welcome
SECURITY_EMAIL_SUBJECT_PASSWORDLESS 设置无密码功能的主题。
SECURITY_EMAIL_SUBJECT_PASSWORD_NOTICE 设置密码通知的主题。
SECURITY_EMAIL_SUBJECT_PASSWORD_RESET 设置密码重置电子邮件的主题。
SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE 设置密码更改通知的主题。
SECURITY_EMAIL_SUBJECT_CONFIRM 设置电子邮件确认消息的主题。
SECURITY_EMAIL_PLAINTEXT 使用*.txt模板以纯文本形式发送电子邮件 。默认为True。
SECURITY_EMAIL_HTML 使用*.html模板将电子邮件发送为HTML 。默认为True。
其他选项
SECURITY_USER_IDENTITY_ATTRIBUTES 指定用户对象的哪些属性可用于登录。默认为['email']。
SECURITY_SEND_REGISTER_EMAIL 指定是否发送注册电子邮件。默认为 True。
SECURITY_SEND_PASSWORD_CHANGE_EMAIL 指定是否发送密码更改电子邮件。默认为 True。
SECURITY_SEND_PASSWORD_RESET_EMAIL 指定是否发送密码重置电子邮件。默认为 True。
SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL 指定是否发送密码重置通知电子邮件。默认为 True。
SECURITY_CONFIRM_EMAIL_WITHIN 指定用户在确认链接到期之前的时间量。始终将此值的时间单位复数。默认为5天。
SECURITY_RESET_PASSWORD_WITHIN 指定用户在密码重置链接到期之前的时间量。始终将此值的时间单位复数。默认为5天
SECURITY_LOGIN_WITHIN 指定用户在登录链接到期之前的时间量。仅在启用无密码登录功能时使用。始终将此值的时间单位复数。默认为1天
SECURITY_LOGIN_WITHOUT_CONFIRMATION 指定用户是否可以在将值SECURITY_CONFIRMABLE设置为 确认其电子邮件之前登录 True。默认为False。
SECURITY_CONFIRM_SALT 生成确认链接/令牌时指定salt值。默认为 confirm-salt。
SECURITY_RESET_SALT 生成密码重置链接/令牌时指定salt值。默认为 reset-salt。
SECURITY_LOGIN_SALT 生成登录链接/令牌时指定salt值。默认为login-salt。
SECURITY_REMEMBER_SALT 生成记忆标记时指定salt值。请记住使用令牌代替用户ID,因为它更安全。默认为 remember-salt。
SECURITY_DEFAULT_REMEMBER_ME 指定登录用户时使用的默认“记住我”值。默认为False。
SECURITY_DATETIME_FACTORY 指定默认的datetime工厂。默认为 datetime.datetime.utcnow。

消息

可以在中找到默认消息和错误级别core.py:
  • SECURITY_MSG_ALREADY_CONFIRMED
  • SECURITY_MSG_CONFIRMATION_EXPIRED
  • SECURITY_MSG_CONFIRMATION_REQUEST
  • SECURITY_MSG_CONFIRMATION_REQUIRED
  • SECURITY_MSG_CONFIRM_REGISTRATION
  • SECURITY_MSG_DISABLED_ACCOUNT
  • SECURITY_MSG_EMAIL_ALREADY_ASSOCIATED
  • SECURITY_MSG_EMAIL_CONFIRMED
  • SECURITY_MSG_EMAIL_NOT_PROVIDED
  • SECURITY_MSG_FORGOT_PASSWORD
  • SECURITY_MSG_INVALID_CONFIRMATION_TOKEN
  • SECURITY_MSG_INVALID_EMAIL_ADDRESS
  • SECURITY_MSG_INVALID_LOGIN_TOKEN
  • SECURITY_MSG_INVALID_PASSWORD
  • SECURITY_MSG_INVALID_REDIRECT
  • SECURITY_MSG_INVALID_RESET_PASSWORD_TOKEN
  • SECURITY_MSG_LOGIN
  • SECURITY_MSG_LOGIN_EMAIL_SENT
  • SECURITY_MSG_LOGIN_EXPIRED
  • SECURITY_MSG_PASSWORDLESS_LOGIN_SUCCESSFUL
  • SECURITY_MSG_PASSWORD_CHANGE
  • SECURITY_MSG_PASSWORD_INVALID_LENGTH
  • SECURITY_MSG_PASSWORD_IS_THE_SAME
  • SECURITY_MSG_PASSWORD_MISMATCH
  • SECURITY_MSG_PASSWORD_NOT_PROVIDED
  • SECURITY_MSG_PASSWORD_NOT_SET
  • SECURITY_MSG_PASSWORD_RESET
  • SECURITY_MSG_PASSWORD_RESET_EXPIRED
  • SECURITY_MSG_PASSWORD_RESET_REQUEST
  • SECURITY_MSG_REFRESH
  • SECURITY_MSG_RETYPE_PASSWORD_MISMATCH
  • SECURITY_MSG_UNAUTHORIZED
  • SECURITY_MSG_USER_DOES_NOT_EXIST
快速入门

安装依赖:

#sqlalchemy
pip install flask-security flask-sqlalchemy
#flask-mongoengine
pip install flask-security flask-mongoengine
#flask-peewee
pip install flask-security flask-peewee
基于sqlalchemy程序

sqlalchemy程序:

from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_security import Security, SQLAlchemyUserDatastore, \
    UserMixin, RoleMixin, login_required

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'

# Create database connection object
db = SQLAlchemy(app)

# Define models
roles_users = db.Table('roles_users',
        db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
        db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))

class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))

# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)

# Create a user to test with
@app.before_first_request
def create_user():
    db.create_all()
    user_datastore.create_user(email='matt@nobien.net', password='password')
    db.session.commit()

# Views
@app.route('/')
@login_required
def home():
    return render_template('index.html')

if __name__ == '__main__':
    app.run()
SESSION的SQLAlchemy程序

app.py:

from flask import Flask
from flask_security import Security, login_required, \
     SQLAlchemySessionUserDatastore
from database import db_session, init_db
from models import User, Role

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'

# Setup Flask-Security
user_datastore = SQLAlchemySessionUserDatastore(db_session,
                                                User, Role)
security = Security(app, user_datastore)

# Create a user to test with
@app.before_first_request
def create_user():
    init_db()
    user_datastore.create_user(email='matt@nobien.net', password='password')
    db_session.commit()

# Views
@app.route('/')
@login_required
def home():
    return render('Here you go!')

if __name__ == '__main__':
    app.run()

databases.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base

engine = create_engine('sqlite:////tmp/test.db', \
                       convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()

def init_db():
    # import all modules here that might define models so that
    # they will be registered properly on the metadata.  Otherwise
    # you will have to import them first before calling init_db()
    import models
    Base.metadata.create_all(bind=engine)

models.py:

from database import Base
from flask_security import UserMixin, RoleMixin
from sqlalchemy import create_engine
from sqlalchemy.orm import relationship, backref
from sqlalchemy import Boolean, DateTime, Column, Integer, \
                       String, ForeignKey

class RolesUsers(Base):
    __tablename__ = 'roles_users'
    id = Column(Integer(), primary_key=True)
    user_id = Column('user_id', Integer(), ForeignKey('user.id'))
    role_id = Column('role_id', Integer(), ForeignKey('role.id'))

class Role(Base, RoleMixin):
    __tablename__ = 'role'
    id = Column(Integer(), primary_key=True)
    name = Column(String(80), unique=True)
    description = Column(String(255))

class User(Base, UserMixin):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    email = Column(String(255), unique=True)
    username = Column(String(255))
    password = Column(String(255))
    last_login_at = Column(DateTime())
    current_login_at = Column(DateTime())
    last_login_ip = Column(String(100))
    current_login_ip = Column(String(100))
    login_count = Column(Integer)
    active = Column(Boolean())
    confirmed_at = Column(DateTime())
    roles = relationship('Role', secondary='roles_users',
                         backref=backref('users', lazy='dynamic'))
基本的MongoEngine程序

core.py:

from flask import Flask, render_template
from flask_mongoengine import MongoEngine
from flask_security import Security, MongoEngineUserDatastore, \
    UserMixin, RoleMixin, login_required

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'

# MongoDB Config
app.config['MONGODB_DB'] = 'mydatabase'
app.config['MONGODB_HOST'] = 'localhost'
app.config['MONGODB_PORT'] = 27017

# Create database connection object
db = MongoEngine(app)

class Role(db.Document, RoleMixin):
    name = db.StringField(max_length=80, unique=True)
    description = db.StringField(max_length=255)

class User(db.Document, UserMixin):
    email = db.StringField(max_length=255)
    password = db.StringField(max_length=255)
    active = db.BooleanField(default=True)
    confirmed_at = db.DateTimeField()
    roles = db.ListField(db.ReferenceField(Role), default=[])

# Setup Flask-Security
user_datastore = MongoEngineUserDatastore(db, User, Role)
security = Security(app, user_datastore)

# Create a user to test with
@app.before_first_request
def create_user():
    user_datastore.create_user(email='matt@nobien.net', password='password')

# Views
@app.route('/')
@login_required
def home():
    return render_template('index.html')

if __name__ == '__main__':
    app.run()
基本的Peewee程序

Peewee 程序:

from flask import Flask, render_template
from flask_peewee.db import Database
from peewee import *
from flask_security import Security, PeeweeUserDatastore, \
    UserMixin, RoleMixin, login_required

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['DATABASE'] = {
    'name': 'example.db',
    'engine': 'peewee.SqliteDatabase',
}

# Create database connection object
db = Database(app)

class Role(db.Model, RoleMixin):
    name = CharField(unique=True)
    description = TextField(null=True)

class User(db.Model, UserMixin):
    email = TextField()
    password = TextField()
    active = BooleanField(default=True)
    confirmed_at = DateTimeField(null=True)

class UserRoles(db.Model):
    # Because peewee does not come with built-in many-to-many
    # relationships, we need this intermediary class to link
    # user to roles.
    user = ForeignKeyField(User, related_name='roles')
    role = ForeignKeyField(Role, related_name='users')
    name = property(lambda self: self.role.name)
    description = property(lambda self: self.role.description)

# Setup Flask-Security
user_datastore = PeeweeUserDatastore(db, User, Role, UserRoles)
security = Security(app, user_datastore)

# Create a user to test with
@app.before_first_request
def create_user():
    for Model in (Role, User, UserRoles):
        Model.drop_table(fail_silently=True)
        Model.create_table(fail_silently=True)
    user_datastore.create_user(email='matt@nobien.net', password='password')

# Views
@app.route('/')
@login_required
def home():
    return render_template('index.html')

if __name__ == '__main__':
    app.run()
邮件配置
# At top of file
from flask_mail import Mail

# After 'Create app'
app.config['MAIL_SERVER'] = 'smtp.example.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = 'username'
app.config['MAIL_PASSWORD'] = 'password'
mail = Mail(app)
Proxy代理配置
# At top of file
from werkzeug.config.fixers import ProxyFix

# After 'Create app'
app.wsgi_app = ProxyFix(app.wsgi_app, num_proxies=1)
数据库模型

插件需要最少字段:

用户表:
  • id
  • email
  • password
  • active
角色表:
  • id
  • name
  • description
附加功能:
根据应用程序的配置,可能需要其他字段添加到用户模型中。
Confirmable:
如果启用用户确认,SECURITY_CONFIRMABLE的值设置为true,则需要在user模型中添加如下字段:
  • confirmed_at
Trackable:
如果通过将应用程序的SECURITY_TRACKABLE 配置值设置为True来启用用户跟踪,则用户模型将需要以下附加字段:
  • last_login_at
  • current_login_at
  • last_login_ip
  • current_login_ip
  • login_count
自定义用户内容
class User(db.Model, UserMixin):
        id = db.Column(db.Integer, primary_key=True)
        email = TextField()
        password = TextField()
        active = BooleanField(default=True)
        confirmed_at = DateTimeField(null=True)
        name = db.Column(db.String(80))

        # Custom User Payload
        def get_security_payload(self):
                return {
                        'id': self.id,
                        'name': self.name,
                        'email': self.email
                }
自定义视图页面
视图views:
  • security/forgot_password.html
  • security/login_user.html
  • security/register_user.html
  • security/reset_password.html
  • security/change_password.html
  • security/send_confirmation.html
  • security/send_login.html
操作:
  1. 创建security文件夹
  2. 覆盖一样文件名的html文件

上下文处理器:

security = Security(app, user_datastore)

# This processor is added to all templates
@security.context_processor
def security_context_processor():
    return dict(hello="world")

# This processor is added to only the register view
@security.register_context_processor
def security_register_processor():
    return dict(something="else")
以下是所有可用的上下文处理器装饰器的列表:
  • context_processor:所有观点
  • forgot_password_context_processor:忘记密码查看
  • login_context_processor:登录视图
  • register_context_processor:注册视图
  • reset_password_context_processor:重置密码视图
  • change_password_context_processor:更改密码视图
  • send_confirmation_context_processor:发送确认视图
  • send_login_context_processor:发送登录视图
表单
from flask_security.forms import RegisterForm

class ExtendedRegisterForm(RegisterForm):
    first_name = StringField('First Name', [Required()])
    last_name = StringField('Last Name', [Required()])

security = Security(app, user_datastore,
         register_form=ExtendedRegisterForm)
class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    first_name = db.Column(db.String(255))
    last_name = db.Column(db.String(255))
表单重写:
  • login_form: Login form
  • confirm_register_form: Confirmable register form
  • register_form: Register form
  • forgot_password_form: Forgot password form
  • reset_password_form: Reset password form
  • change_password_form: Change password form
  • send_confirmation_form: Send confirmation form
  • passwordless_login_form: Passwordless login form
电子邮件模板重写:
  • security/email/confirmation_instructions.html
  • security/email/confirmation_instructions.txt
  • security/email/login_instructions.html
  • security/email/login_instructions.txt
  • security/email/reset_instructions.html
  • security/email/reset_instructions.txt
  • security/email/reset_notice.html
  • security/email/change_notice.txt
  • security/email/change_notice.html
  • security/email/reset_notice.txt
  • security/email/welcome.html
  • security/email/welcome.txt
重写步骤:
  1. 创建security文件夹
  2. 创建email文件夹
  3. 覆盖相同的模板名称

上下文:

security = Security(app, user_datastore)

# This processor is added to all emails
@security.mail_context_processor
def security_mail_processor():
    return dict(hello="world")

异步发送:

# Setup the task
@celery.task
def send_security_email(msg):
    # Use the Flask-Mail extension instance to send the incoming ``msg`` parameter
    # which is an instance of `flask_mail.Message`
    mail.send(msg)

@security.send_mail_task
def delay_security_email(msg):
    send_security_email.delay(msg)

init_app 方式发送:

from flask import Flask
from flask_mail import Mail
from flask_security import Security, SQLAlchemyUserDatastore
from celery import Celery

mail = Mail()
security = Security()
celery = Celery()

def create_app(config):
    """Initialize Flask instance."""

    app = Flask(__name__)
    app.config.from_object(config)

    @celery.task
    def send_flask_mail(msg):
        mail.send(msg)

    mail.init_app(app)
    datastore = SQLAlchemyUserDatastore(db, User, Role)
    security_ctx = security.init_app(app, datastore)

    # Flexible way for defining custom mail sending task.
    @security_ctx.send_mail_task
    def delay_flask_security_mail(msg):
        send_flask_mail.delay(msg)

    # A shortcurt.
    security_ctx.send_mail_task(send_flask_mail.delay)

    return app

Celery:

@celery.task
def send_flask_mail(**kwargs):
        mail.send(Message(**kwargs))

@security_ctx.send_mail_task
def delay_flask_security_mail(msg):
    send_flask_mail.delay(subject=msg.subject, sender=msg.sender,
                          recipients=msg.recipients, body=msg.body,
                          html=msg.html)

Linux相关

linux查找软件(nginx)安装位置及配置信息等
ps -ef | grep nginx
centos6下升级pyhton3

time:2018-09-12

更新时间:2018-11-28

安装pip:

curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python get-pip.py
更新时间:2018-09-24

yum install openssl-devel

在新的服务器重新安装python3的时候 运行flask出现ModuleNotFoundError: No module named '_ssl'错误。经查 , 需要重新安装编译python 才行。

缺少组件会报错需要先安装下组件

yum install -y zlib*
yum install -y gcc
#切换目录
cd /opt/
#下载python36
#wget https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tgz

wget https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tgz

#解压
tar -zxvf Python-3.6.5.tgz
#进入目录
cd Python-3.6.5

#./configure --prefix=/usr/local/python3
./configure --prefix=/usr/local/python3 --enable-optimizations

这里  test400+的东西 用了10+分钟

make
make install
/usr/local/python3/bin/python3 -V
>>Python 3.6.0
#创建软连接
ln -s /usr/local/python3/bin/python3 /usr/bin/python3
python3 -V
>>Python 3.6.5
ok
centos6下安装mysql5.6(转)

filename:centos6_install_mysql5_6.rst

原文连接:https://blog.csdn.net/dwyane__wade/article/details/81082153

# 检查系统是否安装其他版本的MYSQL数据,有则卸载

yum list installed | grep mysql
yum -y remove mysql-libs.x86_64

# 安装及配置

wget http://repo.mysql.com/mysql-community-release-el6-5.noarch.rpm

rpm -ivh mysql-community-release-el6-5.noarch.rpm
yum repolist all | grep mysql

# 安装MYSQL数据库

yum install mysql-community-server -y

# 设置为开机启动
chkconfig --list | grep mysqld
chkconfig mysqld on

# 启动

service mysqld start

# 设置密码
/usr/bin/mysqladmin -u root password '123456'

# 如果上一步执行失败
mysql -uroot -proot尝试


允许远程登录:

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'your_pwd' WITH GRANT OPTION;
修改mysql密码

filename:update_mysql6_pwd.rst

vim /etc/my.cnf

在[mysqld]字段中加上这句话

skip-grant-tables

:wq!保存

重启服务:

service mysqld restart

进入mysql:

mysqld -u root

选择数据库

use mysql

更新密码
UPDATE user SET Password = password ('your_pwd') WHERE User = 'root';

flush privileges ;

quit

删除skip-grant-tables

service mysqld restart

ok
centos6下安装和使用git和git使用出现的一些问题等

filename:centos6_install_git.rst

yum install git

#生成ssh秘钥
ssh-keygen -t rsa -C "anaf@163.com"

回车3次

打开github
settings->SSH And GPG Keys->New SSH key

vi /root/.ssh/id_rsa.pub

复制到github

然后就可以使用 git clone   xxxx了

2018-09-25操作发现github 在git clone 无法clone

搜索git clone太慢得到的结果说这样:

git config --global http.proxy 'socks5://127.0.0.1:1080'

这样就可以了

还说,在hosts文件中添加,不过我没加:

151.101.76.249 github.global.ssl.fastly.net
192.30.253.112 github.com

原文:https://blog.csdn.net/twang0x80/article/details/79777135

https://blog.csdn.net/github_34965845/article/details/80610060

解决pip install xx慢 的问题

pip慢

地址 :https://blog.csdn.net/lambert310/article/details/52412059

加入.gitignore无效的解决方法

git rm -r --cached xxx
git rm --cached demo-project.iml

再重新加入.gitignore文件

git rm -r --cached .
git add .

#提交版本
git commit -m "first commit"
#推送服务器
git push -u origin master
docker学习
基本使用

使用:

#运行ubuntu:15.10 会先下载  打印一句话
docker run ubuntu:15.10 /bin/echo "Hello world"

#运行centos 会先下载 打印一句话
docker run centos /bin/echo "Hello world"
运行交互式的容器

我们通过docker的两个参数 -i -t,让docker运行的容器实现"对话"的能力

docker run -i -t centos:6.8 /bin/bash
各个参数解析:
  • -t:在新容器内指定一个伪终端或终端。
  • -i:允许你对容器内的标准输入 (STDIN) 进行交互。

这样就想一个新系统一样了

可以通过运行exit命令或者使用CTRL+D来退出容器。

启动容器(后台模式)

使用以下命令创建一个以进程方式运行的容器:

docker run -d ubuntu:15.10 /bin/sh -c "while true; do echo hello world; sleep 1; done"
2b1b7a428627c51ab8810d541d759f072b4fc75487eed05812646b8534a2fe63

在输出中,我们没有看到期望的"hello world",而是一串长字符

这个长字符串叫做容器ID,对每个容器来说都是唯一的,我们可以通过容器ID来查看对应的容器发生了什么。

首先,我们需要确认容器有在运行,可以通过 docker ps 来查看:

docker ps

CONTAINER ID:容器ID

NAMES:自动分配的容器名称

在容器内使用docker logs命令,查看容器内的标准输出:

docker logs 2b1b7a428627

docker logs amazing_cori
停止容器

我们使用 docker stop 命令来停止容器:

docker stop  CONTAINER ID

docker stop NAME

docker ps
Docker 客户端

docker 客户端非常简单 ,我们可以直接输入 docker 命令来查看到 Docker 客户端的所有命令选项:

docker

可以通过命令 docker command --help 更深入的了解指定的 Docker 命令使用方法。

例如我们要查看 docker stats 指令的具体使用方法:

docker stats --help
运行一个web应用

前面我们运行的容器并没有一些什么特别的用处。

接下来让我们尝试使用 docker 构建一个 web 应用程序。

我们将在docker容器中运行一个 Python Flask 应用来运行一个web应用:

docker run -d -P training/webapp python app.py
参数说明:
  • -d:让容器在后台运行。
  • -P:将容器内部使用的网络端口映射到我们使用的主机上。
查看 WEB 应用容器

使用 docker ps 来查看我们正在运行的容器:

docker ps

Docker 开放了 5000 端口(默认 Python Flask 端口)映射到主机端口 32769 上。

这里mac 有个小插曲 ,需要在virtualbox上设置端口转发

_images/1537937903837.jpg _images/1537938271293.jpg

我们也可以指定 -p 标识来绑定指定端口:

docker run -d -p 5000:5000 training/webapp python app.py
网络端口的快捷方式

通过docker ps 命令可以查看到容器的端口映射,docker还提供了另一个快捷方式:docker port,使用 docker port 可以查看指定 (ID或者名字)容器的某个确定端口映射到宿主机的端口号。

上面我们创建的web应用容器ID为:7a38a1ad55c6 名字为:determined_swanson

我可以使用docker port 7a38a1ad55c6 或docker port determined_swanson来查看容器端口的映射情况:

docker port 7a38a1ad55c6
docker port determined_swanson
查看WEB应用程序日志

docker logs [ID或者名字] 可以查看容器内部的标准输出:

docker logs 7a38a1ad55c6
docker logs determined_swanson

-f:让 dokcer logs 像使用 tail -f 一样来输出容器内部的标准输出。

从上面,我们可以看到应用程序使用的是 5000 端口并且能够查看到应用程序的访问日志。

查看WEB应用程序容器的进程

我们还可以使用 docker top 来查看容器内部运行的进程:

docker top determined_swanson
检查WEB应用程序

使用 docker inspect 来查看Docker的底层信息。它会返回一个 JSON 文件记录着 Docker 容器的配置和状态信息:

docker inspect determined_swanson
停止WEB应用容器
docker stop determined_swanson
重启WEB应用容器
docker start determined_swanson

docker ps -l 来查看正在运行的容器

移除WEB应用容器
docker rm determined_swanson

删除容器时,容器必须是停止状态,否则会报如下错误

Docker 镜像使用
Docker 镜像使用

当运行容器时,使用的镜像如果在本地中不存在,docker 就会自动从 docker 镜像仓库中下载,默认是从 Docker Hub 公共镜像源下载。

  • 1、管理和使用本地 Docker 主机镜像
  • 2、创建镜像
列出镜像列表

我们可以使用 docker images 来列出本地主机上的镜像:

docker images
各个选项说明:
  • REPOSTITORY:表示镜像的仓库源
  • TAG:镜像的标签
  • IMAGE ID:镜像ID
  • CREATED:镜像创建时间
  • SIZE:镜像大小

同一仓库源可以有多个 TAG,代表这个仓库源的不同个版本,如ubuntu仓库源里,有15.10、14.04等多个不同的版本,我们使用 REPOSTITORY:TAG 来定义不同的镜像。

所以,我们如果要使用版本为15.10的ubuntu系统镜像来运行容器时,命令如下:

docker run -t -i ubuntu:15.10 /bin/bash

如果要使用版本为14.04的ubuntu系统镜像来运行容器时,命令如下:

docker run -t -i ubuntu:14.04 /bin/bash
获取一个新的镜像

当我们在本地主机上使用一个不存在的镜像时 Docker 就会自动下载这个镜像。如果我们想预先下载这个镜像,我们可以使用 docker pull 命令来下载它:

docker pull ubuntu:13.10
查找镜像

我们可以从 Docker Hub 网站来搜索镜像,Docker Hub 网址为:https://hub.docker.com/

我们也可以使用 docker search 命令来搜索镜像。比如我们需要一个httpd的镜像来作为我们的web服务。我们可以通过 docker search 命令搜索 httpd 来寻找适合我们的镜像。

   docker search httpd


- NAME:镜像仓库源的名称
- DESCRIPTION:镜像的描述
- OFFICIAL:是否docker官方发布
拖取镜像

我们决定使用上图中的httpd 官方版本的镜像,使用命令 docker pull 来下载镜像:

docker pull httpd

下载完成后,我们就可以使用这个镜像了:

docker run httpd
创建镜像
当我们从docker镜像仓库中下载的镜像不能满足我们的需求时,我们可以通过以下两种方式对镜像进行更改。
  • 1.从已经创建的容器中更新镜像,并且提交这个镜像
  • 2.使用 Dockerfile 指令来创建一个新的镜像
更新镜像

更新镜像之前,我们需要使用镜像来创建一个容器:

docker run -t -i ubuntu:15.10 /bin/bash

在运行的容器内使用 apt-get update 命令进行更新。

在完成操作之后,输入 exit命令来退出这个容器。

此时ID为e218edb10161的容器,是按我们的需求更改的容器。我们可以通过命令 docker commit来提交容器副本。

docker commit -m="has update" -a="youj" e218edb10161
w3cschool/ubuntu:v2
sha256:70bf1840fd7c0d2d8ef0a42a817eb29f854c1af8f7c59fc03ac7bdee9545aff8
各个参数说明:
  • -m:提交的描述信息
  • -a:指定镜像作者
  • e218edb10161:容器ID
  • w3cschool/ubuntu:v2:指定要创建的目标镜像名

我们可以使用 docker images 命令来查看我们的新镜像 w3cschool/ubuntu:v2:

构建镜像

我们使用命令 docker build , 从零开始来创建一个新的镜像。为此,我们需要创建一个 Dockerfile 文件,其中包含一组指令来告诉 Docker 如何构建我们的镜像。

cat Dockerfile
FROM    centos:6.7
MAINTAINER      Fisher "fisher@sudops.com"

RUN     /bin/echo 'root:123456' |chpasswd
RUN     useradd youj
RUN     /bin/echo 'youj:123456' |chpasswd
RUN     /bin/echo -e "LANG=\"en_US.UTF-8\"" &gt; /etc/default/local
EXPOSE  22
EXPOSE  80
CMD     /usr/sbin/sshd -D

每一个指令都会在镜像上创建一个新的层,每一个指令的前缀都必须是大写的。

第一条FROM,指定使用哪个镜像源

RUN 指令告诉docker 在镜像内执行命令,安装了什么。。。

然后,我们使用 Dockerfile 文件,通过 docker build 命令来构建一个镜像。

w3cschool@w3cschool:~$ docker build -t youj/centos:6.7 .
Sending build context to Docker daemon 17.92 kB
Step 1 : FROM centos:6.7
 ---&gt; d95b5ca17cc3
Step 2 : MAINTAINER Fisher "fisher@sudops.com"
 ---&gt; Using cache
 ---&gt; 0c92299c6f03
Step 3 : RUN /bin/echo 'root:123456' |chpasswd
 ---&gt; Using cache
 ---&gt; 0397ce2fbd0a
Step 4 : RUN useradd youj
......
参数说明:
  • -t :指定要创建的目标镜像名
  • . :Dockerfile 文件所在目录,可以指定Dockerfile 的绝对路径

使用docker images 查看创建的镜像已经在列表中存在,镜像ID为860c279d2fec

我们可以使用新的镜像来创建容器

docker run -t -i youj/centos:6.7  /bin/bash
设置镜像标签

我们可以使用 docker tag 命令,为镜像添加一个新的标签:

docker tag 860c279d2fec youj/centos:dev

docker tag 镜像ID,这里是 860c279d2fec ,用户名称、镜像源名(repository name)和新的标签名(tag)。

使用 docker images 命令可以看到,ID为860c279d2fec的镜像多一个标签。

Docker 容器连接
Docker 容器连接

前面我们实现了通过网络端口来访问运行在docker容器内的服务。下面我们来实现通过端口连接到一个docker容器

网络端口映射

我们创建了一个 python 应用的容器。

docker run -d -P training/webapp python app.py

另外,我们可以指定容器绑定的网络地址,比如绑定 127.0.0.1。

我们使用 -P 参数创建一个容器,使用 docker ps 来看到端口5000绑定主机端口32768。

我们也可以使用 -p 标识来指定容器端口绑定到主机端口。

两种方式的区别是:
  • -P :是容器内部端口随机映射到主机的高端口。
  • -p :是容器内部端口绑定到指定的主机端口。
docker run -d -p 5000:5000 training/webapp python app.py

上面的例子中,默认都是绑定 tcp 端口,如果要绑定 UPD 端口,可以在端口后面加上 /udp。

docker run -d -p 127.0.0.1:5000:5000/udp training/webapp python app.py

docker port 命令可以让我们快捷地查看端口的绑定情况:

docker port adoring_stonebraker 5002
Docker容器连接

端口映射并不是唯一把 docker 连接到另一个容器的方法。

docker有一个连接系统允许将多个容器连接在一起,共享连接信息。

docker连接会创建一个父子关系,其中父容器可以看到子容器的信息。

容器命名

当我们创建一个容器的时候,docker会自动对它进行命名。另外,我们也可以使用--name标识来命名容器,例如

docker run -d -P --name youj training/webapp python app.py

Mac 相关

mac使用小技巧

在mac上使用brew 安装软件的时候, 会更新很慢。解决办法如下:

解决方法,关闭自动更新:

export HOMEBREW_NO_AUTO_UPDATE=true

Windows 相关

安装mysql5.7遇到的坑

2018-10-27 安装mysql8.0遇到了挺多的坑,比如安装完毕navcat无法连接,原因是连接加密了。在安装过程中选中了其中一项需要校验密码,总是错误,另外一项又是无法连接,只要安装回5.7。发现没有弹框配置,只好手动配置。

原链接是这里:

https://blog.csdn.net/bingoxubin/article/details/78490483?utm_source=blogxgwz0

##以下二选一
# mysqld --initialize-insecure #这个方法初始化完后,root用户无密码
# mysqld --initialize --console #这个方法初始化完后,root用户有密码。密码是console中输出的一段字符串(记住该字符串)

##安装服务
mysqld --install

删除mysql服务 sc delete mysql
开启mysql服务 net start mysql
停止mysql服务 net stop mysql

php学习笔记

ThinkPhp3学习笔记

下载对应thinkphp包,复制到www项目目录下,改名web

访问127.0.0.1/web就可以访问了,如果报错注意权限。

菜鸟教程的PHP教程

macOS 使用了MxSrvs软件,相当于 Windows的wamp

语法

PHP 脚本以 <?php 开始,以 ?> 结束:

<?php
// PHP 代码
?>

PHP 文件的默认文件扩展名是 ".php"。

PHP 文件通常包含 HTML 标签和一些 PHP 脚本代码。

<!DOCTYPE html>
<html>
<body>

<h1>My first PHP page</h1>

<?php
echo "Hello World!";
?>

</body>
</html>

浏览器输出文本的基础指令:echo 和 print。

注释:

// 这是 PHP 单行注释

/*
这是
PHP 多行
注释
*/
变量
<?php
$x=5;
$y=6;
$z=$x+$y;
echo $z;
?>
PHP 变量规则:
  • 变量以 $ 符号开始,后面跟着变量的名称
  • 变量名必须以字母或者下划线字符开始
  • 变量名只能包含字母数字字符以及下划线(A-z、0-9 和 _ )
  • 变量名不能包含空格
  • 变量名是区分大小写的($y 和 $Y 是两个不同的变量)
PHP 是一门弱类型语言

在上面的实例中,我们注意到,不必向 PHP 声明该变量的数据类型。

PHP 会根据变量的值,自动把变量转换为正确的数据类型。

在强类型的编程语言中,我们必须在使用变量前先声明(定义)变量的类型和名称。

PHP 变量作用域

变量的作用域是脚本中变量可被引用/使用的部分。

PHP 有四种不同的变量作用域:
  • local
  • global
  • static
  • parameter
局部和全局作用域

在所有函数外部定义的变量,拥有全局作用域。除了函数外,全局变量可以被脚本中的任何部分访问,要在一个函数中访问一个全局变量,需要使用 global 关键字。

在 PHP 函数内部声明的变量是局部变量,仅能在函数内部访问:

<?php
$x=5; // 全局变量

function myTest()
{
    $y=10; // 局部变量
    echo "<p>测试函数内变量:<p>";
    echo "变量 x 为: $x";
    echo "<br>";
    echo "变量 y 为: $y";
}

myTest();

echo "<p>测试函数外变量:<p>";
echo "变量 x 为: $x";
echo "<br>";
echo "变量 y 为: $y";
?>
PHP global 关键字

global 关键字用于函数内访问全局变量。

在函数内调用函数外定义的全局变量,我们需要在函数中的变量前加上 global 关键字:

$x=5;
$y=10;

function myTest()
{
    global $x,$y;
    $y=$x+$y;
}

myTest();
echo $y; // 输出 15
?>

PHP 将所有全局变量存储在一个名为 $GLOBALS[index] 的数组中。 index 保存变量的名称。这个数组可以在函数内部访问,也可以直接用来更新全局变量。

上面的实例可以写成这样:

<?php
$x=5;
$y=10;

function myTest()
{
    $GLOBALS['y']=$GLOBALS['x']+$GLOBALS['y'];
}

myTest();
echo $y;
?>
Static 作用域

当一个函数完成时,它的所有变量通常都会被删除。然而,有时候您希望某个局部变量不要被删除。

要做到这一点,请在您第一次声明变量时使用 static 关键字:

<?php
function myTest()
{
    static $x=0;
    echo $x;
    $x++;
}

myTest();
myTest();
myTest();
?>
echo 和 print 语句
echo 和 print 区别:
  • echo - 可以输出一个或多个字符串
  • print - 只允许输出一个字符串,返回值总为 1

提示:echo 输出的速度比 print 快, echo 没有返回值,print有返回值1。

PHP EOF(heredoc) 使用说明

PHP EOF(heredoc)是一种在命令行shell(如sh、csh、ksh、bash、PowerShell和zsh)和程序语言(像Perl、PHP、Python和Ruby)里定义一个字串的方法。

使用概述:
  1. 必须后接分号,否则编译通不过
  2. EOF 可以用任意其它字符代替,只需保证结束标识与开始标识一致。
  3. 结束标识必须顶格独自占一行(即必须从行首开始,前后不能衔接任何空白和字符)。
  4. 开始标识可以不带引号或带单双引号,不带引号与带双引号效果一致,解释内嵌的变量和转义符号,带单引号则不解释内嵌的变量和转义符号。
  5. 当内容需要内嵌引号(单引号或双引号)时,不需要加转义符,本身对单双引号转义,此处相当与q和qq的用法。
<?php
echo <<<EOF
    <h1>我的第一个标题</h1>
    <p>我的第一个段落。</p>
EOF;
// 结束需要独立一行且前后不能空格
?>
PHP 5 数据类型

String(字符串), Integer(整型), Float(浮点型), Boolean(布尔型), Array(数组), Object(对象), NULL(空值)。

PHP 字符串

一个字符串是一串字符的序列,就像 "Hello world!"。

你可以将任何文本放在单引号和双引号中:

<?php
$x = "Hello world!";
echo $x;
echo "<br>";
$x = 'Hello world!';
echo $x;
?>
PHP 整型

整数是一个没有小数的数字。

整数规则:
  • 整数必须至少有一个数字 (0-9)
  • 整数不能包含逗号或空格
  • 整数是没有小数点的
  • 整数可以是正数或负数
  • 整型可以用三种格式来指定:十进制, 十六进制( 以 0x 为前缀)或八进制(前缀为 0)。
PHP 浮点型

浮点数是带小数部分的数字,或是指数形式。

PHP 布尔型

布尔型可以是 TRUE 或 FALSE。

PHP 数组

数组可以在一个变量中存储多个值。

在以下实例中创建了一个数组, 然后使用 PHP var_dump() 函数返回数组的数据类型和值:

<?php
$cars=array("Volvo","BMW","Toyota");
var_dump($cars);
?>
PHP 对象

对象数据类型也可以用于存储数据。

在 PHP 中,对象必须声明。

首先,你必须使用class关键字声明类对象。类是可以包含属性和方法的结构。

然后我们在类中定义数据类型,然后在实例化的类中使用数据类型:

<?php
class Car
{
  var $color;
  function __construct($color="green") {
    $this->color = $color;
  }
  function what_color() {
    return $this->color;
  }
}
?>
PHP NULL 值

NULL 值表示变量没有值。NULL 是数据类型为 NULL 的值。

NULL 值指明一个变量是否为空值。 同样可用于数据空值和NULL值的区别。

可以通过设置变量值为 NULL 来清空变量数据:

<?php
$x="Hello world!";
$x=null;
var_dump($x);
?>
PHP 5 常量

常量值被定义后,在脚本的其他任何地方都不能被改变。

常量是一个简单值的标识符。该值在脚本中不能改变。

一个常量由英文字母、下划线、和数字组成,但数字不能作为首字母出现。 (常量名不需要加 $ 修饰符)。

注意: 常量在整个脚本中都可以使用。

设置常量,使用 define() 函数,函数语法如下:

bool define ( string $name , mixed $value [, bool $case_insensitive = false ] )
该函数有三个参数:
  • name:必选参数,常量名称,即标志符。
  • value:必选参数,常量的值。
  • case_insensitive :可选参数,如果设置为 TRUE,该常量则大小写不敏感。默认是大小写敏感的。

常量在定义后,默认是全局变量,可以在整个运行的脚本的任何地方使用。

PHP 字符串变量

在 PHP 中,只有一个字符串运算符。

并置运算符 (.) 用于把两个字符串值连接起来。

在下面的实例中,我们创建一个名为 txt 的字符串变量,并赋值为 "Hello world!" 。然后我们输出 txt 变量的值:

<?php
$txt="Hello world!";
echo $txt;
?>
PHP 运算符

算术运算符:

<?php
$x=10;
$y=6;
echo ($x + $y); // 输出16
echo '<br>';  // 换行

echo ($x - $y); // 输出4
echo '<br>';  // 换行

echo ($x * $y); // 输出60
echo '<br>';  // 换行

echo ($x / $y); // 输出1.6666666666667
echo '<br>';  // 换行

echo ($x % $y); // 输出4
echo '<br>';  // 换行

echo -$x;
?>

PHP7+ 版本新增整除运算符 intdiv(),使用实例:

var_dump(intdiv(10, 3));

PHP 赋值运算符:

<?php
$x=10;
echo $x; // 输出10

$y=20;
$y += 100;
echo $y; // 输出120

$z=50;
$z -= 25;
echo $z; // 输出25

$i=5;
$i *= 6;
echo $i; // 输出30

$j=10;
$j /= 5;
echo $j; // 输出2

$k=15;
$k %= 4;
echo $k; // 输出3

$a = "Hello";
$b = $a . " world!";
echo $b; // 输出Hello world!

$x="Hello";
$x .= " world!";
echo $x; // 输出Hello world!

?>

PHP 递增/递减运算符:

<?php
$x=10;
echo ++$x; // 输出11

$y=10;
echo $y++; // 输出10

$z=5;
echo --$z; // 输出4

$i=5;
echo $i--; // 输出5
?>

PHP 比较运算符:

<?php
$x=100;
$y="100";

var_dump($x == $y);
echo "<br>";
var_dump($x === $y);
echo "<br>";
var_dump($x != $y);
echo "<br>";
var_dump($x !== $y);
echo "<br>";

$a=50;
$b=90;

var_dump($a > $b);
echo "<br>";
var_dump($a < $b);
?>

PHP 逻辑运算符:

<?php
$x = array("a" => "red", "b" => "green");
$y = array("c" => "blue", "d" => "yellow");
$z = $x + $y; // $x 和 $y 数组合并
var_dump($z);
var_dump($x == $y);
var_dump($x === $y);
var_dump($x != $y);
var_dump($x <> $y);
var_dump($x !== $y);
?>

三元运算符:

(expr1) ? (expr2) : (expr3)
//对 expr1 求值为 TRUE 时的值为 expr2,在 expr1 求值为 FALSE 时的值为 expr3。

自 PHP 5.3 起,可以省略三元运算符中间那部分。表达式 expr1 ?: expr3 在 expr1 求值为 TRUE 时返回 expr1,否则返回 expr3。
PHP If...Else 语句
在 PHP 中,提供了下列条件语句:
  • if 语句 - 在条件成立时执行代码
  • if...else 语句 - 在条件成立时执行一块代码,条件不成立时执行另一块代码
  • if...elseif....else 语句 - 在若干条件之一成立时执行一个代码块
  • switch 语句 - 在若干条件之一成立时执行一个代码块
<?php
$t=date("H");
if ($t<"20"){
    echo "Have a good day!";
}
?>
PHP Switch 语句
<?php
switch (n)
{
case label1:
    如果 n=label1,此处代码将执行;
    break;
case label2:
    如果 n=label2,此处代码将执行;
    break;
default:
    如果 n 既不等于 label1 也不等于 label2,此处代码将执行;
}
?>
PHP 数组
<?php
$cars=array("Volvo","BMW","Toyota");
$arrlength=count($cars);

for($x=0;$x<$arrlength;$x++)
{
    echo $cars[$x];
    echo "<br>";
}
?>
PHP 数组排序

排列:

$cars=array("Volvo","BMW","Toyota");
//升序
sort($cars);
//降序
rsort($cars);
//根据数组的值,对数组进行升序排列
$age=array("Peter"=>"35","Ben"=>"37","Joe"=>"43");
asort($age);
//根据数组的键,对数组进行升序排列
$age=array("Peter"=>"35","Ben"=>"37","Joe"=>"43");
ksort($age);
//根据数组的值,对数组进行降序排列
$age=array("Peter"=>"35","Ben"=>"37","Joe"=>"43");
arsort($age);
//根据数组的键,对数组进行降序排列
$age=array("Peter"=>"35","Ben"=>"37","Joe"=>"43");
krsort($age);
PHP超级全局变量

PHP中预定义了几个超级全局变量(superglobals) ,这意味着它们在一个脚本的全部作用域中都可用。 你不需要特别说明,就可以在函数及类中使用。

PHP 超级全局变量列表:
  • $GLOBALS
  • $_SERVER
  • $_REQUEST
  • $_POST
  • $_GET
  • $_FILES
  • $_ENV
  • $_COOKIE
  • $_SESSION

详细访问:http://www.runoob.com/php/php-superglobals.html

PHP 循环
在 PHP 中,提供了下列循环语句:
  • while - 只要指定的条件成立,则循环执行代码块
  • do...while - 首先执行一次代码块,然后在指定的条件成立时重复这个循环
  • for - 循环执行代码块指定的次数
  • foreach - 根据数组中每个元素来循环代码块

while 循环:

$i=1;
while($i<=5){
    echo "The number is " . $i . "<br>";
    $i++;
}

do...while 语句:

$i=1;
do{
    $i++;
    echo "The number is " . $i . "<br>";
}
while ($i<=5);

for 循环用于您预先知道脚本需要运行的次数的情况:

for ($i=1; $i<=5; $i++){
    echo "The number is " . $i . "<br>";
}

foreach 循环用于遍历数组:

$x=array("one","two","three");
foreach ($x as $value){
    echo $value . "<br>";
}
PHP 函数
function writeName(){
    echo "Kai Jim Refsnes";
}

echo "My name is ";
writeName();
PHP 函数 - 添加参数
 <?php
function writeName($fname){
    echo $fname . " Refsnes.<br>";
}

echo "My name is ";
writeName("Kai Jim");
echo "My sister's name is ";
writeName("Hege");
echo "My brother's name is ";
writeName("Stale");
?>
PHP 函数 - 返回值
<?php
function add($x,$y){
    $total=$x+$y;
    return $total;
}

echo "1 + 16 = " . add(1,16);
?>
PHP 魔术常量

PHP 向它运行的任何脚本提供了大量的预定义常量。

不过很多常量都是由不同的扩展库定义的,只有在加载了这些扩展库时才会出现,或者动态加载后,或者在编译时已经包括进去了。

有八个魔术常量它们的值随着它们在代码中的位置改变而改变。

__LINE__: 文件中的当前行号

__FILE__:文件的完整路径和文件名。如果用在被包含文件中,则返回被包含的文件名。

__DIR__:文件所在的目录。如果用在被包括文件中,则返回被包括的文件所在的目录。

__FUNCTION__:函数名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该函数被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。

__CLASS__:类的名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该类被定义时的名字(区分大小写)。

__TRAIT__:Trait 的名字(PHP 5.4.0 新加)。自 PHP 5.4.0 起,PHP 实现了代码复用的一个方法,称为 traits。

<?php
class Base {
    public function sayHello() {
        echo 'Hello ';
    }
}

trait SayWorld {
    public function sayHello() {
        parent::sayHello();
        echo 'World!';
    }
}

class MyHelloWorld extends Base {
    use SayWorld;
}

$o = new MyHelloWorld();
$o->sayHello();
?>

__METHOD__:类的方法名(PHP 5.0.0 新加)。返回该方法被定义时的名字(区分大小写)。

__NAMESPACE__:当前命名空间的名称(区分大小写)。此常量是在编译时定义的(PHP 5.3.0 新增)。

PHP 命名空间(namespace)
PHP 命名空间可以解决以下两类问题:
  1. 用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突。
  2. 为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。
定义命名空间

默认情况下,所有常量、类和函数名都放在全局空间下,就和PHP支持命名空间之前一样。

命名空间通过关键字namespace 来声明。如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间。语法格式如下;

<?php
// 定义代码在 'MyProject' 命名空间中
namespace MyProject;

// ... 代码 ...

也可以在同一个文件中定义不同的命名空间代码,如:

<?php
namespace MyProject;

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */  }

namespace AnotherProject;

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */  }
?>

不建议使用这种语法在单个文件中定义多个命名空间。建议使用下面的大括号形式的语法:

<?php
namespace MyProject {
    const CONNECT_OK = 1;
    class Connection { /* ... */ }
    function connect() { /* ... */  }
}

namespace AnotherProject {
    const CONNECT_OK = 1;
    class Connection { /* ... */ }
    function connect() { /* ... */  }
}
?>

将全局的非命名空间中的代码与命名空间中的代码组合在一起,只能使用大括号形式的语法。全局代码必须用一个不带名称的 namespace 语句加上大括号括起来,例如:

<?php
namespace MyProject {

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */  }
}

namespace { // 全局代码
session_start();
$a = MyProject\connect();
echo MyProject\Connection::start();
}
?>

在声明命名空间之前唯一合法的代码是用于定义源文件编码方式的 declare 语句。所有非 PHP 代码包括空白符都不能出现在命名空间的声明之前。

<?php
declare(encoding='UTF-8'); //定义多个命名空间和不包含在命名空间中的代码
namespace MyProject {

const CONNECT_OK = 1;
class Connection { /* ... */ }
function connect() { /* ... */  }
}

namespace { // 全局代码
session_start();
$a = MyProject\connect();
echo MyProject\Connection::start();
}
?>
子命名空间
<?php
namespace MyProject\Sub\Level;  //声明分层次的单个命名空间

const CONNECT_OK = 1;
class Connection { /* ... */ }
function Connect() { /* ... */  }

?>
命名空间使用
PHP 命名空间中的类名可以通过三种方式引用:
  • 非限定名称,或不包含前缀的类名称
  • 限定名称,或包含前缀的名称
  • 完全限定名称,或包含了全局前缀操作符的名称
namespace关键字和__NAMESPACE__常量

PHP支持两种抽象的访问当前命名空间内部元素的方法,__NAMESPACE__ 魔术常量和namespace关键字。

常量__NAMESPACE__的值是包含当前命名空间名称的字符串。在全局的,不包括在任何命名空间中的代码,它包含一个空的字符串。

使用命名空间:别名/导入

PHP 命名空间支持 有两种使用别名或导入方式:为类名称使用别名,或为命名空间名称使用别名。

在PHP中,别名是通过操作符 use 来实现的. 下面是一个使用所有可能的三种导入方式的例子:

1、使用use操作符导入/使用别名:

<?php
namespace foo;
use My\Full\Classname as Another;

// 下面的例子与 use My\Full\NSname as NSname 相同
use My\Full\NSname;

// 导入一个全局类
use \ArrayObject;

$obj = new namespace\Another; // 实例化 foo\Another 对象
$obj = new Another; // 实例化 My\Full\Classname 对象
NSname\subns\func(); // 调用函数 My\Full\NSname\subns\func
$a = new ArrayObject(array(1)); // 实例化 ArrayObject 对象
// 如果不使用 "use \ArrayObject" ,则实例化一个 foo\ArrayObject 对象
?>

2.一行中包含多个use语句:

<?php
use My\Full\Classname as Another, My\Full\NSname;

$obj = new Another; // 实例化 My\Full\Classname 对象
NSname\subns\func(); // 调用函数 My\Full\NSname\subns\func
?>

3、导入和动态名称:

<?php
use My\Full\Classname as Another, My\Full\NSname;

$obj = new Another; // 实例化一个 My\Full\Classname 对象
$a = 'Another';
$obj = new $a;      // 实际化一个 Another 对象
?>

4、导入和完全限定名称:

<?php
use My\Full\Classname as Another, My\Full\NSname;

$obj = new Another; // 实例化 My\Full\Classname 类
$obj = new \Another; // 实例化 Another 类
$obj = new Another\thing; // 实例化 My\Full\Classname\thing 类
$obj = new \Another\thing; // 实例化 Another\thing 类
?>

更详细:http://www.runoob.com/php/php-namespace.html

PHP 面向对象
$mercedes = new Car ();
$bmw = new Car ();
$audi = new Car ();
<?php
class Site {
  /* 成员变量 */
  var $url;
  var $title;

  /* 成员函数 */
  function setUrl($par){
     $this->url = $par;
  }

  function getUrl(){
     echo $this->url . PHP_EOL;
  }

  function setTitle($par){
     $this->title = $par;
  }

  function getTitle(){
     echo $this->title . PHP_EOL;
  }
}
?>

变量 $this 代表自身的对象。

PHP 中创建对象

类创建后,我们可以使用 new 运算符来实例化该类的对象:

$runoob = new Site;
$taobao = new Site;
$google = new Site;

调用成员方法:

// 调用成员函数,设置标题和URL
$runoob->setTitle( "菜鸟教程" );
$taobao->setTitle( "淘宝" );
$google->setTitle( "Google 搜索" );

$runoob->setUrl( 'www.runoob.com' );
$taobao->setUrl( 'www.taobao.com' );
$google->setUrl( 'www.google.com' );

// 调用成员函数,获取标题和URL
$runoob->getTitle();
$taobao->getTitle();
$google->getTitle();

$runoob->getUrl();
$taobao->getUrl();
$google->getUrl();

内容有些多,详情访问:http://www.runoob.com/php/php-oop.html

PHP 表单和用户输入
黄永成thinkphp3.1视频讲解笔记内容
安装

下载3.1核心版本

复制到项目目录,同级新建index.php输入:

<?php

    define('APP_NAME','Index');
    define('APP_PATH','./Index/');
    //define('APP_DEBUG',TRUE);

    include './ThinkPHP/ThinkPHP.php';//下载的核心版本的php文件

?>
配置

运行后会在目录下创建Index目录,进入目录Conf会有配置文件

实例化函数:

function __constuct(){}

公共配置文件Conf/config.php:

<?php
return array(
    'DB_HOST' => '127.0.0.1',
    'DB_USER' => 'root',
    'DB_PWD' => '',
    'DB_NAME' => 'think',
    'DB_PREFIX' => 't_',
);
?>
//获取配置项
C('DB_HOST')
扩展函数

Common/common.php下定义的函数就能自动加载

如果在Common下定义其他名称的函数就要在config.php中配置:

LOAD_EXT_FILE => 'function'; //函数名称

另外一种方法只能在前期方法里面生效:

function public index(){
    load('@.function');
    //xxxx
}
引入静态文件
__PUBLIC__
//更改位置
在配置config.php文件中修改:
'TMPL_PAESR_STRING' => array('__PUBLIC__'=>__ROOT__.'/'.APP_NAME.'/tpl');

U函数的使用 和I函数的使用。

修改后缀:config.php:

'URL_HTML_SUFFIX' => '';

post判断:IS_POST _404()

M函数

应用分组部署及公共项独立

修改配置文件config.php:

return array(
    'APP_GROUP_LIST' => 'Index,Admin',
    'DEFAULT_GROUP' => 'Index',
);

//然后在Action中删掉原来的Action.php  新建Index目录在下面新建Action文件,然后在新建Admin文件里面在创建Action文件

//然后在Conf下创建对应模块的目录就可以实现独立的模块配置访问了

//同理Common也一样。模板也一样。

指定错误页面:修改配置文件config.php:

'TMPL_EXCEPTION_FILE' => 'XXX/error.html'

error.html:

<?php ecch $e['message'];?>

设置默认的I的过滤函数:

'DEFAULT_FILTER' => 'htmlspecialchar',

F 函数用来做数据的存储,看着挺实用的,初始化存储不变的常量

验证码的使用

框架已经帮写好了直接使用:

public function verify(){
    import('ORG.Util.Image');
    Image::buildImageVerify(4,5,'png')
}
后台登录验证与自动运行

新建CommonAction.class.php:

public function _initialize(){
    if(!isset(xxxx)){
        跳转
    }
}

Class IndexAction extends CommonAction{

    里面的所有function都会自动校验
}
分页

可以这样引用css/js:

<css file='__PUBLIC__/style.css'>
<js file='__PUBLIC__/style.css'>

独立分组

功能和普通分组差不多,只是文件夹目录提高一层。

内容为第14章

'APP_GROUP_MODE' => 1. 'APP_GROUP_PATH' => 'app'

RBAC权限

内容挺多的 需要看帮助手册

文章

倔强的程序员

原文:https://www.oschina.net/news/102794/stubborn-programmer

对于程序员来说,大多数人公司都有技术和管理两条发展路线,通常在同一家公司,管理路线的发展可能性,要相对广阔一些;但是技术路线也有技术路线的好处,比如相对而言更依赖于硬实力,因而工作机会丰富。我相信有不少程序员都和我一样,坚守着技术路线,无论是进还是退,都对管理者的岗位没有什么兴趣。

兴许大家都听到软实力和硬实力的概念。对于一个技术人来说,硬实力大致上可以认为是计算机和软件工程相关的技术能力,1 还是 0,是还是非,会不会算法,懂不懂设计,清清楚楚,明明白白; 而软实力则反过来,听起来挺抽象,挺模糊,比如沟通能力,自我管理能力,但是却扮演者重要的角色,甚至随着职业生涯的发展,它的影响力越来越大。而性格,是软实力中一个很特别的影响因素。

下面我讲的是在程序员技术发展路线中,“倔强”性格的影响这一个窄窄的范围,而且是就我的认知而言的。显而易见它不可能是很客观的。我相信会有很多人持有不同的看法。

我想大家都认可的是,基本上每个团队里面都有各种脾气性格的人。记得我刚工作的时候,团队里面平和和好说话的人更多。多数人性格都比较平和,这可能和资历、眼界等等因素也有关。以前读过一篇文章,说一个团队里面,有各种各样的角色,有牛、有猪,有狗、有猴子等等等等,分别代表着不同的性格。随着时间的试炼,大家发展的情况各不相同。在讨论方案和问题的时候,肯定有人不同意,但是只要多数人决定了做法,或者是几个强硬派决定了做法,大多数人也就不再计较,因此 commitment 比较容易做出,且朝着一致的方向。

但是随着工作年头的增加,我发现团队里面个人的性格,普遍是越来越倔了。无论什么时候我们讨论问题,观点不同是司空见惯的。可现在不同的是,要达成一致,并不是那么容易的事情。好吧,大多数人支持方案 A,少数人支持 B,兴许几年前这票支持 B 的就表示愿意按照多数派的 A 来实施了;可是现在呢,少数派一定要争论下去,技术方案的选择不是少数服从多数的选举,为什么要 A,我们来给 A 和 B 做做分析,我们来激烈地争论吧……

以前我认为,职业生涯的发展,到一定阶段,高级别一些的工程师,想必也是性格各异的吧,应该有的人比较强硬,有的人比较容易 pushover 的吧,性格这东西嘛,分布都是有随机性的。可是如今我接触到的情况呢,却恰恰相反。 这些发展比较好的程序员,相对于其年龄和资历,级别比较高的程序员,居然性格几乎无一例外的“倔强”。 而那些性格比较“好”的呢,相对来说发展普遍都没有那么好。看到的案例多了,似乎可以粗略地得到这样的结论:走技术路线的程序员中,性格倔强的人不一定发展得快,但是性格平和的人肯定不行。

虽然我能看得到的案例数量并不大,但我依然觉得这个现象有一定代表性。我觉得这里有这么几个因素:

倔强的程序员,往往也是较真的程序员,他们会追求最佳的解决方案,他们会追求最合理的代码实现,他们可能抠一点某些人看来无足轻重的东西,但是就是这些东西把软件的质量提高。

倔强的程序员,懂得维护自己认为正确的观点,而为了维护这个观点,会反复思考和分析。我没有见过一个能把 trade-off 做得好的人对维护自己的观点抱无所谓的态度。

倔强的程序员,遇到困难也不那么容易退缩。这也是显而易见的,性格软弱的人,通常也不愿意坚持己见。

倔强的程序员,他们享受争论的过程,也就更能够在争论中得到多样的视角。

但是,物极必反,倔强的程序员,也可能死得特别惨。我见过一些 被踢出团队的程序员,大致分为两类。一类是能力实在不足,绩效特别差,比如代码写得又慢 bug 又多;还有一类就是这类硬骨头,倔强到难以维持基本客观的程度,到处树敌,太过拖累整个团队的工作。

再结合程序员工作中的许多具体事情,再进一步谈一谈这些倔强的程序员们。就说个有趣的事情吧。我们把他们中的其中一个,叫做大 Z(这个字母看着就很霸气),而相对不那么“难搞”的程序员,叫做小 s。

在一次的设计讨论会议上大 Z 说对小 s 说,我认为你的方案不如我的好,理由是 xxxxx,于是大 Z 和小 s 来来回回一番争论,刚开始还算可控,但是大 Z 说,“我觉得你缺少扩展性的常识”。有经验的人可能马上意识到,大 Z 的这句话已经从“对事”变成了“对人”,这明显是不对的。于是这句话一冒出来,小 s 马上就不高兴了,再不痛不痒辩论几句以后,没有继续争论下去,显得很失落。

这个场景看起来是不是很熟悉?哪怕小 s 是更在理的一方,也放弃了继续争论下去的欲望,反而落得自己不爽好几天,每次和大 Z 沟通都会想着当时的场景,甚至觉得大 Z 还会有意无意针对他。有人可能会觉得,那大 Z 会不会事后觉得自己过分呢?我想说,大多数情形下,不会的, 以大 Z 的性格来说,他冒犯了小 s,他也许意识到了,也许没意识到,可是这样的事情他根本就不会放在心上 。回到事情本身,谁的方案更合理很难讲,但这件事情本身伤害到了团队中的成员,影响了团队的氛围。我们可能见到类似的事情到处都是,甚至在某些沟通强烈的地方尤为严重,比如 code review。

多数情况下,我们撇开技术本身的因素,谁的发展更好呢?却是大 Z。虽然有少数情况并非如此,但是多数情况下,大 Z 却有着更更为广泛的影响力,而 某些情况下争论所显露出来的 backbone 会盖过他在争论和为人上面的“恶霸”属性 。这也从某种角度说明,为什么到了一定级别的程序员,且不论技术如何,心理承受能力和沟通的技巧,都是有一定造诣的,那些敏感而脆弱的呢,已经挂在晋升的半路上了。

交流和沟通本身就是一个说不清道不明的复杂体,很多人可能会想要安安心心做技术,我相信也有很多公司希望提供这样环境。可事实是,绝大多数情况下,越是这样想的人,就越会发现,这只是一种美好的愿望,不可避免地,有很多为人处世上的“屁事”,未必要上升到“职场政治”那么高的程度,却依然会考验你的心理,磨炼你的性格。

最后,从团队管理的角度来说,哪一种人更合适呢?

其实,“合适”这个词的定位很难讲,但是倔强的程序员通常更难管理,这倒是真的。可是,换一个角度想这个问题,为什么要“管”,管理又要做到怎样的侵入性?理想的状况是,虽然有一些性格似乎比较“强硬”的程序员,但是他们是讲原则,讲道理的,如果团队的成员在总的目标上大致是一致的,团队就能够具备一定的兼容性。可理想毕竟是理想,团队中的磕磕碰碰遇到谁都能喝一壶的。特别是,如果管理者想成为那个决策绝大多数事情的人,碰到这些倔强的“大爷”们,很可能就会碰一鼻子灰。

在我的职业生涯中,待过好些团队,我见过这种管理者和倔强的程序员们之间的碰撞,有在挣扎和妥协中寻求平衡的,有程序员滚蛋的,也有管理者扫地出门的,甚至有两败俱伤,鱼死网破的。这里面也有很多有趣的故事,下次再说吧。

项目

项目相关

Python项目

聊天框架rockat.chat的api集成使用

注解

编写flask api接口,去调用rockat.chat,使项目对接rockat.chat更方便

开始项目:

pip install cookiecutter
cookiecutter https://github.com/sloria/cookiecutter-flask.git

虚拟环境:

python3 -m virtualenv venv
source venv/bin/activate

安装rocketchat_API:

pip install rocketchat_API

安装flask_restful:

pip install flask-restful

安装sphinx:

pip install sphinx
#创建项目
sphinx-quickstart
#第一 Y  第二 回车 第三 项目名  第四作者名  以下N

#
pip install sphinxcontrib-httpdomain

conf.py:

extensions = [
    'sphinx.ext.autodoc',
    'sphinxcontrib.httpdomain',
    'sphinxcontrib.autohttp.flask',
    'sphinxcontrib.autohttp.flaskqref',
]

index.rst:

Welcome to rocket_chat_api's documentation!
===========================================


.. toctree::
   :maxdepth: 2
   :caption: Contents:

   introduction

创建introduction.rst文件:

===================
`myapp`
===================

Oasis Flask app that handles keys requests.

myapp.app
---------------------

.. automodule:: main.api.v1.views
   :members:
   :undoc-members:

ok

公众号《安星物业服务》

模块:请假、出入、违纪

人员角色:学生、教师、家长、校管员、管理员、其他人员

请假:

发起请假:校门卫、教师、家长

批准请假:教师

请假通知:家长、教师

请假查看:校管员、教师、家长

出入:

学生出入:校牌扫码一出一入

车辆出入:固定车辆、临时车辆[往后有更优方案]

其他人员出入:临时出入登记方案

违纪

发起违纪:教师、校检员

发起对象:学生、班级、宿舍

如果对您有用,请我喝杯咖啡吧。

赞助扫码::
_images/apay.jpg _images/pay_wechat.png