微信公众开发——实现功能

承启

由于以前设计了要实现一个微信机器人,以向用户响应响应的文章,这个app十分地简单,并不须要特别深刻的设计,并且个人想法是:拿来主义, 反正github上面那么多的用python写的博客系统,我只须要实现微信响应的部分,也就是从数据库中获取文章的数据,而后将文章的标题,url,图片等信息打包成xml格式返回给微信服务器,服务器再返回给用户。并且我发现,有菜单的会好不少,就像一个完整的app,能够直接点击察看某篇文章,而不是硬邦邦的回复。我是用别人写的一个博客系统进行改造——saepy-log.而这个博客系统又是基于tornado框架的,原本不打算染指tornado的,可是不得不硬着头皮钻研。其中遇到了不少困难,在sql语句的like写法,察看文档方面有了比较大的收获。python


部署与开发

事先说明,因为我是各类折腾,因此可能照本篇文章作是作不成的。下载了saepy-log的源码后,按照这里的操做进行上传后,就能够将博客系统安装在sae平台上了,而后用svn把代码同步下来到本地工做目录,一切准备就绪。咱们会看到咱们的博客已经能够访问了,并且咱们本地还有博客的全部代码:git


咱们要修改的是blog.py是博客的核心功能所在,还有modle.py是数据模型的关键所在,咱们将要扩展数据模型功能,使之完成咱们的微信功能。github


在blog.py里面添加咱们的微信功能类 weixin.py(因为是用tornado框架,因此方法与在django里面略有不一样):web

导入须要用到的包sql

# weixin used package
import xml.etree.ElementTree as ET
import urllib,urllib2,time,hashlib
                                              
import tornado.wsgi
import tornado.escape

主要是xml的解析和一些处理字符串的包,接下来咱们定义weixin类的主体:数据库

# 添加微信推送账号
class WeiXinPoster(BaseHandler):
    #-----------------------------------------------------------------------
    # 处理get方法 对应check_signature
    def get(self):
        global TOKEN
        signature = self.get_argument("signature")
        timestamp = self.get_argument("timestamp")
        nonce = self.get_argument("nonce")
        echoStr = self.get_argument("echostr")
        token = TOKEN
        tmpList = [token,timestamp,nonce]
        tmpList.sort()
        tmpstr = "%s%s%s" % tuple(tmpList)
        tmpstr = hashlib.sha1(tmpstr).hexdigest()
                                             
        if tmpstr == signature:
            self.write(echoStr)
            #return echoStr
        else:
            self.write(None);
            #return None
                                                         
    # 处理post方法,对应response_msg
    def post(self):
        global SORRY
        # 从request中获取请求文本
        rawStr = self.request.body
        # 将文本进行解析,获得请求的数据
        msg = self.parse_request_xml(ET.fromstring(rawStr))
        # 根据请求消息来处理内容返回
        query_str = msg.get("Content")
        query_str = tornado.escape.utf8(query_str)
        # TODO 用户发来的数据类型可能多样,因此须要判别
        response_msg = ""
        return_data = ""
        # 使用简单的处理逻辑,有待扩展
        if query_str[0] == "h":     # send help menu to user
            response_msg = self.get_help_menu()     # 返回消息
            # 包括post_msg,和对应的 response_msg
            if response_msg:
                return_data = self.pack_text_xml(msg, response_msg)
            else:
                response_msg = SORRY
                return_data = self.pack_text_xml(msg, response_msg)
            self.write(return_data)
        # 分类
        elif query_str[0] =="c":
            category = query_str[1:]
            response_msg = self.get_category_articles(category)
            if response_msg:
                return_data = self.pack_news_xml(msg, response_msg)
            else:
                response_msg = SORRY
                return_data = self.pack_text_xml(msg, response_msg)
            self.write(return_data)
        # 列出文章列表 
        elif query_str[0] =="l":
            response_msg = self.get_article_list()
            if response_msg:
                return_data = self.pack_text_xml(msg, response_msg)
            else:
                response_msg = SORRY
                return_data = self.pack_text_xml(msg, response_msg)
            self.write(return_data)
        # 直接获取某篇文章
        elif query_str[0] == "a":
            # 直接获取文章的id,而后在数据库中查询
            article_id = int(query_str[1:])
            # 进行操做
            response_msg = self.get_response_article_by_id(article_id)
            if response_msg:
                return_data = self.pack_news_xml(msg, response_msg)
            else:
                response_msg = SORRY
                return_data = self.pack_text_xml(msg, response_msg)
            self.write(return_data)
                                                         
        # 还要考虑其余
        elif query_str[0] == "s":
            keyword = str(query_str[1:])
            # 搜索关键词,返回相关文章
            response_msg = self.get_response_article(keyword)
            # 返回图文信息
            if response_msg:
                return_data = self.pack_news_xml(msg, response_msg)
            else:
                response_msg = SORRY
                return_data = self.pack_text_xml(msg, response_msg)
            self.write(return_data)
                                                         
        elif query_str[0] == "n":
            response_msg = self.get_latest_articles()
            # 返回图文信息
            if response_msg:
                return_data = self.pack_news_xml(msg, response_msg)
            else:
                response_msg = SORRY
                return_data = self.pack_text_xml(msg, response_msg)
            self.write(return_data)
        # 若是找不到,返回帮助信息
        else:
            response_msg = get_help_menu()
            if response_msg:
                return_data = response_msg
            else:
                return_data = SORRY
            self.write(return_data)
    # n for 获取最新的文章
    def get_latest_articles(self):
        global MAX_ARTICLE
        global PIC_URL
        article_list = Article.get_articles_by_latest()
        article_list_length = len(article_list)
        count = (article_list_length < MAX_ARTICLE) and article_list_length or MAX_ARTICLE
        if article_list:
            # 构造图文消息
            articles_msg = {'articles':[]}
            for i in range(0,count):
                article = {
                        'title': article_list[i].slug,
                        'description':article_list[i].description,
                        'picUrl':PIC_URL,
                        'url':article_list[i].absolute_url
                    }
                # 插入文章
                articles_msg['articles'].append(article)
                article = {}
            # 返回文章
            return articles_msg
                                                 
    #-----------------------------------------------------------------------
    # 解析请求,拆解到一个字典里     
    def parse_request_xml(self,root_elem):
        msg = {}
        if root_elem.tag == 'xml':
            for child in root_elem:
                msg[child.tag] = child.text  # 得到内容
            return msg
                                             
    #-----------------------------------------------------------------------
    def get_help_menu(self):
        menu_msg = '''欢迎关注南苑随笔,在这里你能得到关于校园的资讯和故事。回复以下按键则能够完成获得相应的回应
        h :帮助(help)
        l :文章列表(article list)
        f : 得到分类列表
        n : 获取最新文章
        a + 数字 :察看某篇文章 a2 察看第2篇文章
        s + 关键字 : 搜索相关文章 s科研 察看科研相关
        c + 分类名 : 获取分类文章 c校园生活 察看校园生活分类
        其余 : 功能有待丰富'''
        return menu_msg
    #-----------------------------------------------------------------------
    # 获取文章列表
    def get_article_list(self):
        # 查询数据库获取文章列表
        article_list = Article.get_all_article_list()
        article_list_str = "最新文章列表供您点阅,回复a+数字便可阅读: \n"
        for i in range(len(article_list)):
            art_id = str(article_list[i].id)
            art_id = tornado.escape.native_str(art_id)
                                                         
            art_title = article_list[i].title
            art_title = tornado.escape.native_str(art_title)
                                                         
            art_category = article_list[i].category
            art_category = tornado.escape.native_str(art_category)
                                                         
                                                         
            article_list_str +=  art_id + ' ' + art_title + ' ' + art_category + '\n'
        return article_list_str
                                                     
    # 按照分类查找
    def get_category_articles(self, category):
        global MAX_ARTICLE
        global PIC_URL
        article_list = Article.get_articles_by_category(category)
        article_list_length = len(article_list)
        count = (article_list_length < MAX_ARTICLE) and article_list_length or MAX_ARTICLE
        if article_list:
            # 构造图文消息
            articles_msg = {'articles':[]}
            for i in range(0,count):
                article = {
                        'title': article_list[i].slug,
                        'description':article_list[i].description,
                        'picUrl':PIC_URL,
                        'url':article_list[i].absolute_url
                    }
                # 插入文章
                articles_msg['articles'].append(article)
                article = {}
            # 返回文章
            return articles_msg
    #-----------------------------------------------------------------------
    # 获取用于返回的msg
    def get_response_article(self, keyword):
        global PIC_URL
        keyword = str(keyword)
        # 从数据库查询获得若干文章
        article = Article.get_article_by_keyword(keyword)
        # 这里先用测试数据
        if article:
            title = article.slug
            description = article.description
            picUrl = PIC_URL
            url = article.absolute_url
            count = 1
            # 也有多是若干篇
            # 这里实现相关逻辑,从数据库中获取内容
                                                         
            # 构造图文消息
            articles_msg = {'articles':[]}
            for i in range(0,count):
                article = {
                        'title':title,
                        'description':description,
                        'picUrl':picUrl,
                        'url':url
                    }
                # 插入文章
                articles_msg['articles'].append(article)
                article = {}
            # 返回文章
            return articles_msg
        else:
            return
                                                 
    def get_response_article_by_id(self, post_id):
        global PIC_URL
        # 从数据库查询获得若干文章
        article = Article.get_article_by_id_detail(post_id)
        # postId为文章id
        if article:
            title = article.slug
            description = article.description
            picUrl = PIC_URL
            url = article.absolute_url
            count = 1
            # 这里实现相关逻辑,从数据库中获取内容
                                                         
            # 构造图文消息
            articles_msg = {'articles':[]}
            for i in range(0,count):
                article = {
                        'title':title,
                        'description':description,
                        'picUrl':picUrl,
                        'url':url
                    }
                # 插入文章
                articles_msg['articles'].append(article)
                article = {}
            # 返回文章
            return articles_msg
        else:
            return


可见app的难度并不大,仍是和上次使用微信API里面的和接近,须要用到的几个全局变量,须要本身定义,如token,如PIC_URL等。程序原理其实就是解析用户请求,h开头,则提供帮助菜单,a开头加数字的,就提供某篇文章,等等,而后提供相应的函数进行处理,这里面说明起来比较复杂,就以获取分类文章来说吧:django

须要用到分析用户请求字符串:api

# 分类
        elif query_str[0] =="c":
            category = query_str[1:]
            response_msg = self.get_category_articles(category)
            if response_msg:
                return_data = self.pack_news_xml(msg, response_msg)
            else:
                response_msg = SORRY
                return_data = self.pack_text_xml(msg, response_msg)
            self.write(return_data)

这里要提供get_category_articles(category)的功能,因此须要在weixin类里面实现一个这样的函数:数组

# 按照分类查找
    def get_category_articles(self, category):
        global MAX_ARTICLE
        global PIC_URL
        article_list = Article.get_articles_by_category(category)
        article_list_length = len(article_list)
        count = (article_list_length < MAX_ARTICLE) and article_list_length or MAX_ARTICLE
        if article_list:
            # 构造图文消息
            articles_msg = {'articles':[]}
            for i in range(0,count):
                article = {
                        'title': article_list[i].slug,
                        'description':article_list[i].description,
                        'picUrl':PIC_URL,
                        'url':article_list[i].absolute_url
                    }
                # 插入文章
                articles_msg['articles'].append(article)
                article = {}
            # 返回文章
            return articles_msg

很显然,这里须要和数据库模型Article打交道,看看Article是否实现了该功能,很遗憾没有,那么咱们只好卷起袖子本身干——扩展Article,因此咱们转战到modle.py文件里面了,写下了以下的代码:安全

# 返回一个包含若干篇文章的数组 limit 5
    def get_articles_by_category(self, category):
        sdb._ensure_connected()
        article_list = sdb.query('SELECT * FROM `sp_posts` WHERE `category` = %s LIMIT 5', str(category))
        for i in range(len(article_list)):
            article_list[i] = post_detail_formate(article_list[i])
        return article_list

这里进行了数据库查询,将category参数传递进去,选择category为参数的5篇文章,打包返回。post_detail_formate是博客系统已经写好的,咱们只要拿来用就好了。在python里面,写sql语句要很当心,遇到要传入参数的,最好是用逗号分割开,而不是使用%来填充参数。特别是使用like的时候,咱们每每须要写这样的sql语句:

SELECT * FROM `sp_posts` WHERE `category` LIKE '%study%'

可是python里面又是用%s作参数占位符的,故而会引发不少没必要要的错误,好比这里。总之要安全使用,最好是做为参数传递,python会为他们与原字符串里的%等分开。


上传发布

完成功能后就能够发布了,虽然还有不少不足的地方,可是测试发现,仍是可使用的。本项目将开源到github中,欢迎使用(须要修改部分的参数,例如博客名称等),或者交流研究源代码。实现后的功能是这样字的:

好比回复一个a9,将返回第9篇文章;回复l,列出全部文章。吐槽一下咱们宿舍的网络,差到离谱,我是获取信息以后才截屏的。


该篇并非要将实现的全部细节披露出来,披露细节最好的方法就是公开源代码,接下来会发布到github上面去,只需稍微配置,你就能够拥有一个能够推送到微信的博客了(我不会告诉你wordpress早就出微信插件了)。通过这一番折腾,能够得出一个微信第三方开发的经验,那就是初步制定好本身的app须要提供什么功能,而后接着设计一个类,处理各类的请求,将各类处理部分封装起来,例如按照分类获取文章,或者获取文章列表等,而后在这里面调用数据库模型里面的功能来实现。这是MVC的一种便利之处。此外,还要敢于阅读源代码和英文文档,我以前google了不少字符串转换编码的方法,没有成功,可是阅读了tornado的文档以后,我恍然大悟。例如以前在django里面写这个应用的时候,咱们获取的仍是request.raw_post_data,如今改为了self.request.body,不看文档,想破头也想不出来。功能实现虽然告一段落,可是这个项目还有不少须要重写一次,使得它的表现更好,安全性更高。这里不得不提一下,服务器中采用tornado真的不是通常快~


END

by bibodeng 2013-05-08

相关文章
相关标签/搜索