Hello World

虚拟环境

Virtualenv

  • Virtualenv与VirtualenvWrapper

    • Wrapper是对Virtualenv的一个封装
    • 可以将虚拟环境整合在一个目录下集中管理(~/.virtualenvs)
    • 可以快速切换虚拟环境
  • 安装虚拟环境(以管理员身份安装则所有用户均可使用)

    # 安装virtualenv
    sudo pip install virtualenv
    # 安装virtualenvwrapper
    sudo pip install virtualenvwrapper
    # 启动即加载virtualenv
    echo "export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3" >> ~/.bashrc
    echo "source virtualenvwrapper.sh" >> ~/.bashrc
    
  • 虚拟环境的简单使用

    • 单纯使用Virtualenv

      # 新建虚拟环境
      virtualenv venv
      # 激活虚拟环境
      source ./venv/bin/activate
      # 退出虚拟环境
      deactivate
      # 删除环境
      rm -rf venv/
      
    • 使用VirtualenvWrapper

      # 新建虚拟环境
      mkvirtualenv [--python=python3.8] venv1
      # 激活/切换虚拟环境
      workon venv1
      # 退出虚拟环境
      deactivate
      # 删除环境
      rmvirtualenv venv1
      

Conda(推荐)

  1. 安装:直接到官网下载安装即可(建议安装Miniconda)
  2. 常用命令如下:
# 创建名为fa新的虚拟环境
conda create -n fa python=3.9

# 激活虚拟环境
conda activate fa

# 退出激活虚拟环境
conda deactivate fa

# 删除虚拟环境
conda env remove fa

# 列出当前机器上的所有环境
conda env list

MYSQL的安装与常用命令

# ----------------
# 安装与初始化配置
# ----------------
sudo apt install mysql-server mysql-client libmysqlclient-dev
sudo mysql_secure_installation

# ----------------
# 登录
# ----------------
mysql -uroot -p

# ----------------
# 用户与权限管理
# ----------------
# 创建用户命令:CREATE USER 'username'@'host' IDENTIFIED BY 'password';
# host:指定该用户在哪个主机上可以登录,如果是本地用户可用'localhost',如果想让用户可以从任意远程主机登录,可以使用通配符'%'
# 示例:创建用户名为evan,密码为Hello123的用户
mysql> create user 'evan'@'%' identified by 'Hello123';

# 授权:GRANT privileges ON databasename.tablename TO 'username'@'host'
# 示例:创建数据库HelloDjango并授权给用户evan
mysql> create database HelloDjango charset=utf8;
mysql> grant all privileges on HelloDjango.* to 'evan'@'%';
mysql> flush privileges;

# 撤销权限
mysql> REVOKE privilege ON databasename.tablename FROM 'username'@'host';

# 修改mysql用户密码
mysqladmin -uevan -pHello123 password Hello1234

# 示例:删除用户与数据库
mysql> drop user 'evan'@'%';
mysql> drop database HelloDjango;

# ----------------
# 开启远端访问
# ----------------
netstat -an | grep 3306
# 输出如下则绑定ip为127.0.0.1,则远程无法访问
# tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN
vim /etc/mysql/mysql.conf.d/mysqld.cnf
# 注释 bind-address=127.0.0.1这行并重启mysql
# 同时用户的host应该为'%'

Samba的配置

  • 安装

    sudo apt install samba
    
  • 配置

    # 备份配置文件
    sudo cp /etc/samba/smb.conf /etc/samba/smb.conf.bak
    # 添加当前用户到samba中
    sudo smbpasswd -a evan
    # 编辑/etc/samba/smb.conf文件添加用户目录
    [homes]
       comment = Home Directories
       browseable = no
       read only = no
       create mask = 0755
       directory mask = 0755
       valid users = %S
    # 重启samba服务
    sudo systemctl restart smbd
    
  • 访问

通过在windows中访问\\192.168.137.2\evan\就能访问家目录。

安装常用的flask包

email_validator
flask
flask-admin
flask-bcrypt
bootstrap-flask
flask-login
flask-mail
flask-moment
flask-sqlalchemy
flask-wtf
gunicorn
pyJWT
wtforms

将上述内容保存至一个requirements.txt的文件中,然后在虚拟环境中运行:

pip install -r requirements.txt -i https://pypi.douban.com/simple

单文本应用

创建一个项目,然后创建一个app.py文件内容如下:

from flask import Flask, render_template, jsonify

app = Flask(__name__)

# 返回字符串
@app.route('/')
def hello():
	return 'Hello World!'

# 返回html并传入参数
@app.route('/<username>/')
def user(username):
    return render_template('index.html', user=username)

# 返回json字符串,也可以不采用jsonify方法而直接返回{'name': '张三', 'age': 20}
# 但这样就会造成要使用这个数据的时候还要替换单引号为双引号才能将json字符串用json.loads方法转为字典
@app.route('/json')
def pass_json():
    return jsonify({'name': '张三', 'age': 20})
	
app.run(host='0.0.0.0', port=5000, debug=True)

项目拆分与组织

  • flask-st/:项目根目录
    • config.py:配置文件
    • run.py:启动运行文件
    • requirements.txt:程序依赖库
    • app/:应用程序目录
      • static/:静态文件
        • images/:图片
        • style.css:自定义的样式
        • scripts.js:自定义的js脚本
      • templates/
        • errors/:自定义错误模板,包含400、404、500
        • base.html:基本模板(基于Bootstrap5),内容块为{% block app_content %}
        • navbar.html:导航栏,被base.html所导入
        • index.html:主页,仅显示了一张图片
        • register.html:注册页面
        • login.html:登录页面
      • __init__.py:定义了create_app()工厂函数(创建app实例、注册蓝图、注册错误处理函数、初始化插件)
      • commands.py:定义了一个initdb命令(注册到auth 蓝图上),用于初始化数据库(用法:flask --app app:create('development') auth initdb
      • errors.py:定义了三个错误处理的函数(400、404、500)
      • extensions.py:定义了3个基本插件(bootstrap_flaskflask_loginflask_sqlalchemy),并定义了插件初始化函数
      • forms.py:使用flask_wtf定义了两个表单(RegisterFormLoginForm
      • functions.py:辅助函数文件(默认为空)
      • models.py:模型定义文件(仅定义了User模型)
      • views.py:定义了两个蓝图(mainauth),定义了主页、注册、登录、注销四个路由及视图函数

路由与请求

路由与蓝图

  • 基本路由
app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html', data=data)
  • 蓝图

    • views.py
    from flask import Blueprint
    
    # 定义auth蓝图
    auth = Blueprint('auth', __name__)
    
    # 定义main蓝图
    main = Blueprint('main', __name__)
    
    # 使用蓝图
    @main.route('/')
    def index():
        return render_template('index.html')
    
    @auth.route('/login/')
    def login():
        return render_template('login.html')
    
    # redirect并结合url_for时的使用(加前缀"auth.")
    @auth.route('/register/')
    def register():
        return redirect(url_for('auth.login'))
    
    • __init__.py
    from .views import auth, main
    app.register_blueprint(auth)	# 注册auth蓝图
    app.register_blueprint(main)	# 注册main蓝图
    

路由参数

  • 写法:

    @app.route('/<converter:variable_name>')
    def get_something(variable_name):
        return variable_name
    
  • converter:参数类型

    • string:接收任何没有斜杠(’/’)的字符串(默认);如@app.route('/<string:username>/'),传入/james,返回为username=james
    • int:整型;如@app.route('/<int:age>/'),传入/32,返回为age=32
    • float:浮点型;如@app.route('/<float:money>/'),传入/100.00,返回为money=100.00
    • path:接收路径,可以接收斜线(’/’);如@app.route('/<path:name>/'),传入/hello/world,返回为name=hello/world
    • uuid:只接收uuid字符串,唯一码,一种生成规则;如@app.route('/<uuid:u>'),传入/9fa6101c-51b0-40c4-868c-8a5a91279420,返回为u=9fa6101c-51b0-40c4-868c-8a5a91279420
    • any:从列出的项目中选出一个;如@app.route('/any/<any(apple, orage, banana):fruit>/'),只可以传入/any/apple/any/orange/any/banana,返回分别为fruit=applefruit=orangefruit=banana

请求和响应

请求方法

  • 默认支持GET, HEAD, OPTIONS,如果想支持某一请求方式,需要自己手动指定:

    @app.route('/rule/', methods=['GET', 'POST'])
    
  • methods中可以指定的请求方法

    • GET
    • POST
    • HEAD
    • PUT
    • DELETE
  • 测试用爬虫(不要将GET、POST同时进行)

    import requests
    
    # 模拟GET请求
    res = request.get('http://127.0.0.1:5000/')
    print(res.text)
    
    # 模拟POST请求
    # data为POST请求参数:一个字典
    res = request.post('http://127.0.0.1:5000/', data={'name': 'james', 'age': 33})
    print(res.text)
    

Request

服务器在接收到客户端的请求后,会自动创建Request对象,不可修改

from flask import request

@app.route('/request/')
def get_request():
    print(request.method)	# 'GET'或'POST'
    
    # GET请求参数
    # 访问/request/?name=james&age=33&name=evan,
    print(request.args)		# 返回ImmutableMultiDict([('name', 'james'), ('age', '33'), ('name', 'evan')])
    print(request.args['name'])	# 如果name不存在则会报错,此处返回james
    print(request.args.get('name'))	# 返回第一个name,如果name不存在则返回None,此处返回james
    print(request.args.getlist('name'))	# 返回所有name,如果name不存在则返回None,此处返回['james', 'evan']
    
    # POST请求参数
    print(request.form)				# 与GET请求参数相同
    print(request.form.get('name'))	# 与GET请求参数相同
    
    # cookie获取值方式与GET、POST请求参数相同
  • 属性
    • url:完整请求地址
    • base_url:去掉GET参数的URL
    • host_url:只有主机和端口号的URL
    • path:路由中的路径
    • method:请求方法
    • remote_addr:请求的客户端地址
    • args:GET请求参数
    • form:POST请求参数
    • files:文件上传
    • headers:请求头
    • cookies:请求中的cookie
  • ImmutableMultiDict类型:类似字典的数据结构,但是可以存在相同的键

Response

服务器返回给客户端的数据,由程序员创建,可以返回如下类型的Response对象

  1. 直接返回字符串,可以返回文本内容,状态码:不常用

  2. render_template渲染模板,将模板转换成字符串

    return render_template('index.html', name='James', age=33)
    
  3. 返回json

    # 直接返回字典(不建议)
    data = {'name': 'James', 'age': 33}
    return data
    
    # 返回序列化的字符串
    from flask import jsonify
    return jsonify(data)
    
  4. 自定义响应对象

    # 使用make_response(data, code)
    html = render_template('index.html', name='James', age=33)
    res = make_response(html, 200)
    return res
    
    # 使用Response对象
    html = render_template('index.html', name='James', age=33)
    res = Response(html)
    return res
    

Redirect

# 跳转外部链接
return redirect('https://www.qq.com')

# 跳转内部链接
return redirect('/')

# 结合url_for跳转并传递参数
return redirect(url_for(main.index, name='james', age=33))

会话技术与模板

理论

1709109034263

cookie本身由浏览器保存,通过response.set_cookie将cookie写到浏览器上,下一次访问,浏览器会根据不同的规则携带cookie过来

  • 特点:
    1. 客户端会话技术,浏览器的会话技术
    2. 数据全部储存在客户端中
    3. 使用键值对储存
    4. 特性
      • 支持过期时间
      • 默认会自动携带本网站的所有cookie
      • 根据域名进行cookie储存
      • 不能跨域名
      • 不能跨浏览器
    5. cookie是通过服务器创建的Response来创建的
  • 设置cookie:response.set cookie(key,value[,max_age=None,exprise=None])
    • max_age:整数,指定cookie过期时间
    • expries:整数,指定过期时间,可以指定一个具体日期时间
    • max_age和expries两个选一个指定
  • 获取cookie:request.cookies.get(key)
  • 删除cookie:response.delete_cookie(key)——一般用于注销登录

实践

# 比如在用户登录过程中,验证用户名与密码后服务端要设置cookie才能确认用户已经登录
@auth.route('/login/')
def login():
    username = request.form.get('username')
    password = request.form.get('password')
    # 模拟登录
    if username=='Admin' and password=='123':
        # 设置cookie的三种方式
        response.set_cookie('user', username)	# 没有设置max_age或expries的情况下默认浏览器关闭cookie就消失
        response.set_cookie('user', username, max_age=3600) # 3600秒以后过期
        response.set_cookie('user', username, expires=datetime.datetime(2024, 3, 8)) # 20240308过期 
        
# 浏览器访问index或其他页面的时候通过cookie就能知道是否已经登录
@main.route('/')
def index():
    username = request.cookie.get('user')
    return "Hello %s" % username

# 注销则删除掉判断已经登录的cookie即可
@auth.route('/logout')
def logout():
    # 由于logout页面并没有内容,而且反正会要跳转,所以可以获取访问主页的cookie
    response = redirect(url_for('index'))
    response.cookie.delete('user')
    return response

session

session是服务器端会话技术,依赖于cookie,如果使用cookie就全部用cookie,用session就全部用session,不要混用

  • 特点

    1. 服务端的会话技术
    2. 所有数据存储在服务器中
    3. 默认储存在内存中
    4. 存储结构也是key-value型式:键值对
    5. session 是离不开cookie的
  • Flask中的session

    1. Flask中的session是全局对象
    2. request也是Flask的一个全局对象
  • 常用操作

    1. 设置session:session['key'] = 'value'
    2. 获取session:session.get(key, default=None)
    3. 删除session:
      • session.pop(key):删除某一个键值对
      • session.clear():清除所有session
  • session与cookie的区别

    cookie session
    1. 在浏览器存储
    2. 安全性较低
    3. 可以减轻服务器压力
    1. 在服务器端存储
    2. 安全性高
    3. 对服务器要求较高
    4. 依赖cookie
  • session的设置

    # 设置SECRET_KEY
    app.config['SECRET_KEY'] = 'a very long secret'
    
    # 设置session生效时间
    app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=31)
    
    # 要使上述的PERMANENT_SESSION_LIFETIME在浏览器中生效,需要设置
    session.permanent = True
    

模板

模板语法

  • 变量: {{ var }}
    • 视图传递给模板的数据
    • 前面定义出来的数据
    • 变量如果不存在,默认忽略
  • 标签:{% tag %}
    • 控制逻辑
    • 使用外部表达式
    • 创建变量
    • 宏定义

结构标签

  • block 块操作

    • 父模板挖坑,子模板填坑
    • {% block xxx %}填充内容{% endblock %}
  • extends 继承:

    • {% extends 'xxx.html' %}
    • 继承后保留块中的内容:{{ super() }}
  • include包含:

    • 可以将其他html包含进来
    • {% include 'xxx.html' %}
  • marco 【了解】

    • 宏定义,可以在模板中定义函数,在其他地方调用

      {% macro hello(name) %}
      	{{ name }}
      {% endmacro %}
      
    • 宏定义可以导入:{% from 'xxx.html' import xxx %}

逻辑控制

  • 循环

    • 可以使用python一样的for ... else
    • 可以获取循环信息
      • loop.first: 判断是否是第一个元素
      • loop.last:判断是否是最后一个元素
      • loop.index:1开始的下标
      • loop.index0:0开始的下标
      • loop.revindex:反向下标,不包括0
      • loop.revindex0:反向下标,包括0
    {% for item in cols %}
    	AA
    {% else %}
    	BB
    {% endfor %}
    
  • 判断

    {% if a == 5 %}
    	AA
    {% elif a == 6 %}
    	BB
    {% else %}
    	CC
    {% endif %}
    

常用模板实践

  • views.py
@app.route('/')
def index():
    data = {
        'name': 'james',
        'age': 27,
        'likes': ['ball', 'sing', 'dance', 'code']
    }
    return render_template('index.html', **data)
  • index.html
{% extends 'base.html' %}
{% block app_content %}
<h4>变量:</h4>
<p>
    name: {{ name }} <!-- name: james -->
</p>
<p>
    age: {{ age }} <!-- age: 27 -->
</p>
<p>
    likes: {{ likes }} <!-- likes: ['ball', 'sing', 'dance', 'code'] -->
</p>
{% for like in likes %} <!-- ball sing dance code -->
<p>like</p>
index: {{ loop.index }} <!-- loop可以在循环中直接使用 -->
{% endfor %}
{% endblock %}

过滤器

  • 语法:{{ 变量 | 过滤器1 | 过滤器2 ...}}
  • 常见过滤器:capitalize, lower, upper, title, trim, reverse, striptags, safe, last, first, length, sum, sort

模型

数据库的配置

  • config.py

    class Config(object):
        # Add prefix for different OS
        @staticmethod
        def prefix():
            WIN = sys.platform.startswith('win')
            if WIN:
                prefix = 'sqlite:///'
            else:
                prefix = 'sqlite:////'
            return prefix
    
        prefix = self.prefix()
        # 数据库地址
        SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or prefix + os.path.join(Config.BASEDIR, 'data-dev.sqlite')
        # Database settings
        SQLALCHEMY_TRACK_MODIFICATIONS = False
    
  • .exts.py

    from flask_sqlalchemy import SQLAlchemy
    from flask_migrate import Migrate
    
    db = SQLAlchemy()
    migrate = Migrate()
    
    def init_ext(app):
        db.init_app(app)
        migrate.init_app(app)
    
  • __init__.py

    from flask import Flask
    
    from config import config
    
    def create_app(config_name):
        # 创建app实例
        app = Flask(__name__)
        app.config.from_object(config[config_name])
        # 初始化插件
        from .exts import init_ext
        init_ext(app)
        return app
    

模型字段

class Person(db.Model):
    __tablename__ = 'person' # 表名称
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(20), unique=True)
    age = db.Column(db.Integer, default=1)
  • 字段类型
类型名 Python类型 说明
Integer int 普通整数,一般是32位
SmallInteger int 16位整数
BigInteger int或long 不限制精度的整数
Float float 浮点数
Numeric decimal.Decimal 定点数
String str 变长字符串
Text str 长字符串,对较长或不限长度的字符串
Unicode unicode 变长Unicode字符串
UnicodeText unicode 变长U你code字符串,对较长或不限长度
Boolean bool 布尔值
Date datetime.date 日期
Time datetime.time 时间
DateTime datetime.datetime 日期和时间
Interval datetime.timedelta 时间间隔
LargeBinary str 二进制文件
  • 常用约束
选项名 说明
primary_key 如果设为True,这列就是表的主键
unique 如果设为True,这列不允许出现重复的值
index 如果设为True,为这列创建索引,提升查询效率
nullable 如果设为True,这列允许使用空值;反之不能为空
default 为这列定义默认值

数据迁移

  1. 安装好数据迁移的包:flask-sqlalchemy、flask-migrate
  2. 在exts.py中初始化Migrate和SQLAlchemy
  3. 在models.py中定义好模型
  4. 在views.py中一定要导入models模块
  5. 配置好数据库
  6. 执行数据迁移命令:
    • 现在Terminal中进入项目目录(run.py所在的目录)
    • 创建迁移文件夹migrates(只调用一次):flask --app app:create_app('development') db init
    • 生成迁移文件:flask --app app:create_app('development') db migrate
    • 执行迁移文件中的升级:flask --app app:create_app('development') db upgrade
    • 执行迁移文件中的降级:flask --app app:create_app('development') db downgrade

单表模型操作

增删改

# 增
@main.route('/useradd')
def user_add():
    u = User()
    u.name = 'james'
    u.age = 33
    db.session.add(u)
    db.session.commit()
    # 添加一个列表的数据(假设users是一个用户列表数据)
    # db.session.add_all(users)
    # db.session.commit()
    return 'Success!'

# 数据库操作建议都采用异常处理
try:
    db.session.add(user)
    db.session.commit()
except Exception as e:
    db.session.rollback()	# 出现错误则回滚操作
    db.session.flush()
    return 'Fail:' + str(e)

# 删
user = User.query.first()	# 查询到需要删除的数据
db.sesion.delete(user)		# 删除数据
db.session.commit()

# 改
user = User.query.first()	# 查询到需要修改的数据
user.age = 25				# 修改数据
db.session.commit()

<模型类>.query.<过滤方法(可选)>.<查询方法>

  • 过滤器
过滤器 说明
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对象,它包含制定范围内的结果
  • 查询属性:contains, startswith, endwith, in_, __gt__, __ge__, __lt__, __le__
  • 逻辑运算
逻辑运算 示例
and_ filter(and_(条件1, 条件2...))
or_ filter(or_(条件1, 条件2...))
not_ filter(not_(条件1, 条件2...))
  • 示例
# 查询User表中的所有数据
users = User.query.all()

# 查询年龄大于22岁的用户(条件仅为"="用filter_by,其余均用filter)
users = User.query.filter(User.age>22)

# filter功能比filter_by强大
users = User.query.filter(User.age==22) # filter(类.属性==值)
users = User.query.filter_by(age=22) # filter_by(属性=值)

users = User.query.filter(User.age.__lt__(22)) # <
users = User.query.filter(User.age.__le__(22)) # <=
users = User.query.filter(User.age.__gt__(22)) # >
users = User.query.filter(User.age.__ge__(22)) # >=

users = User.query.filter(User.name.startwith('王')) # 开头匹配
users = User.query.filter(User.name.endwith('强')) # 结尾匹配
users = User.query.filter(User.name.contains('宝')) # 包含
users = User.query.filter(User.age.in_([11, 12, 22])) # in_

users = User.query.filter(User.age >=20, User.age<30) # and_
users = User.query.filter(and_(User.age >=20, User.age<30)) # and_
users = User.query.filter(or_(User.age >=20, User.age<30)) # or_
users = User.query.filter(not_(User.age<30)) # not_

# 排序
users = User.query.order_by('age') # 升序排序
users = User.query.order_by(desc('age')) # 降序排序:from sqlalchemy import desc

# 分页
users = User.query.limit(5) # 取查询结果的前5个
users = User.query.offset(5) # 跳过查询结果的前5个

分页

# paginate对象的属性
# 	items:返回当前页的内容列表
#	has_next:是否还有下一页
#	has_prev:是否还有上一页
#	next(error_out=False):返回下一页的Pagination对象
#	prev(error_out=False):返回上一页的Pagination对象
#	page:当前页的页码(从1开始)
#	pages:总页数
#	per_page:每页显示的数量
#	prev_num:上一页的页码
#	next_num:下一页的页码
#	total:查询返回的记录总数
  • views.py

    @main.route('/showdata/')
    def show_data():
        # 获取页码page和每页数量num
        page = int(request.args.get('page', 1))
        default_per_page = User.query.count() / 10
        per_page = int(request.args.get('per_page', default_per_page))
        p = User.query.paginate(page=page, per_page=per_page, error_out=False)
        return render_template('show_data.html', p=p)
    
  • show_data.html

    {% extends 'base.html' %}
    
    {% block app_content %}
      <table class="table table-striped">
        <thead> {# 表头 #}
          <tr>
            <th scope="col">Name</th>
            <th scope="col">Email</th>
          </tr>
        </thead>
        <tbody> {# 表体 #}
          {% for user in p.items %}
            <tr>
              <th scope="col">{{ user.username }}</th>
              <th scope="col">{{ user.email }}</th>
            </tr>
          {% endfor %}
        </tbody>
      </table>
    
      {# 导航窗格 #}
      <ul class="pagination">
        {# 上一页 #}
        <li class="page-item">
          {% if p.has_prev %}
            <a class="page-link" href="/showdata/?page={{ p.prev_num }}" aria-label="Previous">
          {% else %}
            <a class="page-link" href="javascript:;" aria-label="Previous">
          {% endif %}
              <span aria-hidden="true">&laquo;</span>
            </a>
        </li>
        {% for i in range(p.pages) %}
          {% if p.page == i + 1 %}
            <li class="page-item active">
          {% else %}
            <li class="page-item">
          {% endif %}
            <a class="page-link" href="/showdata/?page={{ i + 1 }}"> {{ i + 1 }}</a>
          </li>
        {% endfor %}
        {# 下一页 #}
        <li class="page-item">
          {% if p.has_next %}
            <a class="page-link" href="/showdata/?page={{ p.next_num }}" aria-label="Next">
          {% else %}
            <a class="page-link" href="javascript:;" aria-label="Next">
          {% endif %}
              <span aria-hidden="true">&raquo;</span>
            </a>
        </li>
      </ul>
    {% endblock %}
    

进阶

类视图和RESTful