公网版Alan's Diary
任务要求
- 将上周应用网站发布为公网稳定服务
- 可以通过固定域名访问系统:
- 每次运行时合理的打印出过往的所有笔记
- 一次接收输入一行笔记
- 在服务端保存为文件
- 同时兼容 3w 的 Net 版本的命令行界面进行交互
- 可以通过本地命令行工具监察/管理网站:
- 获得当前笔记数量/访问数量等等基础数据
- 可以获得所有笔记备份的归档下载
任务分解
- [X] Task1: 部署本地sae环境
- [X] Task2: 用kvdb改写4w代码
- [X] Task3: 为代码添加按日期逆序排列功能
- [X] Task4: 实现
标签
功能 - [X] Task5: 添加
delete
功能 - [X] Task6: 添加访问量记录
- [X] Task7: 将应用部署到新浪云上
- [X] Task8: 改写client代码
部署本地sae-python开发环境
参考链接: 本地开发环境
安装
首先安装开发环境
可以选择用pip安装
$ pip install sae-python-dev
或者从github下载源码安装
$ git clone https://github.com/sinacloud/sae-python-dev-guide.git
$ cd sae-python-dev-guide/dev_server
$ python setup.py install
使用
项目里需要建立index.wsgi
和config.yaml
两个文件
以sae官方的bottle代码为例
index.wsgi
from bottle import Bottle, run
import sae
app = Bottle()
@app.route('/')
def hello():
return "Hello, world! - Bottle"
application = sae.create_wsgi_app(app)
config.yaml里写入个人信息,例如
name: alan
version: 1
之后在命令行运行
$ dev_server.py
就可在http://localhost:8080访问自己的应用。它会感知到index.wsgi的变化并重启服务,但它不会对模板文件的修改做出反应。
ps: 本地运行需预先装好bottle,或将bottle.py放入项目目录里。
使用KVDB
KVDB默认数据存在内存中,dev_server.py进程结束时,数据会全部丢失,如果需要保存数据, 要加'--kvdb-file'参数,后面跟你想将数据保存的位置。
$ dev_server.py --kvdb-file=kvdb.db
用KVDB改写4w代码
对数据形式的初步设计是
key
保存成字符串'key'加编号value
保存成字典{'time':..., 'content':...}
import sae.kvdb
count = 0 # 设置计数器
kv = sae.kvdb.Client()
# 将数据存进数据库
def insert_into_db(post):
global count
count += 1
ctime = time.ctime()
key = 'key' + str(count)
value = {'time':ctime,'content':post}
kv.set(key,value)
# 读取数据库中的日记信息
def get_all_data():
results = []
for item in kv.get_by_prefix('key'):
results.append(item[1])
return results
然后在write.tpl
模板文件里也加入了代码
%import time
%for row in diary:
<div class=post>
<em class=date>{{row['time']}}</em><br>
{{!row['content'].replace('\n','<br/>')}}
</div>
%end
全部代码
按提交时间逆序排列
字典是无序的,而我想让显示结果按提交时间逆序排列。
将get_all_data()函数进行更改
def get_all_data():
tmp = [item[1] for item in list(kv.get_by_prefix('key'))]
results = sorted(tmp, key = lambda x:x['time'], reverse=True)
return results
实现标签
功能
首先在模板里为tag添加一个输入框
<div>
TAG
<input name='tags'></input>
</div>
然后增加一个tag的显示区域
<em class=tags>tags: {{' '.join(row['tags'])}}</em><br>
之后再用正则将post的tags信息提取成一个标签列表,并转为小写。
import re
tags = re.findall(r"[\w']+", request.forms.get('tags'))
tags = [tag.lower() for tag in tags] # 改成小写字母
之后考虑在KVDB储存的值里加tag,然后再额外储存tag->key的键值对
修改insert_into_db()函数为
def insert_into_db(post, tags):
global count
count += 1
ctime = time.ctime()
key = 'key' + str(count)
value = {'time':ctime,'content':post,'tags':tags}
kv.set(key,value)
# 设置tag->key反向链接的键值对
if not tags:
for tag in tags:
if kv.get('tag'):
kv.update(tag, kv.get('tag').append(key))
else:
kv.add(tag, [key])
之后在route里添加一个/tags/<tag>
,用于展示tag对应的日记记录
@app.route('/tags/<tag>')
def read(tag):
mydiary = get_tag_data(tag)
if mydiary:
return template('write', diary=mydiary)
else:
return 'Tag not found'
在完成一个对应的函数get_tag_data(tag)
# 读取对应tag的信息
def get_tag_data(tag):
if kv.get(tag):
keys = kv.get(tag)
tmp = kv.get_multi(keys)
results = [tmp[key] for key in sorted(tmp.keys(), reverse=True)]
return results
else:
return []
之前是在tag页面也沿用主页面的模板,但想想,还是单独写个tag.tpl模板,里面去掉输入框,加上一个返回主页面的链接。
然后想到,那些没有标签的该怎么展示呢,要不把没有标签的都打上'NULL'标签吧。
tags = tags if tags else ['NULL']
添加delete功能
这个感觉比较好做到,可以用kv.getkeys_by_prefix('')来获取所有的key,然后用kv.delete()来删除。
@app.route('/', method='DELETE')
def do_delete():
keys = kv.getkeys_by_prefix('')
for key in keys:
kv.delete(key)
return "All data deleted!<br><a href='/'>返回<a>"
添加访问量记录
首先设置全局变量traffic,然后在app.route('/')下加增量
global traffic
traffic += 1
之后再在模板最底部加入一句本站共被访问{{traffic}}次
部署应用
在新浪云创建应用之后在应用->代码管理
下可以选择用git或svn管理代码,因为没用过svn,所以我选择git。
之后在本地的项目目录里输入
git remote add sae https://git.sinacloud.com/alandiary
最后的alandiary
是我设置的app名称。
在项目目录里
$ git init
$ git add .
$ git commit -m "first version"
$ git remote add sae https://git.sinacloud.com/alandiary
$ git push sae master:1
除此之外,sae官方文档上也介绍了用命令行工具saecloud进行部署。使用saecloud进行部署的话会根据config.yaml里的版本号部署到相应的版本里。
尝试部署上线之后,发现动态路径/tags/<tag>
报错,因为之前看大妈写的to-do应用时发现动态路径的写法不一样,查了之后发现是bottle近期版本更改动态路径的写法,所以这次报错我猜测是sae用的bottle版本的问题。
索性将bottle.py
源文件也一起上传了,一试发现奏效。
改写cli端代码
有了上述在服务器端的一系列修改,cli端的代码就容易写了,无非是用requests库调用不同http方法及不同url而已。
为了解析html方便,给模板文件的日记内容添加<p class=content></p>
标签。
修复访问数错误的bug
bamboom同学提醒我程序有一些bug
- 多输入几条日记,前面的就会不见
- 页面访问计数不对,刚刚还是15的,又刷新了一下变成了8
在本地测试时并没有出现这种情况,分析了一下,觉得应该是
- sae在后台同时运行了app的多个副本,根据网站负荷让用户使用不同的副本
- 在我app里,存储键的id储存在程序内存里,多个副本同时调用会出现覆盖先前日记的bug
- 访问数信息是记录在程序的全局变量里,多个app同时调用时也会出现bug
解决办法:将id和访问数记录在KVDB数据库里
本地测试成功,但部署到新浪云上就出bug了,id和访问数都停在0上,也是醉了...
猜测KVDB的值是一个字典时,但app改变这个字典的值之后并不会被保存到KVDB里,于是稍微换了个写法,部署之后运行成功。
版本控制
在用git push时可以选择推到哪一个版本
例如
$ git push sae master:2
就是推到版本2,对应的网址是http://2.alandiary.sinaapp.com
然后在sae的控制台里能选择默认版本,也即是让http://alandiary.sinaapp.com 使用哪个版本的代码。
删除版本可以用如下命令
$ git push sae :3
这样就把版本3给删除了,要注意的是默认版本无法被这样删除。
这样在开发时可以将代码推到新版本上,而让网站运行稳定的老版本,然后等开发完全之后再将默认版本设置成新版本。
ps: 在翻看sae邮件列表时挖到了大妈以前写的一篇42分钟乱入 SAE 手册
增加tag对中文的支持
之前是用python正则表达式里的"\w+"来对tag进行切分,这个正则模式匹配的是所有字母和数字,但这个模式无法匹配中文。
于是换一种方法,考虑到一般用户是用逗号,
和空格来区分tag,于是先用replace(',',' ')将逗号换成空格,然后在用split()进行分割。
tags = request.forms.get('tags').replace(',',' ').split()
后来觉得中文标点的逗号也应该替换掉
tags = request.forms.get('tags').replace(',',' ').replace(',',' ').split()
或者也可以用正则库re里的split函数
import re
tags = re.split('[ ,,]+', request.forms.get('tags'))
增加日记数数据
日记的数量可以用len(kv.getkeys_by_prefix('key'))
来计算。
接下来只需要在模板里加个位置,然后把参数传进去就行。
但是上传到sae后竟然app都运行不了了,显示Error: 500 Internal Server Error
,明明在本地运行得挺好的。
另外开一个route@app.route('/debug')
,然后在里面显示type(kv.getkeys_by_prefix('key')),发现结果居然是generator
,而在本地调试的结果是list
,也是醉...
知道问题之后就好解决了,改为len(list(kv.getkeys_by_prefix('key')))
就行了。
后来学会了查看服务器日志
在控制台->运维->日志中心
里,日志类型选择错误日志
,然后可以看到如下错误信息
File "/data1/www/htdocs/488/alandiary/2/bottle.py", line 935, in _inner_handle
return route.call(**args)
File "/data1/www/htdocs/488/alandiary/2/bottle.py", line 1888, in wrapper
rv = callback(*a, **ka)
File "/data1/www/htdocs/488/alandiary/2/index.wsgi", line 69, in write
note = len(kv.getkeys_by_prefix('key'))
TypeError: object of type 'generator' has no len() yq26
从错误日志
里可以更方便地找到问题的原因。
为网站添加favicon
favicon
就是打开网页时显示的网页图标,显示效果如下:
实现步骤如下:
- 先找个favicon generator的网站,如这个,将图像文件转换成favicon.ico
- 然后将favicon.ico上传到sae应用的根目录
数据备份与恢复
在sae控制台中找到KVDB,会发现里面没提供任何服务,既不能备份,也不能查看。
好在新浪还有一个Storage的服务,可以把它理解为一个网盘,用于存储文件。
那么我们可以将KVDB里的数据存成json文件放到Storage里,具体代码如下
import json
from sae.storage import Bucket
bucket = Bucket('t')
bucket.put()
myData = dict(kv.get_by_prefix(''))
myDataJson = json.dumps(myData)
bucket.put_object('mydata.json', myDataJson)
大致的逻辑是:
- 用
kv.get_by_prefix('')
提取KVDB里的所有内容 - 上面函数的返回值是
generator
,用dict()
函数进行转换 - 用
json.dumps
进一步将上述值转成json格式 - 用bucket.put_object()函数将json数据储存到名为
mydata.json
的文件里
此外
- bucket可以看做是Storage里的一个存储空间
- sae.storage.Bucket()建立一个bucket实例,里面的参数(如't')表示这个bucket对应的domain,可以把这理解为对应
/t
这个子目录 - bucket.put()创建该bucket
- 更多内容详见官方提供的使用示例
然后我先将上面的代码加到@app.route('/')
对应函数的后面进行测试,也就是说每次打开主页,数据就会备份一次。
本地测试
本地测试的命令是
$ dev_server.py --storage-path=data --kvdb-file=kvdb.db
其中
--kvdb-file
表示本地模拟时kvdb数据存储在哪个文件里--storage-path
表示本地模拟时bucket储存的位置,需要事先建立好这个目录
$ curl http://localhost:8080
之后,会发现备份的数据就在./data/t/mydata.json
里
上线测试
将app重新部署上线,访问主页之后,会在控制台->存储与CDN服务->Storage
里看到新添了一个名为t
的domain,在domain管理里可以进一步看到里面有名为mydata.json
的文件,说明数据备份成功。
公共链接
Storage里面的资源都有相对应的链接,在domain管理里将domain的访问权限设置为public之后,其他人就可以通过链接下载到Storage里的数据。
可以在代码添加bucket.generate_url('mydata.json')
函数,它的返回值是'mydata.json'对应的链接地址。
本地测试得到的是:http://localhost:8080/stor-stub/t/mydata.json
上线测试得到的是:http://alandiary-t.stor.sinaapp.com/mydata.json
这个格式其实就是 http://<myappname>-<mybucketname>.stor.sinaapp.com/path/to/my/<filename>
数据恢复
在index.wsgi里添加下面的代码
@app.route('/recover')
def recover():
if list(kv.get_by_prefix('')):
return 'Data Already Exists'
else:
import json
from sae.storage import Bucket
bucket = Bucket('t')
mydata_json = bucket.get_object_contents('mydata.json')
mydata = json.loads(mydata_json)
for key,value in mydata.items():
kv.set(str(key),value)
return 'Data Recovered'
代码的意思是
- 打开
/recover
路径就触发数据恢复功能 - 如果KVDB里存有数据就不进行恢复
- 如果没有,就将buckt里的'mydata.json'数据恢复进去
- kv.set()时因为要求键是字符串,所以用str()进行转换
上述测试时,我对数据备份和恢复的使用逻辑是
- 用户每次访问主页就进行一次备份
- 因为现在网站访问量小,这样做也没太大问题,但访问量大的话这样就会消耗太多流量
- 更好一点的方法是每次用户post了新数据就进行一次数据备份
- 或者选用其他页面路径触发备份
- 或者也可以在程序里添一段代码,实现定时备份
- 用户访问'/recover'路径就进行数据恢复
- 如何实现恢复到不同版本的数据呢
进一步优化
sae提供了一个名为Cron的服务,可以用来定时触发特定动作。那么我们可以利用Cron来每天触发一次数据备份活动。
首先在config.yaml
里添加cron服务
name: alan
version: 1
cron:
- description: data_backup
url: /data/backup
schedule: "5 0 * * *"
意思是在每天的00:05执行http://alandiary.sinaapp.com/data/backup
一次。
关于schedule对应的语法意义是
* * * * *
- - - - -
| | | | |
| | | | +----- day of week (0 - 6) (Sunday=0)
| | | +------- month (1 - 12)
| | +--------- day of month (1 - 31)
| +----------- hour (0 - 23)
+------------- min (0 - 59)
然后将数据备份的代码稍加修改,将每天备份的数据存到不同的文件里,这里我是存到mydata_年_月_日.json
里
@app.route('/data/backup')
def backup():
import time
import json
from sae.storage import Bucket
date = time.strftime("%Y_%m_%d")
filename = 'mydata_' + date + '.json'
bucket = Bucket('t')
bucket.put()
myData = dict(kv.get_by_prefix(''))
myDataJson = json.dumps(myData)
bucket.put_object(filename, myDataJson)
return 'data-backup done'
相应的数据恢复也可以进行修改,让用户可以选择想要恢复哪一天的数据。
# 数据恢复,恢复'年_月_日'的数据
@app.route('/data/recover/<date>')
def recover(date):
if list(kv.get_by_prefix('key')):
return 'Data Already Exists'
else:
import json
from sae.storage import Bucket
bucket = Bucket('t')
filename = 'mydata_'+date+'.json'
if filename in [i['name'] for i in bucket.list()]:
mydata_json = bucket.get_object_contents(filename)
mydata = json.loads(mydata_json)
for key,value in mydata.items():
kv.set(str(key),value)
return 'Data Recovered for {}'.format(date)
else:
return "backup info not found"
比如数据崩溃以后,想恢复到2015年11月15日的数据,打开http://alandiary.sinaapp.com/data/recover/2015_11_15
就行