[Flask教程] 6.ORM与SQLAlchemy (2) – 模型关系与引用

承接上文,我们的Q&A demo,除了用户表,还需要存储所有问题内容的表questions_info和存储所有评论的表comments_info,并且都和users_info通过外键来关联。我们不排除后续需要更多表的可能性,把所有模型和视图函数写在一起看着也太混乱了!为此,我们新建一个models.py,把三个模型都放在这里。

由于是新建的models.py文件,我们同样要先在开头生成一个名为db的SQLAlchemy对象:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

前文中我们给SQLAlchemy传入了Flask对象app作为参数,这里是不是也要从视图函数文件HarpQA.py导入那个app并传进去呢?并不可以,因为HarpQA.py也要使用到db(如db.session),这样就产生了循环引用,所以在这里不能传入app,而是回到HarpQA.py,使用db.init_app(app)将app和db绑定,避免了循环引用。

users_info表(Users模型)代码如下:

class Users(db.Model):
    __tablename__ = 'users_info'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(32), nullable=False)
    password = db.Column(db.String(100), nullable=False)
    register_time = db.Column(db.DateTime, nullable=False, default=datetime.now())
    # 我们新增了一个avatar_path字段来存用户头像图片文件的路径
    avatar_path = db.Column(db.String(256), nullable=False, default='images/doraemon.jpg')    

questions_info表(Questions模型)代码如下:

class Questions(db.Model):
    __tablename__ = 'questions_info'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.TEXT, nullable=False)
    author_id = db.Column(db.Integer, db.ForeignKey('users_info.id'))
    create_time = db.Column(db.DateTime, nullable=False, default=datetime.now())

    author = db.relationship('Users', backref=db.backref('questions', order_by=create_time.desc()))

这个表存储所有问题的标题、内容、创建时间、作者ID,作者ID通过外键与用户表的ID关联,方式也很简单,在db.Column中用db.ForeignKey(‘users_info.id’)作为参数即可。
再看最后一条语句:

author = db.relationship('Users', backref=db.backref('questions', order_by=create_time.desc()))

db.relationship会自动找到两个表的外键,建立Questions和Users的关系,此时对于任意一个Questions对象question,通过question.author就可获得这个question的作者对应的Users对象,例如获取id为1的问题的作者姓名:

question = Questions.query.filter(Questions.id == 1).first()
author_name = question.author.username

db.relationship的第二个参数backref=db.backref(‘questions’, order_by=create_time.desc())则建立了一个反向引用,这样我们不仅可以使用question.author,还可以使用author.questions获得一个作者所有的问题,并通过order_by=create_time.desc()按创建时间倒序排列(网页的内容按时间倒序排列),返回的是一个Questions对象的列表,可以遍历它获取每个对象,如获取作者Harp的所有问题的title:

author = Users.query.filter(Users.username == 'Harp').first()
for question in author.questions:
    print(question.title)

同理,comments_info表(Comments模型)代码如下:

class Comments(db.Model):
    __tablename__ = 'comments_info'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    content = db.Column(db.TEXT, nullable=False)
    question_id = db.Column(db.Integer, db.ForeignKey('questions_info.id'))
    author_id = db.Column(db.Integer, db.ForeignKey('users_info.id'))
    create_time = db.Column(db.DateTime, nullable=False, default=datetime.now())

    author = db.relationship('Users', backref=db.backref('comments'))
    question = db.relationship('Questions', backref=db.backref('comments', order_by=create_time.desc()))

在HarpQA.py中,我们要从models.py导入db及所有的模型,注意因为上下文的关系,我们这里用with语句把app推入栈中:

from flask import Flask, render_template
from models import db, Users, Questions, Comments

import config

app = Flask(__name__)
app.config.from_object(config)

db.init_app(app)

with app.test_request_context():
    db.drop_all()
    db.create_all()


@app.route('/')
def index():
    return render_template('home.html')


if __name__ == '__main__':
    app.run()

运行脚本,此时数据库已经把三张表都建立好了:

未分类

[Flask教程] 5.ORM与SQLAlchemy (1) – 建立第一个模型

后端一个重要的点就是与数据库联系,例如网页的注册、登录,内容的更新等都需要与数据库建立关系。以MySQL数据库为例,平时我们会用mysqldb(python 2)或者pymysql(python 3)去操作MySQL数据库,但这种方法也是需要自己编写SQL语句的。现在我们有了ORM模型,简单来说,ORM是把数据库中的表抽象成模型,表的列名对应模型的属性,这样我们可以调用类的属性或方法去获得数据库中的数据。例如假设MySQL数据库中有一张表名为table1,使用SELECT * FROM table1 WHERE id=1获取id为1的数据,如果将表table1映射成ORM模型Table,那么可以直接使用Table.query.filter(id=1),这样操作简单了很多,也很利于理解。

SQLAlchemy就是一个这样的ORM,我们可以直接安装flask_sqlalchemy来使用。在这之前我们先在MySQL中手动建立一个数据库harp,在建立的时候把charset设置为utf8,避免存入中文时变成乱码,然后在配置文件config.py中填写好数据库的连接信息:

HOST = "127.0.0.1"
PORT = "3306"
DB = "harp"
USER = "root"
PASS = "Your Password"
CHARSET = "utf8"
DB_URI = "mysql+pymysql://{}:{}@{}:{}/{}?charset={}".format(USER, PASS, HOST, PORT, DB, CHARSET)
SQLALCHEMY_DATABASE_URI = DB_URI

SQLAlchemy依赖mysqldb或者pymysql去连接数据库和执行SQL语句,因为我们用的是python 3,所以需要在配置信息中指明使用pymysql,如果是python 2可以省略,默认是使用mysqldb。

建立好了数据库,我们开始建表,首先建立一张用户表,我们设想它应该有id(作为主键)、用户名、密码、注册时间这些基本的字段,有了ORM,我们就不用再写SQL去建表了,在项目的主py文件中添加以下代码:

from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

import config

app = Flask(__name__)
app.config.from_object(config)

db = SQLAlchemy(app)


class Users(db.Model):
    __tablename__ = 'users_info'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(32), nullable=False)
    password = db.Column(db.String(100), nullable=False)
    register_time = db.Column(db.DateTime, nullable=False, default=datetime.now())


db.create_all()

解读一下这段代码,导入SQLAlchemy和含有数据库连接信息的config,实例化一个SQLAlchemy对象名为db,其传入的参数为Flask实例app。接下来定义了一个User类,这个类就是ORM中的模型,也就是数据库中的表映射的模型,它需要继承自db.Model,tablename这个属性就是建表后,数据库生成的表名;然后使用db.Column来实例化id/username/password/register_time这几个列,db.Column的参数描述列的类型、主键等信息,如db.Integer/db.String(32)/db.DateTime分别代表整形、字符串(最大长度)、时间,primary_key=True说明该字段为主键,autoincrement=True代表自增长,nullable决定是否可为空,default代表默认值。最后用db.create_all()来实现创建。我们暂时不用理解为何SQLAlchemy需要传入Flask实例作为参数,为何模型要继承自db.Model,重要的是可以先把想要的表建立起来。

进入数据库,输入desc user_info;,我们发现表已经建立好了,其结构图如下:

未分类

但它现在还是空的,我们来试着插入一条语句,将视图函数修改为:

@app.route('/')
def index():
    user = Users(username='Harp', password='123456')
    db.session.add(user)
    db.session.commit()
    return render_template('home.html')

代码实例化一个Users的对象user,传入username和password,使用db.session.add(user)将其加入到数据库的session(可以理解为事务)中,然后使用db.session.commit()提交。我们运行程序,然后用浏览器访问,浏览器正常显示了结果,这时再看一眼数据库,发现这条数据已经写入到了数据库:

未分类

查询、修改数据也同样很简单:

@app.route('/')
def index():
    user = Users.query.filter(Users.id == 1).first()    #查找
    print(user.username)
    user.username = 'Harp1207'    #修改
    db.session.commit()    #修改后需提交
    print(user.username)
    return render_template('home.html')

思考问题:

  1. 为何要把模型的操作语句放在视图函数中?(搜索上下文这个概念)
  2. 数据查找,我们用的是Model.query,其实还可以用db.session.query,两者有何区别?filter和filter_by又有何区别?

[Flask教程] 4.模板的继承及Bootstrap实现导航条

在建设一个网站的时候,不同的页面有很多元素是一样的,比如导航条、侧边栏等,我们可以使用模板的继承,避免重复编写html代码。现在我们打算实现一个在网页上方的导航条,并在所有的页面中继承这个导航条。导航条的建立,我们直接使用Bootstrap提供的如下导航条的样式。

未分类

但在使用Bootstrap的导航条样式之前,需要先引用Bootstrap所需要的css和js文件以及jQuery,我们在html的header中插入以下代码完成引用:

<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous">

这里都是通过链接引用网络上的css和js文件,而不是将其下载下来并从本地引用。之后我们把上图中的所有html代码复制到html的body中,浏览器就能显示和图中一样的导航条了。我们再做一些简单的修改和优化,最终我们的导航条是这样的:

未分类

具体修改的点是,我把原始的Brand替换成了一个图片作为logo,第一个下拉控件Dropdown删掉了,最右侧的下拉控件增加了一个选项,并把文字都替换成了我们想要的。然后建立了一个base.css文件来调整图片大小、控件位置、背景色等等,这一部分都是基础的html/css知识,也就不多说。后续所有的网页都要使用这个导航条,我们将含有导航条这个html命名为base.html,并在其body中,导航条代码的下方增加以下代码:

{% block body_part %}
{% endblock %}

然后新建一个home.html,输入以下内容:

{% extends 'base.html' %}
{% block body_part %}
<p>This is body content under nav-bar</p>
{% endblock %}

渲染home.html并访问,我们可以看到这样的结果:

未分类

因此我们不难理解,在home.html中,{% extends ‘base.html’ %}表示继承自base.html,home.html中block和endblock区间的代码块会自动替换到base.html同样名为body_part的block区域。block可以使用多个,例如在中也可以使用,以便于不同的页面设置不同的标题。<br /> 最终base.html代码如下:</p> <pre><code><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <link rel="stylesheet" href="{{ url_for('static',filename='css/base.css') }}"/> <link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico') }}"> <title>{% block page_name %}{% endblock %}-HarpQA</title> </head> <body> <nav class="navbar navbar-default"> <div class="container"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <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"> <img class="logo" src="{{ url_for('static',filename='images/logo.png') }}"> </a> </div> <!-- Collect the nav links, forms, and other content for toggling --> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="#">首页 <span class="sr-only">(current)</span></a></li> <li><a href="#">发布问答</a></li> </ul> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" placeholder="Key Words"> </div> <button type="submit" class="btn btn-default">搜索</button> </form> <ul class="nav navbar-nav navbar-right"> <li><a href="#">登录</a></li> <li><a href="#">注册</a></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">友情链接<span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="mailto:liutao25@baidu.com">联系我</a></li> <li><a href="http://flask.pocoo.org" target="_blank">Flask官网</a></li> <li><a href="https://www.python.org/">Python官网</a></li> <li role="separator" class="divider"></li> <li><a href="https://www.baidu.com" target="_blank">百度搜索</a></li> <li role="separator" class="divider"></li> <li><a href="https://www.google.com.hk" target="_blank">Google Search</a></li> </ul> </li> </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> {% block body_part %} {% endblock %} </body> </html> </code></pre> <p>请注意一下base.css和logo图片的引用,我们也使用了url_for函数,第一个参数是static,代表项目文件夹下static文件夹,第二个参数是以static文件夹为基准静态文件的相对路径,我们把js文件/css文件/图片文件等都放在这个文件夹下,所以这个用法以后会经常使用。我们在收藏网页的时候,网页都有一个小图标,我们也可以在header中使用这行html代码来实现:</p> <pre><code><link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico') }}"> </code></pre> <p>把favicon.ico文件放在static/images文件夹即可,我们使用了Flask的图标,效果如下图:</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-4176820475cb8028cba5de1555563148.png" alt="未分类" width="120" height="33" class="aligncenter size-full wp-image-7238" /></p> <p>base.css代码如下:</p> <pre><code>body{ background: #F3F3F3; } .navbar-brand{ padding: 0 5px; padding-right: 10px; } .logo{ height: 50px; } </code></pre> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/flask-tutorial-4-inheritance-of-template-and-bootstrap-implementation-of-navigation-bar/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:31:30+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/bootstrap/" rel="tag">Bootstrap</a>、<a href="https://devops.webres.wang/tag/flask/" rel="tag">flask</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <article id="post-7234" class="post-7234 post type-post status-publish format-standard hentry category-undefine tag-flask tag-jinja2 tag-render_template"> <header class="entry-header"> <h2 class="entry-title"><a href="https://devops.webres.wang/2018/01/flask-tutorial-3-render-template-rendering-template-and-jinja2/" rel="bookmark">[Flask教程] 3.render_template渲染模板及jinja2</a></h2> </header><!-- .entry-header --> <div class="entry-content"> <p>我们之前的视图函数,返回的都是简单的’Hello Wolrd’之类的字符串,怎么返回一个html呢?首先我们在templates文件夹建立一个html文件,内容随便写一点如下:</p> <pre><code><!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <h1>This is index page</h1> </body> </html> </code></pre> <p>我们可以使用Flask对象app的send_static_file方法,使视图函数返回一个静态的html文件,但现在我们不使用这种方法,而是使用flask的render_template函数,它功能更强大。<br /> 从flask中导入render_template,整体代码如下:</p> <pre><code>from flask import Flask, render_template import config app = Flask(__name__) app.config.from_object(config) @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run() </code></pre> <p>render_template函数会自动在templates文件夹中找到对应的html,因此我们不用写完整的html文件路径。用浏览器访问’/’这个地址,显示结果如下:</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-9530293985cb802888c8b71555563144.png" alt="未分类" width="316" height="142" class="aligncenter size-full wp-image-7230" /></p> <p>那么为何称之为模板呢?因为render_template不仅能渲染静态的html文件,也能传递参数给html,使一个html模板根据参数的不同显示不同的内容,这是因为flask使用了jinja2这个模板引擎。要使用模板,在render_template参数中以key=value形式传入变量,在html中使用{{key}}来显示传入的变量,例如:</p> <pre><code># 视图函数 @app.route('/') def index(): return render_template('index.html', contents='This is index page') # html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <h1>{{ contents }}</h1> </body> </html> </code></pre> <p>浏览器显示的结果与上文是一样的。我们还可以直接把一个类的实例传递过去,并在模板中访问类的属性,例如假设一个类对象obj有a和b属性,关键部分的代码如下:</p> <pre><code># 视图函数中 return render_template('index.html', object=obj) ... # html中 <p>a: {{ object.a }}</p> <p>b: {{ object.b }}</p> </code></pre> <p>传入一个字典也可以,并且在模板中既可以用dict[key],也可以用dict.key。</p> <p>使用过滤器,可以在html中对传入的变量进行处理,其格式是{{ 变量 | 过滤器 }},例如将前文的{{ contents }}修改为{{ contents | upper}},浏览器显示的内容就变成了:</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-20056848865cb80288d576a1555563144.png" alt="未分类" width="344" height="139" class="aligncenter size-full wp-image-7231" /></p> <p>所以我们可以很容易就理解,过滤器其实就是以变量为参数的函数,返回处理后的结果,在后端一样可以先用字符串对象的upper()函数处理好再传递给模板,效果是完全一样的。jinja2自带了一些过滤器,例如length/reverse/lower等等 ,并且我们也可以自己按照需求自定义过滤器,模板还支持{{ 变量 | 过滤器1 | 过滤器2 | … }}这样的操作。想要深入了解的话,可以搜索jinja2过滤器去进一步学习。</p> <p>模板中还可以使用if else和for in控制语句,与变量使用{{ }}不同,控制语句要放在{% %}里,例如前文的contents传入一个list:</p> <pre><code>contents=[i for i in range(10)] </code></pre> <p>html中代码如下:</p> <pre><code><h1> {% for i in contents %} {{ i }}{# 注意i也要用两个大括号 #} {% endfor %} </h1> </code></pre> <p>使用for遍历contents的内容,并用{{ i }}显示出来,同时还用{# #}加了一个注释,还要注意需要使用{% endfor %}来提示循环区域的结束,因为html不像python那样通过缩进来判断循环的区域,if也是同理。浏览器显示结果:</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-3683356425cb80289270de1555563145.png" alt="未分类" width="310" height="136" class="aligncenter size-full wp-image-7232" /></p> <p>最后for和if结合使用:</p> <pre><code><h1>header</h1> {% for i in contents %} <p> {% if i%2 == 0 %} {{ i }}是偶数{# 注意i也要用两个大括号 #} {% else %} {{ i }}是奇数 {% endif %} </p> {% endfor %} </code></pre> <p>结果如下(只是演示一下,不要吐槽排版和美观):</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-3958481005cb802896cd471555563145.png" alt="未分类" width="256" height="333" class="aligncenter size-full wp-image-7233" /></p> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/flask-tutorial-3-render-template-rendering-template-and-jinja2/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:28:43+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/flask/" rel="tag">flask</a>、<a href="https://devops.webres.wang/tag/jinja2/" rel="tag">jinja2</a>、<a href="https://devops.webres.wang/tag/render_template/" rel="tag">render_template</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <article id="post-7229" class="post-7229 post type-post status-publish format-standard hentry category-undefine tag-flask tag-redirect tag-url_for"> <header class="entry-header"> <h2 class="entry-title"><a href="https://devops.webres.wang/2018/01/flask-tutorial-2-reversal-function-url-for-and-redirect/" rel="bookmark">[Flask教程] 2.反转函数url_for与重定向redirect</a></h2> </header><!-- .entry-header --> <div class="entry-content"> <p>在flask中,我们导入url_for和redirect两个函数。</p> <pre><code>from flask import Flask, url_for, redirect </code></pre> <p>首先看url_for,简单来说,这个函数接受视图函数的名字(字符串形式)作为参数,返回视图函数对应的url,例如:</p> <pre><code>@app.route('/') def hello_world(): print(url_for('index')) return 'Hello World' @app.route('/index/') def index(): return 'index' </code></pre> <p>在hello_world函数中使用print(url_for(‘index’)),将会打印出/index/。<br /> 有传参的视图函数怎么办?同样将函数名字符串作为第一个参数,将参数以key=value的形式写在后面,如:</p> <pre><code>@app.route('/') def hello_world(): print(url_for('hello',name='harp')) return 'Hello World' @app.route('/<name>/') def hello(name): return 'Hello %s' % name </code></pre> <p>打印结果为/harp/。</p> <p>redirect则更简单,功能就是跳转到指定的url,大部分情况下,我们都是和url_for一起使用的,例如:</p> <pre><code>@app.route('/') def hello_world(): return 'Hello World' @app.route('/<name>/') def hello(name): if name == 'Harp': return 'Hello %s' % name else: return redirect(url_for('hello_world')) </code></pre> <p>在hello这个视图函数中,如果url传入的参数是Harp(即请求的网址是http://127.0.0.1:5000/Harp/),则返回’Hello Harp’,其他情况则重定向到hello_world这个视图函数对应的网址’/’。</p> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/flask-tutorial-2-reversal-function-url-for-and-redirect/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:25:04+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/flask/" rel="tag">flask</a>、<a href="https://devops.webres.wang/tag/redirect/" rel="tag">redirect</a>、<a href="https://devops.webres.wang/tag/url_for/" rel="tag">url_for</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <article id="post-7228" class="post-7228 post type-post status-publish format-standard hentry category-undefine tag-debug tag-flask tag-url"> <header class="entry-header"> <h2 class="entry-title"><a href="https://devops.webres.wang/2018/01/flask-tutorial-1-the-first-flask-program-debug-mode-and-url-reference/" rel="bookmark">[Flask教程] 1.第一个Flask程序、DEBUG模式及URL传参</a></h2> </header><!-- .entry-header --> <div class="entry-content"> <p>–引言请见我的简书https://www.jianshu.com/p/e27…</p> <p>我们打开PyCharm,新建一个Project,在左侧选择Flask,右侧Location可选择项目存放的位置,Project Interpreter选择使用的编译器,我们当然可以选择已经安装在电脑上的python 3.6,但我建议选择使用Virtualenv来建立一个虚拟的环境,这样在虚拟环境里安装包之类的,不会影响电脑上本身已经装的,并且后续如果项目较多的话也便于区分和管理。</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-9640389665cb802846d3a31555563140.png" alt="未分类" width="554" height="348" class="aligncenter size-full wp-image-7226" /></p> <p>点击Create,创建好之后,pycharm自动在项目的文件夹下建立了static/templates文件夹和HarpQA.py,此外还有一个venv文件夹,是虚拟环境用的,我们可以暂时不用管它。(如果你的PyCharm是社区版的,那么无法像专业版那样建立Flask项目,对应的文件夹需要自己手动建立。)HarpQA.py中的代码如下:</p> <pre><code>from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello World!' if __name__ == '__main__': app.run() </code></pre> <p>首先从flask导入Flask,(我并没有pip install flask,为何能导入呢?我理解是建立flask项目的时候PyCharm自动帮我们做了这个事情),然后初始化一个Flask对象app,参数是<strong>name</strong>,<strong>name</strong>代表的是本身这个模块的名字,我们暂时不用理解为何要传入这个参数。接下来是一个hello world函数,并且有一个装饰器@app.route(‘/’),意思是说,当接收到’/’这个网址的请求时,执行hello world这个函数,即返回字符串’Hello World!’(add_url_rule()也可以实现和@app.route一样的功能 ,但使用装饰器应该更pythonic吧),最后使用app.run()运行。运行脚本后,提示Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)<br /> 我们在浏览器中输入http://127.0.0.1:5000/,就可以在网页中看到’Hello World!’了。</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-216373615cb80284bc3201555563140.png" alt="未分类" width="272" height="101" class="aligncenter size-full wp-image-7227" /></p> <p>我们还可以使用debug模式来运行flask的服务端,开启debug模式后,修改代码不需要关闭程序,可以实时生效。有多种方法来开启debug模式:</p> <ol> <li>在app.run()中添加参数app.run(debug=True);</li> <li>在run之前增加app.debug = True;</li> <li>新建config文件,在config文件中添加DEBUG = True,然后在程序中引入app.config.from_object(config);</li> <li>在run之前增加app.config[‘DEBUG’] = True;</li> </ol> <p>我们使用第3种方法,新建一个独立的文件来保存各种参数,以后项目增大的时候参数增多,便于管理。同样在项目文件夹下新建config.py文件,添加DEBUG = True,DEBUG需大写。在HarpQA.py中import config并添加app.config.from_object(config),再次运行HarpQA.py,提示:</p> <pre><code> * Restarting with stat * Debugger is active! * Debugger PIN: 229-291-890 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) </code></pre> <p>说明Debug模式已经打开,我们可以直接修改代码,例如把’Hello World!’<br /> 修改成’Hello’并保存,显示提示:</p> <pre><code> * Detected change in 'D:\Flask\HarpQA\HarpQA.py', reloading * Restarting with stat * Debugger is active! * Debugger PIN: 229-291-890 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) </code></pre> <p>说明DEBUG模式检测到了代码的变化并自动重载了,这时候刷新网页,结果也变成了只显示’Hello’。</p> <p>如果我们要处理很多URL,可以一个一个去给他们写对应的视图函数,这有时候是不切实际的,我们有更高效的做法,如以下代码:</p> <pre><code>@app.route('/<name>/') def hello(name): return 'Hello %s' % name </code></pre> <p>@app.route中装饰的地址是’//’,注意name外面用了尖括号,代表name是一个参数,例如我们请求http://127.0.0.1:5000/Harp/这个网址,其中的name就是Harp,这个参数会传递给视图函数hello,最终会返回’Hello Harp’。我们可以给将写成,表示传入的参数是字符串类型的,其他的类型还有int,float,path,几种类型的区别可以在网上搜索对比一下,例如path可以将参数里的’/’也传递过来。</p> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/flask-tutorial-1-the-first-flask-program-debug-mode-and-url-reference/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:22:23+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/debug/" rel="tag">DEBUG</a>、<a href="https://devops.webres.wang/tag/flask/" rel="tag">flask</a>、<a href="https://devops.webres.wang/tag/url/" rel="tag">URL</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <article id="post-7225" class="post-7225 post type-post status-publish format-standard hentry category-undefine tag-cesi tag-supervisor tag-web"> <header class="entry-header"> <h2 class="entry-title"><a href="https://devops.webres.wang/2018/01/supervisor-centralized-management-web-tool-cesi/" rel="bookmark">supervisor集中化管理web工具cesi</a></h2> </header><!-- .entry-header --> <div class="entry-content"> <p>linux进程管理器supervisor是会经常被用到的,但服务器多了之后,每个服务器的进程也不方便管理。同时,supervisor自带的web界面比较简陋,所以尝试了一下官网推荐的一些第三方开源软件,推荐一下这个cesi。</p> <p>最终效果如下:</p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-10068921085cb80281d98641555563137.jpg" alt="未分类" width="768" height="362" class="aligncenter size-full wp-image-7223" /></p> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-4480690785cb802823ce771555563138.jpg" alt="未分类" width="768" height="215" class="aligncenter size-full wp-image-7224" /></p> <p><strong>1. 首先是关于supervisor</strong></p> <p>通过apt或pip安装都可以</p> <pre><code>apt-get install supervisor #pip install supervisor echo_supervisord_conf > /etc/supervisor/supervisord.conf </code></pre> <p>关于supervisor配置文件</p> <pre><code>[unix_http_server] #这里配置是否用unix socket通信来让supervisor与supervisorctl做通信 [inet_http_server] #这里是用的http的方式做通信 [supervisorctl] #这里选择supervisorctl到底用以上两种中的哪种方式来与supervisor通信,选择一种即可,记得填写密码 [program:pro_name] stdout_logfile = {path} redirect_stderr = true ;让stderr也写入stdout中 </code></pre> <p>常用操作</p> <pre><code>supervisorctl reload #重启加载supervisor配置文件 supervisorctl update #只增加新增的配置文件 </code></pre> <p><strong>2. 关于cesi</strong></p> <p>项目地址:https://github.com/Gamegos/cesi</p> <p>安装cesi</p> <pre><code>apt-get install sqlite3 python python-flask git clone https://github.com/Gamegos/cesi cd cesi sqlite3 ./userinfo.db < userinfo.sql cp cesi.conf /etc/cesi.conf </code></pre> <p>配置cesi.conf,我的配置如下,一看就懂了</p> <pre><code>[node:118] username = *** password = *** host = 127.0.0.1 port = 9001 [node:calmkart] username = *** password = *** host = 45.76.71.69 port = 9001 [node:121] username = *** password = *** host = *.*.*.121 port = 9001 [environment:my_env] members = 118,calmkart,121 [cesi] database = /root/cesi/userinfo.db activity_log = /var/log/cesi.log host = 0.0.0.0 </code></pre> <p>用supervisor运行cesi,配置文件如下</p> <pre><code>[program:cesi] directory = /root/cesi/cesi/ command = python web.py autostart = true startsecs = 5 autorestart = true startretries = 3 user = root redirect_stderr = true stdout_logfile = /var/log/cesi1.log </code></pre> <p>开启任务</p> <pre><code>supervisorctl start cesi </code></pre> <p>默认账号密码:admin,admin<br /> 端口需要改的自己去web.py里面改</p> <p>you get it</p> <p>本测试服地址:<br /> http://cesi.calmkart.com</p> <p>此外,其他常用的supervisor相关第三方工具还有</p> <ul> <li>suponoff</li> <li>gosuv</li> </ul> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/supervisor-centralized-management-web-tool-cesi/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:17:14+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/cesi/" rel="tag">cesi</a>、<a href="https://devops.webres.wang/tag/supervisor/" rel="tag">supervisor</a>、<a href="https://devops.webres.wang/tag/web/" rel="tag">web</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <article id="post-7222" class="post-7222 post type-post status-publish format-standard hentry category-undefine tag-gunicorn tag-nginx-2 tag-python-3 tag-webpy"> <header class="entry-header"> <h2 class="entry-title"><a href="https://devops.webres.wang/2018/01/python-programming-webpy-gunicorn-nginx-deployment/" rel="bookmark">python编程(webpy + gunicorn + nginx部署)</a></h2> </header><!-- .entry-header --> <div class="entry-content"> <p>之前虽然也用nginx + uwsgi + webpy的方法部署过网站,但是用了gunicorn之后,发现用这种方法部署网站更为简单。下面我详细描述一下如何用这种方法进行网站部署。</p> <h2>1、准备server.py</h2> <p>和uwsgi部署的时候一样,这里仅仅需要设置一个application就可以了。</p> <pre><code>#!/usr/bin/python import web urls = ('/', 'Hello') class Hello(object): def GET(self): return 'Hello world' app = web.application(urls, globals()) application = app.wsgifunc() </code></pre> <h2>2、安装gunicorn</h2> <p>安装gunicorn的方法非常简单,在ubuntu下面一条命令就可以解决,</p> <pre><code>sudo apt-get install gunicorn </code></pre> <h2>3、用gunicorn启动server.py文件</h2> <p>启动的时候注意,最后一个选项是由文件名+wsgifunc组成的。同时,我们在选项中添加了gevent的属性,gunicorn本身支持gevent机制,可以有效提高server的性能。</p> <pre><code>gnicorn -b 127.0.0.1:8080 --worker-class gevent server:application </code></pre> <h2>4、用浏览器做测试</h2> <p>这个时候不出意外,你已经可以用127.0.0.1:8080访问我们的网站了。</p> <h2>5、准备nginx.conf文件</h2> <p>通常为了利用nginx做static文件加速,或者利用nginx做均衡负载,我们常常需要另外安装一下nginx软件。因此,此时nginx.conf必须准备好。当然,为了简单起见,我们这里只做一个代理就可以了,整个conf文件内容如下,</p> <pre><code>worker_processes 1; events{ worker_connections 1024; } http{ sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; location / { proxy_pass http://127.0.0.1:8080; } } } </code></pre> <h2>6、重启启动nginx</h2> <p>nginx.conf准备好后,这个时候先将它copy到/etc/nginx目录下。接下来,我们需要重新启动nginx软件,一个命令就可以了,</p> <pre><code>service nginx restart </code></pre> <h2>7、用浏览器测试80端口</h2> <p>有了nginx做代理,这个时候就可以用浏览器访问127.0.0.1了,因为一般网站默认用80做端口,所以没有意外的话,这个时候你就可以看到webpy给出的打印消息了。</p> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/python-programming-webpy-gunicorn-nginx-deployment/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:13:59+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/gunicorn/" rel="tag">gunicorn</a>、<a href="https://devops.webres.wang/tag/nginx-2/" rel="tag">nginx</a>、<a href="https://devops.webres.wang/tag/python-3/" rel="tag">python</a>、<a href="https://devops.webres.wang/tag/webpy/" rel="tag">webpy</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <article id="post-7221" class="post-7221 post type-post status-publish format-standard hentry category-undefine tag-django tag-gunicorn tag-supervisor"> <header class="entry-header"> <h2 class="entry-title"><a href="https://devops.webres.wang/2018/01/supervisor-and-gunicorn-deploy-django-project/" rel="bookmark">supervisor 和gunicorn部署django项目</a></h2> </header><!-- .entry-header --> <div class="entry-content"> <p>安装使用到的基本软件nginx、supervisor、gunicorn</p> <pre><code>vi /etc/supervisor/conf.d/django_project.conf </code></pre> <pre><code>[program:django_project] command=gunicorn xxx.wsgi:application -b 127.0.0.1:8080 -w 8 user=user #当前用户 directory=/home/user/django_project stdout_logfile=/tmp/var/logs/supervisor/%(program_name)s-stdout.log stderr_logfile=/tmp/var/logs/supervisor/%(program_name)s-stderr.log killasgroup=true stopasgroup=true autorstart=true autorestart=true # rq队列的配置 [program:rqworker] command=python manage.py rqworker default low user=user directory=/home/django_project/platform stdout_logfile=/tmp/var/logs/supervisor/%(program_name)s-stdout.log stderr_logfile=/tmp/var/logs/supervisor/%(program_name)s-stderr.log killasgroup=true stopasgroup=true autorstart=true autorestart=true``` </code></pre> <p>再在nginx配置中的location,添加</p> <p>proxy_pass http://127.0.0.1:8080;</p> <p>另外静态文件的代理可以添加下面的配置</p> <pre><code>location /static/ { alias /django_project/statics/; } </code></pre> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/supervisor-and-gunicorn-deploy-django-project/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:10:25+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/django/" rel="tag">Django</a>、<a href="https://devops.webres.wang/tag/gunicorn/" rel="tag">gunicorn</a>、<a href="https://devops.webres.wang/tag/supervisor/" rel="tag">supervisor</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <article id="post-7220" class="post-7220 post type-post status-publish format-standard hentry category-undefine tag-postgresql"> <header class="entry-header"> <h2 class="entry-title"><a href="https://devops.webres.wang/2018/01/postgresql-shared-cache-management/" rel="bookmark">PostgreSQL共享缓存区管理</a></h2> </header><!-- .entry-header --> <div class="entry-content"> <h2>一、共享缓冲区</h2> <p>KingbaseES中的buffer主要是用来将外存中的数据内容读入到内存中,加速运算过程中对数据的访问速度,同时将数据的修改进行缓存,在必要时再将其写出到外存,避免频繁的I/O,以提高效率。<br /> Buffer的种类有很多如Audit buffers、Clog buffers、Data buffers和Xlog buffers,此处所介绍的buffer管理是针对Data buffers而言的。</p> <h2>二、数据结构</h2> <ul> <li>BufferTag</li> <li>BufferDesc</li> <li>BufferStrategyControl</li> </ul> <h3>1、BufferTag</h3> <pre><code>typedef struct buftag { Oid dbid; /* database identifier */ FileBlock blockNum; /* file and blocknumber */ } BufferTag; </code></pre> <h3>2、BufferDesc</h3> <pre><code>typedef struct sbufdesc { BufferTag tag; /* ID of page contained in buffer */ RelFileNode rnode; /* relation this block belongs to */ BufFlags flags; /* see bit definitions above */ uint16 usage_count; /* usage counter for clock sweep code */ unsigned refcount; /* # of backends holding pins on buffer */ int wait_backend_pid; /* backend PID of pin-count waiter */ slock_t buf_hdr_lock; /* protects the above fields */ int buf_id; /* buffer's index number (from 0) */ int freeNext; /* link in freelist chain */ LWLockId io_in_progress_lock; /* to wait for I/O to complete */ LWLockId content_lock; /* to lock access to buffer contents */ } BufferDesc; </code></pre> <h3>3、引用计数(BufferDesc.refcount)</h3> <p>引用计数(refcount)用于跟踪访问buffer的后台数量,防止错误的将正在被使用的Buffer淘汰。当使用Buffer时,需要将其引用计数(refcount)加1(PinBuffer)。当Buffer不再使用,需要将其引用计数(refcount)减1(UnpinBuffer)。这里需要注意,由于一个后台可以多次访问同一个Buffer,因此后台通过PrivateRefCount来记录自己的引用次数,只有当自己对一个Buffer的引用减少到0,才会真正去修改refcount。PrivateRefCount在后台PinBuffer时将其值加1,UnpinBuffer时将其值减1。</p> <h3>4、使用计数(BufferDesc.usage_count)</h3> <p>usage_count用来标记Buffer被使用的次数,usage_count值越大,说明该Buffer经常被使用,那么在未来的一段时间里被使用的可能就比较大,所以这样的Buffer不能作为被替换的对象;相反,usage_count值越小,说明经常不被使用,可以作为替换的对象。在KingbaseES中,只有当usage_count为0时,才可能作为替换的对象。<br /> usage_count是在一个后台不再使用该Buffer即UnpinBuffer将后台的PrivateRefCount减少为0的时候将其值加1,以表示该Buffer最近被一个后台使用了。对VACUUN操作来说,不会修改usage_count的值,且如果refcount和usage_count的值都为0,则将buffer放入到FreeList的尾部。</p> <h3>5、BufferStrategyControl</h3> <pre><code>typedef struct { int nextVictimBuffer; // 指向下一Buffer int firstFreeBuffer; // 第一个空闲缓冲块id int lastFreeBuffer; // 最后一个空闲缓冲块id } BufferStrategyControl; /* Pointers to shared state */ static MT_LOCAL BufferStrategyControl *StrategyControl = NULL; </code></pre> <h3>6、Buffer Descriptors</h3> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-11250682355cb8027ae72fd1555563130.png" alt="未分类" width="873" height="546" class="aligncenter size-full wp-image-7216" /></p> <h2>三、主要函数</h2> <ul> <li>InitBufferPool</li> <li>BufferAlloc</li> <li>StrategyGetBuffer</li> <li>FlushBuffer</li> <li>PinBuffer</li> <li>UnpinBuffer</li> </ul> <h3>1、InitBufferPool流程</h3> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-18796337075cb8027b381dc1555563131.png" alt="未分类" width="278" height="546" class="aligncenter size-full wp-image-7217" /></p> <h3>2、BufferAlloc流程</h3> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-21230926085cb8027b87ff11555563131.png" alt="未分类" width="729" height="678" class="aligncenter size-full wp-image-7218" /></p> <h2>四、缓冲区替换策略</h2> <ul> <li>FreeList</li> <li>Clock-sweep</li> <li>buffer-ring</li> </ul> <h3>1、FreeList</h3> <p>当执行DROP TABLE时,可以确定该表的所有buffer都会失效,因此将此表的所有buffer都放入到Freelist的头部,这样可以在下一次分配buffer时,直接从Freelist中得到buffer,而不需要执行Clock Sweep算法。</p> <h3>2、Clock-sweep</h3> <p>当Buffer的refcount计数变成0的时候,代表当前系统没有后台引用此数据块。在KingbaseES中,为了能够减低锁的粒度、提高并发性,引用计数等于0的的Buffer并没有被放入Freelist中。在随机访问大量磁盘块、并且没有VACUUM的干扰下,Freelist几乎是空的(除了刚刚启动时)。这里的策略主要是为了避免不必要的持有操作Freelist的互斥锁。<br /> 由于大部分时候Buffer不会立即被放入到Freelist中,因此使用了一种被称为Clock Sweep的算法来分配Buffer。此算法类似教科书中时钟算法,每当需要使用Clock Sweep算法选择一个Buffer时,就从上次分配的Buffer的下一个位置开始,搜索引用计数为0(既没有被pin的Buffer)且usage_count为0的Buffer。如果该Buffer不满足上述条件,就将usage_count减1。</p> <h3>3、Clock-sweep</h3> <p><img src="https://devops-cdn.webres.wang/wp-content/uploads/2019/04/linux-10425009965cb8027be232c1555563131.png" alt="未分类" width="552" height="325" class="aligncenter size-full wp-image-7219" /></p> <p>在上图中Clock Sweep算法从4号buffer开始查找(记录在StrategyControl结构体中)可用的buffer。4号buffer因为引用计数大于0,因此不能被替换。5号buffer虽然没有人引用,但是其usage_count大于0,因此表示此buffer使用频率较高,因此将usage_count减1,并查看6号buffer。6号buffer的引用计数和usage_count都为0,因此选择将6号buffer淘汰。记录下一次搜索的位置是7号,并退出选择算法。</p> <h3>4、buffer-ring</h3> <p>批量读或者vacuum等操作可能会需要占据大量的buffer,影响其他正常业务。buffer-ring机制在批量读等占用的buffer数量达到某个程度(比如总buffer的1/4)时,分配给该操作固定的buffer数量,之后只能使用为其分配的buffer,而不能替换其他buffer。</p> </div><!-- .entry-content --> <footer class="entry-footer"> <span class="posted-on"><span class="screen-reader-text">发布于 </span><a href="https://devops.webres.wang/2018/01/postgresql-shared-cache-management/" rel="bookmark"><time class="entry-date published updated" datetime="2018-01-18T15:04:58+08:00">2018年1月18日</time></a></span><span class="cat-links"><span class="screen-reader-text">分类 </span><a href="https://devops.webres.wang/category/undefine/" rel="category tag">未分类</a></span><span class="tags-links"><span class="screen-reader-text">标签 </span><a href="https://devops.webres.wang/tag/postgresql/" rel="tag">PostgreSQL</a></span> </footer><!-- .entry-footer --> </article><!-- #post-## --> <nav class="navigation pagination" role="navigation"> <h2 class="screen-reader-text">文章导航</h2> <div class="nav-links"><a class="prev page-numbers" href="https://devops.webres.wang/author/specs/page/113/">上一页</a> <a class='page-numbers' href='https://devops.webres.wang/author/specs/'><span class="meta-nav screen-reader-text">页 </span>1</a> <span class="page-numbers dots">…</span> <a class='page-numbers' href='https://devops.webres.wang/author/specs/page/113/'><span class="meta-nav screen-reader-text">页 </span>113</a> <span aria-current='page' class='page-numbers current'><span class="meta-nav screen-reader-text">页 </span>114</span> <a class='page-numbers' href='https://devops.webres.wang/author/specs/page/115/'><span class="meta-nav screen-reader-text">页 </span>115</a> <span class="page-numbers dots">…</span> <a class='page-numbers' href='https://devops.webres.wang/author/specs/page/399/'><span class="meta-nav screen-reader-text">页 </span>399</a> <a class="next page-numbers" href="https://devops.webres.wang/author/specs/page/115/">下一页</a></div> </nav> </main><!-- .site-main --> </section><!-- .content-area --> </div><!-- .site-content --> <footer id="colophon" class="site-footer" role="contentinfo"> <div class="site-info"> <a href="https://cn.wordpress.org/" class="imprint"> 自豪地采用WordPress </a> </div><!-- .site-info --> </footer><!-- .site-footer --> </div><!-- .site --> <script type='text/javascript' src='https://devops.webres.wang/wp-content/themes/twentyfifteen/js/skip-link-focus-fix.js?ver=20141010'></script> <script type='text/javascript'> /* <![CDATA[ */ var screenReaderText = {"expand":"<span class=\"screen-reader-text\">\u5c55\u5f00\u5b50\u83dc\u5355<\/span>","collapse":"<span class=\"screen-reader-text\">\u6298\u53e0\u5b50\u83dc\u5355<\/span>"}; /* ]]> */ </script> <script type='text/javascript' src='https://devops.webres.wang/wp-content/themes/twentyfifteen/js/functions.js?ver=20150330'></script> <script type='text/javascript' src='https://devops.webres.wang/wp-includes/js/wp-embed.min.js?ver=5.1.19'></script> </body> </html>