如何用web.py写一个博客demo?

基础篇

本文使用python2 + windows10 的环境进行开发

(一)web.py的安装

如果你使用的python2, 直接运行下面的命令即可(版本为: 0.39)

pip install web.py

关于包的名称 web.py 竟然带了.py这个后缀一开始也让我小小的疑惑了一下,不过各位安装的时候还是完整填写名称的好

对于习惯用python3的朋友,请使用下面的命令安装web.py

pip install web.py==0.40-dev1

(二)第一个web.py程序

一个很宏大的目标,往往是从一个小的demo开始的,所以我先来学习web.py最基础的Hello World程序。

import web

urls = (
	'/', 'index'
)

class index:
	def GET(self):
		return 'Hello, World!'

if __name__ == "__main__":
	app = web.application(urls, globals())
	app.run()

保存上面的代码到脚本文件 demo.py, 然后运行

python demo.py

之后访问 http://0.0.0.0:8080 即可查看到我们的代码运行效果了

(三)web.py如何与数据库交互?

学习制作网站,了解web框架是如何与数据库交互的至关重要,至少需要学会使用他;web.py的官网文档中给出了两种主流数据库的交互参考,这里以postgresql为例,进行学习。

  1. 首先,你需要在自己的电脑上安装配置postgresql,而我直接在Linux环境下运行下面的命令安装(树莓派 + Raspbian)
sudo apt-get install postgresql

具体配置方式可以自行百度,网上有非常多的教程。

2. 其次,安装Python和Postgresql交互的中间件,这里采用官网给出的一种中间模块:psycopg2. 安装方式如下:

pip install psycopg2

3. 然后,使用数据库操作工具,创建一个名为 webpy的数据库,再创建表。表的创建SQL如下:

CREATE TABLE todo (
  id serial primary key,
  title text,
  created timestamp default now(),
  done boolean default 'f'    );

4. 然后使用模板,在脚本根目录下创建template 文件夹存放 HTML文档,我在里面创建了一个main.html文件,然后将下面的代码插入了进去:

$def with (todos)
<ul>
$for todo in todos:
    <li id="t$todo.id">$todo.title</li>
</ul>

这串HTML代码中,以 $符号起的行即为Python代码,这里的意思是如果todos存在, 就循环遍历todos,然后生成li标签,而这就是模板。若直接在Python代码中插入HTML不够美观也不符合软件设计原则,而在HTML中插入Python代码则相对来说更加美观和整洁,故如此处理。

5. 最后,数据库简单操作的完整代码如下:

# encoding: utf-8
import web


urls = (
	'/', 'index'
)

render = web.template.render('template/')
db = web.database(dbn='postgres', user="postgres", pw="123456", db="webpy", 
	host="192.168.3.20", port=5432)

class index:
	def GET(self):
		todos = db.select('todo') # 查询
		return render.main(todos)


if __name__ == "__main__":
	app = web.application(urls, globals())
	app.run()

额外增加一个写入数据库的例子:

# encoding: utf-8
import web


urls = (
	'/', 'index',
	'/add', 'add'
)

render = web.template.render('template/')
db = web.database(dbn='postgres', user="postgres", pw="123456", db="webpy", 
	host="192.168.3.20", port=5432)

class index:
	def GET(self):
		todos = db.select('todo') # 查询
		return render.main(todos)

class add:
	def POST(self):
		i = web.input() # input函数可以访问form提交的任何数据
		n = db.insert("todo", title=i.title)
		raise web.seeother('/') # 重定向到index页面

if __name__ == "__main__":
	app = web.application(urls, globals())
	app.run()

也就是在前一个demo的基础上,增加了一个add类和一个URL链接导航 /add 用来接收从前端传过来的参数。前端模板如下:

$def with (todos)
<ul>
	$for todo in todos:
	<li id="t$todo.id">$todo.title</li>
</ul>

<form method="post" action="add">
	<p><input type="text" name="title" /><input type="submit" value="Add"></p>
</form>

四、web.py如何返回Json数据

def POST():
    web.header('Content-Type', 'application/json')
    return json.dumps({"json": "yes"})

实战篇

(一)需求分析

在写代码之前,充分分析需求是避免重复劳动的重要步骤,由于我没有学过软件工程及设计模式,这里我想到什么写什么,设计模式在这篇文章写完后我就去学习。

现在可以基本理清思路。

  • 博客需要展示,因此有一个列表界面和一个完整显示文章的界面
  • 博客发布页面
  • 用户管理界面,包括对博客的增删改查
  • 管理登录界面

(二)数据库设计

按照我的理解,一般的内容型网站的编写,好的数据库逻辑关系,会大大影响代码操作的骚的程度。我没那么骚,就简单弄个,下面是创建表的SQL

CREATE TABLE blog (
	id serial primary key,
	title text,
	contents text,
	created timestamp default now(),
	updated timestamp default now(),
	author INTEGER
);

CREATE TABLE blog_users (
	id serial primary key,
	username VARCHAR(255),
	pwd VARCHAR(255)
)

手动插入用户

INSERT INTO blog_users (username, pwd) VALUES ('admin', 'admin');

(三)URL设计

其实我一开始是想学习一下Restful API的设计规则的,然而我真的很懒,随便设计一下吧,反正需求挺简单的

/        index
/index   index
/?p=1    分页
/?c=1    blog内容详情
/login   管理界面登录
/reset   退出登录
/post    发布、更新、删除链接
/admin   管理界面

代码如下

urls = (
	'/', 		'index',
	'/index',    'index',
	'/login',    'login',
	'/reset',	'reset',
	'/post', 	'post',
	'/admin',	'admin'
)

(四)实现

1. 登录(Login)功能实现

首先,既然是登录,就必须要考虑cookie的处理,不过还好,web.py已经提供了session模块

session = web.session.Session(app, web.session.DiskStore('sessions'), initializer={'login': 0})

第一个参数: app即我们初始化的应用。

第二个参数: DiskStore函数,用于在本地存储session文件;相对的还有DBStore函数,可在数据库存储session数据,我记得以前实习的公司就是用memcache在云端存储的session,以达到多服务器共享的session的目的。

第三个参数:initializer 是一个字典类型的参数,用来存放session中可能用到的数据,比如这里的login,就可以用来判断用户是否已经登录。比较抽象,但一读代码就能理解他究竟做了些什么。

登录的判断逻辑是我从官网的cookbook里面抄的,通过login为1或0来判断用户的登录状态,然后实现Login代码,代码如下

def logged():
	if session.login == 1:
		return True
	else:
		return False

class login:
	def GET(self):
		if logged():
			raise web.seeother("/")
		else:
			return render.login()

	def POST(self):
		username, password = web.input().username, web.input().password
		indent = db.select('blog_users', where='username=$username', vars=locals())[0]
		web.header('Content-Type', 'application/json')
		try:
			if password == indent.pwd:
				session.login = 1
				return json.dumps({'login':1, 'status': 'success'})
			else:
				session.login =0
				return json.dumps({'login':0, 'status': 'failed'})				
		except:
			session.login = 0
			return json.dumps({'login':0, 'status': 'failed'})

此代码仅用于学习,千万不要弄到生产环境咯,不然会有非常大的安全隐患,后面我还会专门探讨一下web安全的问题,最近刚好在作CTF题。

其次,登录的模板。我做了个非常简单的模板,途中发现web.py的文件里面没办法直接用jquery的语法,关键字冲突了,只能尝试用静态文件的办法去处理了,具体代码如下

Login.html

<form method="post" action="login">
	<input type="text" name="username" placeholder="username">
	<input type="password" name="password" placeholder="password">
		<!-- <input type="submit" name="login"></p> -->
	<input type="button" name="login" class="login" value="Login">
</form>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js">
</script>
<script type="text/javascript" src="http://127.0.0.1/static/login.js"></script>

login.js

$(".login").on("click", function(event){
	var data = {};
	$("input[name!=login]").each(function(i){
		data[$(this).attr('name')] = $(this).val();
	})
	$.post('/login', data, function(payload){
		console.log(payload);
		if (payload.login == 1){
			alert("登录成功");
			window.location.href = '/';
		}else{
			alert("登录失败,请重新登录");
		}
	})
})

由于不能直接在模板中写jquery代码,所以我配置了一下nginx服务器,提供静态文件访问,就成功绕过了直接在模板中写代码的尴尬。配置如下

        location /static {
            root K:/Git/web-demo;
        }

至此,Login的基础功能基本完成。

2. 管理功能(admin)实现

先修改login页面的跳转,让登录用户跳转到admin页面

class login:
	def GET(self):
		if logged():
			raise web.seeother("/admin")
		else:
			return "%s" % render.login()

然后开始编写admin这个类

class admin:
	def GET(self):
		if logged():
			entries = db.select("blog")
			return render.admin(entries)
		else:
			raise web.seeother("/login")

我让admin这个类,主要用来展示博客文章,并添加可以编辑的按钮;对于没有登录的用户则直接跳转到登录页面,下面是admin的模板

$def with (entries)
<ul>
	$for article in entries:
	<li id="$article.id">
		<h4><a href="/edit?p=$article.id">$article.title</a>
			<span class="btn" id="delete" onclick="dArticle($article.id)">删除</span></h4>
		<div>$article.created</div>
	</li>
</ul>

<div class="add" id="add" onclick="add()">添加博客</div>

<script type="text/javascript">
	function add(){
		window.location.href = "/edit";
	}

	function dArticle(dataid){
		var url = "/delete";
		var postStr = "p=" + dataid;
		var ajax = null;

		if (window.XMLHttpRequest){
			ajax = new XMLHttpRequest();
		} else if (window.ActiveXObject()){
			ajax = new ActiveXObject("Microsoft.XMLHTTP");
		} else {
			return ;
		}

		ajax.open("POST", url, true);
		ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
		ajax.send(postStr);

		ajax.onreadystatechange = function(){
			if (ajax.readyState == 4 && ajax.status == 200){
				location.reload(); // 刷新当前页面
			}
		}
	}
</script>

通过js发起post请求的方法是我刚从 《白帽子说web安全》里面学来的,所以这里我偷懒直接写了js,没有新增文件去使用jquery的代码写post请求。多读书还是有好处的

如果你仔细看了的话,你可能会发现我在这儿使用了一个 edit和一个delete类, 这个类主要是为了处理博客的增删改添加得类,于是我得重新注册一遍url

'/edit', 'edit',
'/delete', 'delete'

3、添加、更新文章功能

edit这个类有两个功能,其一是添加和更新博客,其二是返回编辑的前端页面,下面是我的代码:

class edit:
	def GET(self):
		if logged():
			i = web.input(p=None)
			if i.p:
				print(i)
				entries = db.select("blog", i, where="id=$p")[0]
				return render.edit(entries)
			else:
				return render.edit(entries=None)
		else:
			raise web.seeother("/login")

	def POST(self):
		if logged():
			i = web.input(p=None, title=None, content=None)
			web.header('Content-Type', 'application/json')
			if not i.p:
				title = i.title if i.title != None else " "
				contents = i.contents if i.contents != None else " "
				db.insert("blog", title=title, contents=contents, author=1);
				return json.dumps({'success':1, "message": "add blog success"})
			elif i.p:
				title = i.title if i.title != None else " "
				contents = i.contents if i.contents != None else " "
				db.update("blog", title=title, where="id=" + i.p, contents=contents, author=1, updated="now()");
				return json.dumps({'success':1, "message": "add blog success"})	
		else:
			return "Please Login."

可以看到,POST函数处理post请求,通过logged() 判断登录情况,通过web.input()获取请求参数,然后分类处理添加文章和更新文章;在处理GET时,通过判断有没有标识 文章的 id存在,来安排编辑的前端页面,对照着前端页面看更容易理解。下面是我的前端代码

$def with (entries)
<!DOCTYPE html>
<html>
<head>
	<title> 博客编辑 - demo</title>
</head>
<body>
	<div id="add">
		<h2>博客编辑</h2>
		<div class="bar">
			<div class="move-back btn" id="move-back">返回</div>
			<div class="save btn" id="save">
				保存
			</div>
		</div>
		<div class="title">
			$if entries:
				<input type="text" name="title" value="$entries.title">
			$else:
				<input type="text" name="title" />
		</div>
			<div class="content">
				<textarea id="content" rows="3" cols="20" value=""></textarea>
		</div>
		$if entries:
			<div style="display: none;" id="blog-id" data-id="$entries.id"></div>
	</div>
</body>
</html>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript" src="http://127.0.0.1/static/edit.js"></script>
<script type="text/javascript">
	$if entries:
		addContent("$entries.contents");	
</script>

果然前端代码都是超级多的,后面我就直接贴Github得链接了。不过这儿,edit.js我还是贴出来

document.getElementById("move-back").addEventListener("click", function(){
	window.location.href = "/admin";
})

$("#save").on("click", function(event){
	var title = $("input[name=title]").val();
	var content = $("#content").val();
	var blog_id = $("#blog-id");
	var data = {};
	if (blog_id != null){
		data.p = blog_id.attr('data-id');
	}
	data.title = title;
	data.contents = content;

	$.post("/edit", data, function(payload){
		if (payload.success == 1){
			window.location.href = "/admin";
		} else {
			alert("添加失败,请检查网络");
		}
	})
})


function addContent(data){
	$("#content").val(data);
}

4、 博客删除功能

删除我又单独写了一个类,哈哈哈哈,感觉写得好乱

delete类的代码

class delete:
	def POST(self):
		if logged():
			web.header('Content-Type', 'application/json')
			i = web.input(p=None)
			if i.p:
				db.delete("blog", where="id="+i.p)
				return json.dumps({"success": 1,"message": "删除成功"})
			return json.dumps({"success": 0, "message": "删除失败"})

和edit类的代码基本一致

到此为止,只剩下一个首页还没写好了。

5、首页实现

首页无非是直接显示内容,当然可能需要做个分页,不着急,我慢慢写。

首页的前端后端代码很简单,查询数据库,拿出数据即可

class index:
	def GET(self):
		entries = db.select('blog') # 查询
		return render.index(entries)

前端代码直接放在li列表里面吧

	<div class="content">
		<ul>
			$if entries:
				$for arti in entries:
					<li>
						<div><a href="/?p=$arti.id">$arti.title</a></div>
					</li>
		</ul>
	</div>

遍历所有文章,生成列表。

然后就是,文章详情页面了,我给他取名叫single

class index:
	def GET(self):
		i = web.input(p=None)

		if not i.p:
			entries = db.select('blog') # 查询
			return render.index(entries)
		elif i.p:
			entries = db.select("blog", where="id=" + i.p)[0]
			return render.single(entries)

修改index页面,让带参数p的请求转到详情页面

然后,详情页面改成这样

<div class="main-content">
    $if entries:
        <div class="title" id="title">$entries.title</div>
        <div class="datetime" id="datetime">$entries.created</div>
        <div class="data">
            $entries.contents
        </div>
</div>

OK,大功告成。这个demo基本就完成了。

四、优化工作

优化工作再慢慢写吧


完整项目代码: Github


# 2019年7月6日19点47分

经过两天的努力,终于把这个demo的admin基本写完了。然后就是慢慢写博客了,最近在浏览别人的博客的时候,忽然发现有非常多的优秀的博客,当然大多是一些技术博客,毕竟我学识有限,所知不多,但是这给了我一个很好的启发,有没有办法更好的整理这些博客的内容呢?要不要开发一个整理的网站试试?

然后挑选一些大佬的博客放在上面,并归类整理一些近期的文章出来。

废话到这儿,继续写博客


暂无评论

发表评论

您的电子邮件地址不会被公开,必填项已用*标注。

相关推荐