2010年5月30日星期日

[非技术] 战争进行时

现在三更半夜的,我在一台陌生的电脑上写这篇blog。女友加班,我来接她,但是看来事还没做完,所以我临时找了一台她同事的电脑上网。今天我也加了一天的班,任务就是让毒霸变得更好,让360去见鬼。
 
珠规院的工作环境真好啊,独立的大隔间工作台,24寸的显示器……我看到了遨游,点开开始上网。当然,我也不由自主的检查了一下xp右下角的图标们,发现了NOD32以及……360安全卫士……没有网盾……
 
当然,这是别人的电脑,我还是忍住了卸载360的冲动。
 
一分钟之后360提示升级,升级到第二步提示此电脑已经安装了网盾,显然我是不会让360把网盾卸掉的,于是我取消了这次升级,然后360很愤愤然的在右下角弹了一个框框,说金山网盾欺负他。我把这个弹窗关掉,30秒之后网盾也弹了一个框框,说360不是东西。你来我往,热闹得很。
 
作为一个Linux用户,这真是一次难得的机会,让我体验到了这场“战争”不是嘴上说的,是正在进行的。网盾的确有做的不好,或者说不对的地方,那就是和遨游捆绑在一起,而且是秘密的捆绑在一起。就像我刚才说了,右下角的图标中就没有网盾,但是进程管理器里面你可以看到kswebshield.exe这个进程在工作。这是在损害用户的知情权,是非常不好的用户体验。但360绝对不是什么好鸟,最可恶的就是用语言对用户进行恐吓和威胁,披着“安全”的皮推销私货。这是一种不那么容易被看出来的伎俩,但我想假以时日,众多的普通用户也迟早会发现这一点。

这两天我也会关注一下新浪微博上关于“金山”和“360”两个关键字的评论。双方都有硬伤。对金山有利的证据包括上个星期网友贴出的网盾和360安全卫士可以共存的视频,以及可牛论坛上的那篇360升级文件分析的帖子;对周鸿祎有利的证据就是今天,或者说昨天,网盾的升级包出了一个bug,使得网盾真的不能兼容360了。呵呵,真是忙中出错。周拿这一点大说特说,也说明在此之前,360安全卫士和金山网盾完全是可以共存的么,360的这次蓄谋已久的进攻的理由完全就是借口。
 
这是一场迟早要打的战争,只是我没想到会来的这么快。我也不想去翻那些周鸿祎的旧账,我只是觉得我周围毒霸的开发同事都很单纯,考虑界面、功能、用户体验等等,都是从如何做好自己出发,而并不是时刻想着怎么打击别人。
 
今天我可以拒绝360一次,这台电脑的主人会拒绝下一次么?我们在24小时之内又会失去一位用户了么……

2010年5月27日星期四

RLE 压缩算法


RLE 压缩算法是一个很简单的压缩字符串的算法,不知道的自己百度一下哈。前一段时间有人考我,让我快速实现它,于是我写了一个非常naive的函数。debug了之后它长的这个样子:

def rle_compress(input_str):
    """ RLE Compression
    >>> rle_compress('get uuuuuuuuuup'):
    '1g1e1t1 10u1p'
    """
    if len(input_str) == 0:
        return ''

    if len(input_str) == 1:
        return '1' + input_str

    tmp, counter, output = input_str[0], 0, ''
    for i in xrange(len(input_str)):
        if input_str[i] == tmp:
            counter += 1
        elif counter > 0:
            output += (str(counter) + tmp)
            tmp = input_str[i]
            counter = 1
    output += (str(counter) + tmp)

    return output

其实这个真的不够Pythonic哦。Pythonic的方式应该是这样的,灰常nb的one-liner:

from itertools import groupby

def rle_compress_iter(input_str):
    """ RLE Compression, using *groupby* from itertools
    >>> rle_compress('get uuuuuuuuuup'):
    '1g1e1t1 10u1p'
    """
    return ''.join([str(len(list(group))) + name for name, group in

        groupby(input_str)])

当然,我是写不出这么强大的函数的,思路来自于《Expert Python Programming》一书。(打个广告,这本书非常好,您得备一本!)

问题来了,虽然使用python的iterator/generator绝对是每一个python程序员应该实践的best practice,但这两个函数的性能差距还真是有点大(反复执行1000次的总用时,以下计时皆是):

manual compress: 0.663447s
original calls: 2.723927s

测试用的文本是wikipedia上关于Issac Assimov的一段介绍(为了能让RLE算法有点成就感,我自己往里面加了一些重复的字符):

TEXT = """
The Foooooooooooooooooooooooooundation Series is a science fiction series by Isaac Asimov which covers a
span of about 500 years. It ccccccccccconsists of seven volumes that are closely linked to
each other, alttttttttttthough they can be read separately. The term "Foundation Series"
is often used more generaaally to include the Robot Series and Empire Series,
which are set in the same fictional universe, but in earlier time periods. In
total, there are fifteen nnnnnnnnnnnnnnnnnovels and dozens of short stories written by Asimov,
and six novels written by other authors after his death, expanding the
timeeeeeeeeeeeeeeeeee
spanned by more than nnnnnnnnnnnnnnnnnnnnnnntwenty thousand years. The series is highly acclaimed,
winninggggggggggggggggggggggggg the one-time Hugo Award for "Best All-Time Series" in 1966.[1]
"""

我XX,怎么会这样……
好吧,也许不是itertools.groupby的错,我写这个函数的时候只是希望尽快的能够通过doctest。让我们来优化一下吧。

第一次尝试:
用加号来连接name和group的长度?用 '%d%s' % (....) 如何?

return ''.join(['%d%s' % (len(list(group)), name) for name, group
        in groupby(input_str)])

事实证明,结果很悲惨:

strformat calls: 3.933721s

第二次尝试:
group本身也是一个iterator,用 len(list(group)) 是完全没有优化过的第一想法,应该可以有更好的办法来求出一个iterator的长度,比如这样:

def rle_compress_iter_leniter(input_str):
    def leniter(iterator):
        l = 0
        for _ in iterator:
            l += 1
        return l
    return ''.join([str(leniter(group)) + name for name, group in groupby(input_str)])

哈,虽然这样看起来很丑,但是进步不少哦:

leniter calls: 0.928560s

需要说一下的是iterator本身是肯定不会有 len() 函数可以使用的,iterator 可能是无穷的。

第三次尝试:

说过了,上一种方法太丑了,我想让它看起来更优雅一点,用 list comprehension 吧:、

return ''.join([str(sum([1 for _ in group])) + name for name, group
        in groupby(input_str)])

用到了内置的sum函数来求出iterator的长度,但是看起来效果不是很好:

strsum calls: 1.262965s

第四次尝试:
再看一次代码,发现用sum函数求和其实没必要,这个list的长度就等于iterator的长度,所以用len来替换sum也许会更好一些:

return ''.join([str(len([1 for _ in group])) + name for name,
        group in groupby(input_str)]

结果是的确改进了一些,但是还是没有leniter函数快:

strlen calls: 1.059268s

第五次尝试:
用str()来把整数转化为字符串也许还可以改进,要把一个对象转化为字符串,我们还可以用repr()来试试:

return ''.join([repr(len([1 for _ in group])) + name for name,
        group in groupby(input_str)])

OK,这次真的有提高哦,真的很高哦,超过了leniter:

repr calls: 0.840248s

第六次尝试:
上上个星期网易的大神沈崴来金山帮我们对一个程序进行了一番改造。他的代码中有一些“很奇怪”的地方,其实就是我们这些python的新手没怎么接触过的“历史用法”。用'`'(backtick) 来代替 repr() 就是一例:

return ''.join([`len([1 for _ in group])` + name for name, group in
        groupby(input_str)])

事实证明backtick比直接调用repr()还要快!

backtick calls: 0.784968s

---------------------------

真的很接近最初的naive算法了,但我有点黔驴技穷了。如果有谁还有更好的办法,请一定发邮件告诉我。

one-liner真的让你想用lambda来代替def来定义这个函数,但其实lambda会更慢……

虽然naive的代码很傻很天真,但是pythonic的函数始终没能追上它。(至少我这种臭水平是没辙了)下次在写对性能有要求的python代码时,一定要记得放弃那些花花招式,先从最简单开始。

Python的struct.pack函数的一点小小需要注意的地方

struct包是用来对数据和二进制字符串进行相互转换的,如果不讲求效率,这东西还是蛮好用的。上个星期在摆弄的时候,忽然发现了一个有趣的地方:

>>> import struct
>>> struct.pack('B', 1)
'\x01'
>>> struct.pack('H', 200)
'\xc8\x00'
>>> struct.pack('BH',1, 200)
'\x01\x00\xc8\x00'
>>> struct.calcsize('BH')
4

可以看到,struct.pack('B', 1) 与 struct.pack('H', 200) 的结果之和和 struct.pack('BH', 1, 200) 的输出不一样。后者的结果中多了一个 '\x00'。想到我们一般都是用的CPython,这就好理解了。C的struct中存在内存对齐的做法,这样可以得到更好的性能。Python的struct包在默认情况下是不会干涉这一点的,所以就出现了上面的情况。想要去掉这个 '\x00' 也很简单,在格式字符串之前加上 '=' 即可,像这样:

>>> struct.pack('=BH',1, 200)
'\x01\xc8\x00'

2010年5月2日星期日

用site包添加Python的导入路径

我前两天在Buzz上抱怨Windows系的同事在CentOS下喜欢在/data目录下建一个programfiles目录来安装软件是一件非常汗的事情,有同学回复说我是Linux原教旨主义者。我完全不是,我只是希望用什么就像什么,尽量沿着best practices的路子走,不要自己发明轮子,像我现在维护的代码,把Python用成了Java。这么用可以么?没事,当然可以,但是这是这肯定是在走弯路,而且在弯路上不知道埋伏着什么bug,会在你不注意的时候跳出来咬你。昨天我就是因为这样的bug而加班的。虽然硬着头皮上也能解决,最后也能学到东西,但我希望不要总是用这种方式才能学到东西,因为我很想在五一这个美好的假日能够捧上一本好书惬意的在阳光下睡觉。

问题的起源是我没法把在一个用apache+mod_wsgi跑起来的application之中import任何包,标准库也不行,比如logging或者sys。这个application原来是用tornado直接跑的。我想用框架写网站的同学可能都没有遇到过,我也是。一开始以为这大概是apache的配置问题,但左查右查都没有结果,因为后来管机器同事一气之下把python/apache/mod_wsgi全部重新make/install了一遍就好了,最后也没查出原因。没有logging这样的模块,仅仅靠apache那点可怜的日志,鬼知道后面出的错如何解决。

CentOS是个很保守的系统,自带的Python还是2.4的,升级是不可能完成的任务。于是一般的做法是自己编译安装一个Python2.5/2.6来用。我这时才发现programfiles这个奇葩的所在。/usr/local/目录下面也不是标准的bin/lib/share...的目录结构,而是和这个/data/programfiles一一对应的软链接-_-。在让mod_wsgi使用我们自己编译安装的Python2.5之后,发现无法import第三方的库,但这个第三方的库明明用easy_install安装过啊,2.4/2.5都装了,咋还不行捏?经过差不多是把显示器贴到脸上去的检查,发现标准的路径(sys.path)里写的是 /usr/local/lib/python2.5/site-packages,而我们的山寨路径是 /usr/loca/python/lib/python2.5/site-packages。这两个有区别哈,但是第一眼真的很难看出来。

OK,把山寨路径加入到sys.path中?还是不行。我现在的感情真的很脆弱,干脆坏事做到底,直接把 /usr/local/lib/python2.5 目录删了,改成软链接连到 /usr/local/python/lib/python2.5 目录下。现在第三方库可以了。哦,不,还不是全部可以,而是有的可以有的不行……囧!实验了多次,发现一个问题,.pth文件指向的那些路径下的包都没法import。上google,上stackoverflow,上baidu,".pth"这个关键字相关的东西非常少,人家一般都给你匹配成path。就在我晕头转向之际,不知道什么时候手一抖,点开了一个stackoverflow问题的链接,里面有一位大神说,只把路径加入到sys.path是不够的,这样.pth文件不会被解析,要用site.addsitedir才行。哎呀呀,终于成了……

不写了,爬山去了。