Flask教学篇:实战项目《ETRO website》源代码讲解[第一期]

我最近并没有更新系列《Python百天教程》,这是由于我正在忙着部署我的网站。它正是我使用Flask编写。如今已经正式上线。现在请让我为大家解剖该项目(全部源码见 github仓库 )。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sKWPBD5R-1689648927045)(https://earthtrigonometrichub.cpolar.top/static/favicon.ico)]

由于代码量较大,我们分开来看。本文讲解的是: 主程序:app.py

from pymysql.converters import escape_string as est#字符串安全化
from threading import Thread#简单的多线程(用于实现AJAX异步)
from flask_ckeditor import CKEditor#富文本编辑器实例化类
from flask import (
    Flask,
    render_template,
    flash, redirect,
    url_for,
    Markup,
    request,
    abort,
    session,
    send_file,
    )
from forms import (
    sjn,
    sjs,
    get_time,
    send_email,
    user_base,
    an_article,
    del_arti,
    del_user,
    uploadfile,
    article_input,
    Check_code,
    Signin,
    Login,
    cursor,
    connect,
    logst,
    write_to_sql,
    new_times,
    search,
    _patch_1,
    _patch_2,
    _patch_3,
    getvip,
    setvip,
    ImageSpider,
    yincai
    )
from shutil import rmtree
from pymysql.err import IntegrityError
spider=ImageSpider()
app=Flask("app")
app.secret_key="asdfsauo79va98s012n780asf8dn78a0qwn8f0asdag3412t462"
ckeditor=CKEditor(app)

#* os文件处理
import os
app.config["UPLOAD_PATH"]=os.path.join(app.root_path,"uploads")
app.config["WTF_I18N_ENABLED"]=False
app.config["MAX_CONTENT_LENGTH"]=5*1024*1024

部分前置的处理.我在本项目中并未使用Flask自带的SQLArchamey.它效率不如MySQL(如果你不熟悉MySQL的语法的话,那还是用Flask自带的NoSQL罢 它具有多映射等特点 且操作更为方便).
from forms import ...中引入的内容在同目录下的form.py中定义,我将在下篇文章中讲解它们.
app.secret_key是密钥,用于加密Cookie(session)等内容.它不能被公开.此处我随机生成了一个,你可以替换掉它.
app即Flask应用;ckeditor是实例化的CKEditor对象.它也会在下次讲到.

#* 根路由 /
@app.route("/")
def index():
    from_=request.headers["X-Real-Ip"]
    body_="进入了主界面"
    logst(from_=from_,body=body_)
    if "logged-in" in session and not session["admin"]:
        return render_template("main_in.html",i=session['to_up2'])
    elif "logged-in" in session and session["admin"]:
        return render_template("main_in_admin.html")
    else:
        return render_template("main.html")

@app.route(endpoint,methods)装饰器用于绑定方法和路由.它修饰的方法会在进入endpoint路由时被调用,自带上下文.其中包含请求(request)等.我们后面碰到再说.
这个index()函数会在进入路由/时调用.例如,你的网站名叫xxx.yyy.zzz,那么它就会在进入(https://)xxx.yyy.zzz/时调用.
这里的session是Cookie对象.它也是自带上下文的.它是一个字典,里面的内置键有"User-Agent"等,可以用F12查看.客户端每发送一个请求时都会将session放在请求头部,而服务端可以更改它.它使用了CSRF令牌加密.
logst方法是我定义的日志方法.它用于将日志存进服务端文件.
render_template方法用于返回一个Jinja2模板的html文件.它的格式在后几篇文章中会提到.

#* 招募界面 /join_us
@app.route("/join_us")
def join_us():
    from_=request.headers["X-Real-Ip"]
    body_="进入了join_us界面"
    logst(from_=from_,body=body_)
    return """<a href="/">Return</a><br>Our Email: ETRO_gfyx@163.com<br>Or you can contact with me(ETRO.omega) by ETRO_omega@outlook.com"""

这里应该就很清楚了.路由xxx.yyy.zzz/join_us,返回的是一个超文本字符串.它将直接被显示在浏览器上而不附带格式.

vipcodes=[
    "a",
    "b"
]

vip兑换码.我用两个占位符代替它们.后面有用(还不拿小本本记下来,要考! )

#* 创建新账户(输入信息界面)
@app.route("/signup1",methods=["GET","POST"])
def signup():
    form=Login()
    from_=request.headers["X-Real-Ip"]
    body_="尝试创建账户"
    logst(from_=from_,body=body_)
    if form.validate_on_submit():
        from_=request.headers["X-Real-Ip"]
        body_="在创建账户界面提交了请求"
        logst(from_=from_,body=body_)
        username=form.username.data
        password=form.password.data
        moreinfo=form.moreinfo.data
        emaiaddr=form.email.data
        ifhisvip=form.vip_code.data
        checkcode0=sjs()
        cursor.execute("select uuid from users where userid=%s",username)
        answer=cursor.fetchall()
        cursor.execute("select uuid from users where email=%s",str(emaiaddr))
        result=cursor.fetchall()
        if len(answer)==0 and len(result)==0:
            new_user=user_base(str(username),str(password),str(emaiaddr),checkcode0,description=str(moreinfo))
            if new_user.is_valid_in():
                t=Thread(target=send_email,args=(emaiaddr,checkcode0))
                t.start()
                session["to_up2"]=username
                session["stdinfo"]=True
                session["n"],session["w"],session["d"],session["e"],session["c"],session["a"],session["t"] = new_user.out()
                print(session["c"])
                if ifhisvip in vipcodes:
                    session["ifvip"]=True
                return redirect(url_for("check"))
            else:
                from_=request.headers["X-Real-Ip"]
                body_="提交了创建账户申请后由于非法字符而失败了"
                logst(from_=from_,body=body_,level=2)
                return render_template("dlbc1.html",ly="创建账户界面:输入的内容含有非法字符,如<等",fangfas=["重新输入所有信息,确保密码、用户名、邮箱等信息中不包含空格等非法字符"])
        else:
            from_=request.headers["X-Real-Ip"]
            body_="提交了创建账户申请后由于用户名或邮箱已存在而失败了"
            logst(from_=from_,body=body_,level=2)
            return render_template("dlbc1.html",ly="创建账户界面:用户已存在 或 该邮箱已被注册 ",fangfas=["更换用户名或修改邮箱地址"])
    return render_template("signup.html",form=form)

创建账户界面.
渲染表单(表单就是一组输入框).methods是提交数据方法,POST用于从客户端提交表单数据到服务端,经过轮询返回数据到客户端;GET方法不提交额外数据,直接问服务器要一个页面的渲染好的代码,服务器返回这个页面.
这一段比较难啃.当然我也承认我的代码就是shit山.慢慢看吧.
学过MySQL的应该cursorconnect懂的都得好吧.
request.headers["X-Real-Ip"]是浏览器发送的请求头部储存的请求来源的IP.

#* 创建新账户(确认验证码界面)
@app.route("/signup2",methods=["GET","POST"])
def check():
    if "stdinfo" in session:
        form=Check_code()
        from_=request.headers["X-Real-Ip"]
        body_="进入了创建账户的确认验证码界面"
        try:
            cursor.execute("insert into user_ip (userid,ip,sm) values (%s,%s,\"未知\")",(session["to_up2"],from_))
            cursor.commit()
        except:
            pass
        logst(from_=from_,body=body_)
        if form.validate_on_submit():
            print("Coding:%s"%(session["to_up2"]))
            real_code=session["c"]
            print(real_code)
            getd_code=form.checkcode.data
            if str(getd_code)==str(real_code):
                session["logged-in"]=True
                session["admin"]=False
                from_=request.headers["X-Real-Ip"]
                n,w,d,e,c,a=session["n"],session["w"],session["d"],session["e"],session["c"],session["a"]
                body_=n+"创建账户成功"
                logst(from_=from_,body=body_,level=1)
                session["t"]=p2times=3
                v=0
                if "ifvip" in session:
                    v=1
                    session["t"]=p2times=64
                    logst(from_=from_,body="开通了vip服务(来源:signup验证码兑换)",level=3)
                t2=Thread(target=write_to_sql,args=(n,w,d,e,c,a,v,p2times))
                t2.start()
                cr=send_file("files\\others\\用户须知.txt",as_attachment=True)
                fr=redirect(url_for("index"))
                payload = (cr,fr)
                #TODO: cr未实现返回
                return fr
            else:
                from_=request.headers["X-Real-Ip"]
                body_="提交了创建账户申请后由于邮箱验证码错误而失败了"
                logst(from_=from_,body=body_,level=2)
                return render_template('dlbc2.html',ly='验证码错误 ',fangfas=['重新查看并输入验证码'])
        return render_template("check.html",form=form)
    else:
        from_=request.headers["X-Real-Ip"]
        body_="可能的黑客攻击:直接进入了验证码确认界面而未绑定邮箱"
        logst(from_=from_,body=body_,level=5)
        return render_template("dlbc1.html",ly="创建账户界面:未绑定邮箱但意外地进入了确认验证码界面",fangfas=["退回到创建账户界面"])

在上一个界面输入邮箱后服务端已经异步发送了验证码.这里直接获取然后验证即可.
那个write_to_sql方法挺反人类的,但我懒得改了.

#* 登录账户界面
@app.route("/signin",methods=["GET","POST"])
def signin():
    from_=request.headers["X-Real-Ip"]
    body_="尝试登录"
    logst(from_=from_,body=body_,level=1)
    form=Signin()
    if form.validate_on_submit():
        from_=request.headers["X-Real-Ip"]
        body_="提交了登录申请"
        logst(from_=from_,body=body_,level=1)
        username=form.username.data
        get_password=form.password.data
        cursor.execute("select password,if_admin,p2times from users where `userid`='%s'"%username)
        password=cursor.fetchall()
        if len(password) ==0:
            from_=request.headers["X-Real-Ip"]
            body_="提交了登录申请后由于错误的用户名而失败了"
            logst(from_=from_,body=body_,level=2)
            return render_template("dlbc3.html",ly="用户不存在",fangfas=["检查用户名"])
        else:
            password0=password[0]
            password1=password0[0]
            if_admin=password0[1]
            if password1==get_password:
                if if_admin==0 or not if_admin:
                    from_=request.headers["X-Real-Ip"]
                    session["to_up2"]=username
                    session["logged-in"]=True
                    session["admin"]=False
                    body_="登录成功"+username
                    logst(from_=from_,body=body_,level=1)
                    flash("Welcome back, %s"%username)
                else:
                    from_=request.headers["X-Real-Ip"]
                    body_="admin账号登录(请确认ip地址正确与否)"
                    logst(from_=from_,body=body_,level=3)
                    session["to_up2"]=username
                    session["logged-in"]=True
                    session["admin"]=True
                    flash("Welcome back, %s"%username)
                session["n"],session["w"],session["a"],session["t"]=username,password1,if_admin,password0[2]
                return redirect(url_for("index"))
            else:
                from_=request.headers["X-Real-Ip"]
                body_="提交了登录申请后由于密码错误而失败了"
                logst(from_=from_,body=body_,level=2)
                return render_template("dlbc3.html",ly="密码不正确",fangfas=["重新输入密码"])
    return render_template("signin.html",form=form)

登录界面.获取用户名和密码,然后从sql验证.
重复一遍,这是shit山代码.

#* 不展示: 转跳后登出
@app.route("/signout")
def signout():
    try:del session["logged-in"]
    except:del session["logged_in"]
    try:del session["admin"]
    except:pass
    try:del session["ifvip"]
    except:pass
    try:del session["n"]
    except:pass
    try:del session["w"]
    except:pass
    try:del session["d"]
    except:pass
    try:del session["e"]
    except:pass
    try:del session["c"]
    except:pass
    try:del session["v"]
    except:pass

    return redirect(url_for("index"))

前面说过,session是存储cookie的实例对象.直接按照字典的方法用就行,加入:session[新键名]=值,修改session[原键名]=新值,取值value=session[键名],判断是否存在键if 键名 in session.

#* 查看全部用户
@app.route("/usersall",methods=["GET","POST"])
def userall():
    if "logged-in" in session:
        cursor.execute("select userid,if_vip from users")
        users=cursor.fetchall()
        USER=[]
        for a_user in users:
            a=a_user[0]
            b=a
            if a_user[1] or a_user[1]==1:
                b=a+"(★★★vip★★★)"
            USER.append({"a":a,"b":b})
        return render_template("showallusers.html",all_user=USER)
    return redirect("/signin")

从数据库中取出全部用户名和对应是否vip,加入到列表后用render_template函数渲染html文件,在html中用{{ 变量名 }}来从Python获取值,例如这里就是{{ all_user }}.Jinja2提供了几个简单的循环和判断语法,我们下下篇讲.(例如{% for i in all_user %}等.)

#* 查看全部文章
@app.route("/articles",methods=["GET","POST"])
def articles():
    if "logged-in" in session:
        cursor.execute("select head from article")
        articles=cursor.fetchall()
        ARTIS1=[]
        for a_article in articles:
            ARTIS1.append(a_article[0])
        ARTIS2=[]
        for a_article in articles:
            ARTIS2.append(a_article[0].replace("_"," "))
        reponse=[]
        for i in range(0,len(ARTIS1)):
            a=ARTIS1[i]
            b=ARTIS2[i]
            reponse.append({"xhx":a,"ykg":b})
        return render_template("articles.html",ARTICLES=reponse)
    return redirect("/signin")

进一步加深对表单的理解.form表单可变性极高.这里没有表单但加了POST方法是为了给服务器减轻压力.

@app.route("/allfiles")
def allfiles():
    if "logged-in" in session:
        cursor.execute("select head,_time from files")
        answers=cursor.fetchall()
        HEAD=[]
        TIME=[]
        for answer in answers:
            HEAD.append(answer[0])
            TIME.append(answer[1])
        RESULT=[]
        for i in range(0,len(HEAD)):
            a=HEAD[i]
            b=TIME[i]
            RESULT.append({"a":a,"b":b})
        return render_template('files.html',all_file=RESULT)
    return redirect("/signin")

类似于前一段articles方法(准确来说是视图函数),从数据库取得数据后提交到jinja2遍历.

#* 创建一条内容
@app.route("/create",methods=["GET","POST"])
def create():
    form=article_input()
    if form.validate_on_submit():
        if "logged-in" in session:
            username=session["to_up2"]
            text_head=form.texthead.data
            text_inputd=form.body.data
            getname=sjn()
            with open("templates/articles/%s.txt"%getname,"w",encoding="utf8") as wr:
                wr.write("<h1>%s</h1>\n"%text_head)
                wr.write("<h5>作者: %s&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;创建时间:%s</h5>\n"%(username,get_time()))
                wr.write("<body>\n")
                wr.write(text_inputd)
                wr.write("</body>\n")
                wr.write("<a href=\"\"")
            cursor.execute("insert into article (head,body_place) values (%s,%s)",(text_head,getname))
            connect.commit()
            os.rename("templates/articles/%s.txt"%getname,"templates/articles/%s.html"%getname)
            flash("Article created successfully!")
            from_=request.headers["X-Real-Ip"]
            body_="上传了文章:%s"%text_head
            logst(from_=from_,body=body_,level=1)
            return redirect(url_for("index"))
        else:
            from_=request.headers["X-Real-Ip"]
            body_="可能的黑客攻击:在未登录状态下请求了上传文章"
            logst(from_=from_,body=body_,level=3)
            return redirect(url_for("signin"))
    return render_template("write.html",form=form)

这段是重难点.CKEditor还是挺难掌握的,我自创了这种从客户端获取数据直接存进了服务器数据表和对应html的方法(WCS, Wholly from Client to Server).好处是避免了在日后查看该文章时对内容的重复渲染,坏处是容易导致XSS攻击.我这里没有做好防护.在实际部署时应当加上.
CKEditor我不准备讲. 看这一篇就够了.

#* 查看某条内容
@app.route("/article")
def article():
    head=request.args.get('head','Admin_article_3')
    cursor.execute("select body_place from article where head=\"%s\""%head)
    answer=cursor.fetchall()
    if len(answer)==0:
        return redirect(url_for("articles"))
    result=answer[0]
    filepath=result[0]
    return render_template("articles/%s.html"%filepath)

因为在创建文章时就已经将数据存进了html文件,此时直接拿出来返回给客户端即可,不需要再重复渲染.很大程度上减少了服务器运载量.

app.config['CKEDITOR_ENABLE_CODESNIPPET'] = True

为了成功渲染CKEditor中的代码片,这里要配置一下.

#* 上传文件页面
@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = uploadfile()#file submit
    if form.validate_on_submit():
        if "logged-in" in session:
            f = form.file.data
            head = f.filename#! head:实际名称
            head = Markup.escape(head)
            type1= head.split(".")
            type_="."+type1[-1]#! filetype:类型
            f0=sjn()
            filename=f0+type_#! body_place:保存名
            author=session["to_up2"]#! author: 作者
            _time=get_time()#! _time: 时间
            f.save(os.path.abspath(os.path.dirname(__file__))+"\\files\others\\"+filename)
            cursor.execute("select body_place from files where head=\"%s\""%head)
            ans=cursor.fetchall()
            if len(ans)==0:
                cursor.execute("insert into files (head,filetype,body_place,author,_time) values    (\"%s\",\"%s\",\"%s\",\"%s\",\"%s\")"%(head,type_,f0,author,_time))
                connect.commit()
                flash('Upload success.')
                from_=request.headers["X-Real-Ip"]
                body_="上传了文件%s"%head
                logst(from_=from_,body=body_,level=1)
                return redirect(url_for('index'))
            else:
                from_=request.headers["X-Real-Ip"]
                body_="因重名而上传文件失败(名称%s)"%head
                logst(from_=from_,body=body_,level=2)
                return render_template("dupload.html",form=form)
        else:
            return redirect(url_for("signin"))
    return render_template('upload.html', form=form)

上传文件界面.这里还没有很完善,上传的文件直接摆在根目录下.上传文件的表单只是换个名字,然后用二进制写进服务器数据库就行.不难但烦.容易出错.

#* 下载文件页面(转跳下载 不显示)
@app.route("/download")
def download_file(ih=False):
    if not (ih):
        if "logged-in" in session:
            head=request.args.get("head")
            cursor.execute("select body_place,filetype from files where head=\"%s\""%head)
            body_place=cursor.fetchall()
            if len(body_place)==0:
                abort(1024)
            a=body_place[0]
            body_place=a[0]
            filetype__=a[1]
        else:
            return redirect("/signin")
    else:
        body_place="用户须知"
        filetype__=".txt"
    path="files\\others\\"+body_place+filetype__
    from_=request.headers["X-Real-Ip"]
    body_="下载了文件%s"%head
    logst(from_=from_,body=body_,level=1)
    return send_file(path,as_attachment=True)

send_file函数用于打开一个下载界面.使用相当简单,传入文件在服务器端的位置即可.
以上是网站主体代码(app.py)的第一部分.实现了上传下载文件、编写查看文章、登录登出和注册.第二部分将实现购买vip、admin页面和功能、工具箱功能、错误处理(如400,401,404).
下一篇将app.py第二部分.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值