我最近并没有更新系列《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的应该cursor
和connect
懂的都得好吧.
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 创建时间:%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第二部分.