Linux使用crontab运行Java程序定时任务小记

Linux中,crontab的功能是十分强大的,能够方便的调度程序的运行,甚至在很多时候能够替代程序中的定时任务。

它的命令格式和主要参数如下:

命令格式
crontab [-u user] [ -e | -l | -r ]

命令参数
-u user:用来设定某个用户的crontab服务;
file:file是命令文件的名字,表示将file做为crontab的任务列表文件并载入crontab。如果在命令行中没有指定这个文件,>crontab命令将接受标准输入(键盘)上键入的命令,并将它们载入crontab。
-e:编辑某个用户的crontab文件内容。如果不指定用户,则表示编辑当前用户的crontab文件。
-l:显示某个用户的crontab文件内容,如果不指定用户,则表示显示当前用户的crontab文件内容。
-r:从/var/spool/cron目录中删除某个用户的crontab文件,如果不指定用户,则默认删除当前用户的crontab文件。
-i:在删除用户的crontab文件时给确认提示。

图片化格式说明:

未分类

本次因为需要推送一批数据,简单起见用Java写了个jar程序,用命令行java -jar push.jar调用完事,但是数据是每天都要推送的,难道每天都要手工去执行一下命令行?这显然不切实际。

为了这么个小程序开发个定时任务也嫌麻烦,后来就想到了Linux系统的crontab,但是在使用过程中还是碰到了几个问题,在此记录一下。

第一步,编写start_pust.sh文件,内容简单如下:

#!/bin/bash
java -jar /home/liyd/push.jar

为了避免路径问题引起的错误,这里使用了绝对路径来保证执行正确。

第二步,编写crontab.txt文件,简单的一行指定执行时间:

34 1 * * * /home/liyd/start_push.sh

每天的1点34分调用push.jar。

最后指定crontab运行:

crontab crontab.txt

到这里设置就都完成了,按照我们的预想每天的1点34分就会执行pust.jar推送数据。

可是事情往往不会按我们预想的发展,我们发现程序根本就没有执行,这是为什么呢?因为在这之前我用命令行直接./start_push.sh都是可以的呀。

只能查查资料了,发现网上也有很多人碰到这个问题,总结起来两点:

一、路径问题

二、环境变量问题

这里我为了避免出错已经使用了绝对路径,那看来就是环境变量的问题了。

原来crontab并不会加载环境变量配置,需要我们在脚本中设置,Java程序没有JDK等环境变量当前不能运行了。

修改前面的start_push.sh脚本,加入profile文件的读取:

#!/bin/bash
. /etc/profile
. ~/.bash_profile
java -jar /home/liyd/push.jar

到这里,程序能够正常运行了,使用ps aux | grep java能够看到执行的进程,但是我的Java程序死活没有日志输出啊,查看我的日志配置:

  <appender name="DEFAULT-APPENDER" class="org.apache.log4j.DailyRollingFileAppender">
  <param name="file" value="${user.dir}/logs/common-default.log"/>
  <param name="append" value="true"/>
  <param name="encoding" value="UTF-8"/>
  <layout class="org.apache.log4j.PatternLayout">
  <param name="ConversionPattern" value="[%x][%r][%p][%t] %d{HH:mm:ss,SSS} method:%l %m%n"/>
  </layout>
  </appender>

本来应该是输出到当前项目的logs文件夹下的,这里初步估计应该是${user.dir}这个变量又找不到了吧。

再次修改start_pust.sh文件,加入user.dir参数:

#!/bin/bash
. /etc/profile
. ~/.bash_profile
java -Duser.dir="/home/liyd/" -jar /home/liyd/push.jar

到这里,终于一切正常!

python、virtualenv和virtualenvwrapper

简介

搭建不同版本的python环境,并且能够随意的创建、删除和管理,在不同的python环境下切换

安装环境

centos7.4、 VMware Workstation 14 Pro

安装步骤

1、查看现有的python环境

python -V

2、安装必要的工具

yum -y install zlib zlib-devel openssl-devel gcc

3、编译安装不同的python版本

1)安装python2.7.13

cd && wget https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz

tar -zxvf Python-2.7.13.tgz

cd Python-2.7.13

./configure --prefix=/usr/local/python2.7 && make -j 4 && make install

2)安装python3.6.3

cd && wget https://www.python.org/ftp/python/3.6.3/Python-3.6.3.tgz

tar -zxvf Python-3.6.3.tgz

cd Python-3.6.3

./configure --prefix=/usr/local/python3.6 && make -j 4 && make install

4、安装配置virtualenv

easy_install pip 

(

如果没有 easy_install可下载安装 

wget https://bitbucket.org/pypa/setuptools/downloads/ez_setup.py && python ez_setup.py

)

pip install virtualenv

配置pip

cat << EOF  > /etc/pip.conf

[list]

format=columns

EOF

pip completion --bash >> ~/.bash_profile

5、安装配置virtualenvwrapper

pip install virtualenvwrapper

echo "export WORKON_HOME=~/test/venv" >>~/.bash_profile

source ~/.bash_profile

source virtualenvwrapper.sh

使用说明

1、创建python环境

mkvirtualenv -p /usr/local/python3.6/bin/python3.6 venv301

mkvirtualenv -p /usr/local/python2.7/bin/python2.7 venv201

2、查看python环境

lsvirtualenv

3、给所有python环境安装flask

allvirtualenv pip install flask

4、切换python环境

workon venv301

workon venv201

5、直接进入python环境的目录下

cdvirtualenv

6、退出python环境

deactivate

7、删除python环境

rmvirtualenv venv201

8、其它

钩子脚本,可自行查询相关内容

FLASK实现机器负载监控

  • 系统环境:centos7.4

  • 主机IP:192.168.134.149

Flask代码段:

未分类

moitor.html页面:

<!DOCTYPE html>

<html>

<head>

<meta charset=”utf-8″>

<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>

<title>运维管理系统</title>

<link rel=”shortcut icon” href=”favicon.ico”>

<link href=”{{url_for(‘static’,filename=’css/plugins/morris/morris-0.4.3.min.css’)}}” rel=”stylesheet”>

</head>

<body>

<script src=”{{ url_for(‘static’,filename=’js/jquery.min.js’) }}”></script>

<script src=”{{url_for(‘static’,filename=’js/plugins/morris/raphael-2.1.0.min.js’)}}”></script>

<script src=”{{url_for(‘static’,filename=’js/plugins/morris/morris.js’)}}”></script>

<script type=”text/javascript”>

$(function () {

function monitor() {

$(‘#myfirstchart’).html(“”);

var SCRIPT_ROOT = {{ request.script_root|tojson|safe }}

$.post(SCRIPT_ROOT + ‘/monitor’,function(data){

Morris.Line({

element: ‘myfirstchart’,

data: data ,

xkey: ‘timenow’,

ykeys: [‘a’,’b’,’c’],

labels: [‘1分钟’, ‘5分钟’,’15分钟’]

});

})

};

setInterval(monitor,3000)

})

</script>

<div id=”myfirstchart” style=”height: 250px;”></div>

</body>

</html>

效果图:

未分类

使用python3和flask构建RESTful API(接口测试服务)

引言

构建RESTful API貌似是开发的工作,和测试有和关系?

其实测试开发需要构建RESTful API的场景很多。比如测试Android应用,一般的接口测试只考虑了服务器端,至于客户端在网络异常或者服务端异常时如何反应,多数天朝的测试人员是没有考虑到的。客户端在对这些异常处理不够充分的时候,会出现崩溃等各种莫名其妙的问题。

为此一些走在前沿的测试人员会自己写一些RESTful API, 把服务端的域名劫持到自己的API,故意返回各种异常,看客户端的稳定性。

另外测试开发的测试工具需要和其他系统对接等场景也经常需要API。

术语

REST: REpresentational State Transfer

目标

  • GET – /api/Category – Retrieve all categories

  • POST – /api/Category – Add a new category

  • PUT – /api/Category – Update a category

  • DELETE – /api/Category – Delete a category

  • GET – /api/Comment – Retrieve all the stored comments

  • POST – /api/Comment – Add new comment

要求

  • python3.*
  • PostgreSQL

工程目录

project/
├── app.py
├── config.py
├── migrate.py
├── Model.py
├── requirements.txt
├── resources
│   └── Hello.py
│   └── Comment.py
│   └── Category.py
└── run.py

requirements.txt的内容如下:

flask
flask_restful
flask_script
flask_migrate
marshmallow
flask_sqlalchemy
flask_marshmallow
marshmallow-sqlalchemy
psycopg2
  • flask – Python的微框架

  • flask_restful – 这是Flask的扩展,可快速构建REST API。

  • flask_script – 提供了在Flask中编写外部脚本的支持。

  • flask_migrate – 使用Alembic的Flask应用进行SQLAlchemy数据库迁移。

  • marshmallow – ORM/ODM/框架无关的库,用于复杂数据类型(如对象)和Python数据类型转换。

  • flask_sqlalchemy – Flask扩展,增加了对SQLAlchemy的支持。

  • flask_marshmallow – 这是Flask和marshmallow的中间层。

  • marshmallow-sqlalchemy – 这是sqlalchemy和marshmallow的中间层。

  • psycopg – Python的PostgreSQL API。

安装依赖

# pip3 install -r requirements.txt

安装配置PostgreSQL

这里以 Ubuntu 16.04为例:

# sudo apt-get update && sudo apt-get upgrade
# apt-get install postgresql postgresql-contrib
# su - postgres
$ createdb api
$ createuser andrew --pwprompt #创建用户
$ psql -d api -c "ALTER USER andrew WITH PASSWORD 'api';"

参考资料:

https://linode.com/docs/databases/postgresql/how-to-install-postgresql-on-ubuntu-16-04/

https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-14-04

配置

# -*- coding: utf-8 -*-
# Author:    xurongzhong#126.com wechat:pythontesting qq:37391319
# CreateDate: 2018-1-10

from flask import Blueprint
from flask_restful import Api
from resources.Hello import Hello
from resources.Category import CategoryResource
from resources.Comment import CommentResource


api_bp = Blueprint('api', __name__)
api = Api(api_bp)

# Routes
api.add_resource(Hello, '/Hello')
api.add_resource(CategoryResource, '/Category')
api.add_resource(CommentResource, '/Comment')

快速入门

app.py

from flask import Blueprint
from flask_restful import Api
from resources.Hello import Hello

api_bp = Blueprint('api', __name__)
api = Api(api_bp)

# Route
api.add_resource(Hello, '/Hello')

resource/Hello.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Author:    xurongzhong#126.com wechat:pythontesting qq:37391319
# CreateDate: 2018-1-10

from flask_restful import Resource


class Hello(Resource):
    def get(self):
        return {"message": "Hello, World!"}

    def post(self):
        return {"message": "Hello, World!"}

run.py

from flask import Flask


def create_app(config_filename):
    app = Flask(__name__)
    app.config.from_object(config_filename)

    from app import api_bp
    app.register_blueprint(api_bp, url_prefix='/api')

    return app


if __name__ == "__main__":
    app = create_app("config")
    app.run(debug=True)

启动服务

$ python3 run.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 136-695-873

用浏览器访问: http://127.0.0.1:5000/api/Hello

{
    "hello": "world"
}

接入数据库

from flask import Flask
from marshmallow import Schema, fields, pre_load, validate
from flask_marshmallow import Marshmallow
from flask_sqlalchemy import SQLAlchemy


ma = Marshmallow()
db = SQLAlchemy()


class Comment(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    comment = db.Column(db.String(250), nullable=False)
    creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False)
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id', ondelete='CASCADE'), nullable=False)
    category = db.relationship('Category', backref=db.backref('comments', lazy='dynamic' ))

    def __init__(self, comment, category_id):
        self.comment = comment
        self.category_id = category_id


class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(150), unique=True, nullable=False)

    def __init__(self, name):
        self.name = name


class CategorySchema(ma.Schema):
    id = fields.Integer()
    name = fields.String(required=True)


class CommentSchema(ma.Schema):
    id = fields.Integer(dump_only=True)
    category_id = fields.Integer(required=True)
    comment = fields.String(required=True, validate=validate.Length(1))
    creation_date = fields.DateTime()

migrate.py

from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from Model import db
from run import create_app

app = create_app('config')

migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)


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

数据迁移

$ python3 migrate.py db init
$ python3 migrate.py db migrate
$ python migrate.py db upgrade

修改Category.py 和Comment.py, 完整代码

测试

可以使用curl,比如:

curl http://127.0.0.1:5000/api/Category --data '{"name":"test5","id":5}' -H "Content-Type: application/json"

也可以在chrome中使用postman:

未分类

未分类

未分类

未分类

[Flask教程] 9.注册和登录功能实现(2)—— 注册与登录的错误提示

在注册和登录功能实现(1)中,我们已经获取到了页面POST过来的登录或者注册数据,接下来我们需要与数据库中的数据做验证,验证通过才能登录或者注册。我们平时在登录网站时,如果输入的用户名或者密码错误,有的网站是在登录框附近提示错误,也有的是跳转到一个页面提示出错,并经过几秒倒计时再返回原来的页面。
我们在后续做搜索功能的时候,用页面跳转来处理未找到结果的情形,这里就通过使用Flask的flash功能,直接在当前页面显示错误提示。简单来说,步骤就是在视图函数中flash一个字符串,在html模板中使用get_flashed_messages()去获取这个字符串,并显示在网页中。
首先,我们先新建一个exts.py,用于存放一些功能性的函数,在其中写一个去验证登录和注册信息的函数,如下:

from models import Users

def validate(username, password1, password2=None):
    user = Users.query.filter(Users.username == username).first()
    if password2:
        if user:
            return '用户名已经存在'
        else:
            if len(username) < 4:
                return '用户名长度至少4个字符'
            elif password1 != password2:
                return '两次密码不一致'
            elif len(password1) < 6:
                return '密码长度至少6个字符'
            else:
                return '注册成功'
    else:
        if user:
            if user.password == password1:
                return '登录成功'
            else:
                return '密码错误'
        else:
            return '用户名不存在'

要使用flash功能,还得设置一个名为SECRET_KEY的参数,用于加密数据,我们在config.py中写进去,随便取个值SECRET_KEY = “THIS-A-SECRET-KEY”。然后在HarpQA.py中,从flask中导入flash,从exts.py中导入validate,修改register视图函数,如下:

from flask import Flask, render_template, request, flash
from models import db
from exts import validate
import config

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

...

@app.route('/register/', methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    else:
        username = request.form.get('username')
        password1 = request.form.get('password1')
        password2 = request.form.get('password2')
        message = validate(username, password1, password2)
        flash(message)
        return render_template('register.html')

在视图函数中flash了message,接下来我们需要在html中显示它,我们再去修改base.html(这样对register.html和login.html都能起作用),在body区域尾部增加如下代码(直接从Flask官方文档复制修改的):

...
<nav><!-- 导航条内容 -->
...
</nav>
<div class="body-container">
    {% block body_part %}
    {% endblock %}
</div>
<div class="flash-message">
    {% with messages = get_flashed_messages() %}
        {% if messages %}
            <ul>
            {% for message in messages %}
                <li>{{ message }}</li>
            {% endfor %}
            </ul>
        {% endif %}
    {% endwith %}
</div>
</body>

然后运行程序,访问注册页,随便输入几个试试,发现已经能用了:

未分类

只是不太美观,我们再用css调整一下,借助Boostrap中的警告框样式,最终结果如下:

未分类

登录也是同理,就不演示啦。

2017年1月17日补充内容:
当我们登录或注册出现问题,出现的警告框是如上图红色的,显然我们不希望登录或者注册成功的时候也是红色的,我们将提示成功的颜色设置为蓝色,然后传入一个参数给模板,告诉模板现在是成功还是失败的情况,然后在html中增加if,现在的html代码如下:

<div class="flash-message">
    {% with messages = get_flashed_messages() %}
        {% if messages %}
            <ul>
            {% for message in messages %}
                {% if status == 'OK' %}
                    <li><div class="alert alert-info" role="alert">{{ message }}</div></li>
                {% else %}
                    <li><div class="alert alert-danger" role="alert">{{ message }}</div></li>
                {% endif %}
            {% endfor %}
            </ul>
        {% endif %}
    {% endwith %}
</div>

那么status这个参数是怎么传递给模板呢,我们之前提到在render_templates函数中传入,现在我们使用@app.context_processor这个装饰器,它其实是上下文管理器,其装饰的函数返回的内容对所有html模板都起作用,用法如下:

@app.context_processor
def my_context_processor():
    status = session.get('status', '')
    return {'status': status}

将其放在HarpQA.py中,它返回一个字典,在任意html中使用{{ key }},就能得到这个字典key对应的value。那么session是什么?在现在这个case中,我们简单理解其为一个保存数据的容器,在登录和注册的视图函数中,验证完账号密码之后,将验证结果的信息存入session,例如注册函数修改如下:

@app.route('/register/', methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    else:
        username = request.form.get('username')
        password1 = request.form.get('password1')
        password2 = request.form.get('password2')
        message = validate(username, password1, password2)
        flash(message)
        if message == '注册成功':
            session['status'] = 'OK'
            return redirect(url_for('login'))
        else:
            session['status'] = 'BAD'
            return render_template('register.html')

如果注册成功,就向session中写入status,值为’OK’,反之则为’BAD’。这样html模板就能根据status显示不同的颜色了。flask中还有个g对象也可用于保存和共享数据,但g对象是基于每一个请求的,不能跨请求使用。我们注册成功之后,要跳转到登录页,此时g就无法使用了,而session是基于这一次http连接的,不同请求都能使用。

未分类

实际上更简单的方法是,我们直接对传入的{{ message }}进行判断,如果带有’成功’字符串,就显示蓝色,否则就显示红色。上文主要是为了说明@app.context_processor这个装饰器,以及session和g对象的区别。

[Flask教程] 8.注册和登录功能实现(1)—— 页面设计和获取POST数据

在导航条中,右侧有登录和注册两个链接,储存用户数据的Users模型也有了,现在我们来实现登录和注册的功能。
先来看注册功能的实现,新建一个视图函数,如下:

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

在导航条模板base.html中,为注册添加链接,使用url_for函数将其href属性值修改为register.html,如下:

<li><a href="{{ url_for('register') }}">注册</a></li>

接下来我们要去制作register.html了,这部分是基础的html/css知识,控件也是用的Bootstrap框架,最终注册页http://127.0.0.1:5000/register/的效果图如下(结尾贴出代码):

未分类

register.html中的表单控件,我们设置了其方法为POST,注册按钮的type为submit,这样点击提交按钮的时候,浏览器就会以POST方法去请求当前网址,但我们的视图函数默认只能接受GET方法,因此我们要为其添加POST方法,并使用flask中的request对象获取表单提交的数据,最终视图函数代码如下:

@app.route('/register/', methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    else:
        username = request.form.get('username')
        password1 = request.form.get('password1')
        password2 = request.form.get('password2')
        print(username, password1, password2)
        return ' '

request.form.get方法需要传入表单控件的name属性的值,这样就能获取到对应的填入到表单中的值了,为了演示,我们在结尾将其打印了出来,结果显示无误,说明后端已经获取到了POST提交的数据。
登陆页面也是一样的原理,拿register.html简单修改成login.html,模仿register再增加一个login视图函数,然后访问http://127.0.0.1:5000/login/,效果如下:

未分类

结尾贴上register.html的内容:

{% extends 'base.html' %}

{% block static_files %}
<link rel="stylesheet" href="{{ url_for('static',filename='css/register_login.css.css') }}"/>
{% endblock %}

{% block page_name %}注册{% endblock %}

{% block body_part %}
<h3>注册</h3>
<form class="form" action="" method="POST">
    <div class="input-group input-group-lg">
      <span class="input-group-addon" id="sizing-addon1">用户</span>
      <input type="text" class="form-control" placeholder="请输入用户名" aria-describedby="sizing-addon1" name="username">
    </div>
    <div class="input-group input-group-lg">
      <span class="input-group-addon" id="sizing-addon1">密码</span>
      <input type="password" class="form-control" placeholder="请输入密码" aria-describedby="sizing-addon1" name="password1">
    </div>
    <div class="input-group input-group-lg">
      <span class="input-group-addon" id="sizing-addon1">密码</span>
      <input type="password" class="form-control" placeholder="请再次确认密码" aria-describedby="sizing-addon1" name="password2">
    </div>
    <div class="register-button">
        <button type="submit" class="btn btn-primary btn-lg btn-block">注册</button>
    </div>
</form>
{% endblock %}

代码中引入的register_login.css是register.html和login.html通用的,其内容如下:

h3{
    text-align: center;
    padding-top: 10px;
}

.form{
    width: 400px;
    margin: 0 auto;
}

form.form > div{
    padding: 8px;
}

此外还对导航条模板做了简单的调整,第一是增加了一个block,给每个继承它的html一个放自定义css文件的地方,第二是在body中增加了一个div块,即register.html和login.html中间白色的内容区域,最终base.html代码如下:

<!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') }}">
    {% block static_files %}{% endblock %}
    <title>{% block page_name %}{% endblock %}-HarpQA</title>
</head>
<body>
    <nav><!-- 导航条内容 -->
        ...省略...
        <li><a href="{{ url_for('login') }}">登录</a></li>
        <li><a href="{{ url_for('register') }}">注册</a></li>
        ...省略...
    </nav>
    <div class="body-container">
        {% block body_part %}
        {% endblock %}
    </div>
</body>
</html>

base.css 也添加了 <div class="body-container"> 的样式:

.body-container{
    width: 600px;
    background: white;
    margin: 0 auto;
    border-radius: 5px;
}

[Flask教程] 7.ORM与SQLAlchemy (3) – flask-migrate数据库迁移

在上一遍文章中,我们增加了两个模型Questions和Comments,并为Users增加了avatar_path这个字段,然后通过这段代码更新到数据库:

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

因为当使用过db.create_all()之后,再次直接使用db.create_all(),对模型的修改并不会更新到数据库,我们要使用db.drop_all()先把数据库中所有的表先删除掉,然后再db.create_all()一次。听上去是不是很麻烦?更糟糕的是,原先数据库的的数据也就没有了。所以我们不用这种简单粗暴的方式去更新数据库结构,而是借助flask-migrate这个专门用于迁移数据库的工具,它可以在保留数据库原始数据的情况下,完成模型的更新。此外,我们还将结合flask-script一起使用,简单来说flask-script让我们可以使用命令行去完成数据库迁移的操作。

在项目主文件夹下新建一个manage.py,代码如下:

from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from HarpQA import app, db
from models import Users, Questions, Comments

manager = Manager(app)

migrate = Migrate(app, db)

manager.add_command('db', MigrateCommand)


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

首先导入相关的类,注意模型要全部导入过来,即使代码中并没有显式地使用它们。然后传入app或db来构建Manager和Migrate两个类的实例,最后将MigrateCommand的命令加入到manager中。

此时我们假设要更新模型的结构,在models.py的User模型结尾添加一行代码test = db.Column(db.Integer),然后点击PyCharm下方的Terminal,自动进入到了虚拟环境的命令行中,输入python manage.py db init来初始化,这一步主要是建立数据库迁移相关的文件和文件夹,只是在第一次需要使用。接着依次使用python manage.py db migrate和python manage.py db upgrade,待运行完成,查看users_infor表的结构,结果如下:

未分类

可以看到test字段已经添加到表中了。

[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-## --> <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/2018/01/page/14/">上一页</a> <a class='page-numbers' href='https://devops.webres.wang/2018/01/'><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/2018/01/page/14/'><span class="meta-nav screen-reader-text">页 </span>14</a> <span aria-current='page' class='page-numbers current'><span class="meta-nav screen-reader-text">页 </span>15</span> <a class='page-numbers' href='https://devops.webres.wang/2018/01/page/16/'><span class="meta-nav screen-reader-text">页 </span>16</a> <span class="page-numbers dots">…</span> <a class='page-numbers' href='https://devops.webres.wang/2018/01/page/33/'><span class="meta-nav screen-reader-text">页 </span>33</a> <a class="next page-numbers" href="https://devops.webres.wang/2018/01/page/16/">下一页</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>