给博客园闪存添加第三方功能 —— 云计算
2012-11-18 21:32 wid 阅读(4663) 评论(42) 编辑 收藏 举报前言:
博客园有个闪存功能想必大家是都知道的, 如果你是第一次听说博客园的闪存, 那么可以先到这了解一下闪存的情况 http://home.cnblogs.com/ing/ 闪存每次最多能够发布300个字符, 比微博要长, 据我长期刷闪存的体验来看, 闪存的作用至少有两点, 一是及时记下自己瞬间的灵感, 二是大家在一起聊聊, 嗯, 挺好挺温馨的。
事情是这样的, 前几天, 笔者在使用XP自带的计算器的时候, 感觉十分不顺手, 不顺手就在于它的输入不是可随便插入删除的文本框, 输入时不够自由, 像括号这事先写一半再写运算表达式, 再写另一半括号, 括号一多肯定要乱套鸟, 或者当输入很长时发现其中的某个数写错了, 又得删除直到到错误的地方重新输如, 不给力的有木有! 又去到网上找个自由点的计算器, 最好能支持超大数运算的能再附加一些比如base64编码解码、明文的散列/哈希计算之类的就更好了, 可惜啊, 不给力的有木有!
有句话说, 自己动手才是王道, 我是不认为这句话是100%对的, 就比如现在, 笔者想起Python的解释器不就是个牛X的计算器么? 超大数运算, 数学模块、base64模块、hashlib等等, 各种方便超给力, 于是立即再次装上当计算器使用那是完爆各种计算器啊!
先拿出来亮亮相:
IDLE 2.6 >>> print int(1000 * '9') * int(500 * '8')
888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888879999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999911111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111112 >>>
这里计算的就是长度为99999999...(1000个9)再乘上88888888...(500个8)的结果, 很给力, 表达式输入自由, 和普通的运算输出基本没什么区别, 这里的int(1000*9)只是为了快度生成1000个9并转为int型而已, 难道你想让我输入1000个9?
"独乐(yuè)乐(lè)不如众乐乐" —— 出自《孟子·梁惠王下》, 这里这句话就不要翻译成"自己奏乐自己高兴不如大家一起奏乐一起高兴。"了, 咱们就翻译成独乐(lè)乐(lè)算了, 自己方便了, 不如让大家一起方便(这句话毫无歧义), 好, 那下面就开始讨论下如何让大家一起方便。
功能设计
先不考虑其他复杂的功能, 就先尝试下普通的支持大数计算的计算器并且再附加一个base64编解码的小工具, 输入框就拿博客园的闪存发布输入框为数据入口, 计算的结果就以闪存的回复为输出, 请求计算就以"@wid"作为标识符, 理想中的效果如下:
当输入的表达式错误的时候的我们给出一个指令有误的回复:
必要的时候我们还可以回复上出错的信息, 就比如当1/0这样的错误时我们就回复一个, 这里就先不实现了:
ZeroDivisionError: integer division or modulo by zero
然后我们还需要一个简单的日志系统, 来记录程序的各种运行状态, 万一跑着跑着挂了也好从日志中找到些原因。
实现分析
细节分析:
1>. 如何获取指令:
获取指令可以通过正则表达式或者干脆就用字符串的一些方法找到每条闪存, 并且判断该条闪存是否是指令, 若属于指令, 就进行执行返回然后回复结果, 否则休息1-2秒再进行下一轮的检测。
2>. 如何执行指令:
思路一:
解析指令中的表达式或语句的含义, 若合法, 则取出其中的数据进行计算, 不合法抛出异常。
难度分析: 不难但比较麻烦。
思路二:
不解析指令的含义, 采用Python中的exec函数直接执行用户指令,
IDLE 2.6 >>> exec('print "Hello World"') Hello World >>>
可以看到, 字符串'print "Hello World"'中的print "Hello World"就被直接执行了。
十分需要注意的是: 使用该函数的风险极大, 使用时必须检测需要执行的指令中有没有恶意代码, 如果不进行检测将相当于把你的管理终端放在大街上一样, 谁见了都可以充当下管理员玩玩你的服务器。
难度分析: 难度较低但不够安全
思路三:
不解释指令的含义, 采用Python中的eval函数直接执行用户指令, eval语句用来计算在字符串中的有效Python表达式, 举例:
IDLE 2.6
>>> eval('(123+5)*2') 256 >>>
难度分析: 难度较低但不够碉堡有木有! 就只能充当个普通的计算器。
开始Coding
第一步: 日志系统
日志系统十分简易, 将运行的日志保存在当前的文件夹即可, 如果你喜欢, 也可以进行备份或者为了安全再即时传送到另一个云端。
#日志写入 def WriteLog( strContent ): ''' 功能: 用于程序运行的日志记录 NOTE : WriteLog( string strContent ) -> None ''' currentTime = str( time.strftime("%Y-%m-%d %X", time.localtime()) ) #获取当前时间 try: #尝试写入日志 with open( "service.log", "a" ) as flog: flog.writelines( "%s : %s \r\n"%(currentTime , strContent) ) except: #当日志无法写入时在errors.log日志中写入一条错误报告 with open( "errors.log", "a" ) as ferr: ferr.writelines( "%s : 日志文件写入出错.\r\n"%currentTime )
日志写入预览:
第二步: 登录博客园
#登录博客园 def LoginCnblogs( username, pwd ): ''' 功能: 登录博客园 NOTE: LoginCnblogs( string username, string pwd ) -> int Login Succeed -> return 1 Login Failed -> return 0 Other Exception -> return -1 ''' params_post = urllib.urlencode({ '__VIEWSTATE': r'/wEPDwULLTE1MzYzODg2NzZkGAEFHl9fQ29udHJvbHNSZXF1aXJlUG9zdEJhY2tLZXlfXxYBBQtjaGtSZW1lbWJlcm1QYDyKKI9af4b67Mzq2xFaL9Bt', '__EVENTVALIDATION': r'/wEWBQLWwpqPDQLyj/OQAgK3jsrkBALR55GJDgKC3IeGDE1m7t2mGlasoP1Hd9hLaFoI2G05', 'tbUserName': username, 'tbPassword': pwd, 'btnLogin' : '登录' }) try: #尝试登录园子 cookie = cookielib.CookieJar() opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) urllib2.install_opener(opener) login_response = urllib2.urlopen( 'http://passport.cnblogs.com/login.aspx?', params_post ) txt = login_response.read() #读取返回数据 if ( txt.find("编辑个人资料") != -1 ): return 1 #成功登陆返回1 else: return 0 #用户名或密码错误返回0 except: return -1 #其他错误导致的登录失败返回-1
登录园子这步使用了cookielib中的一些函数, 目的是为了能够实现带Cookies的浏览以及后面的闪存回复。采用通过判断返回的数据是否含有"编辑个人资料"这个字符串, 如果有就说明已经成功登录了。
第三步: 从闪存中获取指令
园子提供了PC端网页版和手机网页版, 这里使用的是通过获取手机网页版的闪存列表来获取指令, 为什么不用PC端的网页版? 哪个方便用哪个呗~ 好吧... 我又懒了。。。
看一下一条闪存的页面源码部分:
<li class="entry_a"> <div class="feed_avatar"><a href="/u/dudu/" target="_blank"><img width="36" height="36" src="http://pic.cnblogs.com/face/u1.jpg" alt=""/></a></div> <div class="feed_body" id="feed_content_328315"> <a href="/u/dudu/" class="ing-author" target="_blank">dudu</a>: <span class="ing_body" id="ing_body_328315">终于处理完堆积的邮件</span> <a class="ing_time gray" href="/ing/328315/" title="发布于 11-18 09:46:41,点击进入详细页面" target="_blank">4小时前</a> <a href="#" id="a_328315" onclick="showCommentBox(328315,1);return false;" class="ing_reply gray" title="点击进行回应">1回应</a> <div id="ing_comment_l_b_328315"></div><script type="text/javascript">$(function(){ GetIngComments(328315,true);});</script> </div> <div class="clear"></div> </li>
我们需要的数据有3条, 一是用户名, 二是闪存中的指令, 三是该条闪存的id, 用户名在回复时是不必要的, 但是我们需要写入到日志当中, 万一有请求中有邪恶的指令也好快速的定位到id不是, 我又邪恶了...
根据这个手机版页面的网页源码可以看出, 用户名在第二行和第四行都有, 一看就知道这条闪存是dudu发的, 那么闪存的id呢? 目测也能测出来数字328315就是这条闪存的id, 指令就在<span>标签内了, "终于处理完堆积的邮件"就是我们要取出来的指令了。
取出HTML源码中的元素的方法有很多, 很多, 可以使用解析HTML的模块, 例如HTMLParser、sgmllib、htmllib, 也可以使用正则表达式, 比如你想把第六行的
<a class="ing_time gray" href="/ing/328315/" title="发布于 11-18 09:46:41,点击进入详细页面" target="_blank">4小时前</a>
中的闪存id取出来就[1-9][0-9]{5,}这样就行了, 当然, 也可以在其他行找。
还可以使用最基本的字符串方法, 要取出指令咱们就得换个例子了, 因为上一个例子中没有@xx的标签, 看这一个:
<span class="ing_body" id="ing_body_328352"><a href="/u/392976/" target="_blank">@白糖365</a> 啊哈~ 我在呢, 园子今天我来值班!</span>
嗯, 这个有了, 如果以@wid作为请求的指令, 那么这里就应该是@wid, 先不管那么多了, 要使用基本的字符串方法的话先找找看其中的一些固定标志, <a>的结束标签</a>算是指令的起始标志了, </span>标签就是指令的结束标志, 那么取出其中的指令的方法就应该是:
string[ string.index("</a>") + len('</a>') + 1 : string.index("</span>") ]
看下实际的效果:
IDLE 2.6 >>> s = r'<span class="ing_body" id="ing_body_328352"><a href="/u/392976/" target="_blank">@白糖365</a> 啊哈~ 我在呢, 园子今天我来值班!</span>' >>> cmd = s[ s.index("</a>") + len('</a>') + 1 : s.index("</span>") ] >>> print cmd 啊哈~ 我在呢, 园子今天我来值班! >>>
目测没有问题。
完整的代码如下:
#获取指令 def GetIngCommand(): ''' 功能: 从闪存中获取指令 NOTE: GetIngCommand(None) -> dict cmd Succeed : return cmd{'username':username, 'command':command, 'ingID':ingID} Failed : return None ''' username = re.compile('/u/(.*)/"') #匹配用户名的正则表达式 ingID = re.compile('[1-9][0-9]{5,}') #匹配闪存id的正则表达式 response = urllib.urlopen( 'http://home.cnblogs.com/ing/mobile/home' ) ingTxt = response.readlines() cmd = {} #使用字典记录请求的信息 for i in range(len(ingTxt)): if ingTxt[i].find('@犇犇') != -1: #如果在这行找到了"@wid"字符 try: cmd['username'] = username.findall(ingTxt[i-1])[0] #在上一行找用户名 cmd['command'] = ingTxt[i][ingTxt[i].index('</a>') + len('</a>') + 1 : ingTxt[i].index('</span>') ] #在当前行找到指令 cmd['ingID'] = ingID.findall(ingTxt[i])[0] #在下一行寻找该条闪存的id return cmd except: return None #找不到时返回None
第四步: 恶意指令过滤
之所以要过滤恶意指令在上面也有提到, 就是用户可能自己import os/sys等能对系统进行命令行操作的模块, 我们要过滤的就是禁止用户加载这些模块, 或者禁止用户自己加载模块。
其实笔者首先想到的就是检查这些模块名是否在指令当中, 但是再一想发现这样是行不通的, base64编解码指令需要获取用户的字符串, 这些字符串中可能包含有正常的os、sys等字符。
如果要在exec中执行一段程序, 注意, 这里讲的是一段指多行, 必须能让exec知道谁和谁是一句, 这样就必须使用分号(;)进行断句, Python是强制缩进, 没有了分号(;)想再加载其他模块并且使用就难了。(笔者目前不太清楚Python中除了使用;号进行断句外还有没有其他可以用来在exec中断句的符合, 如果哪位有知道的, 求赐教! 不胜感激!)
也就是说, 将指令中的;号全部替换掉, 替换成空格, 这样, 没有断句的情况下, 再使用exec进行执行就会报错, 应该就能够从根据上解决用户自己加载并使用其他模块的情况了。什么? base64编解码里需要分号怎么办? 好吧! 可以将base64中的函数设为例外, 不对其进行检查, 砖已经抛了, 这里就不再实现了。
还有需要注意的是, HTML中的一些实体字符我们需要手动还原它, 比如'''和''"', 分别代表'和", 这里我们全部替换为("), 千万不要先替换分号(;), 否则一会就麻烦了, '''和''"'后面都有分号.
完成代码如下:
#检查过滤恶意指令 def CheckCommand( command ): if "'" in command: command = command.replace("'", "\"") elif """ in command: command = command.replace(""", "\"") elif ";" in command: command = command.replace(";", " ") return command
第五步: 回复闪存
这部分就是表单提交了, 传入闪存的id和要回复的内容就行了。
#回复闪存 def ReplyIng( ingID, content ): ''' 功能: 回复闪存 NOTE: ReplyIng( string ingID, string content ) -> None ''' try: post_url = 'http://home.cnblogs.com/ajax/ing/PostComment' params_post = urllib.urlencode({ 'ContentId': ingID, 'Content': content }) pubIng_response = urllib2.urlopen( post_url, params_post ) responseInf = pubIng_response.read() if responseInf.find('"IsSuccess":true') != -1: #检测是否回复成功 WriteLog("闪存ID : %s, 回复内容: %s"%(ingID, content)) else: WriteLog("该闪存回复失败 : 闪存ID : %s, 回复内容: %s"%(ingID, content)) except: WriteLog("其他异常导致回复失败 : 闪存ID : %s, 回复内容: %s"%(ingID, content))
第六步: 执行指令并获取执行结果
在这一步要为用户加载好合法的模块math和base64, 因为我们目前只允许使用这两个模块, 然后就是执行指令将文本重定向到文本里, 这样, 我们才方便讲执行的结果取出并反馈给用户, 当用户的指令正确时将正确的执行结果返回, 否则返回"指令有误"。
完整的代码:
#执行指令并获取执行结果 def GetResult( command ): ''' 功能: 执行指令并获取执行结果 NOTE: GetResult( string command ) -> None ''' safeModule = "import math; import base64; print " #为用户加载合法模块 oldStdout = sys.stdout sys.stdout = open("result.txt", "w") #重定向输出到文件 try: exec( safeModule + command ) #执行远程指令 sys.stdout = oldStdout with open("result.txt", 'r') as f: result = f.read() return result except: sys.stdout = oldStdout return '指令有误'
第七步: 集成
集成自然就是将这些函数组装起来了。
完成的代码:
def Start(): ''' 功能: 组装函数, 使其能够向用户提供服务 NOTE: Start(None) -> None ''' WriteLog('服务已启动!') try: if LoginCnblogs('mr_wid', 'miao17hd') != 1: WriteLog('无法登录博客园!') return 0 oldCmdIngID = '' #用来记录上一条处理的闪存id while True: try: cmd = GetIngCommand() if cmd != None and cmd['ingID'] != oldCmdIngID: WriteLog( "用户名: %s , 闪存ID: %s , 指令: %s"%(cmd['username'],cmd['ingID'], cmd['command'] ) ) #写入日志文件 command = CheckCommand(cmd['command']) #检查指令 result = GetResult(command) #获取执行结果 ReplyIng( cmd['ingID'], str(result) ) #@将结果回复到闪存 WriteLog('----------') #写入分割线 oldCmdIngID = cmd['ingID'] #更新上一条处理的闪存id else: pass time.sleep(2) #休息两秒 except: WriteLog('程序在处理指令时发生异常, 并已跳过异常!') except: WriteLog('程序发生未知异常, 已退出!') return 0
这样就算差不多完成了, 这个过程使用了一个全局的异常捕获并且在while内使用了一个异常捕获, while内出现的异常主动跳过, 如果在while外的就将程序主动挂掉, 再补一个完整版的:
#coding:utf-8 #!/usr/bin/python #------------------------------------------------------------------------------- # Name: CloudIng.py # Purpose: # # Author: wid # # Created: 17-11-2012 # Copyright: (c) wid 2012 # Licence: <your licence> #------------------------------------------------------------------------------- import re import sys import time import urllib import urllib2 import httplib import cookielib #日志写入 def WriteLog( strContent ): ''' 功能: 用于程序运行的日志记录 NOTE : WriteLog( string strContent ) -> None ''' currentTime = str( time.strftime("%Y-%m-%d %X", time.localtime()) ) #获取当前时间 try: #尝试写入日志 with open( "service.log", "a" ) as flog: flog.writelines( "%s : %s \r\n"%(currentTime , strContent) ) except: #当日志无法写入时在errors.log日志中写入一条错误报告 with open( "errors.log", "a" ) as ferr: ferr.writelines( "%s : 日志文件写入出错.\r\n"%currentTime ) #登录博客园 def LoginCnblogs( username, pwd ): ''' 功能: 登录博客园 NOTE: LoginCnblogs( string username, string pwd ) -> int Login Succeed -> return 1 Login Failed -> return 0 Other Exception -> return -1 ''' params_post = urllib.urlencode({ '__VIEWSTATE': r'/wEPDwULLTE1MzYzODg2NzZkGAEFHl9fQ29udHJvbHNSZXF1aXJlUG9zdEJhY2tLZXlfXxYBBQtjaGtSZW1lbWJlcm1QYDyKKI9af4b67Mzq2xFaL9Bt', '__EVENTVALIDATION': r'/wEWBQLWwpqPDQLyj/OQAgK3jsrkBALR55GJDgKC3IeGDE1m7t2mGlasoP1Hd9hLaFoI2G05', 'tbUserName': username, 'tbPassword': pwd, 'btnLogin' : '登录' }) try: #尝试登录园子 cookie = cookielib.CookieJar() opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie)) urllib2.install_opener(opener) login_response = urllib2.urlopen( 'http://passport.cnblogs.com/login.aspx?', params_post ) txt = login_response.read() #读取返回数据 if ( txt.find("编辑个人资料") != -1 ): return 1 #成功登陆返回1 else: return 0 #用户名或密码错误返回0 except: return -1 #其他错误导致的登录失败返回-1 #获取指令 def GetIngCommand(): ''' 功能: 从闪存中获取指令 NOTE: GetIngCommand(None) -> dict cmd Succeed : return cmd{'username':username, 'command':command, 'ingID':ingID} Failed : return None ''' username = re.compile('/u/(.*)/"') #匹配用户名的正则表达式 ingID = re.compile('[1-9][0-9]{5,}') #匹配闪存id的正则表达式 response = urllib.urlopen( 'http://home.cnblogs.com/ing/mobile/home' ) ingTxt = response.readlines() cmd = {} #使用字典记录请求的信息 for i in range(len(ingTxt)): if ingTxt[i].find('@犇犇') != -1: #如果在这行找到了"@wid"字符 try: cmd['username'] = username.findall(ingTxt[i-1])[0] #在上一行找用户名 cmd['command'] = ingTxt[i][ingTxt[i].index('</a>') + len('</a>') + 1 : ingTxt[i].index('</span>') ] #在当前行找到指令 cmd['ingID'] = ingID.findall(ingTxt[i])[0] #在下一行寻找该条闪存的id return cmd except: return None #检查过滤恶意指令 def CheckCommand( command ): if "'" in command: command = command.replace("'", "\"") elif """ in command: command = command.replace(""", "\"") elif ";" in command: command = command.replace(";", " ") return command #回复闪存 def ReplyIng( ingID, content ): ''' 功能: 回复闪存 NOTE: ReplyIng( string ingID, string content ) -> None ''' try: post_url = 'http://home.cnblogs.com/ajax/ing/PostComment' params_post = urllib.urlencode({ 'ContentId': ingID, 'Content': content }) pubIng_response = urllib2.urlopen( post_url, params_post ) responseInf = pubIng_response.read() if responseInf.find('"IsSuccess":true') != -1: #检测是否回复成功 WriteLog("闪存ID : %s, 回复内容: %s"%(ingID, content)) else: WriteLog("该闪存回复失败 : 闪存ID : %s, 回复内容: %s"%(ingID, content)) except: WriteLog("其他异常导致回复失败 : 闪存ID : %s, 回复内容: %s"%(ingID, content)) #执行指令并获取执行结果 def GetResult( command ): ''' 功能: 执行指令并获取执行结果 NOTE: GetResult( string command ) -> None ''' safeModule = "import math; import base64; print " #为用户加载合法模块 oldStdout = sys.stdout sys.stdout = open("result.txt", "w") #重定向输出到文件 try: exec( safeModule + command ) #执行远程指令 sys.stdout = oldStdout with open("result.txt", 'r') as f: result = f.read() return result except: sys.stdout = oldStdout return '指令有误' def Start(): ''' 功能: 组装函数, 使其能够向用户提供服务 NOTE: Start(None) -> None ''' WriteLog('服务已启动!') try: if LoginCnblogs('mr_wid', 'miao17hd') != 1: WriteLog('无法登录博客园!') return 0 oldCmdIngID = '' #用来记录上一条处理的闪存id while True: try: cmd = GetIngCommand() if cmd != None and cmd['ingID'] != oldCmdIngID: WriteLog( "用户名: %s , 闪存ID: %s , 指令: %s"%(cmd['username'],cmd['ingID'], cmd['command'] ) ) #写入日志文件 command = CheckCommand(cmd['command']) #检查指令 result = GetResult(command) #获取执行结果 ReplyIng( cmd['ingID'], str(result) ) #@将结果回复到闪存 WriteLog('----------') #写入分割线 oldCmdIngID = cmd['ingID'] #更新上一条处理的闪存id else: pass time.sleep(2) #休息两秒 except: WriteLog('程序在处理指令时发生异常, 并已跳过异常!') except: WriteLog('程序发生未知异常, 已退出!') return 0 if __name__ == '__main__': Start()
测试的情况如下:
接下来就是把它挂到阿里云上, 一块钱也不能白花。
如果现在程序没挂掉那么应该已经开始提供服务了, 大家可到闪存那测试下。传送门: http://home.cnblogs.com/ing/
支持的运算:
1>. 常规的数学表达式;
2>. Python中math模块中的所有函数, 调用示例: @wid math.sqrt(100)
3>. Python中base64模块中的所有函数, 调用示例: @wid base64.encodestring("对字符串进行base64编码")
总结:
你看, 有云, 有计算, 云计算, 我会乱说?