web2

前面介绍过网页html的一些技术实现和大体架构,但对于后端boy,还是不太想计较那么多html方面的事,所以想把注意力集中回原先的python体系。针对简单的html的动态生成和一些个底层tcp/udp乃至上层http连接,都不用关注,只需要编写对应的接口响应函数,这种技术叫做WSGI,Web Server Gateway Interface。通常编写样式都是这样:

def application(environ, start_response):
    start_response('200 OK', [('content-type', 'text/html')])
    return [b'<h1>Hello, wsgi!</h1>']

这么一个接口就编写完毕,但还需要进行调用,另有一个python内置了的WSGI的服务实现,叫wsgiref,可以通过这个库来调用自己编写的WSGI接口,如下:

from wsgiref.simple_server import make_server
# 引入自己编写的接口
from wsgiapp import application

# 实现一个监听8080端口、引用application自定义接口处理的server对象
httpd = make_server('', 8080, application)
print('servint http on port 8080...')
# 持续跑起来
httpd.serve_forever()

和很多程序一样,想要让它停止,就crtl+c(windows),linux就用crtl+z来终止运行。

效果

如上就是简单的WSGI的web app效果,不用自己编写html,它会动态生成html,客户在浏览器访问127.0.0.1:8080/就可以得到服务,当然127.0.0.1就决定了客户只能是本机PC了,哈哈哈。

重写一下application,使其功能丰富点:

def application(environ, start_response):
    start_response('200 OK', [('content-type', 'text/html')])
    # 读取url中传入信息,再构造响应
    body = '<h1>Hello, {}!</h1>'.format(environ['PATH_INFO'][1:] or 'web')
    return [body.encode('utf-8')]

效果

上面的框架还是比较的难维护,因为上面还没添加请求方式和不同路由的判断就还是比较简短,所以现在大多数都是用的比较流行的web框架。

一、轻便的flask

flask的使用,和上面的比较相似,都是编写对应的接口处理函数,但在使用形式上还是有差异,毕竟flask作为轻便式实现,提供了许多便利的装饰器,使用前准备:
pip install flask

from flask import Flask

app = Flask(__name__)

@app.route('/')
def sayhello():
    return 'Hello, web!'


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

上面flask的代码实现了和最初始的web页面一样的效果,都是简单输出h1标签对应样式的hello,一开始就定义了app对象,其后的路由定义都是在app的route装饰器进行,定义完处理函数后,就run,大致就是这么个流程,如果是命令行跑起来,就需要强行要求脚本命名为app.py,如果想要自定义运行端口、ip等参数,就需要在run函数中传参了。

效果

@app.route('/<name>')
def sayhi(name):
    return '<h1>welcome, {}</h1>'.format(name)

效果

嗯,简单实现就这样,再来多点。

1.1 flask的各种接口的实现

一个登录页面的实现

from flask import Flask
from flask import request

app = Flask(__name__)

@app.route('/')
def index():
    # 下面的html是一个用户登录页面的实现,form标签的action属性指明了请求url,method指定请求方法
    return """<form action="sign" method="post" style="text-align: center;">
                <p>用户名:<input name="username"></p>
                <p>密码:<input name="password" type="password"></p>
                <p><button type="submit">sign in</button></p>
                </form>"""

@app.route('/sign', methods=["POST"])
def sign():
    # 如果是admin用户且密码为admin就返回登录后页面,否则回应登录失败。
    if request.form['username'] == 'admin' and request.form['password'] == 'admin':
        return '<h1>Welcome to the first page!</h1>'
    return '<h1>Failed in Authentication</h1>'


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

效果如下:
登录页面
登录成功页面
登录失败页面

一个图片上传实现

from flask import jsonify
from flask import redirect
import os

app.config['UPLOAD_FOLDER'] = 'upload'

@app.route('/')
@app.route('/home')
def home():
    return """<form action="upload" enctype="multipart/form-data" method="post">
            <input type="file" name="fiiii">
            <input type="submit" value="上传">
            </form>"""

@app.route('/upload', methods=["POST"])
def upload():
    if not os.path.exists('upload'):
        os.makedirs('upload')
    f = request.files['fiiii']
    print(app.config['UPLOAD_FOLDER'], f.filename)
    upload_path = os.path.join('upload', f.filename)
    f.save(upload_path)
    if os.path.exists(upload_path):
        return jsonify({'errno':0, 'msg':'上传成功'})
    else:
        return jsonify({'errno':110, 'errmsg':'上传失败'})

关于save接口,最好拼接好保存文件名再传给save参数,不然容易出现permission error,另外就是jsonify返回响应信息中文乱码的问题,有介绍app.config['JSON_AS_ASCII'] = False可以,但不管用。加点东西,上传图片成功后就显示图片:

@app.route('/upload', methods=["POST"])
def upload():
    if not os.path.exists('upload'):
        os.makedirs('upload')
    f = request.files['fiiii']
    print(app.config['UPLOAD_FOLDER'], f.filename)
    upload_path = os.path.join('upload', f.filename)
    f.save(upload_path)
    if os.path.exists(upload_path):
        return redirect('show/{}'.format(upload_path))
    else:
        back_msg = {'errno':110, 'errmsg':'上传失败'}
        return jsonify(back_msg)

@app.route('/show/<name>')
def display(name):
    html = '<img src="{}" alt="{}" height="400", width="auto">'.format(name, name.split('\\')[-1].split('.')[0])
    print(html)
    return html

但在上传成功后返回图片页面,但直接构造html img的办法好像不管用,还是要手动来构造响应,或者使用template,返回构造的html模板页面。在项目中创建templates文件夹并放置下面index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>display</title>
</head>
<body>
    <img src="{{path}}", alt="{{alt_name}}", height="400", width="auto"/>
</body>
</html>

接口的return语句修改一下

from flask import render_template
@app.route('/upload', methods=["POST"])
def upload():
    if not os.path.exists('upload'):
        os.makedirs('upload')
    f = request.files['fiiii']
    upload_path = os.path.join('upload', f.filename)
    f.save(upload_path)
    if os.path.exists(upload_path):
        return render_template('index.html', path=upload_path, alt_name=f.filename.split('.')[0])
    else:
        back_msg = {'errno':110, 'errmsg':'上传失败'}
        return jsonify(back_msg)

不过这样也不行,总是在get请求图片的时候失败,不知道为什么,单独构造html的时候也没问题啊,难受,只能用构造response的方法了。

@app.route('/upload', methods=["POST"])
def upload():
    if not os.path.exists('upload'):
        os.makedirs('upload')
    f = request.files['fiiii']
    upload_path = os.path.join('upload', f.filename)
    f.save(upload_path)
    if os.path.exists(upload_path):
        with open(upload_path, 'rb') as f:
            response = make_response(f.read())
            response.headers['content-type'] = 'image/png'
            return response
    else:
        back_msg = {'errno':110, 'errmsg':'上传失败'}
        return jsonify(back_msg)

效果

百度了不少,对口答案太少了,后面根据日志输出确定了图片的获取是会另外请求,而没有编写这个路由就导致了图片的html加载不完全:

@app.route('/upload', methods=["POST"])
def upload():
    f = request.files['fiiii']
    name = f.filename
    upload_path = os.path.join('upload', name)
    f.save(upload_path)
    if os.path.exists(upload_path):
        return render_template('index.html',
                               path=name,
                               alt_name=name.split('.')[0])
    else:
        back_msg = {'errno':110, 'errmsg':'上传失败'}
        return jsonify(back_msg)

# 错开一点,防止同名路由访问,顺带还需要修改一下index.html
@app.route('/img/<name>')
def send_file(name):
    with open('upload/'+name, 'rb') as f:
        response = make_response(f.read())
        response.headers['content-type'] = 'image/png'
        return response

因为index.html中设定img的src来源为upload_path,也即是upload/pic路径,所以要针对性写一个这样的url路由,这就是send_file的来源。
输出效果
输出效果

本地测试html总让我有种错觉就是,html和内部的img是一体的,这个习惯要改一改。功能能跑了,完善一下,只允许图片的上传,并且根据jinja2语法加个判断,因为gif和普通图片的img属性略有不同,再修改一下样式使得它居中:

from flask import Flask
from flask import request
from flask import jsonify
from flask import render_template
from flask import make_response
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'upload'
app.config['JSON_AS_ASCII'] = False

ALLOW_PICS = ['png', 'jpeg', 'jpg', 'PNG', 'JPG', 'JPEG', 'gif', 'GIF']

@app.route('/sign')
def sign():
    # 下面的html是一个用户登录页面的实现,form标签的action属性指明了请求url,method指定请求方法
    return """<form action="sign" method="post" style="text-align: center;">
                <p>用户名:<input name="username"></p>
                <p>密码:<input name="password" type="password"></p>
                <p><button type="submit">sign in</button></p>
                </form>"""

@app.route('/sign', methods=["POST"])
def signin():
    # 如果是admin用户且密码为admin就返回登录后页面,否则回应登录失败。
    if request.form['username'] == 'admin' and request.form['password'] == 'admin':
        return '<h1>Welcome to the first page!</h1>'
    return '<h1>Failed in Authentication</h1>'

@app.route('/')
@app.route('/home')
def home():
    return """<form action="upload" enctype="multipart/form-data" method="post">
            <input type="file" name="fiiii">
            <input type="submit" value="上传">
            </form>"""

@app.route('/upload', methods=["POST"])
def upload():
    f = request.files['fiiii']
    name = f.filename
    upload_path = os.path.join('upload', name)
    if f and name.split('.')[-1] in ALLOW_PICS:
        if 'gif' or 'GIF' in name:
            is_gif = True
        else:
            is_gif = False
        f.save(upload_path)
        if os.path.exists(upload_path):
            return render_template('index.html',
                                   is_gif = is_gif,
                                   path=upload_path,
                                   name=name.split('.')[0])
        else:
            jsonify({'errno':1101, 'errmsg':'failed in saving'})
    else:
        return jsonify({'errno':110, 'errmsg':'failed to upload, check the format'})

@app.route('/upload/<name>')
def send_file(name):
    with open('upload/'+name, 'rb') as f:
        response = make_response(f.read())
        response.headers['content-type'] = 'image/png'
        return response


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

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>display</title>
</head>
<body>
{% if is_gif %}
    <div style="text-align:center;"><img src="{{path}}", alt="{{name}}", height="400", width="auto" loop="true" autoplay="true"/><p>{{name}}</p></div>
{% else %}
    <div style="text-align:center"><img src="{{path}}", alt="{{name}}", height="400", width="auto"/><p>{{name}}</p></div>
{% endif %}
    <br/>
    <form action="upload" enctype="multipart/form-data" method="post" align="center">
            <input type="file" name="fiiii">
            <input type="submit" value="上传">
    </form>
</body>
</html>

好了,效果上就是每次上传图片成功都会展示,并且还可以继续上传图片。

1.2 摸一摸jinja2语法

首要的简单语法:

  • {{ ... }},标记html中的变量,和语言沟通的变量实体;
  • {% ... %},标记语句,各种逻辑语句诸如if、for等,都用这个,标记了开头需要标记结束;
  • {# ... #},用来写注释语句。

简单的使用语句:

变量

<body>
    <!--name变量,在python脚本中就直接在render_template中传入指定参数即可-->
    <p>hello, {{name}}</p>
</body>

分支

<body>
    {% if is_or_not %}
    <p>is_or_not为true</p>
    {% else %}
    <p>is_or_not为false</p>
    {% endif %}
</body>
<body>
    {% for comment in comments %}
        <li>{{comment.id, comment.name}}</li>
    {% endfor %}
</body>

注释就自然不用多言了,很明了了。继续展开,针对变量,不同类型的变量,存储的信息也不同,比如针对python的各种标准对象诸如字典、列表等等,在python里面怎么用,在这里就怎么用。除此以外,jinja2的特色就是针对变量有着过滤器的功能,形式就是{{变量|过滤器}},具体语义如下:

过滤器名 说明
safe 不转义
capitalize 变量首字母大写,其余小写
lower 变量小写
upper 变量大写
title 变量中每个单词首字母大写
trim 变量首尾空格去除
striptags 去除变量中的html标签

它的样式是变量后加上管道符|然后接api,就有点像过滤的样子才叫过滤器吧,更多的看官网吧。

模板继承

和面向对象的特性那样,jinja语法提供模板的继承,在应用当中,可以提供一个base模板,这个模板只提供关于UI界面的简单界定划分,后续的内容填充,都交给子模板。看了jinja中文网的例子,大致是这样:

给定一个base,

<!DOCTYPE html>
<html>
<head>
    {% block head %}
    <link rel="stylesheet" href="style.css" />
    <title>{% block title %}{% endblock %} - My Webpage</title>
    {% endblock %}
</head>
<body>
    <div id="content">{% block content %}{% endblock %}</div>
    <div id="footer">
        {% block footer %}
        &copy; Copyright 2008 by <a href="http://domain.invalid/">you</a>.
        {% endblock %}
    </div>
</body>

block和endblock包起来的区域就是可操作区域,对于我来说就是相当于另类的虚函数,可以被子模板重写。针对base的子模板扩展:

{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
    {{ super() }}
    <style type="text/css">
        .important { color: #336699; }
    </style>
{% endblock %}
{% block content %}
    <h1>Index</h1>
    <p class="important">
      Welcome on my awesome homepage.
    </p>
{% endblock %}

上面的extends表明继承于某某,super就有点类似python了,一样的意思,然后用上和base相同的block,再选择性重写里面的内容就ok了。因为引入了block的使用,它也支持嵌套概念,所以为了可读性的需求,运行在endblock的时候添加命名。

针对循环

在for循环中,有着方便查询当前迭代在循环中的位置的接口:

变量 描述
loop.index 当前循环迭代的次数,从1开始
loop.indexo 从0开始计数的当前循环迭代次数
loop.revindex 到循环结束还需要迭代的次数,从1开始
loop.revindexo 到循环结束还需要迭代的次数,从0开始
loop.first 判断是否是第一次迭代
loop.last 判断是否是最后一次迭代
loop.length 循环需要迭代的次数

除此以外,还有一些辅助性的特殊函数,比如range、cycler等,这种接口可以查看中文网。另外也还有更多的可用功能,但这里不想详述太多,烦人,要查的时候再看中文网吧。

后面的内容比较多,发布在了个人网站上,可以移步到那里:web2

posted @ 2023-05-01 10:04  夏目&贵志  阅读(32)  评论(0编辑  收藏  举报