2005年06月26日

因为学习 Ajax, 需要使用web server,因此就使用起Karrigell。不是为它做广告,的确是非常方便的一个web framework。做些东西太方便了,就象随手写一个小的 Python 程序一样,非常容易地可以体验web开发。我这个有一个毛病,觉得什么东西好就喜欢向别人推销,就象我向我的同事推销 Python 一但,不过他们是坚决的反对,没有一个心动。不过我也无所谓,你不学我照样学,在网上不是还有许多的同好可以交流嘛。我想他们不想学也是有理由的,编程是件痛苦的事情,如果你无法投入,你基本上就编不了程序了。许多人架构理论一堆,让他编个程序实现一下子就不行了。但一个精通编程的人却依然可以提出架构理论来。只不过现在这个世道,如果你是一个程序员,那好,我提出架构你去实现就行了。至于你提出的什么架构,对不起,我可没功夫与你交流,因此你也别向我提。当然事情未必就是这么极端,绝大多数情况是,做架构理论的人基本上不会征求程序员对架构的看法。我感觉是这样的,因此上别人不找我我也绝对不发表什么意见。真是有些道不同不相为谋。我怎么就这样了呢?是被谁逼的呢?

呵呵,以上的话是从做广告想到的,无聊的东西。

那么关于 Karrigell 有什么事情我想说得呢。一个就是关于 Karrigell 与 Apache的结合的问题。在我以前的Blog中曾经写到 Karrigell 可以通过mod_python与Apache紧密结合,但后来有人说文档中根本没看到。于是我仔细查了不同版本的 Karrigell 的文档,然后又在它的maillist中提问最后明白是这样的:

在2.1.2的文档还是通过mod_python与Apache相结合,但在最新的2.1.5版本已经变成了使用反向代理的方法,与CherryPy, Snakelets所使用的方法一样。为什么会这样,这是作者(Pierre Quentel)的回答:

 There was a problem with mod_python. It worked fine on Windows, but on
Linux, Apache works on a multi-process mode, creating different
instances of the Python interpreter. For the scripts that use sessions,
since Karrigell stores them in memory, if a request is handled in a
different process than the previous ones by the same user-agent, the
session object can’t be found

I tried to fix that with persistent sessions but couldn’t come to a good
solution. So I finally decided to drop the mod_python integration and
recommand to configure Apache as a proxy to the built-in server in
Karrigell. This is the approach of many other Python web frameworks
(Snakelets and WebWare for instance)

If you have a Windows machine you can still use mod_python as described
in version 2.1.2, with ApacheHandler

可以看到是由于Apache多进程的问题造成的,由于 Karrigell 的session的管理是在内存中,在多进程的情况下不好处理,他正在解决这个问题。因此目前建议是使用反向代理。但在windows下还是可以继续按2.1.2中的说明进行配置,使用mod_python。我试了的确也是可以的(因为我就是在windows下试的),不过好象有问题。不过关于这个问题的答案已经知道了,下面要看作者是如可解决它。

另一件关于 Karrigell 的事就是作者开始提议也开发一个Blog系统,我是很支持的。我想开发Blog系统并不是为了跟风,而是增加在 Karrigell 上的应用,同时可以很好的宣传 Karrigell,如果在这个过程中又可以学到东西不是非常好的一件事情吗。我要继续关注此事。也希望有兴趣地朋友也一起来关注此事。

2005年06月22日

这是我在 Karrigell 邮件列表看到的:

 I often google to search mentions of Karrigell and I recently found that
is is used as an example of web framework in a new book published by
O’Reilly in French : Apprendre à programmer avec Python (Learning to
program with Python)

See http://www.ulg.ac.be/cifen/inforef/swi/python.htm

The book explains how to install the software, launch the server, write
static files, then it gives examples of programs in Python, using
session management, etc. By permission of O’Reilly, the book can be
dowloaded at
http://www.ulg.ac.be/cifen/inforef/swi/download/python_notes.pdf

Champagne !
Pierre

看到O’Reilly的书提到了Karrigell感到很高兴。尽管下载了pdf文件,但因为是法语根本看不懂。希望Karrigell越来越好。

2005年06月21日

在一般的web server中,对于请求和返回的文件名,会根据它的文件名后缀猜测它对应的mimetype类型。那么在 Python 中提供了mimetypes 这个模块,可以做这件事。在导入这个模块后,它自动会装入几个可能有mimetype的文件,但看了看程序,全部都是Linux下的,如:

knownfiles = [
    "/etc/mime.types",
    "/usr/local/etc/httpd/conf/mime.types",
    "/usr/local/lib/netscape/mime.types",
    "/usr/local/etc/httpd/conf/mime.types",     # Apache 1.2
    "/usr/local/etc/mime.types",                # Apache 1.3
    ]

当然在Windows下,这些文件名就不可能找到了。在这个时候,mimetypes会缺省设置一些扩展名对应的mimetype,当然也就不全了。

这不我在Karrigell中测试就发现了问题,如果我请求favicon.ico,它就会报错。如果你是windows平台,你可以在idle或 NewEdit 中试一下:

>>> import mimetypes
>>> mimetypes.guess_type(‘favicon.ico’)
(None, None)

看到了吧,根本找不到。那么在Karrigell就会报错。但通过修改Karrigell的配置文件可以解决这个问题。只要如下修改:

[Applications]
ico=image/x-icon

这样就行了。

2005年06月19日

Karrigell 支持Gzip,很有趣。以前都没有仔细看过 Karrigell 的配置文件。这回在测试Ajax时想输出结果信息时才发现,怎么东西是乱码。仔细看了程序才知道原来是使用gzip压缩的缘故。如果想支持gzip,则在配置文件中设置:

[server]
gzip = 1

不过缺省就是这样的。当我想去掉gzip时,把gzip=1改为gzip=0,问题出来了。显示的结果依然是压缩的。查来查去看到KarrigellRequestHandler.py中测试是否可以使用gzip的处理如下:

    def testGzip(self):
        """Test if content should be gzipped"""
        if not gzip_support:
            return False
        accept_encoding = self.HEADERS.get(‘accept-encoding’,”).split(‘,’)
        # if gzip is supported by the user agent,
        # and if the option gzip in the [Server] section of the
        # configuration file is set,
        # and content type is text/ or javascript,
        # set Content-Encoding to ‘gzip’ and return True
        if ‘gzip’ in accept_encoding and k_config.gzip and \
            (self.ctype.startswith(‘text/’) or
            self.ctype==’application/x-javascript’):
            self.RESPONSE['Content-Encoding']=’gzip’
            return True
        return False

请注意红色的部分。

再让我们看一看k_config.py是如何将gzip读出来的:

# gzip support
try:
    gzip=conf.get("Server","gzip")
    if silent in ["1", "true", "yes"]:
        gzip=1
    else:
        gzip=0
except:
    pass

红色部分是我加的,原来没有。那么上面代码会有什么问题呢?当我们在配置文件中设置gzip=0时,conf.get("Server", "gzip")返回的是"0",而不是数值0。因此对于k_config.gzip的判断为真,尽管它是"0"。因此这是一个bug,我修改了k_config.py,程序就正常了。我已经汇报了这个bug,不过目前没有回答。这个bug还出现在silent上。

在使用时请大家注意这个bug。

在前面测试Ajax时,由于我需要后台以XML格式来返回数据,因此希望了解 Karrigell 返回的Content-Type到底对不对。而Karrigell不提供输出响应信息的功能,因此我不得不Hack源代码加入自已的调试代码。下面是我阅读响应处理的阅读理解。

主要的代码集中在KarrigellRequestHandler.py中,代码还是挺精练的。因为代码比较长我只能简单描述了。如果有可能大家一边读代码一边看Blog为好。不是主要的,或是我没有注意的就跳过去了,只讲我已经理解的东西。

KarrigellRequestHandler是专门用来处理请求的处理器。整个处理过程集中在handle_data方法中。

self.HEADERS是用来收集所有的请求头的。

self.RESPONSE是用来收集所有响应头的。在开始时初始为’text/html’。

self.SET_COOKIE将用来读出和保存cookie的设置。

self.namespace是用来执行脚本(.py, .pih, .hip, .ks)时,将一些CGI和Karrigell内部的一些环境传递给脚本,并由脚本进行修改使用。现在可用的值参见prepare_env方法,下面列出已经存在的一些变量:

RESPONSE 响应头
HEADERS 请求头
AUTH_USER 认证用户
AUTH_PASSWORD 认证口令
QUERY 请求变量,它是一个字典,就是对应key=value对。
SET_COOKIE 用来设置cookie
ACCEPTED_LANGUAGES 可接受的语言
HTTP_REDIRECTION 重定向,这是一个异常,它的特殊性在于用于页面的重定向,但采用异常的写法
HTTP_ERROR 错误,它是一个异常,引发它将跳转到出错页面
AUTH_ABORT 用户退出
SERVER_DIR 服务器目录
Session self.Session方法,它可以用为返回一个session对象
Authentication self.Authentication方法,它用于产生一个认证处理
os os模块
Cookie Cookie模块
string string模块

在运行时在namespace中有可能还会产生:

REQUEST_HANDLER 运行KarrigellRequestHandler的当前实例
THIS 当前脚本
SCRIPT_END 结束脚本运行

另外对于表单或URL所提交的变量还会被在变量名前加上’_'放在namespace中以方便处理。这些在Karrigell的教程中都有所体现。那么如果仔细看代码,上面的名字对应的有些是字典,有些是模块,有些是方法。是这一个很有趣的用法。在你的cgi脚本中你可以引用上面的名字,把它作为全局变量来使用。部分namespace中的名字在程序的开始有说明,可以看一下。

关于提交变量放在namespace的处理在Template.py中Script的render的方法中,代码为:

        if nameSpace.has_key("QUERY"):
            for item in nameSpace["QUERY"].keys():
                nameSpace["_"+item]=nameSpace["QUERY"][item]

SCRIPT_END变量的处理也是在Template.py中。

接下来查找一个文件,如果不存在则返回404没找到的错误并返回。

如果是一个目录,则查找index文件。如果不存在index,则重定向到目录显示页面并返回。

如果是一个.ks后缀的文件,则重定向到.ks中的index方法并返回。

接下来根据文件名猜测它的mimetype。它调用了标准mimetypes模块的guess_type方法。我试了,如果是一个xml后缀的,返回结果为’text/xml’。

这时设置self.RESPONSE['Content-Type']为猜出的mim,etype。

然后是将当前目录保存,然后得到文件的目录并切换。

接着是cache的处理。

如果文件是可执行的脚本后缀(.py, .pih, .hip, .pyk, .ks)则先准备环境(调用prepare_env()),然后调用self.execute()来执行脚本,并由它进行后续处理。如果是静态文件,则判断是否浏览器支持gzip(self.testGzip()),如果支持则进行压缩处理(self.doGzip())。可以看到Karrigell想得还是很周到的。不过在不想使用gzip时有一个bug,我会另写文章描述的。

最后是切换回原来的目录。

在self.execute()中有对于debug的支持。如果配置了debug(k_config.debug,它可以在配置文件中配置)为真,则在每次执行脚本时会reload()脚本。这样给调试带来很大的方便,不用每次重启server了。

关于如何执行脚本要看Template.py中的Script类的render(),这里不再讲述了。

其实这个脚本还是有许多值是学习的,如:

认证处理的Http头的写法
重定向处理的Http头的写法
Gzip的压缩处理

其实很简单,直接使用sys.stderr.write()即可。

在k_utils.py中有一个trace()函数就是这样使用的:

def trace(data):
    sys.stderr.write(str(data)+"\n")

只不过在.py, .pih, .hip, .ks中不能直接使用trace()罢了。

2005年04月25日

虽然 Karrigell 的数据库操作简单,但我依然碰到了问题:

  1. 创建表时出错
    表名就是按照我的设计所写的,但发现创建时报错,总是说Group什么的错。我把gadflyStorage改成了sqliteStorage也是一样出错。后来才想到我起的表名 Group 与 SQL 的 group 子句关键字一样了,因此将表名改为Team,结果通过了。
  2. 如何初始化数据库
    使用Storage创建数据库后,可能会有一些数据要初始化。在文档和Demo中可以通过判断db.state的状态来实现。如果state状态为new就表示新建,可以在这个时候来执行一些初始化的操作。那么建库的标志建议为"c"。如果表建得有问题,那么使用"n"就可以删除旧的库而创建新的库。
  3. 记录的__id__
    Karrigell 在使用Storage来创建表时会自动为每个表增加一个__id__字段,这个字段是递增的,用来唯一标识一条记录,非常方便。但它的值我发现是从0开始的,因此要特别注意。要是从1开始就好了。同时__id__为一个整数,因此如果从form中上传这个值的话,要将字符串转成int类型。从这一点来看,如果可以象 Zope 中的对form变量名加类型的处理可能就更方便了。

文档发布系统对文档的处理要求集中体现在用户的管理上。那么主要的需求就是:

  • 管理员可以发布新的文档
  • 用户可以分组
  • 用户可以以个人身份或组身份登录
  • 每个文档可供不同的个人或组进行查看

基本上就这些。从这里看,我要做的文档发布系统对用户管理的要求并不多。相对复杂一些的只是在查看文档时要求多一些,发布文档只能是管理员才可以做的事情。

原本我想象 Zope 一样加入角色(role)的管理,并且可以有一个独立的管理层进行配置。但想来想去,都是 Zope 的模式,也就是用户管理是整个 web 平台自身的一部分,才能很好的实现独立的安全、用户的配置处理。因此我放弃了实现角色的处理。还是集中目标在我要实现的文档发布系统这个实际的系统上来。

从设计上用户管理为一部分功能,权限或功能管理为另一部分功能。

用户管理上将设计:User(用户表)、Group(组表)、GroupUser(组与用户关系表)

User(login, user, password,type)

login为登录用的名字。user为显示的用户名字。type为’u'表示一般用户,为’g'表示组用户。

Group(login, name)

login为登录用的名字。name为显示的组名字。

GroupUser(gid, uid)

那么在创建组时,系统应该自动根据组id在User表中生成一个组用户,这样用户就可以以组的身份进行登录。

在功能上将创建文档与查阅者之间的关系:ArticleUser(文档与用户关系表)

ArticleUser(aid, uids)

uids将是用户id的列表。

这样设计应该可以满足我的要求了。不过也可以看出,这样并不是很灵活,如果以后有变化再改吧,先这样。

2005年04月24日

用户管理是我将要实现的文档发布系统中非常重要的一个功能,那么如何实现呢?好在 Karrigell 所带的 Portal Demo 中有用户注册,申请新用户的功能,这样我可以好好参考一下。那么先好好学习一下。那么我主要关心的重点是用户信息的保存和验证,再有就是session是如何与用户认证结合起来的。那么在学习前,应该先学习一下数据库的处理,在前面的Blog中我已经学习过了。

Portal 在demo/portal子目录下。在测试中首先进入的是 index.pih 。在开始有这么一段代码:

import gadflyStorage
import portalClasses

db=portalClasses.db

so=Session()

if not hasattr(so,"user"):
    so.user=None

如果用户没有经过认证,则session中不会有用户信息,因此在第一次访问时会创建一个新的session,并将用户(user)设为None。

然后我们会看到下面包括了另外一个文件:

<% Include ("header.hip") %>

这个文件是用来显示所有页面共同的信息,同时还有用户信息的处理结果显示。在header.hip 的开始处我们可以看到:

import gadflyStorage
import portalClasses

so=Session()
user=None
if hasattr(so,"user") and so.user is not None:
    portalClasses.db.User.set_key("__id__")
    user=portalClasses.db.User[so.user]

这段代码首先根据 session 来判断是否存在一个用户,如果存在则user为用户名。如果不存在则user为None。

然后下面一段代码:

if user is None:
    ‘<td>’
    ‘<a href="login.pih?user=old">%s</a>’ %_("Login")
    ‘<br><a href="login.pih?user=new">%s</a>’ %_("New user")
    ‘</td>’
else:
    ‘<td><b class="login">%s</b>’ %user.login
    ‘<br><font size="-1"><a href="editPreferences.pih">%s</a>’ %_("Preferences")
    ‘<br><a href="publishNews.pih">%s</a>’ %_("Publish news")
    ‘<br><a href="logout.py">%s</a>’ %_("Logout")
    ‘</font></td>’

可以看出当user为None时将显示用户登录和注册的内容。当user不为None时,将显示用户姓名、修改配置、发布新闻和注销的功能。

通过以上的分析,我知道了在首页先根据session来判断是否一个用户已经通过了认证,根据认证结果显示不同的处理信息。

如果用户未注册过,user值为None,则首页将显示登录与注册新用户的链接。仔细查看上面的代码(红色部分),我们会发现登录和注册都是使用同一个文件(login.pih)来处理的,区别只是在于传入的query的值不同。对于用户的登录传入user=old,对于新用户注册传入user=new。

让我们再看一看login.hip的代码:

<% Include ("header.hip") %>

<%
if _user=="old":
    action="authenticate.hip"
elif _user=="new":
    action="newuser.hip"
%>

也是先调了header.hip,然后根据不同的user值,赋给 action 不同的处理程序的名字。这里_user是 Karrigell中比较特殊的变量,它就是前面a href="login.pih?user=old传进来的结果,在 Karrigell 中可以使用QUERY["user"]来使用这个值,也可以使用_user来直接使用这个值。再往下看:

<form action="<%=action%>" method="post">

可以看出,根据不同的操作,将调用不同的后台处理程序。再往下:

<%
if _user=="new":
    print """<tr>
    <td>%s</td>
    <td><input type="password" name="password2"></td>
    </tr>""" %_("Confirm password")
%>

如果是新注册用户,则会多显示口令确认的一个输入框。

接下来再看一下用户认证的处理。authenticate.hip

import md5

def displayAuthentError(message):
    Include("header.hip")
    "<center><p><b>%s : %s</b>" %(_("Authentication error"),message)
    raise SCRIPT_END

so=Session()
import portalClasses

if not hasattr(portalClasses.db,"User"):
    displayAuthentError(_("unknown user"))

try:
    portalClasses.db.User.set_key("login")
    user=portalClasses.db.User[_login]
except:
    displayAuthentError(_("unknown user"))

pw=md5.new(_password).digest()
if pw==user.password:
    so.user=user.__id__
    so.login=user.login
    raise HTTP_REDIRECTION,"index.pih"
else:
    displayAuthentError(_("incorrect password"))

可以看出用户的口令是使用md5处理后保存在数据库中的,因此是不可逆的,一旦丢失就找不回来了。这与大部分用户可以找回口令的处理不同。不过这也并未大部分情况下的处理,知道即可。上面红色的两行是用来查找用户的,一旦找到则根据用户输入的口令先成生md5的摘要码,然后与数据库中的值进行比较。一旦正确则设置session值为相应的用户信息。然后调用兰色的代码重定向到首页。在portalClasses模块中是用来生成相应的要保存到数据库中的类结构,同时将类结构注册到 dbStorage 中去。具体的处理,大家可以自已看,参照着数据库的使用文档就行了。

让我们最后看一下新用户的注册过程(newuser.hip):

import traceback,cPickle,time,sys
import portalClasses

if not _password==_password2:
    Include("header.hip")
    print "<center><p>%s" %_("You must enter the same password twice")
    raise SCRIPT_END

so=Session()

if hasattr(portalClasses.db,"User"):
    portalClasses.db.User.set_key("login")
    if portalClasses.db.User.has_key(_login):
        Include("header.hip")
        print "<center><p>%s <b>%s</b>"  %(_(" Login"),_login)
        print _("is already used, please choose another one")
        raise SCRIPT_END

user=portalClasses.User(_login,_password)   # creates a User instance

portalClasses.db.write(user)                # stores it in database
portalClasses.db.commit()
so.user=user.__id__

raise HTTP_REDIRECTION,"index.pih"

处理也很简单。先是验证一下两个口令是否一致。然后判断用户名是否存在,如果存在则报告“用户已经存在,请换一个用户”。如果不存在则生成一个新的用户对象,然后写到数据库中,并且提交。再将用户的ID保存到session的user变量中。最后引发重定向异常,使页面转到首页。

学习这个过程主要是可以了解到用户信息的数据库的处理,session与用户认证的处理,及公共页面的检查和处理。这样在我自已的系统中就可以根据需要进行裁剪了。

2005年04月23日

Karrigell 集成了一个名为 Gadfly 的数据库。那么针对平时对数据库的操作:比如使用关系数据库首先要创建表结构,然后是插入数据,接着可能是查询或修改等操作。那么这一些都需要编写 SQL 语句来进行。如果你想保存一个 Python 的对象,那么可能需要先根据 Python 对象的属性来创建一个表,然后把 Python 对象的属性保存起来。

那么 Karrigell 为了解决对象的保存问题,特意开发了一个 dbstorage 接口,可以方便地进行这类的处理。关于数据库的使用 Karrigell 的文档说得非常详细了,下面针对我的理解写一些学习的心得。

1. 数据库的创建

目前已经实现的 dbStorage 接口的有gadfly和sqlite这两个数据库,因此用哪个都可以。不过我也发现,使用哪个数据库的接口必须要显示地导入,而不是通过配置来实现的,我觉得这是一点可以改进的地方。

import gadflyStorage
db=gadflyStorage.Storage(path,"r")

上面我们可以看出,先是一个数据库的文件名字,然后是打开标志。文件名如果不带路径的话可能就放在调用脚本所在的目录下了。标志有三个可选值:

‘n’ 表示先创建一个新的库,再打开

‘c’表示先打开,如果不存在则创建新的库

‘r’ 打开已经存在的库

2. 类与表的对应

dbStorage 是为了解决将某类的对象保存在数据库中,它的好处就是不用你显示的建表了,它会在插入数据库的第一条记录时自动根据对象的属性的类型进行创建,比如:string, int, float,对于复杂的数据和对象,它会使用pickle之类的技术将对象dump出来并保存为string类型。那么表名就是类名,一个类对应一个表。为了使用这些表,需要在 dbStorage 中登记这些类名。并且这些类名只能登记一次。因此可以通过判断 dbStorage 的状态是否为 "new" ,从而得知表数据库是否是新创建的。如果是新建则创建。

db=gadflyStorage.Storage("portal","c")
if not db.state=="new":
    db.register(User,News)

这里登记了两个表 User, News。在脚本中使用时,需要在运行脚本的名字空间中有相应的类存在。

3. 单表与多表

对于单表的数据库,在得到一个 dbStorage 对象之后,可以不带表名直接可以操作,如:

db.find()
db.keys()
db.values()

但对于多表数据库,则还要带上表名,如:

db.User.find()

4. 类字典的操作

可以象处理字典一样对 dbStorage 对象进行操作。这一点在文档有详细介绍。

5. 支持类SQL语法的查找

如:

objList=f.find(where=whereclause,order_by=orderbyclause)

从理论上看操作还是非常方便的。

以上是我学习的记录,详细的代码示例看文档吧。