网页版日记系统

在3wd4公开课之前看了一点bottle官网上的介绍,然后芝麻星放出4W任务时就在琢磨要怎么使用bottle框架,当时不清楚的两个点是:

  • 如何将数据传入bottle框架的server
  • 如何同时兼容命令行界面交互

然后在公开课上大妈对我这两个问题对给出了解决的提示

  • 用post
  • python调用curl

知道了这两点,我就可以着手进行开发,来完成这周的基本任务。

本周遵循的原则

  • 用最小代价完成基本任务,之后进行联系和迭代
  • 教程写给6个月前的自己

任务要求

通过网页访问系统: 每次运行时合理的打印出过往的所有笔记 一次接收输入一行笔记 在服务端保存为文件 同时兼容 3w 的 Net 版本的命令行界面进行交互

背景信息

  • 系统:OS X 10.11
  • python:Python 2.7.10
  • 编辑器:Sublime Text 3
  • 终端:iterm2
  • shell:zsh
  • 浏览器:safari

下载bottle.py

官方文档给出的命令是

$ wget http://bottlepy.org/bottle.py

想试试用curl,结果$ curl http://bottlepy.org/bottle.py之后发现返回状态是302,然后curl重定向的资源返回的又是302,这样重复几次之后总算找到了文档储存的地方。

$ curl -o bottle.py https://raw.githubusercontent.com/bottlepy/bottle/master/bottle.py

之后再试了wget之后发现它会去挖重定向的资源,直到下到需要的文档。想用curl达到同样的效果需要用-L参数。

$ curl -L http://bottlepy.org/bottle.py -o bottle.py

在bottle框架下使用post

官方文档给出的代码是

from bottle import route, request

@route('/login')
def login():
    return '''
        <form action="/login" method="post">
            Username: <input name="username" type="text" />
            Password: <input name="password" type="password" />
            <input value="Login" type="submit" />
        </form>
    '''

@route('/login', method='POST')
def do_login():
    username = request.forms.get('username')
    password = request.forms.get('password')
    if check_login(username, password):
        return "<p>Your login information was correct.</p>"
    else:
        return "<p>Login failed.</p>"

可以看出

  • route里加(method='POST')来跟同样路径的get行为进行区分
  • bottle.request.forms.get(<nametag>)来获取post的数据

模板

模板的后缀名是.tpl,然后模板里变量的写法是加两层花括号

@route('/hello')
@route('/hello/<name>')
def hello(name='World'):
    return template('hello_template', name=name)

livereload

能获取代码的及时反馈可以大大增加写代码的效率

使用bottle进行livereload只需

from bottle import debug, run
debug(True)
run(..., reloader=True)

server端思路

  • 读取post的信息,写入log文档里
  • 页面下方加入<textarea>区域
    • 读取log文档,将日记信息在<textarea>区域里展示

server端的代码写的很顺利, 网页上测试也顺利。

client端思路

接着考虑兼容命令行交互。

首先考虑用subprocess.call调用curl --data命令进行post

写完测试发现,虽然post成功了,但会输出curl抓取到的网页信息,需要一个办法来取消或隐藏curl的输出信息

隐藏curl的输出

网上搜索之后找到了一个帖子:Hide curl output

从里面学到两点

  • 通过将stdout输出到/dev/null,来隐藏输出
  • 通过使用curl-s--silent参数来取消其他输出

将curl的输出返回给python

接着考虑怎么在client端读取日记信息,还是尝试通过调用curl命令来触发网页操作,但这时碰到一个问题,怎么返回而不是打印subprocess.call调用curl得到的信息。

网上找的一篇问答:

Retrieving the output of subprocess.call()

从里面了解到使用subprocess.check_out命令可以将命令得到的输出作为返回值,那么问题就很好解决了。

@route('/read_raw')

后来又碰到一个问题是通过curl返回的信息含有html信息,这是在命令行读取日记信息所不需要的。

想了一个讨巧的办法,在server端再开一个页面'/read_raw',专门读取不经html渲染的log文档数据。

version1.0 代码

server端

# coding=utf-8
from bottle import route, run, request, debug, template
from alan_diary import append_text, get_text

@route('/write')
def write():
    mydiary = get_text() # 获取日记数据
    return template('write', diary=mydiary)

@route('/write', method='POST')
def do_write():
    input_md = request.forms.get('input_md') #抓取form里提交的数据
    append_text(input_md) #将数据写入log文件
    mydiary = get_text()
    return template('write', diary=mydiary)

# 为client.py提供读取log文件的接口
@route('/read_raw')
def read_raw():
    f = open('alan_diary.log')
    content = f.read()
    f.close()
    return content  

if __name__ == '__main__':
    debug(True)
    run(host='localhost' ,port=4000, reloader=True) #增加live_reload

client端

# coding=utf-8
import subprocess

# 帮助信息
HELP = '''\
输入 ?/h/H 打印当前帮助
输入 q/quit/exit 退出日记本
输入 r/read 读取所有日记内容
'''

# 将用户输入数据传进server
def postdata(data):
    # curl localhost:4000/write --silent --data input_md=hello > /dev/null
    command = ['curl','localhost:4000/write', '--silent', '--data','input_md={}'.format(data), '>', '/dev/null']
    subprocess.call('curl localhost:4000/write --silent --data input_md="{}" > /dev/null'.format(data), shell=True)

# 从服务器读取日记数据
def readdiary():
    return subprocess.check_output('curl -s localhost:4000/read_raw', shell=True)

if __name__ == '__main__':
    print HELP
    while True:
        data = raw_input("该写点啥呢\n> ")
        if data in ['h', 'H', '?']:
            print HELP
        elif data in ['q', 'quit', 'exit']:
            print "bye, 亲~"
            break
        elif data in ['r', 'read']:
            print readdiary()
        else:
            postdata(data)

结合1w的文档读写alan_diary.py

# coding=utf-8
import os
import time


def append_text(text):
    f = open('alan_diary.log', 'a')
    message = '{}: {}\n'.format(time.strftime("%Y-%m-%d %H:%M:%S"), text)
    f.write(message)
    f.close()


def get_text():
    if not os.path.exists('alan_diary.log'):
        f = open('alan_diary.log', 'a')
        f.close()
    else:
        f = open('alan_diary.log')
        text = f.read()
        f.close()
        return text

if __name__ == '__main__':
    while True:
        text_input = raw_input('What do you want to write today:\n> ')
        if text_input.lower() in ['quit', 'q', 'exit']:
            break
        append_text(text_input)
        print("you diary:\n", get_text())

使用截屏:

  • 网页交互

snapshot1

  • 命令行交互

snapshot2


看了大妈发的福利

几点收获

  • route声明可以叠加
@route('/index.html')
@route('/index.htm')
@route('/')
def index():
    # ...
  • bottle.py中的TEMPLATE_PATH记录了程序会在哪些目录里搜索模板文件,它一般是././views/,也即是说可以把模板文件放在这这些目录里。模板文件后缀名为tpl


之后开始学习使用Berkeley DB,但碰到了许多坑

  • BDB在6.0之后的版本改变了版权许可
  • python2.6版本之后,bsddb模板不再被支持

虽然可以改为使用BDB5.3和bsddb3。但这样用得太闹心,决定及时止损,考虑其他的数据库。

考虑到python自带sqlite3库,决定还是使用sqlite数据库

数据库学习资源:

bottle模板里避免给变量增加逃逸符

在tpl模板文件里,一般用两个花括号代表要替代的信息,例如{{content}},运行程序后会发现原信息中的特殊符号被替代成html中的格式。

如果要避免逃逸特殊符号,可以在花括号里加感叹号,如{{! content}}

bottle模板里的python代码使用

在bottle的模板文件里,以%开头的行和被<%%>包围的部分会被解释为代码。那么可以将原本在server.py里运行的一些代码转到模板文件里。

%import time
%for row in diary:
    <div class=post>
      <em class=date>{{time.ctime(row[0])}}</em><br>
      {{!row[1].replace('\n','<br/>')}}
    </div>
%end

上面是我在模板里加进的一段代码,用于展示日记的历史信息:

  • diary是server.py中从数据库读取出来的历史记录
    • 它是个列表
    • 列表中的内容为日记条目元组
    • 元组第一个内容是时间
    • 第二个内容是记录的内容
  • 因为数据库中的时间是用秒数来储存的,所以用time.ctime来转换成文本
  • 将日记内容中的换行符\n转换为html标签<br/>,以便在网页中显示换行。

按日期逆序展示日记信息

使用久了这个日记系统,会发现更合理的展示方法是最近加入的信息最先展示。于是我在提取信息执行的sql语言里加了order by time desc来根据时间逆序排序结果。

然后在阅读其他同学代码时发现他们用其他巧妙的方法解决了这个问题:

  • @liangpeili同学通过编写reverse()函数将log文档逆序转换为新文档
  • @zoejane同学在每次写入新信息前用f.seek(0,0)将光标提到文档最前端,也即将新信息插入到文档最前面。

迭代清单:

  • [X] 学习数据库,用数据库代替log文档
  • [X] 用requests库改写client.py
  • [ ] 添加首页index.html
  • [ ] 学习bootstrap,添加css样式

results matching ""

    No results matching ""