公网版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.wsgiconfig.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

实现步骤如下:

  • 先找个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就行

results matching ""

    No results matching ""