sqlalchemy update的并发问题

sqlalchemy中,遇到多个进程同时自增某个字段时,会产生错误的数据,比如说,你一个帐户充值操作

account.balance += 1

当你多个进程同时操作时,这个account.balance必然出错,打开sqlalchemy的echo会发现,sqlalchemy并不是按你想的如下操作去处理:

update account set balance = balance + 1 where id=1;

sqlalchemy会将实际的数据进行赋值,这样,就产生了一个问题,只要并发稍高一些,数据必然是出错了。

limodou之前也提过一个解决方案:

Table.update().where(Table.xxxxxxx).values({Table.c.value:Table.c.value+1});

这种写法,如果一个函数里多次修改数据,就很难受了。

也许下面的写法,是较能接受的了。

account.balance = Account.__table__.c.balance + 1

这样提交的数据,在sqlalchemy里会如预期的自增1

让suds支持异步

因为项目中经常需要调用第三方webservices,使用的是suds库,tornado线程是block的,当tornado调用第三方时间较长时,很头痛!

那就只能让suds支持异步了,在suds官方trac上有一个用户提交过一个解决方案:https://fedorahosted.org/suds/ticket/312

原理是在options.py加一个async参数,client这样使用:

client = suds.client.Client(wsdl, async=True)

而在client.py里,如果参数async为true,则将原先的调用处理成两段,返回的是request类,有三个属性 url, headers和message,使用异步,便是在这里了,我们使用tornado的AsyncHTTPClient,示例如下:

import tornado.httpclient
import tornado.ioloop
import suds
wsdl = 'http://....'
client = suds.client.Client(wsdl, async=True)
iploop = tornado.ioloop.IOLoop.instance()
http_client = tornado.httpclient.AsyncHTTPClient()

# request即suds的异步Client返回的值
request = client.service.method(args)
http_request = tornado.httpclient.HTTPRequest(
                                        request.url,
                                        method='POST',
                                        headers=request.headers,
                                        body=request.message)
def callback(response):
    if response.error:
        print response.error
    else:
        # 解析suds
        reply = suds.transport.Reply(response.code, response.headers, response.body)
        content = client.service.method.processreply(reply)
        print content
    ioloop.stop()
    return

http_client.fetch(http_request, callback)
ioloop.start()

如果要使用超时,可以加在tornado的HTTPRequest里。

不得不说,这个方法不是很友好,但也没办法,将就用吧。

我将修改后的options.py和client.py放在gits上, 大家要用的可以去下载:https://gist.github.com/2584448

用tornado写一个redis的web管理工具

学习redis的最好方法,就是动手做一个有关redis的项目,考虑了下,决定做一个redis的web manager,定名redis-admin,UI就直接拿官网主页的。

redis-admin

已完成的功能:

  1. keys树形菜单
  2. keys搜索,比如session:*
  3. 根据key获取value(所有类型的key)
  4. 当点击keys菜单(比如session:*),需要合并出所有“session:”子键的data,作出类似select * from table的效果. 并加入分页处理.
  5. 全局功能: flushall, flushdb,info
  6. keys功能: edit, expire, move, delete
  7. hash,set,zset的单项删除功能
  8. 切换db功能 (connect db)
  9.  list的pop和push功能

待完善功能:

  1. 备份 (backup)
  2. 加入管理员权限控制
  3. 加入远程连接(启用redis-py的连接池)

进度日志:   

  • 2012-4-3: 完成菜单与基本功能
  • 2012-4-9: 完成values获取
  • 2012-4-12: 完成编辑与keys结果集显示
  • 2012-4-16: 完善不同keys的不同操作,例如list,应用lpush, rpush, rpop, lpop功能
  • 2012-4-19: 完成db切换
  • 2012-4-20: 完成新的key-value添加功能

欢迎大家提需求,在文章下面评论即可,我会尽力完善功能。

项目在github: https://github.com/laoqiu/redis-admin

tornado的session支持redis或secure_cookie

tornado的session机制跟flask的一样,都采用的secure_cookie方式,存储在客户端,这样做不需要考虑分布式的session文件或内容的存储,也很方便,而且防篡改,但缺点也很明显,不适合存储较大,或隐私级别较高的内容。

不如试试redis,git上正好有人写了一个,但存在一个session生命期的问题,原代码默认为2小时,但tornado却默认存储30天secure_cookie 。这样需要添加一个能在保存值时,设置有效期的函数。改过后的代码如下:

try:
    import cPickle as pickle
except ImportError:
    import pickle
import time
import logging
from uuid import uuid4

class RedisSessionStore(object):
    def __init__(self, redis_connection, **options):
        self.options = {
            'key_prefix': 'session',
            'expire': 7200,
        }
        self.options.update(options)
        self.redis = redis_connection

    def prefixed(self, sid):
        return '%s:%s' % (self.options['key_prefix'], sid)

    def generate_sid(self):
        return uuid4().get_hex()

    def get_session(self, sid, name):
        data = self.redis.hget(self.prefixed(sid), name)
        session = pickle.loads(data) if data else dict()
        return session

    def set_session(self, sid, session_data, name, expiry=None):
        self.redis.hset(self.prefixed(sid), name, pickle.dumps(session_data))
        expiry = expiry or self.options['expire']
        if expiry:
            self.redis.expire(self.prefixed(sid), expiry)

    def delete_session(self, sid):
        self.redis.delete(self.prefixed(sid))


class RedisSession(object):
    def __init__(self, session_store, session_id=None, expires_days=None):
        self._store = session_store
        self._sid = session_id if session_id else self._store.generate_sid()
        self._dirty = False
        self.set_expires(expires_days)
        try:
            self._data = self._store.get_session(self._sid, 'data')
        except:
            logging.error('Can not connect Redis server.')
            self._data = {}

    def clear(self):
        self._store.delete_session(self._sid)

    @property
    def id(self):
        return self._sid
    
    def access(self, remote_ip):
        access_info = {'remote_ip':remote_ip, 'time':'%.6f' % time.time()}
        self._store.set_session(
                self._sid,
                'last_access',
                pickle.dumps(access_info))

    def last_access(self):
        access_info = self._store.get_session(self._sid, 'last_access')
        return pickle.loads(access_info)

    def set_expires(self, days):
        self._expiry = days * 86400 if days else None

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value
        self._dirty = True

    def __delitem__(self, key):
        del self._data[key]
        self._dirty = True

    def __len__(self):
        return len(self._data)

    def __contains__(self, key):
        return key in self._data

    def __iter__(self):
        for key in self._data:
            yield key

    def __repr__(self):
        return self._data.__repr__()

    def __del__(self):
        self.save()

    def save(self):
        if self._dirty:
            self._store.set_session(self._sid, self._data, 'data', self._expiry)
            self._dirty = False

这样还不够,如果我想在原来的secure_cookie基础上加redis的支持,又想保留原来的机制呢?

比如说,我只需要在settings里设置redis_session的开关,而且有效期也能统一成redis_session一样,是不是会更好呢?

这样需要将原来的secure_cookie的方法改下了。

class Session(object):
    def __init__(self, get_secure_cookie, set_secure_cookie, name='_session', expires_days=None):
        self.set_session = set_secure_cookie
        self.get_session = get_secure_cookie
        self.name = name
        self._expiry = expires_days
        self._dirty = False
        self.get_data()
    
    def get_data(self):
        value = self.get_session(self.name)
        self._data = pickle.loads(value) if value else {}

    def set_expires(self, days):
        self._expiry = days

    def __getitem__(self, key):
        return self._data[key]
    
    def __setitem__(self, key, value):
        self._data[key] = value
        self._dirty = True
    
    def __delitem__(self, key):
        if key in self._data:
            del self._data[key]
            self._dirty = True
        
    def __contains__(self, key):
        return key in self._data
    
    def __len__(self):
        return len(self._data)
    
    def __iter__(self):
        for key in self._data:
            yield key
    
    def __del__(self):
        self.save()

    def save(self):
        if self._dirty:
            self.set_session(self.name, pickle.dumps(self._data), expires_days=self._expiry)
            self._dirty = False

是的,这个方法像是个简化版的redis_session,能像下面这样:

self.session['user'] = user
self.session.save()

是不是很像flask的session? 哈哈,还能更像。当然还有一个settings和handler.session的工作要做,settings上添加两个属性REDIS_SERVER和 PERMANENT_SESSION_LIFETIME,如果设置PERMANENT_SESSION_LIFETIME为None或0时都能使用浏览器生命期。

# REDIS_SERVER = False

# If set to None or 0 the session will be deleted when the user closes the browser.
# If set number the session lives for value days.
PERMANENT_SESSION_LIFETIME = 1 # days

当然还有继承tornado.web.RequestHandler子类的session:

@property
    def session(self):
        if hasattr(self, '_session'):
            return self._session
        else:
            self.require_setting('permanent_session_lifetime', 'session')
            expires = self.settings['permanent_session_lifetime'] or None
            if 'redis_server' in self.settings and self.settings['redis_server']:
                sessionid = self.get_secure_cookie('sid')
                self._session = RedisSession(self.application.session_store, sessionid, expires_days=expires)
                if not sessionid: 
                    self.set_secure_cookie('sid', self._session.id, expires_days=expires)
            else:
                self._session = Session(self.get_secure_cookie, self.set_secure_cookie, expires_days=expires)
            return self._session

大功告成,如果要查看整个文件,点击这里

设计作品: 七宝莲池

客户比较满意的作品:七宝莲池, 分享一下 :)

qibaolianchi

作品后期制作成了FLASH站点,链接点这里: 七宝莲池

为css文件的背景图片属性使用data uri

Data URI在处理小图片,我觉得会有一定的好处,减少连接数,载入速度快。

缺点是浏览器不缓存这类数据,但会缓存css文件,所以,将css文件中的小背景图处理成datauri,我想是最合适的。

注:IE6-7不支持

对于已有的css文件,一个一个属性去改实在有些累人,python脚本处理一下吧:

#!/usr/bin/env python
#coding=utf-8
"""
    Base64 encoded images embedded in CSS file
    ~~~~~~~~~~~
    :author: laoqiu.com@gmail.com
"""
import sys
import os
import re
from base64 import encodestring

_url_re = re.compile(r'url\([\'\"]?(?P<url>[^\'\"\)]+)[\'\"]?\)')
        
class Base64Image(object):
    """
        bi = Base64Image()
        bi.encode_file('path/to/file.css', max_size=5) # max_size = 5k
    """
    def __init__(self):
        pass

    def encode_image(self, image_path, base_path='', max_size=1):
        full_path = os.path.join(base_path, image_path)

        if not os.path.isfile(full_path):
            return image_path

        if os.path.getsize(full_path) > max_size * 1024:
            return image_path

        template = 'data:image/%s;base64,%s'

        extension = os.path.splitext(os.path.basename(image_path))[-1][1:]
        assert extension in ('gif','png'), extension

        data = encodestring(file(full_path,'rb').read()).replace('\n','')
        result = template % (extension, data)
        
        return result

    def encode_file(self, file_path, max_size=1):
        """ max_size = 1 (k) """
        if not os.path.isfile(file_path):
            return "No such file: '%s'" % file_path
        
        base_path = os.path.dirname(file_path)
        
        outfile = open('%s_base64%s' % os.path.splitext(file_path), 'wb')

        with open(file_path, 'r') as f:
            for line in f:
                print >> outfile, self._encode(line, base_path).replace('\n','')
        return
    
    def _encode(self, value, base_path):
        f_list = _url_re.findall(value)

        if f_list:
            s_list = _url_re.split(value)

            for background_attr in _url_re.finditer(value):
            
                url = background_attr.group('url')
                index = s_list.index(url)

                s_list[index] = "url('%s')" % self.encode_image(url, base_path)

            return u''.join(s_list)

        return value


if __name__=="__main__":
    if len(sys.argv) < 2:
        print 'python static_base64.py path'
        sys.exit(1)

    path = sys.argv[1]

    b = Base64Image()
    if os.path.splitext(path)[1]=='.css':
        print b.encode_file(path)
    else:
        print b.encode_image(path)

凰家庭园设计稿

凰家庭园,别墅庭园设计

引导页: huangjiatingyuan_1 首页:

huangjiatingyuan_2

关于sqlalchemy的mysql server has gone away错误

遇到sqlalchemy mysql server has gone away错误起初,还真难理解这是为何。按道理sqlalchemy应该有管理好自己的连接池,而且是当服务运行一段时间才报这样的错误。

搜索了一把,得知应该设置一个连接池回收时间pool_recycle,看了下flask-sqlalchemy是有默认设置为7200,sqlalchemy官方也是这么推荐的。

设置过后,运行一段时间,仍然有报类似的错误,仔细查看flask-sqlalchemy源码,发现作者在flask的每次请求完成之后,都对session连接回收了.... 

于是,tornado里,需要在继承tornado.web.RequestHandler子类的on_finish里设置db.session.remove()

而且此方法只在tornado2.2版本中才开始支持。so...低版本就升级吧!

告别前端已有数年 写文留个纪念

已有些许年头没有做前端了,虽然也有关注前端动态,有写js和css,但毕竟不是自己主业了,怀念当初的种种经历,还是有一些感概的。

2005年,第一本关于前端的书《网站重构》,让我对前端这个技术,产生无比兴趣,对于现在想学前端或者已是前端未读过此书的朋友,我想推荐一下,这是让你重新认识网站的书。

虽然前端写的css,html,都不是很严格的语言,但它们一样是有理有据的,并非是胡乱搞一通,做出像模像样的,就算是好的代码,甚至是拿去w3c验证一下,以通过验证为依据。我只想说,这是一个误区。

在我看来,学习一样东西,必须知道,为什么要这么做,这么做有什么好处,其实就是找出内在的原理和含义。

做前端必要掌握的几点技能或理论:

  1. xhtml所有标签和html5新加入的一些标签(了解标签的默认样式)
  2. html内联元素和块级元素的区别
  3. css2的所有属性(css3的一些常用属性及css复合属性书写)
  4. css选择器(包括css3选择器)以及css选择器优先级判断
  5. css图片的合并
  6. 主流浏览器最简单的hack(包括清浮动的两种常用方法)
  7. ie6已知的常见的bug(已被命名的,比如“浮动双边距”等)

什么才是好的代码?

至于什么样的代码才是好的,有一个很好很简单的方法帮你辨别html代码,使用firefox的“查看” -> "页面风格" -> "无风格", 如果在去除css的情况下,你的html代码仍保持良好的可读性,这会是好代码。

至于css代码,就没有这么简单了,不仅要考虑html的语义化,也要考虑到css继承等诸多方面,有几点可以肯定的是

  • 少用hack或不用hack
  • 更标准的浏览器将具备更好的视觉效果

多去看看国内较强的ued团队项目,或者多看看国外网站,也许帮助更大。

轻编辑器ueditor 支持ajax文件上传

现流行的所有编辑器,要么太大,太复杂,要么就是过小,不符合项目要求。

 google了一段时间,决定自己用国外的小巧编辑器改写一个,以后可以开发为轻博客的编辑器核心,又可以做为普通博客的编辑器,在网上找到一个uEditor,可惜,bug一大堆,功能又太简陋,无奈,大改之!仍命名为uEditor。

 皮肤ico,直接下载的KissEditor(淘宝编辑器),内置的ajax上传图片功能,是使用的ajaxUpload的jquery插件。

原uEditor项目地址:http://www.upian.com/upiansource/ueditor/en

功能原理

其实原理也不复杂,使用的是document.execCommand命令,比如加粗:

document.execCommand('bold', false, null)

当然,也有很多命令不是所有浏览器都支持的,或者根本没有。

其中遇到不少bug啊,还有坑爹的ie问题,一一解决,可费不少心思。

典型的IE bug

1. IE下不能在iframe里添加内容,会直接添加到父级的html里。

解决方案:

iframe.contentWindow.focus();

需要让iframe得到焦点

2. IE下不能正确为文本格式化为p标签

解决方案:

insertNewParagraph : function(elementArray, succeedingElement) {
                var body = $(this.iframe).contents().find('body');
                var paragraph = this.iframe.contentWindow.document.createElement("p");
                $(elementArray).each(function(){
                    $(paragraph).append(this);
                });
                if (typeof(succeedingElement) != "undefined"){
                    try {
                        body[0].insertBefore(paragraph, succeedingElement);
                    }
                    catch (e) {
                        body[0].insertBefore(paragraph, succeedingElement[0]);
                    }
                } else {
                    body.append(paragraph);
                }
                return true;
            }

IE和firfox等浏览器对于节点的解析,还真的不太一样

其他像清除样式等,也费神不少。总的来说,有一个自己知根知底的编辑器,还真是感觉不一样 ;)

项目地址:https://github.com/laoqiu/uEditor

tornado的自定义错误页

tornado在很多细节上真的很逊呢,出错页面太素也就算了,竟然连自定义404错误页都无法被handler识别,看了源码才明白,根本不会获得继承的RequestHandler的新类的自定义404页,如何解决呢?又不可能让tornado底层改变,看来又要走偏门了!

创建一个ErrorHandler,继承自定义过get_error_html的tornado.web.RequestHandler的子类,然后在app的handlers的结尾加上一条处理其他不能被匹配的url,代码如下:

class ErrorHandler(RequestHandler):
    """raise 404 error if url is not found.
    fixed tornado.web.RequestHandler HTTPError bug.
    """
    def prepare(self):
        self.set_status(404)
        raise tornado.web.HTTPError(404)

在tornado上支持flashed_messages

其实flask中有许多东西还是很实用的,比如说flash,后端要显示给前端一些信息的时候,相当方便,tornado上实现起来也不难,代码如下:

class FlashMessageMixIn(object):
    """
        Store a message between requests which the user needs to see.
        views
        -------
        self.flash("Welcome back, %s" % username, 'success')
        base.html
        ------------
        {% set messages = handler.get_flashed_messages() %}
        {% if messages %}
            {% for category, msg in messages %}
            {{ msg }}
            {% end %}
        {% end %}
    """
    def flash(self, message, category='message'):
        messages = self.messages()
        messages.append((category, message))
        self.set_secure_cookie('flash_messages', tornado.escape.json_encode(messages))
    
    def messages(self):
        messages = self.get_secure_cookie('flash_messages')
        messages = tornado.escape.json_decode(messages) if messages else []
        return messages
        
    def get_flashed_messages(self):
        messages = self.messages()
        self.clear_cookie('flash_messages')
        return messages

代码不多,也不用解释了 ;)

关于tornado多语言的forms支持

tornado的多语言不仅支持gettext方式,还支持导入csv文件,挺意外的,但我还是习惯使用gettext方式,因为我使用的是wtforms,却遇到一个多语言支持问题,我的forms不能使用多语言。

其实问题的原因跟tornado的locale使用方法有关啊,又是只能在handler里使用,在RequestHandler里重置get_user_locale来获取当前语言,但却不能重新生成forms。

github上已有人出过不少解决方法,经我测试后,确定了其中一个比较靠谱的方法,原理很简单,将生成forms写成一个方法,获得语言支持列表后,生成多语言版本的forms,存在一个字典里,在使用时,区别语言获取不同的forms类。

代码如下:

def create_forms():
    _forms = {}
    for locale in tornado.locale.get_supported_locales(None):
        _ = tornado.locale.get(locale).translate
        is_username = regexp(USERNAME_RE, 
                             message=_("You can only use letters, numbers or dashes"))
        
        class FormWrapper(object):
            class LoginForm(Form):
                pass        
        
        _forms[locale] = FormWrapper    
    return _forms

深入理解sqlalchemy之orm.attributes

在sqlalchemy里如何存储数据库操作记录?

比如在commit的时候,将数据库的修改明细,保存下来。

在原flask的sqlalchemy插件里,有after_commit,当有commit的时候,flask将得到操作的model,以及操作类型('insert', 'update', 'delete')

def _record(self, mapper, model, operation):
    pk = tuple(mapper.primary_key_from_instance(model))
    orm.object_session(model)._model_changes[pk] = (model, operation)
    return EXT_CONTINUE

但是在实际应用中,只获取到model,远远不够,我们需要知道操作的字段,以及原来的数据是什么。

经研究,sqlalchemy.orm.attributes.History,能获取到这些信息。

此属性由三个元组构成,分别是added, unchanged, deleted

插入新数据时,added为新数据,unchanged和deleted分别为空

删除时,unchanged里为原数据

修改时,added里是新数据,deleted里为老数据

原_recode代码修改如下:

def _record(self, mapper, model, operation):
        pk = tuple(mapper.primary_key_from_instance(model))
        #orm.object_session(model)._model_changes[pk] = (model, operation)
        changes = {}
        
        for prop in object_mapper(model).iterate_properties:
            if not isinstance(prop, RelationshipProperty):
                try:
                    history = attributes.get_history(model, prop.key)
                except:
                    continue

                added, unchanged, deleted = history

                newvalue = added[0] if added else None

                if operation=='delete':
                    oldvalue = unchanged[0] if unchanged else None
                else:
                    oldvalue = deleted[0] if deleted else None

                if newvalue or oldvalue:
                    changes[prop.key] = (oldvalue, newvalue)
        
        orm.object_session(model)._model_changes[pk] = (model.__tablename__, pk[0], changes, operation)
        return EXT_CONTINUE

在tornado上支持routing

tornado的路由方式是正则匹配,在app创建时,作为参数,或者在app创建后,用 app.add_handlers 函数添加,也支持反向路由(reserver_url),此方法唯一缺点,不能在handler以外使用,比如models里,如果想定义一个 url 属性需要用到反向路由,便非常头疼。

其实github上已有许多人写到过这个routes,但也没有写到反向路由,其实原理很简单,只是用一个方法集中routes,再进行后续处理。代码如下:

from tornado.web import url

class Route(object):
    _routes = {}
    def __init__(self, pattern, kwargs={}, name=None, host='.* ):
        self.pattern = pattern
        self.kwargs = {}
        self.name = name
        self.host = host

    def __call__(self, handler_class):
        spec = url(self.pattern, handler_class, self.kwargs, name=self.name)
        self._routes.setdefault(self.host, []).append(spec)
        return handler_class

    @classmethod
    def routes(cls, application=None):
        if application:
            for host, handlers in cls._routes.items():
                application.add_handlers(host, handlers)
        else:
            return reduce(lambda x,y:x+y, cls._routes.values()) if cls._routes else []

    @classmethod
    def url_for(cls, name, *args):

        named_handlers = dict([(spec.name, spec) for spec in cls.routes() if spec.name])

        if name in named_handlers:

            return named_handlers[name].reverse(*args)

        raise KeyError("%s not found in named urls" % name)

route = Route

我加入一个url_for的类方法,这样,在models里也可以使用route.url_for('xx')来代替reverse_url了。

使用方法也很简单:

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            # ...
        ] + Route.routes()

也可以在app创建后使用Route.routes(app)的方法

用tornado重写的pypress

flask是一个很不错的框架,写项目比较轻松,较多高质量的插件,帮助python新手能快速创建自己的项目。而且也有不少国外项目可以参考,比如newsmeme,原先的pypress就是学习的这个项目而创建的,非常受用。

pypress原也是自己的学习项目,没想到还有许多朋友来邮件表示在用,原想在flask上继续更新,但又学习上了tornado,于是直接使用tornado重写了。

说下tornado,是一个不错的server,但做框架,还是缺少太多东西,写了一段时间tornado,我已经开始怀念flask的高效了....

反正是学习,我将flask自己比较中意的插件,应用在了tornado上,并做了相应修改:

  1.  sqlalchemy.py 改动最大,比如分页类(Pagination),以及_SinalTrackingMapperExtension类_record输入处理等
  2. forms.py 对wtforms包装,只是简单的对tornado的request进行了处理  
  3. cache.py 这是从werkzeug源码里copy的类进行了修改
  4. signals.py 这是纯从flask里拿来的,只是对blinker的导入进行了简单处理而已
  5. routing.py 这是为tornado的反向路由写的一个比较简单的类,解决了tornado只在handler里使用reverse_url的问题
  6. permission.py 这个也是纯从flask里拿来的,只是做了一点点小改动,让它能在tornado里使用

当然,还有一些小功能,比如类似flask的flash message,debug下友好显示错误,以及flask一样获取request.argument方法等(在views/base.py里)

pypress目前功能少于原先flask版,只是加入了一个评论验证码,更多时间花在了对tornado的完善上,希望能让tornado写起来跟flask一样高效

项目地址: https://github.com/laoqiu/pypress-tornado

新浪应用与淘宝应用

 前段时间做了个新浪应用,在新浪微博上分享淘宝商品。淘宝api申请好几次都没能通过审核。于是把代码共享,为学习flask的朋友多一个参考项目。

项目地址:https://github.com/laoqiu/sinaapp

UI的浏览地址: tuibei.viimii.li (网站功能已无法正常使用,淘宝api已失效)
 
用新浪帐户登录,授权应用,
通过淘宝地址,获取淘宝商品,
分享到新浪微博,不论买家还是发博文的,都能得到淘宝客返利
 
在新浪微博上的效果如下:
 
 
应用主要的功能:
1.auth模块的openid处理
2.淘宝api的调用
3.商品图片处理
 
 
介绍下项目内容:
weibo----微博SDK, qq, sina
webapp
|---__init__.py   
|---config.cfg    flask配置
|---extensions.py 插件, 新浪微博appkey,淘宝appkey在这里设置
|---helpers.py    一些函数(注:大部分来自flask网站的snippets和newsmeme里)
|---models        数据models
|    |---....
|
|---forms         表单forms
|    |---....
|
|---scripts
|    |---taobaoapi.py   方便调用taobao api的简单类
|    |---mytimer.py     定时器,用于定时获取taobao api数据
|    |---models.py      定时获取用到的models
|    |---mydb.py        从flask-sqlalchemy1.0以前取下来应用的插件
|    |---taobao_func.py 淘宝api的调用函数
|---utils
|    |---imageProcess.py   这个图片处理函数,加水印文字等
|
|---permissions.py      flask权限设置
|---templates
|---static

pypress改进计划

停了一段时间没有维护pypress, 感谢许多朋友对pypress提了宝贵的意见, 我最近将对pypress做较大改动,除了一些bug的修复外,还有下面一些功能改动:

  1. 为评论加验证码(从我的这个博客上的垃圾评论就可知道,真烦人呐)
  2. 加入多微博(新浪,腾讯,豆瓣)同步发推
  3. 去除twitter的发推
  4. 制作一套皮肤
  5. 将发布功能做成轻博客形式,可能需要更换编辑器
  6. 加入各种文档(doc, cvs, pdf, rar, zip...) 的发布显示及下载

暂时就这些想法,如果朋友们有其他较好的想法,欢迎评论。

最近一季度做了许多事,写了许多代码,包括一些无意义的事,得到许多,也失去许多。值得高兴的是,认识了许多朋友,谢谢大家。

xtrabackup备份mysql数据库

今天试了下xtrabackup,作个记录

下载:

ubuntu 10.04下,更新下源:

$vi /etc/apt/sources.list
deb http://repo.percona.com/apt lucid main
deb-src http://repo.percona.com/apt lucid main
$apt-get update $apt-get install xtrabackup

全备份:

$xtrabackup --defaults-file=/etc/mysql/my.cnf --backup --target-dir=/root/backup/mysql/201104/

增量备份:

$xtrabackup --defaults-file=/etc/mysql/my.cnf --backup --target-dir=/root/backup/mysql/201104.15/ --incremental-basedir=/root/backup/mysql/201104/

全备份恢复:

需要执行两次xtrabackup --prepare

$xtrabackup --defaults-file=/etc/mysql/my.cnf --prepare --target-dir=/root/backup/mysql/201104/

.....

增量恢复:

需要分别对全量、增量备份各做一次prepare操作。

....

$xtrabackup --prepare --target-dir=/root/backup/mysql/201104/ --incremental-dir=/root/backup/mysql/201104.15/

flask项目案例之pypress

 开源项目pypress(基于flask的团队博客)

 这个项目是我年初建立的,原意是一个团队博客,如淘宝ued这样的,当然也可以做个人博客,目前还没有时间制作一个比较漂亮一点的皮肤。

项目用到的插件和技术:

  • flask_themes: 皮肤,博客必不可少的
  • flask_sqlalchemy: flask对sqlalchemy的插件,定义了一些方法,使创建models和输出query更方便
  • flask_wtf: 对wtforms的插件,默认加入了csrf功能(防止表单重复提交)和Recaptcha(验证码)
  • flask_uploads: 上传文件的插件
  • flask_cache: 缓存插件(支持memcached,gaememcached,filesystem,simple等)
  • flask_principal: 权限插件 (众多插件中比较复杂的一个, 但也是作用很大的一个),支持各种权限方式,较django admin的权限,我只能说,这个插件让你知道,权限其实很简单。
  • flask_mail: 发送邮件插件
  • flask_script: 项目管理插件,类似django的manager
  • flask_babel: 多语言支持,使用非常方便,(request.accept_languages.best_match判断语言有点怪,好象会根据系统语言判断,待深究)
  • singals: 其实信号不常用,因为sqlalchemy太强大了,不过也会有用它的地方的。
  • twitter: 这个非flask插件,是twitter的api,很有意思的功能,在线发推啦(国内主机不能支持这个功能)
  • pygments: 代码高亮
  • 前端方面我用到了kissy-editor,淘宝编辑器,这个编辑器很不错。

项目结构:

laoqiu.com

    |---manage.py

    |---babel.cfg

    |---pypress

             |---__init__.py

             |---config.cfg

             |---helpers.py

             |---extensions.py

             |---permissions.py

             |---singals.py

             |---twitter.py

             |---static

             |        |---images

             |        |---...

             |---models

             |        |---__init__.py

             |        |---users.py

             |        |---types.py

             |        |---blog.py

             |---views

             |        |---__init__.py

             |        |---frontend.py

             |        |---account.py

             |        |---post.py

             |        |---link.py

             |        |---comment.py

             |        |---feeds.py

             |---forms

             |        |---blog.py

             |        |---account.py

             |        |---validators.py

             |---templages

             |        |---base.html

             |        |---.....

             |---themes

             |---translations

 

flask对项目的结构没有严格的控制,只有static和templates是默认的(也可以改动),其他随意搭配了,比如上面的models,views,forms结构就够用了,项目大了,可以考虑使用独立apps。

 

Flask的优点:

  • 项目很新,由pocoo.org出品,发展迅速
  • 较多的第三方插件,且挑选的都是最优秀的第三方,都有较好的文档说明。如sqlalchemy和jinja2
  • 有一颗稳定的心(werkzeug),也是uliweb的核心
  • 松耦合,定制性强

 

Flask的缺点:

  • 国内flask开源项目较少

 

项目是开源的:

开源地址在这:https://github.com/laoqiu/pypress

欢迎大家跟我讨论关于flask

flask项目案例之优容网

很早就答应大郎要写下flask的项目案例,一直没抽出时间。今天把最近几个月的项目成果分享下。

较早之前写过一篇“便宜否”的淘宝客应用项目,是用web.py写的,当时也写过不少web.py的项目,大都是自己在内部使用,“便宜否”是为朋友写的唯一在外部使用的。学习flask也是朋友Davidx的推介。

先介绍下优容网(yoro),这是我flask的第二个项目,主要是做女性化妆品分享社区,希望女生通过这个平台能找到适合自己肤质的化妆品。

这个项目还有一个主力是cloud,参于服务器架构到核心代码编写。先说说为什么选择flask,最初我们都使用过django,都不喜欢django的紧耦合;试了下web.py,又发现虽然东西小,要自己写的东西太多了,特别是form和db方面;最后决定了flask,有django一样的templates和models风格,又有web.py的小巧,而且还有许多强大的第三方插件,团队pocoo.org也是比较靠谱的。

项目结构:受到django的影响,yoro的项目结构我们使用了独立app的形式,里面包含app各自的models,views和forms,这样使用的好处是分离app各功能,尽量做到独立应用,缺点是项目中的交叉引用会显得比较乱。

用到的flask插件有:

  • flask_sqlalchemy: flask对sqlalchemy的插件,定义了一些方法,使创建models和输出query更方便
  • flask_wtf: 对wtforms的插件,默认加入了csrf功能(防止表单重复提交)和Recaptcha(验证码)
  • flask_uploads: 上传文件的插件
  • flask_cache: 缓存插件(支持memcached,gaememcached,filesystem,simple等)
  • flask_principal: 权限插件 (众多插件中比较复杂的一个, 但也是作用很大的一个),支持各种权限方式,较django admin的权限,我只能说,这个插件让你知道,权限其实很简单。
  • flask_mail: 发送邮件插件
  • flask_script: 项目管理插件,类似django的manager

当然还有很多有用的插件如flask_oauth,flask_themes,flask_babel等,稍后在pypress项目中介绍。

项目实现的主要功能:

  • 用户系统
  • 站内邮件系统
  • 基本的sns(关注/我说)
  • 商品点评推荐
  • 任务平台
  • 兑换平台
  • 后台管理

特别的技术点:

  • 站内邮件系统中有一项功能比较特别,当系统发送信息给所有用户,考虑到一次性存大量数据到数据库是不合理的,采用了lazy_load模式,只有当用户登录了,才会对用户发送信件。
  • SNS方面,使用了通用模块,类似django中的contenttype,将评论和推荐应用到了所有模块上。这个是flask里缺少的功能。
  • yoro的对外邮件提醒功能,采用了rabbitMQ,这个队列异常强大。
  • logging项目日志功能,当项目在运行时,报过何种异常或是错误,都会邮件提醒,很酷吧。
  • 前端使用了较多的ajax调用,也较多的使用到了html5技术。ps: flask里的jsonify功能要慎用,ie6下面无法正常接收json的mime,需要使用text/html形式的

yoro这个项目还在紧张的测试中,近期产品还会有一次大的调整,大家可以让自己的女性朋友试用试用,对产品提提建议,如果想探讨项目中使用到的技术,或是学习flask,欢迎给我写邮件。 

学习macvim配置

 macvim很漂亮,默认配色已经满足我了,而且自带大部分好用的插件和功能,比如高亮显示各种文件,包括css,html,python,ruby,c.... 

首先,我们创建vim的配置目录和文件。

mkdir -p ~/.vim

在下面建立需要的文件夹如autoload, plugin, dict....

如果是前端,可以再添加一个插件zencoding,可直接到http:// github.com/mattn/zencoding-vim下载zencoding.vim文件,一共两个,分别放在autoload和plugin下

建立配置文件vimrc

vi ~/.vimrc

对于学习python的,需要设置一些其他特别的功能。

set nocompatible
set history=500 
set helplang=cn " 帮助文档设置中文
 
set number " 显示行号
set ruler " 状态行显示光标位置
 
set foldmethod=indent " 语法折叠为缩进
set autoindent " 自动缩进
set smartindent " 智能缩进
set tabstop=4 " 设定 tab 长度为 4
set shiftwidth=4 " 设定 << 和 >> 命令移动时的宽度为 4
set softtabstop=4 " 使得按退格键时可以一次删掉 4 个空格
 
set textwidth=80 " 行宽度字符限制
set linebreak " 使英文单词在换行时不被截断
set wrap " 自动换行
 
set hlsearch " 高亮显示搜索结
set incsearch " 输入搜索内容时就显示搜索结果果
set ignorecase  " 搜索忽略大小写
set smartcase  " 有一个或以上大写字母时仍保持对大小写敏感
 
" 下面两条设置并不在终端下执行,原因是终端下会报错
if has('gui_running')
set autochdir " 自动切换当前目录为当前文件所在的目录
set autoread " 当文件在外部被修改,自动更新该文件
endif
 
" 配置vim文件目录变量$VIMFILES
if has('unix')
    let $VIMFILES=$HOME.'/.vim'
else
    let $VIMFILES=$VIM.'/vimfiles'
endif
 
" 显示Tab符
set listchars=tab:\|\ ,extends:>,precedes:<
autocmd filetype python set list
 
" python 中使用空格替换tab
autocmd filetype python set expandtab
 
" 设置各文件的补全字典文件
set complete+=k   " 记住,这句非常重要,我就卡在这,不设置这句,下面的dictionary是不会生效的
autocmd filetype python set dictionary=$VIMFILES/dict/python.dict
autocmd filetype css set dictionary=$VIMFILES/dict/css.dict
autocmd filetype javascript set dictionary=$VIMFILES/dict/javascript.dict
 
这个配置暂时已满足我的需要了。
如果有更高要求的配置,可以查看这篇博文:http://nootn.com/blog/Tool/22/
 

用vps搭建vpn服务

最近gmail老是掉线,如果有使用gmail的视频功能的更是痛苦,没过几分钟必掉。

还好,我有vps,但一直是在windows下通过firefox翻的墙,在mac下当然有更方便的了,vpn,网上这篇文章写得很好:

用vps搭建vpn

我这里只是做个笔记,以防忘记。

1. 安装服务

做vpn需要安装两个服务,一个pptpd和iptables,都通过apt-get安装了。

2. 配置pptpd

配置ip地址

$vi /etc/pptpd.conf

localip 192.168.0.1

remoteip 192.168.0.234-238,192.168.0.245

增加用户

$vi /etc/ppp/chap-secrets

username pptpd password *

*号代表允许任何ip地址接入

 

DNS解析设置

$vi /etc/ppp/options

ms-dns 208.67.222.222

ms-dns 208.67.220.220

允许转发

$vi /etc/sysctl.conf

net.ipv4.ip_forward=1

一般去前面注释就行

设置好后要使设置生效

$sysctl -p

重启pptpd服务

$service pptpd restart

打开iptables转发支持

$/sbin/iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j MASQUERADE

3. 解决问题

如果连上vpn却不能上网,一般是转发设置或dns问题,可尝试更改dns或查看ipv4_forward是否为1,设置后使用sysctl -p使设置生效。

mac下安装pil库的jpeg支持

今天刚买到mac pro mc700,拿到后已经迫不急待地安装软件,安装到pil的时候,我一直不能支持jpeg,google了很多文章,发现都不是很正确,后来自己测试后发现,其实可以很简单的。

我开始是参照http://wiki.python.org/moin/MacPython/UniversalLibrariesAndExtensions做,其实这个教程版本老了

当然,必须要有xcode,新版在安装盘里可以找到。

再是需要安装freetype2,直接上http://easynews.dl.sourceforge.net/sourceforge/freetype/freetype-2.1.10.tar.gz下载,解压后直接编译,不要按之前教程来了,因为你下载的也不是2.1.10,是2.4.4

  1. ./configure
  2. sudo make
  3. sudo make install

再是安装jpeg-8c, 也是上面三步,不需要改其他任何东西。

之后再通过easy_install pil 来安装吧,你会发现jpeg已支持上了。

ps. 我也试过使用brew来安装pil,但我发现安装完成后在python里是无法import到Image的,所以,这个也不用试了。

希望能帮到新买mac又写python的朋友 

自动补全未闭合html标签

在写pypress的时候,需要做一个类似wordpress加入一个“查看全文”的功能,是一个带more-id 的p标签,在显示在列表上的时候,需要截取这个p前面部分显示。但如果这个p放置的位置在嵌套的div里,这时候就会形成未闭合的标签,导致页面的布局混乱。

这时候需要写一个自动补全未闭全HTML标签的函数。

思路是这样的,先定义所有不需要闭合的标签,对传过来的html代码进行正则匹配,找出所有开始标签和结束标签,放在两个列表里。

对开始标签过滤掉自闭合标签,和结束标签对比个数,如果相同就返回掉。

对开始标签反序,遍历,如果开始标签在结束标签里,就把这个标签从结束标签里T除一个,反之加入到html后面。全部代码如下:

def endtags(html):
    """ close all open html tags at the end of the string """

    NON_CLOSING_TAGS = ['AREA', 'BASE', 'BASEFONT', 'BR', 'COL', 'FRAME',
            'HR', 'IMG', 'INPUT', 'ISINDEX', 'LINK', 'META', 'PARAM']

    opened_tags = re.findall(r"<([a-z]+)[^<>]*>",html)
    closed_tags = re.findall(r"<!--([a-z]+)-->",html)

    opened_tags = [i.lower() for i in opened_tags if i.upper() not in NON_CLOSING_TAGS]
    closed_tags = [i.lower() for i in closed_tags]

    len_opened = len(opened_tags)

    if len_opened==len(closed_tags):
        return html

    opened_tags.reverse()

    for tag in opened_tags:
        if tag in closed_tags:
            closed_tags.remove(tag)
        else:
            html += "</%s>" % tag
     
    return html

此函数只对原先嵌套合理的html代码有效。当然,我的目的也是如此。

搭建自己的oauth服务

开放API的网站,一般都提供OAuth服务,大家也许都知道如何连接别人的API, 网站教程也很多。

但做为自己做站的,你需要的是如何塔建自己的Oauth服务了。下面我讲讲我用flask 尝试的过程:

1. 准备工作

用到的oauth2库

easy_install oauth2

需要用到两个表:提供api申请consumer key的Api表,和记录用户授权access_token的token表Token

Api表需要记录用户、API名称、API基本信息、consumer_secret、回调地址等

Token表则需要记录用户授权动作中要使用到的两对token、token_secret和verifier,关联到user和api

2. 授权流程

  1. apikey apply: 用户申请api应用,提交表单过来,我们要生成consumer_key和consumer_secret, 可以使用uuid生成
  2. request token: 应用使用申请的consumer_key和consumer_secret用get方式请求未授权token, 我们服务端获取到request后需要进行验证,通过后返回未授权的token_key和token_secret(也是服务器生成)
  3. authorize: 应用使用未授权的token_key和token_secret请求用户授权,这时生成一个允许应用授权的页面,需要用户登录后操作,用户点击"同意"时,服务器生成verifier并跳转到应用的回调地址,传出授权后的token_key/secret和verifier
  4. access_token: 应用使用授权的token换取access_token和access_secret

注意:每个过程都需要服务端验证。

如果不知道如何使用应用发送请求请参考豆瓣的文档

 

3. 处理问题

知道流程了,事情就好办了,我一路做下来,只有一个难点,那就是对应用发送过来的request进行验证。

原因是oauth.Request.from_request函数需要的headers跟werkzug的request字典有出入,需要重新设置,django的request也是一样的。于是有了如下的函数:

def oauth_request_from_flask_request(request):
    url = request.base_url
    params = dict([(str(k), v) for k, v in request.values.items()])
  
    post_data = request.data if request.method == 'POST' else ""
    headers = {}
    if 'HTTP_AUTHORIZATION' in request.environ:
        headers['Authorization'] = request.environ['HTTP_AUTHORIZATION']

    oauth_request = oauth.Request.from_request(
        request.method,
        url,
        headers=headers,
        query_string=post_data, 
        parameters=params)
  
    return oauth_request

 这样,我便可以使用_check_signature来验证了,实验证明上面函数非常好用。

consumer = oauth.Consumer(consumer_key, consumer_secret)
token = oauth.Token(token_key, token_secret)

# re-construct request
req = oauth_request_from_flask_request(request)

# check signature
oauth_server._check_signature(req, consumer, token)

好了。也许这个并不是其他人的难点,但我着实费了一点心思,当然,熟悉oauth服务的流程也是很重要的,当你熟悉后,玩起别人的oauth API那是得心应手。

全文完

强大的sqlalchemy技巧总结

现在python的web框架很多都加入了sqlalchemy 的支持,用sqlalchemy快一年了,有很多技巧在这里总结下。

1. 多用类方法

类方法是sqlalchemy里比较常见的,比如一个user类,有email字段,你可以定义一个类方法 get_avatar_url 来取得gravatar头像地址。

class User(Base):
    __tablename__ = 'user'
    id = Column('user_id', Integer, primary_key=True)
    name = Column('user_name', String(50))
    email = Column('email', String(50))

    def __init__(self):
        ....

    def get_avatar_url(self):
        hash = hashlib.md5(self.email).hexdigest()
        return 'http://www.gravatar.com/avatar/%s' % hash

 

2.  创造虚拟字段

有时候,你需要对一个关联表进行计算出结果再order_by,这时候虚拟一个字段来进行,会大大减少您的代码量,而且sqlalchemy还能优化性能。 还是上面的User类,如果User有一个一对多关系表address(此表略),需要按address数量排序,这时候可以这样:

class User(Base):
    __tablename__ = 'user'
    id = Column('user_id', Integer, primary_key=True)
    name = Column('user_name', String(50))
    avatar = Column('email', String(50))

    def __init__(self):
        ....
    
    num_address = column_property(
        select([func.count(Address.user_id)]).\
            where(Address.user_id==id).as_scalar())

这样的num_address可以像其他字段一样使用,不过在order_by时不能使用string,需要.order_by(User.num_address.desc())这样使用。

还有,如果Address对面定义在User下面,将会报错,你可以将num_address的定义放在Address类的后面,如User.num_address = .... 当然相应的id要改为User.id

 

3. 日期字段的搜索

也许很多人都遇到过这样的问题,想要搜索出一个时间段的内容怎么办?也许我只想匹配年份,在django里有created_at.__year,在sqlalchemy里需要使用func.year(created_at) 。

PS: 这里修正一下,func.year在非mysql数据库引擎下会报错,按weizi的说法,修改成db.extract('year',created_at)即可。

4. 自定义query

比如原本使用的User.query.get(1),如果找不到用户直接做404处理,你可以定制一个get_or_404的方法。

class PostQuery(BaseQuery):

    def get_or_404(self, id):
        post = self.get(id)
        if post is None:
            ...error...
        return post

class Post(Base):
    __tablename__ = 'posts'

    query_class = PostQuery
    
    id = Column('user_id', Integer, primary_key=True)    
    ....

也许你应该写一个search?或是as_json用于输出json格式的数据...等等

5. 使用更多的column属性

sqlalchemy支持所有原生的数据库属性和功能,比如index、onupdate、nullable、unique等等

这里要介绍下onupdate,如果单是设置这个属性,这是不应用到数据库的设计中的。只是存在于映射类中。如果你需要应用到数据库设计中,需要使用server_onupdate。default也是如此。

6. 其他重构功能

像对字段进行包装,对__init__进行重构,这些都是python里常见的方法,在sqlalchemy里使用真是如鱼得水...

 

7. 查看sqlalchemy的数据库查询语句

新手都很想知道sqlalchemy处理的sql语句是怎么样的,也有助于自己学习和优化sql语句。sqlalchemy在create_engine中一个参数echo设置为True就能看到了。

	engine = create_engine('sqlite:///mydatabase.db', echo=True)

sqlalchemy还有更多功能需要在使用中去熟悉和了解的,有关sqlalchemy的教程可以参看:

PS. 新手总结,如有错漏请大牛们指正。

python模拟浏览器登录淘宝抓取内容

一个网络爬虫,经常要用到支持cookie的方式登录网站,进行抓取工作。下面介绍下如何通过python模拟浏览器登录到淘宝后台。

首先cookie支持需要使用cookielib的CookieJar

cookie = cookielib.CookieJar()
cookie_support= urllib2.HTTPCookieProcessor(cookie)
opener = urllib2.build_opener(cookie_support)

想要取得cookie里的值,你可以使用cookie._cookies,这是一个字典。

有了cookie支持,下面是如何得到发送到淘宝的headers,通过抓包工具我们可以很轻松地分析出这个headers,生成一个headers字典。

通过抓包发现,我们需要一个_tb_token_,访问淘宝登录页,分析页面可以得到这个值,然后加入到headers,POST到淘宝后,返回的是一个js跳转页面,也通过正则分析出这个地址,我们照样访问过去。这样便模拟出来登录效果。

 

注意:如果店铺开启过了淘宝的防盗功能,使用代理很可能无法登录,原因是淘宝对店铺经常登录的IP有记录,差别大时会要求通过手机验证登录。

下面是全部代码

import urllib, urllib2, socket, cookielib
import json, re, os
import time, datetime

# from gzipSupport import ContentEncodingProcessor

# set timeout
timeout = 20
timesleep = 10
socket.setdefaulttimeout(timeout)

httpHandler = urllib2.HTTPHandler()
httpsHandler = urllib2.HTTPSHandler()

# cookie support
cookie = cookielib.CookieJar()
cookie_support= urllib2.HTTPCookieProcessor(cookie)

# gzip support
# gzip_support = ContentEncodingProcessor

opener = urllib2.build_opener(cookie_support, httpHandler, httpsHandler)
urllib2.install_opener(opener)

def get_headers():
    headers = {
        "User-Agent":"Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13",
        #"User-Agent" = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.10 (maverick) Firefox/3.6.13",
        "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language":"zh-cn,zh;q=0.5",
        #"Accept-Encoding":"gzip,deflate",
        "Accept-Charset":"GB2312,utf-8;q=0.7,*;q=0.7",
        "Keep-Alive":"115",
        "Connection":"keep-alive"
    }

    return headers

def get_login_data():
    login_data = {
            'TPL_username':u'用户名'.encode('gbk'),
            'action':'Authenticator',
            'event_submit_do_login':'anything',
            'TPL_redirect_url':'',
            'from':'tb',
            'fc':'2',
            'style':'default',
            'css_style':'',
            'tid':'',
            'support':'000001',
            'CtrlVersion':'1,0,0,7',
            'loginType':'3',
            'minititle':'',
            'minipara':'',
            'pstrong':'3',
            'longLogin':'-1',
            'llnick':'',
            'sign':'',
            'need_sign':'',
            'isIgnore':'',
            'popid':'',
            'callback':'',
            'guf':'',
            'not_duplite_str':'',
            'need_user_id':'',
            'poy':'',
            'gvfdcname':'10',
            'from_encoding':''
            }
    return login_data

def login(source=None):
    """ login """
    url = 'https://login.taobao.com/member/login.jhtml'
    if not source:
        source = request(url=url)
    token_list = re.findall(r"input name='_tb_token_' type='hidden' value='([a-zA-Z0-9]+)'", source)
    login_data = get_login_data()
    login_data['_tb_token_'] = token_list[0] if token_list else ''
    login_data['TPL_password'] = raw_input("input password:")

    source = request(url=url, data=login_data)
    r = re.findall(r'window.location = "([\w\W]+)";', source)
    if r:
        redirect_url = r[0]
    else:
        print "login failed, valid password and try again"
        return False

    request(url=redirect_url)
    return True

def request(url, headers=None, data=dict()):
    if headers is None:
        headers = get_headers()
    
    data = urllib.urlencode(data) if data else None
    req = urllib2.Request(
            url = url,
            data = data,
            headers = headers
            )
    try:
        request = urllib2.urlopen(req)
        source = request.read()
        # print url
        # print request.code,request.msg
        request.close()
    except:
        source = None
        print "connect timeout"

    return source

if __name__=="__main__":
    login()