检查IP是否可用的python脚本

  明显的,本文所说的检查IP可用是指墙内可用的google,twitter,facebook等网站的IP。本文讲的是我折腾脚本查找可用IP的经验,脚本已经扔在github上,欢迎fork && pr。脚本只能在python3,Linux,root权限下可用,这也是本文描述中默认的环境。
  在开始检查IP是否可用之前,我们得先有找到IP。google有公开其持有的IP段。详见google帮助。但这个IP列表就很长了,我的做法是偷懒,github上跟踪数个hosts源,把历史hosts文件都取出来,再做检查。
  历史hosts文件还能使用是基于这么一个假设:GFW出于性能或者其他什么原因,会随机释放一些之前被block的IP,另外GFW存在地域和运营商的差别,对别人不可用的IP在我这可能还是可以用的。在我的实践中,这个假设确实是成立的。
  进入正题,首先是跟目标IP建立一个socket连接,类似于telnet。目标端口首先是80,然后443。对目标IPconnect之后,还需要再send一个随机字符串,没抛出异常才是正常可用的。因为存在一些IP,是能建立socket的,只是建立完会马上断掉。然后是443,如果目标IP是支持HTTPS的,只是被GFW了,那么这里一般是抛出socket.timeout的异常,少数会是socket.error的,而如果目标IP不支持HTTPS,会是普通的异常,通过这个可以避免误伤只有80的IP。
  然后是对支持HTTPS的IP进行证书的检查。现在网上有一些hosts是用一个google的IP来通吃google的所有域名,但因为各个IP上证书存在差异,而且大部分google的客户端是不支持SNI的,所以像googlepais.com得用和google.com不一样的IP。但是我不知道怎么直接用python去取到证书的DNS里配置的域名,但我有另外一个简单的办法,用requests,带验证ssl地去打开https://IP,这时候会抛出requests.exception.SSLError的异常,而在异常的信息里就有IP证书支持的域名。
  也有一些域名,虽然支持https,但证书不对也还能用,这种我也无法判断,只能在配置文件里加上一个白名单,好在这种情况不多,目前只发现包括ingress在用的几个appspot.com域名。
  再然后是打开页面测试。这里因为有一些IP虽然端口证书都正常,但真正用到hosts上,打开域名是404,或者502,还有可能是204没有内容。最常见的就是google北京的203.208开关的那一批IP了。
打开测试的方法是用urllib.request.urlopen,然后重写HTTPHandler和HTTPSHandler,主要是重写DNS,将域名解析到想要测试的IP上。
  这里打开测试又有一个另外的问题是一些CDN类型的域名,本来就是无法直接打开的,这个我也没有很好的办法,只有在配置文件里加一个配置节,将不需要打开测试的域名,比如*client*, *static*这类的写入打开测试的黑名单。好在使用多次之后,这个黑名单也比较稳定不经常要调整了。
  我还有遇到一个奇葩的是mtalk.google.com这个域名,用于GMS推送用的,即使IP证书使用的正常也不能正常使用,目前发现只有188结尾的googleIP能用于这个域名。。
  一个IP通过上面的测试就基本能用了,最后就是用ping结果来排序,取延时的丢包最小的用。这里为了优雅一点,我没有调用shell使用系统ping,而是用了python-ping这个模块,原生python操作raw ICMP包,这也是脚本需要root权限的原因。
  以上~ 后面有新发现再补充。

优雅地使用python

1、if-else

a=2
b=3
x = 3 if (a==2 and b==3) else 0
#相当于
if a==2 and b==3:
   x = 3
else:
   x = 0
#显然第一种写法更优雅,读起来也更自然,这种语法是python2.7以上版本的新特性

if all(x > 3 for x in lst): #python2.6以上版本语法
    do_sth()
#相当于
for x in lst:
  if x <= 3:
     return False
return True

2、xrange
  在for循环中使用xrange代替range,可以节省内存和提高性能,如下:

#Yes:
for i in xrange(0,10):
    print i
#No:
for i in range(0,10):
    print i

  这是因为range函数返回一个list对象,而xrange会返回一个generator对象。关于generator的特性我准备通过下面的一段代码来说明:

def fun1():
    yield 1
    yield 2
    yield 3

def fun2():
    return [1,2,3]
# 下面两个循环会得到一样的输出
for i in fun1():
    print i
for i in fun2():
    print i

  这里的fun1可以看成是我们构造的一个类generator,当我们调用fun1的时候,函数会在yield语句处暂停,然后返回当前的值并储存函数的调用状态,当下一次调用时,函数会从上次停止的状态继续执行,直到下一个yield语句。
  回到range和xrange上,这里我们讨论的是python2下面的情况,在python3里,xrange消失了,range则表现得跟原来的xrange一样了,这种情况下如果确实要得到一个list,就得用这种方式: list(range())

3、为无限循环使用 while 1 代替 while True
  这是因为,在python2里,True不是一个关键字而只是一个全局变量,而 while 1 就只是一个单步操作。

4、用join()进行字符串拼接
  这里先要说明一下,在python中,一个对象一但创建,它在内存中的大小就是不变的了,那些需要容纳可变长度数据的对象只能在对象内维护一个指向一块可变大小内存区域的指针,这么设计主要是为了方便内存管理和对象管理。
  而对于用 + 来拼接str,如 a+=b ,因为python中字符串对象长度不可变,这里就需要先创建一个新的字符串对象,再将a和b复制过去,因此对于大量的字符串拼接,应当尽可能使用”.join()的方式。两种这点在PEP8里也有提及如下:

text=''
#No
for svr in svrlist:
   text += svr + 'n'
#Yes
text = 'n'.join(svrlist)

5、写简洁的list
  python支持如下一种简洁而优雅的构造list的方法:

mylst = [i for i in range(10) if i%2 == 0]

6、交换变量

 x, y = y, x

7、尽量使用局部变量
Python 检索局部变量比检索全局变量快. 这意味着,避免 “global” 关键字。

8、executemany
  这个其实是MySQLdb的一个函数,单独拿出来讲主要是因为确实好用有性能提升,但又有个小坑。先上个简单的代码

sql = "insert into table values(%s)"
sqlargvs=[i for i in xrange(1,10)]
cursor.executemany(sql,sqlargvs)

  按help文档的说法,使用executemany比做个循环再使用execute,在insert和delete操作上有极大的性能提升。然而,当我们要插入的数据过多,大过于lmysql server设定的阀值时,就会爆出一个“max sockets”的错误(具体的错误码我忘了)。最后还是要用循环+execute的方式。
  因此这个东西就有点鸡肋了,数据少时性能提升有限,数据大时有被爆菊的风险。。

9、decorator
  decorator本质上是支持了在函数和类中嵌入或修改代码,下面的代码简单地介绍了其用法,首先是函数形式的:

class myDecorator(object):
    def __init__(self, f):
        print "init"
        self.f = f
    def __call__(self):
        print "call"
        self.f()
if __name_- == "__main__":
    @myDecorator
    def func()
        print "func"

    print func.__class__
    func()

  这段代码会输出:

init
<class '__main__.entryExit'>
call
func

  也即当我们用@符号调用myDecorator时,函数对象func被传递给类myDecorator,然后创建了一个类函数对象取代原来的func()函数。当我们调用func()时,便不再用原来的代码而开始调用myDecorator.__call__()方法。这里要注意的是,用于decorator的类必须实现__call__方法。
  以我粗浅的理解,这里的decorator的应用的场景应该是当我们有一个类,类里有比较复杂的实现,但我们外部的调用却只需一个简单接口即可以满足,这时候我们就可以通过decorator暴露出来,同时也可以在类中嵌入或修改代码(在func()函数中)
  decorator也可用于函数,示例代码如下:

def myDecorator(f):
    def newf()
        print "inside newf"
        f()
    return newf

if __name__ == "__main__":
    @myDecorator
    def func():
        print "inside func"

    print func.__name__
    func()

  以上代码会输出:

newf
inside newf
inside func

  类似的,当decorator用于函数myDecorator,myDecorator()返回的必须是可调用的东西
  在网上我也看到有人将decorator用于做缓存,下面是一个在网上到处能看到的用decorator做cache计算斐波那契数的例子:

def cache(func):
    fibcache = {}
    def wrap(n):
        if n not in fibcache:
            fibcache[n] = func(n)
        return fibcache[n]
    return wrap

@cache
def fib(n):
    if n < 2:
        return 1
    return fib(n-1) + fib(n-2)

  在这个例子使用decorator的优雅之处在于,他既做了cache,又无需改变原来的fib()。如果我们不用decorator,而是将字典fibcache设置为全局变量,并在fib()函数里做多一些判断,也同样能达到同样的cache的效果,但这样一来,代码就混乱了许多。

10、cProfile
  最后再简单介绍一下cProfile这个工具,cProfile是profile的C语言版本,有着跟profile一样的接口,但效率比profile更高,两者都是python的标准库。可以统计程序里每一个函数的运行时间。
这个模块用得最多就是cProfile.run(statement, filename=None, sort=-1)了,第一个参数statement是要统计的函数或命令,第二个参数是指定输出报表保存的文件名,如果未设定则输出到控制台,第三个参数是对输出进行排序,具体的值所对应的排序的种类,其实也就是pstats.Stats.sort_stats的排序参数,先上一个简单的代码:

def foo():
    for i in xrange(100):
        print i

if __name__ == "__main__":
    import cProfile
    cProfile.run(statement="foo()",filename="prof",sort="calls")

  这里sort=”calls”是指按指令的调用次数排序。
  cProfile也支持命令行的方式:

python -m profile -o prof testprof.py

  但对于保存到prof文件里的数据,是不能简单cat出来看的,得用pstats模块:

import pstats
p = pstats.Stats("prof")
p.print_stats()
#在pstats这也能再做排序
p.sort_stats("time").print_stats()

  可选的排序选项可以在pstats的帮助文档里找到。

facebook样式丢失

  最近发现用goagent爬梯子上facebook,虽然能上去但是样式会丢掉,一开始以为是firefox的插件冲突,一番调试之后才发现是翻墙不完全导致。
  我用的AutoProxy来管理翻墙的iplist,而facebook的一些css或js文件需要访问到fbstatic-a.akamaihd.net,因此,在AutoProxy里把akamaihd.net和facebook.com都标记为通过goagent访问,才能正常访问facebook。

为xfdown添加BT任务

  xfdown是一个用python写的QQ旋风离线下载脚本,其本质是把网页版QQ旋风搬到脚本上了以方便使用。这个脚本很早就出现了,但一直缺少添加torrent文件下载任务的功能,要添加还要去网页版添加,略麻烦,这么多年也似乎一直没人去解决这个问题,估计是QQ旋风的用户量太少。。。
  所以呢,正好十一长假没什么事我就想着把这个给解决了,完成的代码在这里:https://github.com/chliny/xfdown 这是我fork了原项目并做了修改的版本,因为还需要更多时间和测试来验证添加BT任务没有问题,因此还未向上游pull request
  在干活之前,我先去了解了一下BT协议,然后就找到了这篇博客,写得很清楚~

  下面是解决的过程。
  通过在QQ旋风离线下载网页版抓包可知,QQ旋风添加Bt任务需要两次post,一次将整个torrent文件上传到服务器,一次上传选择的要下载的torrent里的文件。
  第一次post的地址是http://lixian.qq.com/handler/bt_handler.php?cmd=readinfo,上传的包格式很简单:

{
  "myfile":open("path to torrent")
}

  正常的返回值是一个包含了该torrent文件信息的json结构。下面是我上传ubuntu-13.04-server-amd64.iso.torrent的返回值:

{
  "ret":0,
  "hash":"e50331a0a8499d95ef8ebd546113cd021275c877",
  "name":"ubuntu-13.04-server-amd64.iso",
  "files":[{
    "file_name":"ubuntu-13.04-server-amd64.iso",
    "file_size":"701M",
    "file_size_ori":735051776,
    "file_index":0
}]
}

  这里因为是上传一整个文件,还是非文本文件,要用到multipart格式,urllib2对此比较无力。python下能干这个活的我知道的有poster和requests。
  用poster的方法如下:

from poster.streaminghttp import register_openers
from poster.encode import multipart_encode
import urllib2
register_openers()
data,header = multipart_encode({"myfile":open("path to torrent","rb")})
url = "http://lixian.qq.com/handler/bt_handler.php?cmd=readinfo"
req = urllib2.Request(url,data,header)
torinfo = urllib2.urlopen(req).read()

  poster的问题是目前还不支持python3,而xfdown脚本最开始的设计是兼容python2和python3,于是我把目光投向requests,用requests的方法如下:

import requests
url = "http://lixian.qq.com/handler/bt_handler.php?cmd=readinfo"
req = requests.post(url,files={"myfile":open("path to torrent",'rb')})
torinfo = req.text

  这里有个坑,当torrent文件名带有unicode字符时,requests.post会报错,报错的原因是requests去encode(‘ascii’)了。。这对unicode字符无解,于是我只能统一把文件重命名来workaround。。

  第二次post的地址是”http://lixian.qq.com/handler/xfjson2012.php”,数据格式如下:

{
  "cmd":"add_bt_task",             
  "filename":file_name,#多个文件名以#隔开
  "filesize":file_size, #多个文件大小以#隔开
  "hash":hash,#整个文件的hash值              
  "index":file_index,#以#隔开多个文件的offset
  "taskname":name,
  "r":random.random()#随机值
}

  可以看到,上面需要用到的值都可以在第一次post的返回值中得到,这次的返回值就没什么特别的了,同样以上传ubuntu-13.04-server-amd64.iso.torrent为例:

try{
  callback({
    "ret":0,
    "data":[{
      "mid":"2615312072053681544",
      "file_name":"ubuntu-13.04-server-amd64.iso",
      "file_size":"735051776",
      "bt_id":"F51331A0A8499195EF8EBD546115CD021275C877_0"
     }]
  });
}catch(e){}

  其中ret值为0是添加成功的标志。
  事实上如果不进行第一次post,直接通过libtorrent模块解析torrent文件的信息然后直接第二次post也是能正常添加BT任务的,但当离线服务器上没有该资源时,离线服务器便无法去下载文件。从第二次post的内容也可以看到,这里并没有上传下载BT最重要的tracker信息,显然这是在第一次post的时候获取的。
  至于这第二次的post的代码,就只需用urllib2做一个普通的post即可,这里不再给出。

解决openwrt不能解析内网地址域名

  其实之前就有发现,我在路由器(openwrt)后面的机器不能正常解析映射到内网地址的域名,我当时的做法是在openwrt的hosts文件里加上相应的项来workaround这个问题。当时我还以为只是我自己网络配置出的问题。直到昨晚,微博上有同学跟我聊起这个问题,我才意识到这可能是个普遍性的问题,于是便想google一下解决这个问题。

  最终呢,确认是openwrt下dnsmasq的配置问题。只需将openwrt下/etc/config/dhcp 文件下,dnsmasq块下的这一句:

 option rebind_protection 1

改为

 option rebind_protection 0 

即可。
  你可以通过下面这一句命令来修改

 sed -i 's/option rebind_protection 1/option rebind_protection 0/'  /etc/config/dhcp 

  看了一下openwrt的wiki,rebind_protection这一个选项的作用是Enables DNS rebind attack protection by discarding upstream RFC1918 responses。按我的理解,即是为了保护网络不受DNS rebind attack(DNS重定向攻击?),路由器会将上由路由的对RFC1918域(0.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16,即内网IP段)的响应包扔掉。
  显然这是一个dnsmasq的DNS解析的问题,因此如果我们把/etc/resolv.conf下的nameserver 127.0.0.1删除掉,也能workaround这个问题,但那样就相当于放弃了dnsmasq的DNS缓存功能,得不偿失。

广研面试经历

  事实上,我是没有资格写面经这么高端大气的东西的,因为从学期初找实习以来,除了腾讯这次,我的网申都是被刷== 所以面试经验也只有广研创新班-腾讯实习offer这一条线上的三次面试。但看到好多人都写了,所以我也跟风写一下吧

  先费话一下个人经历。
  一面是广研创新班的入口之面,也是我的处女面。面试问题有三个,第一题是笔试的最后一题,大数据存储的,不懂。然后面试官就写下了下面这个玩意让我实现:

void* memcpy(void* src, void* dsc, size_t length)

这个函数大家都知道是干什么的,实现也就一个循环复制,问题出在返回值上,我一直没想明白返回值应该返回什么,面试官一再提醒也不懂。。后来才知道要返回指向复制结束位置的指针。。。然后面试官又问了如何建立一个tcp连接还有tcp和udp的区别。这个问题算是吞吞吐吐给答了一点,没办法,没复习,知识都忘光了。
  本以为我这么一问三不知的孩子广研不会要的,但好像面试官实在是nice,居然放我过了。可能也与我报的运营方向竞争者相对较少有关系。。
  在广研那半个月过得比较苦逼,而且越往后越苦逼,我前面有两篇讲广研的作业的,略也提及苦逼之处。
  广研之后,是在一个下午的两次面试,决定是否能留在腾讯实习。
  后来知道第一个面我的是腾讯邮箱运营组的boss。这次的面试感觉最好了。面试官问的都是技术问题,没有主观题。问题包括:linux下top命令展示的指标的意义,netstat相关的问题,简叙建立socket的过程,map的数据结构和检索的时间复杂度,二叉树的建立和遍历。我比较幸运,这些问题大都答对或猜对了。。。面试的最后面试官说想把我推荐到运维组,问我是否有兴趣。这是整个面试过程的唯一主观题。运维本来就是我的方向,只是因为之前创新班没运维这个方向才报了运营。但我还是很矜持地问了一下运维是干什么的然后才说我很有兴趣。。真装13.。
第二个面我的是腾讯邮箱的boss。。可能因为boss是做产品经理不写代码的,所以没问到什么技术细节,问题是“你印象最深刻的项目经历是什么”之类的宏观的,吹水的题目。也问到了在网络中心和5D的工作。然后在听到说5D只有四台服务器的时候说了一句:“这么少。。”嗯,跟腾讯邮箱的几千台服务器比是很少。。。

  费话结束,说点经验想法。

  首先是简历,如思思所言,写上技术博客和Github很有用,如果StackOverflow或知乎积分够高的话也可写上ID,顺手再吹一下我用Git管理简历的方法,很简单,以最初的简历为master branch,对每一份要新投的岗位新创一个branch,名字为该公司/部门,然后在这个branch上做对应的修改。这样既可保持目录下的简洁,又可完整保持简历的各个历史版本。
  然后是个人觉得,在面试中关于 懂/不懂,熟悉/不熟 这样的描述是很不精确的,可能你觉得你懂的东西在面试官看来你其实还没懂,可能你觉得不熟的东西在面试官看来你已经比其他同学熟悉了,所以最好是不要用这类词语,代之以具体的经历。比如想表达你熟悉Python,那你可以说你用Python做过什么项目,项目的规模等等。而如果是面试官问你这类问题,同样答之以具体的经历。比如面试官问你懂不懂Linux,可以答你有XX年的Linux使用经验,如果实在没底,就再补充上一句:但我感觉我对Linux还是不熟悉。
  然后根据本人粗浅的经历(真的是粗浅,真正的运维技术面也就一次),运维的技术面问的问题集中在:C/C++基础,数据结构基础(这两个什么技术面都有的吧),TCP/UDP,Socket编程,Linux基础,Bash/Python等脚本语言。其中Linux又集中在top、netstat,tcpdump,grep,sed,awk等几个命令上。所以其实运维要速成应该还算容易的。另外具体到不同的公司也应该会有些细微的增删,如阿里的运维应该会对数据库尤其是Oracal的数据库比较有要求。

oscToWordPress

  之前我的博客是放在开源中国上的,后来租了这个空间,搭了个自己的博客,我产出量少,博客看起来就挺不好看的,就想说法把oschina里之前的博客给导到这里来。但osc导出的格式是html的,我翻遍了wordpress的导入工具,没一个支持。。。本来那里博客也不多,一篇篇复制还可行,但,这显然不符合程序员的哲学的。符合哲学的做法应该是,自己写个脚本转化成wordpress可识别的xml格式!不好意思,我自夸了。
  脚本我用的是前几天刚速成的python,算是练手了~

#!/usr/bin/env python2.7
#coding=utf-8

import codecs
import time
import re
import sys
reload(sys)
sys.setdefaultencoding('utf8')

class oscToWordpress:
    
    def readFile(self,filename):
        file = codecs.open(filename,'rb','utf8') 
        content = file.read()
        patern = ur'<div class='blogList'>.*?</body>'
        contentObj =re.search(patern,content,re.S)
        file.close()
        return contentObj.group()

    #写入新的可被wordpress导入的文件
    def writeFile(self,filename,content):
        file = codecs.open(filename,'w','utf8')
        file.write(content)
        file.close()

    #获取文章内容
    def getPost(self,content):
        patern = ur'<div class='content'>.*?(</div>|<div class='commentList'>)'
        outPut = re.search(patern,content,re.S)
        result = self.filter_tags(outPut.group())
        return result

    #获取标题
    def getTitle(self,content):
        patern = ur'<a name="blog_.*?</a>'
        titleObj = re.search(patern,content)
        title = self.filter_tags(titleObj.group())
        return title

    #获取文章发表时间
    def getPubdate(self,content):
        patern = ur'<div class='date'>([^<]*)</div>'
        dateObj = re.search(patern,content)
        dateObj = re.search(ur'uFF1A(.*)',dateObj.group(1))
        return dateObj.group(1)

    #获取标签,多个标签以,隔开
    def getTags(self,content):
        patern = ur'<div class='tags'>([^<]*)</div>'
        tagsObj = re.search(patern,content)
        tagsObj = re.search(ur'uFF1A(.*)',tagsObj.group(1))
        return tagsObj.group(1)
    
    #获取文章分类
    def getCatalog(self,content):
        patern = ur'<div class='catalog'>([^<]*)</div>'
        catalogObj = re.search(patern,content)
        catalogObj = re.search(ur'uFF1A(.*)',catalogObj.group(1))
        return catalogObj.group(1)

    #获取文章链接
    def getLink(self,content):
        patern = ur'href="([^"]*)">'
        linkObj = re.search(patern,content)
        return linkObj.group(1)

    #从文章链接获取文章Id
    def getId(self,link):
        item = re.split('/',link)
        return item[-1]
    
    #过滤html标签
    def filter_tags(self,htmlstr):
        #先过滤CDATA
        re_cdata=re.compile('//<![CDATA[[^>]*//]]>',re.I) #匹配CDATA
        re_script=re.compile('<s*script[^>]*>[^<]*<s*/s*scripts*>',re.I)#Script
        re_style=re.compile('<s*style[^>]*>[^<]*<s*/s*styles*>',re.I)#style
        re_br=re.compile('<brs*?/?>')#处理换行
        re_h=re.compile('</?w+[^>]*>')#HTML标签
        re_comment=re.compile('<!--[^>]*-->')#HTML注释
        re_prevPre = re.compile('<pre[^>]*>')#代码高亮标签
        re_nexPre = re.compile('</pre>')
        s=re_cdata.sub('',htmlstr)#去掉CDATA
        s=re_script.sub('',s) #去掉SCRIPT
        s=re_style.sub('',s)#去掉style
        s=re_br.sub('n',s)#将br转换为换行
        s=re_prevPre.sub('1',s)#同上,处理闭标签部分
        s=re_h.sub('',s) #去掉HTML 标签
        s=re_comment.sub('',s)#去掉HTML注释
        #去掉多余的空行
        blank_line=re.compile('n+')
        s=blank_line.sub('n',s)
        s=self.replaceCharEntity(s)#替换实体
        return s
     
    ##替换常用HTML字符实体.
    #使用正常的字符替换HTML中特殊的字符实体.
    #你可以添加新的实体字符到CHAR_ENTITIES中,处理更多HTML字符实体.
    #@param htmlstr HTML字符串.
    def replaceCharEntity(self,htmlstr):
        CHAR_ENTITIES={'nbsp':' ','160':' ',
                        'lt':'<','60':'<',
                        'gt':'>','62':'>',
                        'amp':'&','38':'&',
                        'quot':'"','34':'"',}
        re_charEntity=re.compile(r'&#?(?P<name>w+);')
        sz=re_charEntity.search(htmlstr)
        while sz:
            entity=sz.group()#entity全称,如&gt;
            key=sz.group('name')#去除&;后entity,如&gt;为gt
            try:
                htmlstr=re_charEntity.sub(CHAR_ENTITIES[key],htmlstr,1)
                sz=re_charEntity.search(htmlstr)
            except KeyError:
                #以空串代替
                htmlstr=re_charEntity.sub('',htmlstr,1)
                sz=re_charEntity.search(htmlstr)
        return htmlstr
     
    def repalce(self,s,re_exp,repl_string):
        return re_exp.sub(repl_string,s)
 
    #对所有文章进行切分
    def cutPost(self,allContent):
        patern = ur'<div class='blog'>'
        contentList = re.split(patern,allContent,re.S)
        del contentList[0]
        outPut = []
        for word in contentList:
             result = {}
             result['post'] = self.getPost(word).strip() 
             result['title'] = self.getTitle(word)
             result['tags'] = self.getTags(word)
             result['link'] = self.getLink(word)
             result['id'] = self.getId(result['link'])
             result['catalog'] = self.getCatalog(word)
             result['date'] = self.getPubdate(word)
             outPut.append(result)
        return outPut

    #整合成一个完成的xml
    def toWordpress(self,contentList):
        header = self.getHeader()
        contentStr = header[:]
        for blog in contentList:
            contentStr += u't<item>n'
            contentStr += u'tt<title>'+blog['title']+'</title>n'
            contentStr += u'tt<link>'+blog['link']+'</link>n'
            contentStr += u'tt<pubDate>'+blog['date']+'</pubDate>n'
            contentStr += u'tt<dc:creator>'+self.author+'</dc:creator>n'
            contentStr += u'tt<description></description>n'
            contentStr += u'tt<content:encoded><![CDATA['+blog['post']+']]></content:encoded>n'
            contentStr += u'tt<excerpt:encoded><![CDATA[]]></excerpt:encoded>n'
            contentStr += u'tt<wp:post_id>'+blog['id']+'</wp:post_id>n'
            contentStr += u'tt<wp:post_date>'+blog['date']+'</wp:post_date>n'
            contentStr += u'tt<wp:post_date_gmt>'+blog['date']+'</wp:post_date_gmt>n'
            contentStr += u'tt<wp:comment_status>'+self.comment_status+'</wp:comment_status>n'
            contentStr += u'tt<wp:ping_status>open</wp:ping_status>n'
            contentStr += u'tt<wp:post_name>'+blog['title']+'</wp:post_name>n'
            contentStr += u'tt<wp:status>publish</wp:status>n'
            contentStr += u'tt<wp:post_parent>0</wp:post_parent>n'
            contentStr += u'tt<wp:menu_order>0</wp:menu_order>n'
            contentStr += u'tt<wp:post_type>post</wp:post_type>n'
            contentStr += u'tt<wp:post_password></wp:post_password>n'
            contentStr += u'tt<wp:is_sticky>0</wp:is_sticky>n'

            for tag in blog['tags'].split(','):
                contentStr += u'tt<category domain="post_tag" nicename="'+tag+'"><![CDATA['+tag+']]></category>n'

            contentStr += u'tt<category domain="category" nicename="'+blog['catalog']+'"><![CDATA['+blog['catalog']+']]></category>n'
            contentStr += u'tt<wp:postmeta>n'
            contentStr += u'ttt<wp:meta_key>_syntaxhighlighter_encoded</wp:meta_key>n'
            contentStr += u'ttt<wp:meta_value><![CDATA[1]]></wp:meta_value>n'
            contentStr += u'tt</wp:postmeta>n'
            contentStr += u'tt<wp:postmeta>n'
            contentStr += u'ttt<wp:meta_key>_edit_last</wp:meta_key>n'
            contentStr += u'ttt<wp:meta_value><![CDATA[1]]></wp:meta_value>n'
            contentStr += u'tt</wp:postmeta>n'
            contentStr += u't</item>n'

        footer = self.getFooter()
        contentStr += footer

        return contentStr 

    def getHeader(self):
        header = u'<?xml version="1.0" encoding="UTF-8" ?>n
n
<rss version="2.0"n
txmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"n
txmlns:content="http://purl.org/rss/1.0/modules/content/"n
txmlns:wfw="http://wellformedweb.org/CommentAPI/"n
txmlns:dc="http://purl.org/dc/elements/1.1/"n
txmlns:wp="http://wordpress.org/export/1.2/"n
>n
n
<channel>n
t<title>'+self.blogTitle+'</title>n
t<link>'+self.blogLink+'</title>n
t<description></description>n
t<pubDate>'+str(time.time())+'</pubDate>n
t<language>zh-CN</language>n
t<wp:wxr_version>1.2</wp:wxr_version>n
t<wp:base_site_url>'+self.blogLink+'</wp:base_site_url>n
t<wp:base_blog_url>'+self.blogLink+'</wp:base_blog_url>n'
        return header

    def getFooter(self):
        footer = '</channel>n</rss>'
        return footer


    def trans(self):
        allContent = self.readFile(self.filename)
        contentList = self.cutPost(allContent)
        wordpressContent = self.toWordpress(contentList)
        self.writeFile('wordpress'+str(time.time())+'.xml',wordpressContent)

    def setAuthor(self,name):
        self.author = name

    def setCommentStatus(self,status):
        self.comment_status = status

    def setBlogTitle(self,title):
        self.blogTitle = title

    def setFile(self,name):
        self.filename = name

    def setBlogLink(self,link):
        self.blogLink = link

    def __init__(self,filename=""):
        self.setFile(filename)

 filename = ''
    author = u'chliny'#作者
    comment_status = u'open'#是否开放评论
    blogTitle = u'chliny'#原博客名称
    blogLink = u'http://my.oschina.net/chliny/blog'#原博客地址

  上面是转化的类,具体的使用由下面的代码执行:

#!/usr/bin/env python2.7
#coding=utf-8

from oscToWordpress import oscToWordpress

Mytrans = oscToWordpress()

Mytrans.setFile('blogs_backup.html')#从oschina导出html文件
Mytrans.setCommentStatus(u'open')#是否开放评论
Mytrans.setAuthor(u'chliny')#作者

Mytrans.setBlogLink(u'http://myoldbloglink')#原博客地址,非必须
Mytrans.setBlogTitle(u'chliny')#原博客名称,非必须

Mytrans.trans()#开始转换

  导入后看差博客里满满的博文,真是满足。
  我把代码也放到github上了:https://github.com/chliny/oscToWordpress 看着github上C,PHP,Python,Bash都齐了,又满足了~
  
  我想,只需要稍微修改一下匹配标题正文等等的正则,这个脚本也应该能将其他html格式的导出文件转化成wordpress可识别的xml格式。

广研班作业之短文本聚类

  这个是广研班的最后的大作业,具体是导师给了一堆垃圾邮件的标题,大约有1W条,然后我们的事是把这些短文本按照相似性进行聚类。或许对于有过文本聚类的同学来说不算难题,但对于我来说还真是一个很难的问题。题目是按小组来完成的,我所在小组有三人,都没接触过这类问题,我们大约是周五下午拿到的题目,周日晚即要交作业。所以最后能作出东西来,聚类效果也还不错的时候,我便又忍不住佩服我们小组的同学,包括我自己。。。。
  这次作业有很多是小组另外两个同学完成的,感谢他们两位。

  因为小组讨论后决定用python来完成作业,对于没有用python写过代码的我,只好用了半个晚上速成了一下python,然后第二天就提抢上马用pyhton写作业了。。可见python易用性还是很好的,感谢python,感谢Guido Rossum。

  首先是文本的分词与过滤。因为导师不允许我们使用现有的中文分词器及带有频率信息的字典,所以我们只能自己写分词模块及过滤模块。分词我们用的是正向匹配,这这个模块是我们组另外一个同学完成的。完成的分词效果应该说虽不完善但可用。主要是时间的限制,我们只选择了相对简单的正向匹配,而没有使用更复杂的双向匹配,也受时间限制,没能找到更全更大的字典。
  前向匹配的算法思想是,对于文本内容,从左到右扫描,如果连续的几个字在字典中存在且这个字跟后面的字不再构成字典里存在的词,则将这几个字”切“出来。这种算法简单好实现,但缺点也明显,他的准确度完全依赖于字典的完整度。而且字典越大,效率越低。
  分词之后是对过滤,主要是过滤掉一些停词,心腹一些无意义的单字,比如垃圾邮件标题中常见的火星文及全角符号,这部分是我做的。这里要说明一下,用正向匹配分词后,不能被字典的匹配的会以单个字存在保留下来。本来过滤没什么好说的,就拿个停词表字符串匹配一下就完事,但在实践中我们发现,停词表远远不够,单字的漏网之鱼太多。后来我就想到用一个常用中文字表,再过滤一下:不在常用中文字表里的单字,全部过滤掉。这样下来,效果就很好了,我们得到了纯净的,基本是包含这个短文本关键信息的词语或单字。

  接着是对取出的单词计算权重。这里我们用的tf-idf,这部分是我做的。其实对于用什么来提取短文本的特征向量,计算各单词的权重我们队伍讨论过多次,因为对于短文本来说,几乎每一个分词出来的单词都是唯一的,于是tf就几乎没有意义了。但最后我们也想不其他可行的算法,于是我便决定反正先写出来,用着试一下好了。后来用着对最后的结果也是只有一点细微的提升。
  tf-idf的算法思想是,tf*idf,tf是词频,指单词在其所在文档中出现的频率。idf是逆向文档频率,idf基于这样一个假设,如果某个单词所在文档在所有文档中出现次数越小,则说明这个单词越特殊,越能代表其所在的文档。idf的算法是,总文档数目除以出现该单词的文档的数目,再将得到的商取对数。
  从上面的描述,我们直观都可以感觉到,它对短文本聚类真没啥大用。。。
  这部分代码着实简单,如下。因为当时讨论时有想过不用tf-idf只用idf,因此我的代码里将tf、idf、tf-idf三者分开实现,实际上的tf-idf是可以不用到下面的tf和idf函数的。

#!/usr/bin/env python2.7
#coding=utf-8

#Tfidf类
import math

class Tfidf:
    newWords = {}#单词频率字典 
    wordsNum = 0#总文档数目

    #获取并统计所有单词
    def getWords(self,allWords):
        for lineWords in allWords:
			for word in lineWords:
				if word in self.newWords:
					self.newWords[word] += 1  
				else:
					self.newWords[word] = 1
			self.wordsNum += 1

 
    #计算tf
    def tf(self,line):
        tfDict = {}
        lineLen = float(len(line))
        for word in self.tfDict:
            tfDict[word] = line.count(word)/lineLen
        return tfDict 

    #计算idf
    def idf(self):
        idfDict = {}
        for word in self.newWords:
            idfDict[word] = math.log(float(self.wordsNum)/self.newWords[word])
        return idfDict

    #计算tfidf
    def tfidf(self,line):
        tfidfDict = {};
        lineLen = float(len(line))
        for word in line:
            wordFrequency = float(self.newWords[word])
            tfidfDict[word] = line.count(word)/lineLen * math.log(self.wordsNum/wordFrequency)
        return tfidfDict

    def __init__(self,allWords):
        self.getWords(allWords)

  接着是对这些单词作simhash,这也算是这个作业或者说项目的核心了,这部分也是队伍里的另一个同学主要完成的,膜拜一下他。simhash其实也还简单,他的算法思想是,对文本中的提取出来的单词,先用普通hash(如md5)降维,得到一串固定长度的二进制值,对该值的每一位,加减该单词权重(0则减,1则加),又得到一串值,再将所有单词算出来的值按位相加,对结果取其符号,又得到一串二进制值,成为该文本的fingerprint。
  从上面的算法就可以看到,simhash是一种LSH(局部敏感性哈希)哈希。
  完成simhash之后是将文本的fingerprint比较海明距离,低于阈值则将之放进一同一个聚类的桶里。海明距离是指两fingerprint间不同的位的个数,海明距离越小则说明两个文本越相似。

  在正式比较fingerprint之前,我想到用一个字典来存储,健为fingerprint,值为对应的文本,先对fingerprint做一次去重。因为对于完全相同或极度相似的文本来说,经过提取单词,过滤后,他们计算出来的fingerprint应该是一样的,因些先做一次去重可以降低后面参与比较海明距离的数量。对于我们这个项目,导师给的约1.2W条文本,去重后就只剩下大约6000条数据了。

  比较海明距离是整个项目中最费时间的一步,如果是普通的两两比较,其时间复杂度可以去到O(n^2)。而导师对我们的要求是,要在5分钟内完成聚类。如果是前面没有去重,那么这样的复杂度是难以接受的。
  后来队伍的同学想到的是将每一个fingerprint只与桶中已有的每一个文本做比较,这样子做虽然不能降低比较的时间复杂度的量级,但能降低比较的常数。于是我们的时间进一步降下来了。最后,我们的程序大概在I7的机器上跑1分半钟。满足了。

  PS:为什么挂出最没用的tf-idf的代码,因为只有这一部分是全部是我做的,其他的都是小组三人的作品,不好随便挂。。。

广研班作业之微信公众平台

  意料之外地进了腾迅的广研创新班。还记得面试的时候,面试官问了我。。算了等再开一文写这个,反正就是一问我三不知,但最后还是让我进了。。只能说面试官真nice。
  在创新班确实还是学到不少东西,但因为广研班是不分前端后端终端运营一起教学的,所以也顺手学了些Objective-c和JS等,虽然我觉得我不大会用到。我发现我越来越抗拒跟UI有关系的东西。估计是之前学CSS+HTML被伤得太深,再则学得越多发现不懂的越多,人的精力有限,总不可能把面辅得太广,只能在有限有方向上前进。

  扯远了,这个本来是想说一个广研班的作业,即运营一个微信的公众平台。

  个人觉得我的想法还是很有创意的,我想做一个基于LBS的沙盒游戏,它是在一个我失眠的晚上闯进我脑海的,具体如下:
游戏地图即为现实地图,但在虚拟世界里,这个地图上布满可以获取的虚拟物品,但玩家需要到达物品所在位置相对应的现实世界位置附近,打开微信并向公众账号发送相应命令,才能获取到物品。玩家背包空间是有限的。玩家可以将自己已经获取到的物品进行合成(merge),拆分(split),或者扔掉。物品所在位置与现实世界有一定的关联,如鱼会出现在河边,土块或砖块则只有在空旷的地方才有可能获取到,但如果是玩家主动扔掉的物品则不受此限制。系统只提供基础物品,但玩家可以自己合成更高级的物品。
  显然,这个游戏的可玩度,取决于物品的丰富度和合成关系的复杂程度。也即游戏中科技树的丰满程度。这显然需要大量的时间和精力,我表示我办不到,至少在要求完成作业的时间内办不到,所以到目前具体的基础物品列表,合成关系表都还只设计在我的脑海中。目前代码的编写也停滞在跟物品相关的部分。

  当然难得想到这么好的idea我是不会轻易扔掉的,后面我还会继续维护这个项目,算是给自己又挖了一个坑。具体什么时候能完成我也不知道,自从广研班结业并顺利拿到腾迅实习offer后,从之前广研班的高压下解放出来后整个人都懒散了很多,效率跟之前广研班高压下的高效率判若云泥。
  后台的代码我也已经开源到github上:https://github.com/chliny/SearchBaby
  微信公众账号名是 寻宝江湖 微信号是 SearchBaby,简称SB ,当然现在微信上还不具备可玩性,但也够大体感受一下了 :-)。

  上面还只是我YY中的游戏基础版本,在游戏的进阶版本中,还可以有下面的一些元素:

  等级制度:玩家合成或拆分物品可以积累经验,进而增加等级,等级的增加可以提高背包空间上限,延长饥饿时间(见下面)等等。
  饥饿:玩家长时间未进食则会失去力量,冻结合成或拆分物品的能力。这里的时间与现实世界的时间同步。玩家的进食可以使用基础物品如野果,也可以  使用合成的高级食物。不同食物有不同的饱合值,可延长的饥饿时间也不一样。
  物品生存期:除系统提供的基础物品之外,玩家合成的高级物品会有一个生存期。超过生存期将变成其他物品或消失。如“米饭”,过两天之后会变成“发霉的米饭”,再过三天则会消失。
  交易系统:即玩家之间可以以物易物。

  在YY的过程中,我甚至连这个游戏的盈利模式都想好了:跟线下实体商家结合。比如说麦当劳给我钱肯德基不给我钱,我就在地图上给麦当劳店所在地方放很多高质量的物品,在肯德基那就什么不放。如果游戏够流行够红火的话,这样子的对用户的引导应该是有效的,至少比直接在网站挂个flash广告有效~

  另外听说最近微信也想做游戏,如果微信做游戏的话,应该就不会跟普通手机游戏一样了——那是腾迅移动产品线的活,我估计着游戏的形式应该也是基于LBS的,更有可能采用我上面YY的这种盈利模式!说到这里,我就忍不住佩服自己了,都跟张小龙想到一块了!腾迅应该给我个产品经理的实习岗位的,哦,不,应该给个产品经理的正式岗位!

arch下搭建objective-c编译环境

  我们知道,Xcode编译Objective-c代码用的就是GCC处理的,那么,在同样拥有GCC的linux下,只要配置好相应的环境,也是一样能编译Objective-c代码的。

  首先是安装相应的包:

#pacman -S gnustep-core;
#pacman -S gcc-objc/gcc-objc-multilib ##具体视你的gcc版本

  当然,用GCC编译的时候也是要加上一点参数的,如将一个Hello.m 编译到 Hello,可以用如下的命令:

#gcc -lobjc -fconstant-string-class=NSConstantString -lgnustep-base Hello.m -o Hello 

  其中,-fconstant-string-class=NSConstantString 参数的作用,在GCC的man文件里的解释是:

-fconstant-string-class=class-name
Use class-name as the name of the class to instantiate for each literal string specified with the syntax “@”…””.

  我的理解是,这个参数会决定编译器用哪个类来来处理“@。。。”这种语法。classs-name的值在“GNU runtime”下默认是“NXConstantString“,在”NeXT runtime“下默认是”NSConstantString“。至于你是在那种运行环境下,则取决于你所在的平台及参数 -fgnu-runtime||-fnext-runtime 。 在MAC OS X下,默认是用-fnext-runtime,即Next运行环境,在其他大多数平台下则默认是-fgnu-runtime,即GNU运行环境。
  -lobjc 参数是用于链接Objective-c程序
  -lgnustep-base 是告诉编译器查找并链接gnustep-base库。
  此外,网上不少讲linux下编译Objective-c的文章是基于Ubuntu的,那些需要用-I参数指定头文件所在位置,而在arch下,gnustep或gcc-objc的头文件就是在/usr/include/目录下的,因此不需要此参数。

  最后是在我的vimrc里加上按F5编译运行的快捷键(之前已经有按F5编译运行C/C++的代码)

map &lt;F5&gt; :call CompileRunGcc()&lt;CR&gt;
func! compileRunGcc()
     exec &quot;w&quot;
     ...
     ...
     ...
     if &amp;filetype == 'objc'
        exec &quot;!gcc -lobjc -fconstant-string-class=NSConstantString -lgnustep-base % -o -%&lt;&quot;
        exec &quot;./%&lt;&quot;
     ...
     ...
     ...
     endif
endfunc

  关于vim,还有一个插件cocoa可以用于Objective-c代码的高亮与自动补全。

  需要说明的是,这里虽然能够编译出程序,但要想让程序能在Mac OS X下运行,还是要在Mac OS X下再编译一遍,毕竟系统架构,接口都不太一样。但是在GCC的man文件中,对上面提到的 -fgnu-runtime||-fnext-runtime 参数的解释是,生成兼容GNU|next运行环境的目标代码或输出。但我在加上-fnext-runtime 参数后会出现错误:

错误:找不到类‘NSConstantString’的引用标记

  或许还要再加上其他参数吧,容我再行探究。