转载于:http://pdf.us/2017/10/03/451.html,感谢这位大神
《Flask Web开发:基于Python的Web应用开发实战》学习笔记
这里是第一部分的学习笔记。第一部分:Flask简介
准备工作Git
git clone https://github.com/miguelgrinberg/flasky.git
git checkout 1a #签出到某个版本
git reset --hard #若修改代码,强行还原到签出状态
git fetch --all
git fetch --tags
git reset --hard origin/master
fetch从远程仓库更新本地仓库的提交历史和标签,但并不改动原文件;git reset才真正写入。
git diff 2a 2b #查看版本间的区别
git tag #查看所有标签
git log --oneline --graph #查看标签历史
第一章 安装
flask官网:http://flask.pocoo.org/
使用虚拟环境
pip install virtualenv (15.1.0)
yum install python-virtualenv (1.10.1)
virtualenv venv #创建虚拟环境
. venv/bin/activate #进入虚拟环境
deactivate #退出虚拟环境
pip install flask #安装flask
第二章 程序的基本结构
初始化
from flask import Flask
app = Flask(__name__)
__name__参数用于决定程序的根目录,以便找到相对根目录的其它资源
路由和视图函数
路由,处理URL与函数之间的关系,常使用app.route修饰器
视图函数的返回值是响应
动态类型:
默认,字符串;int,整数,如<int:id>;float,浮点数;path,包含斜线/的字符串
启动服务器
if __name__='__main__':
app.run(debug=True) #激活调试器和重载程序
一个完整的程序
hello.py:
请求-响应循环
上下文在线程级别全局可访问
user_agent=request.headers.get('User-Agent') #该方法获取User-Agent
Flask上下文全局变量:
变量名 | 上下文 | 说明 |
current_app | 程序上下文 | 当前激活程序的程序实例 |
g | 程序上下文 | 处理请求时用作临时存储,每次请求均会重设 |
request | 请求上下文 | 请求对象,封装客户端请求内容 |
session | 请求上下文 | 用户会话,存储请求间需要记住的内容 |
app.app_context()可以获得程序上下文,之后再.push()推送上下文,然后才可以使用current_app;收回程序上下文则是pop()。
form flask import current_app
app_ctx=app.app_context()
app_ctx.push()
current_app.name
app_ctx.pop()
请求调度
生成映射的方法:1、app.route修饰器;2、app.add_url_rule()。app.add_url_rule('/ma/','index',hello.index)
查看url映射:app.url_map。 请求方法中,HEAD,OPTIONS由Flask自动处理,不需要单独写方法。
请求钩子
请求钩子用于在请求处理之前或之后执行,使用修饰器实现。
1、before_first_request:在处理第一个请求这前执行;
2、before_request:在每次请求前执行;
3、after_request:若无未处理的异常,在每次请求后执行;
4、teardown_request:即便有未处理的异常,也在每次请求后执行
在请求钩子函数和视图函数之前共享数据,一般使用g
响应
响应可指定状态码,如:return '<h1>Hello,World!</h1>',400
响应还可带第三个参数,由首部(header)组成的字典。响应也可以返回给Response对象,该对象也包含三个参数。与前面的相对应。response设置cookie的例子:
重定向302
...
return redirect('http://pdf.us/')
abort函数,用于处理错误,但不会将控制权还给调用它的函数,而是抛出异常将控制权交给Web服务器
...
user=load_user(id)
if not user:
abort(404)
return '<h1>Hello,%s</h1>' % user
Flask扩展
Flask-Script扩展,支持命令行选项,安装:pip install flask-script
在程序中注册扩展的方法
manager=Manager(app)
#...
if __name=='__main__':
manager.run()
专门为Flask开发的扩展都暴露在falsk.ext命令空间下。
python hello.py runserver -h 0.0.0.0 -p 80 -d -r #启动开发服务器的正确姿势
第三章 模板
业务逻辑与表现逻辑
其中,index.html和user.html在templates目录下。user.html内容为:<h1>Hello,{{name}}</h1>
模板中的变量
形如{{ mydict['key'] }} {{ mylist[3] }} {{ myobj.somemethod() }} 都支持
过滤器
{{ <h1>Hello</h1>|safe }}
过滤器 | 说明 |
safe | 渲染值时不转义 |
capitalize | 首字母大写 |
lower | 小写 |
upper | 大写 |
title | 每个单词首字母大写 |
trim | 渲染时去掉首尾空格 |
striptags | 渲染之前把值中所有HTML标签都删掉 |
控制结构
条件控制 if...else...endif
for循环
宏,类似于函数
宏也可以导入使用
能重复使用的代码片也可以使用导入
模板的继承
基模板base.html
衍生模板index.html
注意head块,因为不是空的,所以使用super()获取原来的内容。
使用Flask-Bootstrap
pip install flask-bootstrap
from flask.ext.bootstrap import Bootstrap #from flask_bootstrap import Bootstrap
#...
bootstrap = Bootstrap(app)
使用{% extends "bootstrap/base.html" %},然后可以定义各种块。在定义styles和scripts块时,需要特别注意,要使用{{super()}},否则基模板中的相应块中的内容会丢失。
自定义错误页面
采用模板继承机制,简化模板制作
链接
url_for('index') 得到 '/' 这样引用:{{ url_for('index') }}
url_for('index',_external=True) 得到绝对地址
生成动态地址时,将动态部分作为关键字参数传入:url_for('user',name='john',_external=True)
动态地址还可以传入额外参数:url_for('index',page=2) 生成:/?page=2
静态文件
通常存放地static文件夹下面,可以有子文件夹
url_for('static',filename='css/style.css',_external=True)将返回.../static/css/style.css
Flask-Moment本地化日期时间
pip install flask-moment
from flask.ext.moment import Moment #from flask_moment import Moment
moment=Moment(app)
使用moment时,还需要引入jquery.js库【该库bootstrap已经引用】和moment.js库
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
使用:
from datetime import datetime
#...
@app.route('/')
def index():
return render_template('index.html',current_time=datetime.utcnow())模板:
{{ moment(current).format('LLL') }} #L~LLLL代表不同复杂度,LTS
{{ moment(current).fromNow(refresh=True) }} #相对时间,a minute ago
Flask-Moment实现了moment.js中的format(),fromNow(),fromTime(),calendar(),valueOf(),unix()方法,具体查看文档http://momentjs.com/docs/#/displaying/
设置语言:{{ moment.lang('es') }}
第四章 Web表单
request.form能获取POST请求中提交的表单数据
或者使用Flask-WTF扩展
pip install flask-wtf
WTF需要设置一个密钥,用于验证表单数据真伪,设置方法:
app.config['SECRET_KEY'] = 'yourkey'
app.config字典用于存储框架、扩展和程序本身的配置变量。
表单类
每个Web表单都继承自Form类,这个类定义表单中的字段,每个字段都用对象表示,字段对象可附属验证函数。表单的字段都定义为类变量,类变量的值是相应字段类型的对象。
定义表单类
其中,from flask.ext.wtf import Form可用from flask_wtf import Form替换
字段类型 | 说明 |
StringField | 文本字段 |
TextField | 多行文本 |
PasswordField | 密码文本 |
HiddenField | 隐藏文本 |
DateField | 文本,值为datetime.date格式 |
DatetimeField | 文本,值为datetime.datetime格式 |
IntegerField | 文本,值为整数 |
DecimalField | 文本,值为decimal.Decimal |
FloatField | 文本,值为浮点 |
BooleanField | 复选框,值为True或False |
RadioField | 一线单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下接列表,可多选 |
FileField | 文件上传 |
SubmitField | 提交按钮 |
FormField | 嵌套表单 |
FieldList | 一组指定类型的字段 |
验证函数 | 说明 |
电子邮件地址 | |
EqualTo | 比较两个字段的值,常用于二次密码验证 |
IPAddress | IPv4地址 |
Length | 字符串上度 |
NumberRange | 数字范围 |
Optional | 无输入值时跳过其它验证函数 |
Required | 不为NULL |
Regexp | 使用正则表达式验证 |
URL | URL |
AnyOf | 确保输入值在可选值列表中 |
NoneOf | 确认输入值不在可选值列表中 |
渲染表单
表单字段可调用,在模板中调用后会渲染成HTML。
例如视图函数中将表单通过参数form传入模板,则在模板中这样渲染:
更好的方式是使用bootstrap:
{{ wtf.quick_form(form) }}
在视图函数中处理表单
form.validate_on_submit方法在所有字段通过验证函数,且点击提交后,为True,否则为False.
重定向和用户会话
最好不要将POST请求作为浏览器发送的最后一个请求!
Post/重定向/Get模式
默认情况下,用户会话session保存在客户端cookie中,使用SECRET_KEY进行加密签名,如果篡改,签名就会失效
Flash消息
确认消息、警告消息、错误消息
Flask开放函数get_flashed_messages()给模板,用于模板获取并渲染消息
{{ message }}
{% endfor %}
第五章 数据库
对象关系映射——ORM——SQL
对象文档映射——ODM——NoSQL
Flask-SQLAlchemy
pip install flask-sqlalchemy
数据库引擎 | URI |
MySQL | mysql://username:password@hostname/database |
Postgres | postgresql://username:password@hostname/database |
SQLite | sqlite:absolute/path/to/database |
SQLite | sqlite:///c:/absolute/path/to/database |
使用mysql时,pip install mysql-python 安装MySQLdb模块,然后手动新建数据库database
初始化数据库
定义模型
模型是一个类,模型一般对应一张表,类属性对应数据库表中的列
常用列类型及对应的Python类型
类型名 | 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 | 二进制文件 |
选项名 | 说明 |
primary_key | 主键 |
unique | 不允许出现重复值 |
index | 索引 |
nullable | 允许使用空值 |
default | 为该列定认默认值 |
关系
一对多关系:Role(一),User(多)
关于一对多关系的进一步说明:在多的一方添加外键,外键指明了对应另外一方的哪一列。在一的一方的users属性代表这个关系的面向对象视角,对Role类的实例(行)可以通过users属性返回关联列表,relationship第一个参数指明了关系的另一端,backref参数向User模型添加role属性,从而定义反向关系。
选项名 | 说明 |
backref | 在关系的另一个模型中添加反向引用 |
primaryjoin | 明确指定两个模型之间使用的联结条件。只在模棱两可的关系中指定 |
lazy | 指定如何加载相关记录 |
select(首次访问按需加载) | |
immediate(源对象加载后就加载) | |
joined(加载记录,但使用联结) | |
subquery(立即加载但使用子查询) | |
noload(永不加载) | |
dynamic(不加载记录,但提供加载记录的查询) | |
uselist | 若设为False,不使用列表,而使用标量值 |
order_by | 指定排序方式 |
secondary | 指定多对多关系中关系表的名字 |
secondaryjoin | SQLAlchemy无法自行决定时,指定多对多关系中的二级联结条件 |
一对一关系:使用一对多关系,但调用db.relationship()时,把uselist设为False,把多变成一;
多对一关系:使用一对多关系,对调两个表;或者把外键和db.relationship()都放到多这一侧;
多对多关系:使用关系表
数据库操作
根据模型类创建数据库:db.create_all()
如果数据库表已经存在于数据库,那么create_all将不会重新创建或更新这个表。如果修改数模型后,要将修改应用到数据库,则只有先删除再重新创建[会清空数据]:
db.create_all()
>>> 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)
>>> user_dong=User(username='dong',role_id=1)
>>> 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)
>>> db.session.add(user_dong)
### db.session.add_all([admin_role,mod_role,user_role,user_joho,user_susan,user_david,user_dong])
>>> db.session.commit()
### db.session.rollback() 回滚会话
>>> db.session.add(admin_role)
>>> db.session.commit()
>>> db.session.commit()
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
### 使用过滤器
>>> User.query. filter_by(role=user_role).all()
[<User u'susan'>, <User u'david'>]
### 查询原生SQL
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username, users.role_id AS users_role_id \nFROM users \nWHERE %s = users.role_id'
常用的查询过滤器:
过滤器 | 说明 |
filter() | 把过滤器添加到原查询上,返回一个新查询 |
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询 |
limit() | 限制原查询返回结果数量,返回一个新查询 |
offset() | 偏移原查询返回结果,返回一个新查询 |
order_by() | 对原查询结果进行排序,返回一个新查询 |
group_by() | 对原查询进行分组,返回一个新查询 |
查询执行函数
方法 | 说明 |
all() | 以列表形式返回所有结果 |
first() | 返回查询第一个结果,若无,返回None |
first_or_404() | 返回查询第一个结果,若无,中止,返回404 |
get() | 返回指定主键对应的行,若无,返回None |
get_or_404() | 返回指定主键对应的行,若无,中止,返回404 |
count() | 返回查询结果数量 |
paginate() | 返回Paginate对象,包含指定范围的结果 |
一对多关系的查询
>>> users=user_role.users
>>> list(users)
[<User u'susan'>, <User u'david'>]
>>> users[0].role
<Role u'User'>
>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>]
>>> user_role.users.count()
2L
在视图函数中操作数据库
注意,这里并没有db.session.commit(),
因为app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
集成Python shell
使用shell时,每次需要导入app,db,User,Role,为简化操作,可以为shell添加上下文:
数据库迁移
不丢失数据的更新数据库
pip install flask-migrate
from flask.ext.migrate import Migrate,MigrateCommand
#from flask_migrate import Migrate,MigrateCommand
#...
migrate=Migrate(app, db)
manager.add_command('db',MigrateCommand)
维护数据库前,先创建迁移仓库:
python hello.py db init
会生成migrations目录
upgrade()把改动应用到数据库
downgrade()将改动删除
自动创建的迁移不一定总是正确,需要认真检查!!
python hello.py db migrate -m "inital migration"
检查并修正好脚本后(位于migtarions/versions目录)后,使用db upgrade迁移:
python hello.py db upgrade
第六章 电子邮件
pip install flask-mail
MAIL_SERVER:localhost
MAIL_PORT:25
MAIL_USE_TLS:False
MAIL_USE_SSL:False
MAIL_USERNAME:None
MAIL_PASSWORD:None初始化:
from flask_mail import Mailapp.config['MAIL_SERVER']='smtp.126.com'
app.config['MAIL_PORT']=465
app.config['MAIL_USE_SSL']=True
app.config['MAIL_USERNAME']=os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD']=os.environ.get('MAIL_PASSWORD')
#export MAIL_USERNAME='mymailname'
#export MAIL_PASSWORD='mymailpassword'mail=Mail(app)
命令行测试发送邮件
>>> from hello import mail
>>> msg=Message('test',sender='xxx@126.com',recipients=[' xxx@163.com'])
### 奇怪的是只能自己给自己发,给别人发会报535错误,被当成垃圾邮件啦?
### msg.body='text body'
### msg.html='<h1>test mail</h1>
>>> with app.app_context():
... mail.send(msg)
在程序中添加发送邮件功能
为配合,需要templates目录下新建mail子目录,里面存放邮件的模板。模板示例:
User <b>{{ user.username }}</b> has joined.
异步发送邮件
第七章 大型程序的结构
项目结构:
配置选项
使用配置类。公共配置放于Config类,开发,测试,生产分别使用继承自Config类的类,最后生成一个config字典。调用字典时:config['DevelopmentConfig']()生成所有配置。
配置条目相应要做修改:
app.config['SECRET_KEY'] = os.environ.get('Key') or 'hard to guess string'
改为:
SECRET_KEY= os.environ.get('Key') or 'hard to guess string'
如果环境中有,则使用环境中的值,环境中无,则使用配置中的值
程序包
使用程序工厂函数
延迟创建程序实例,创建时使用工厂函数。工厂函数在app包的构造文件中定义__init__.py
在蓝本中实现程序功能
蓝本中定义的路由处于休眠状态,只有蓝本注册到程序上后,路由才成为程序的一部分。
app/main/__init__.py创建蓝本
注册蓝本到程序,app/__init__.py中
蓝本中的错误处理程序:app/main/errors.py
蓝本中的路由:app/main/views.py
Flask会为蓝本全部端点加上命名空间,空间名就是蓝本名,所以这里的url_for函数参数为main.index可简写为.index。同一蓝本中重定向可以简写,但是跨蓝本的重定向必须使用有命名空间的端点名。
启动脚本
manage.py
需求文件
requirements.txt用于记录所有依赖包及精确的版本号
生成:pip freeze >requirements.txt
使用:pip install -r requirements.txt
单元测试
test/test_basics.py
setUp()和tearDown()分别在测试前后运行,名字以test_开头的函数都作为测试执行。
test/__init__.py可为空,因为unittest会扫描所有模块并查找测试。
为方便测试,可以添加启动命令:
测试:python manager.py test
创建数据库
python manager.py db upgrade