序
某一天上着多核程序设计的课上,无聊刷rss,偶然看见一片文章,一个哥们用微信公众帐号自动回复接口和php写了一个自动翻译的机器人,让我眼前一亮,我决定自己动手做一个,技术上选了Django,服务器用的是SAE。
==Update==
本项目源码:https://github.com/liushuaikobe/littlesnail
Fork me:https://github.com/liushuaikobe
准备工作
Sina App Engine
首先作为一个屌丝开发者要解决服务器的问题。去SAE的官网上用新浪微博的帐号注册一个帐号,成功后会赠给你500云豆,可供一个开发者试用大概5天。SAE非常优秀,如果以后我们想在上面写点应用什么的,可以去申请实名认证和开发者认证,那样每个月都会给你一定数量的云豆,应该能满足日常需求。由于现在SAE上部署Python还处于公测阶段,因此我们要去申请开通可以在上面部署Python程序的权限,现在很好申请的,不一会就会收到已经为你开放了部署Python应用权限的邮件,网上搜到的啥啥还需要排队都是过去了(不排除当你看到这篇文章时SAE已经可以允许所有开发者部署Python的应用了)。
OK,完成这些后,就可以到SAE的文档中心读文档来照着文档里面的样例创建一个应用了。
有道API
然后,去有道API申请一个key,申请的时候网站地址随便填就行。有道API非常简单,直接以GET的形式把要翻译的文本发送到指定的url,然后它会给我们回复翻译结果,我们可以选择xml、json等返回格式,我选得是xml,接着,在浏览器里面按着指定的格式输入url,就可以看到返回结果啦:
- <?xml version="1.0" encoding="UTF-8"?>
- <youdao-fanyi>
- <errorCode>0</errorCode>
- <query><![CDATA[这里是有道翻译API]]></query>
- <!-- 有道翻译 -->
- <translation>
- <paragraph><![CDATA[Here is the youdao translation API]]></paragraph>
- </translation>
- </youdao-fanyi>
<?xml version="1.0" encoding="UTF-8"?>
<youdao-fanyi>
<errorCode>0</errorCode>
<query><![CDATA[这里是有道翻译API]]></query>
<!-- 有道翻译 -->
<translation>
<paragraph><![CDATA[Here is the youdao translation API]]></paragraph>
</translation>
</youdao-fanyi>
注意,如果是对词进行翻译的话有的词还会返回一些啥网络释义,基本释义啥的,具体对这个xml解析的方法请看下面的代码。
微信公众帐号
接着,我们要去微信的公众帐号平台去申请一个公众帐号,不能用现有的已绑定私人微信帐号的QQ号申请,我用的是一个平时不用的QQ号申请的,申请成功后,可以大致看看微信公众帐号的管理平台(现在你知道那些公众帐号,比如王力宏的帐号啥的是怎么运作的了吧),接着去这里仔细阅读微信公众帐号自动回复开放接口的文档,你要从这里学一种如何让用户认证的思想(就是如果用户做了XX,给我返回了XX结果,那么我就能确定,用户是“合法”的),或者认证的方法。大致有一个认识后,赶紧下载他给的样例php源码,也是唯一的可以参考的源码,仔细阅读,如下:
- <?php
- /**
- * wechat php test
- */
- //define your token
- define("TOKEN", "weixin");
- $wechatObj = new wechatCallbackapiTest();
- $wechatObj->valid();
- class wechatCallbackapiTest
- {
- public function valid()
- {
- $echoStr = $_GET["echostr"];
- //valid signature , option
- if($this->checkSignature()){
- echo $echoStr;
- exit;
- }
- }
- public function responseMsg()
- {
- //get post data, May be due to the different environments
- $postStr = $GLOBALS["HTTP_RAW_POST_DATA"];
- //extract post data
- if (!empty($postStr)){
- echoStr
- $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
- $fromUsername = $postObj->FromUserName;
- $toUsername = $postObj->ToUserName;
- $keyword = trim($postObj->Content);
- $time = time();
- $textTpl = "<xml>
- <ToUserName><![CDATA[%s]]></ToUserName>
- <FromUserName><![CDATA[%s]]></FromUserName>
- <CreateTime>%s</CreateTime>
- <MsgType><![CDATA[%s]]></MsgType>
- <Content><![CDATA[%s]]></Content>
- <FuncFlag>0</FuncFlag>
- </xml>";
- if(!empty( $keyword ))
- {
- $msgType = "text";
- $contentStr = "Welcome to wechat world!";
- $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
- echo $resultStr;
- }else{
- echo "Input something...";
- }
- }else {
- echo "";
- exit;
- }
- }
- private function checkSignature()
- {
- $signature = $_GET["signature"];
- $timestamp = $_GET["timestamp"];
- $nonce = $_GET["nonce"];
- $token = TOKEN;
- $tmpArr = array($token, $timestamp, $nonce);
- sort($tmpArr);
- $tmpStr = implode( $tmpArr );
- $tmpStr = sha1( $tmpStr );
- if( $tmpStr == $signature ){
- return true;
- }else{
- return false;
- }
- }
- }
- ?>
<?php
/**
* wechat php test
*/
//define your token
define("TOKEN", "weixin");
$wechatObj = new wechatCallbackapiTest();
$wechatObj->valid();
class wechatCallbackapiTest
{
public function valid()
{
$echoStr = $_GET["echostr"];
//valid signature , option
if($this->checkSignature()){
echo $echoStr;
exit;
}
}
public function responseMsg()
{
//get post data, May be due to the different environments
$postStr = $GLOBALS["HTTP_RAW_POST_DATA"];
//extract post data
if (!empty($postStr)){
echoStr
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$fromUsername = $postObj->FromUserName;
$toUsername = $postObj->ToUserName;
$keyword = trim($postObj->Content);
$time = time();
$textTpl = "<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%s</CreateTime>
<MsgType><![CDATA[%s]]></MsgType>
<Content><![CDATA[%s]]></Content>
<FuncFlag>0</FuncFlag>
</xml>";
if(!empty( $keyword ))
{
$msgType = "text";
$contentStr = "Welcome to wechat world!";
$resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
echo $resultStr;
}else{
echo "Input something...";
}
}else {
echo "";
exit;
}
}
private function checkSignature()
{
$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$token = TOKEN;
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr);
$tmpStr = implode( $tmpArr );
$tmpStr = sha1( $tmpStr );
if( $tmpStr == $signature ){
return true;
}else{
return false;
}
}
}
?>
很简单吧,以至于被一些人说写的很水,但是我觉得,这份php源码还是很有含金量的;网上一些哥们还抱怨啥啥的直接部署样例php不能运行啥的,拜托,有点职业精神好不好,连我这个之前完全不会php的人都能看出来要调用里面的responseMsg( )方法才能实现自动回复,样例里面的只是调用了微信接入的认证功能的函数。
上面的代码写的很好,不需要我多解释,相信大家能看出来它是怎么工作的。
设计与实现
接着就可以实现我们自己的应用了,首先把我们在SAE上创建的应用通过SVN检出到本地,然后切换到检出的目录,用Django的命令创建一个应用,目录结构如下:
其中,index.wsgi和config.yaml是SAE规定的文件,具体请仔细阅读SAE的文档。
之后就可以编写我们自己的服务端代码了,大致思想就是:用户A向公众帐号发送一条消息,微信平台会按着公众帐号预先的设置,把用户A的消息内容和一些其他信息(如创建时间等)以xml的形式post到我们预先设置好的url上(这个url的服务端就是我们要写的在SAE上的应用),我们要做的就是每当接受到微信post请求,我们解析微信平台post过来的xml,得到用户A的消息内容,把消息内容以get的形式发送到有道API,获取有道API返回的xml(或json等),解析,之后按微信平台规定的格式构造成一个xml,作为微信平台post请求的结果给其返回,微信平台收到结果后,会把消息自动回复给用户,用户就能收到翻译结果了。
用一个图表示上述过程如下:
源码
下面贴出逻辑处理部分代码如下(Views.py),各函数功能不言而喻:
- # -*- coding: utf-8 -*-
- from django.http import HttpResponse
- from django.template import RequestContext, Template
- from django.views.decorators.csrf import csrf_exempt
- from django.utils.encoding import smart_str, smart_unicode
- import xml.etree.ElementTree as ET
- import urllib,urllib2,time,hashlib
- TOKEN = "你设置的Token"
- YOUDAO_KEY = 你申请到的有道的Key
- YOUDAO_KEY_FROM = "有道的key-from"
- YOUDAO_DOC_TYPE = "xml"
- @csrf_exempt
- def handleRequest(request):
- if request.method == 'GET':
- #response = HttpResponse(request.GET['echostr'],content_type="text/plain")
- response = HttpResponse(checkSignature(request),content_type="text/plain")
- return response
- elif request.method == 'POST':
- #c = RequestContext(request,{'result':responseMsg(request)})
- #t = Template('{{result}}')
- #response = HttpResponse(t.render(c),content_type="application/xml")
- response = HttpResponse(responseMsg(request),content_type="application/xml")
- return response
- else:
- return None
- def checkSignature(request):
- global TOKEN
- signature = request.GET.get("signature", None)
- timestamp = request.GET.get("timestamp", None)
- nonce = request.GET.get("nonce", None)
- echoStr = request.GET.get("echostr",None)
- token = TOKEN
- tmpList = [token,timestamp,nonce]
- tmpList.sort()
- tmpstr = "%s%s%s" % tuple(tmpList)
- tmpstr = hashlib.sha1(tmpstr).hexdigest()
- if tmpstr == signature:
- return echoStr
- else:
- return None
- def responseMsg(request):
- rawStr = smart_str(request.raw_post_data)
- #rawStr = smart_str(request.POST['XML'])
- msg = paraseMsgXml(ET.fromstring(rawStr))
- queryStr = msg.get('Content','You have input nothing~')
- raw_youdaoURL = "http://fanyi.youdao.com/openapi.do?keyfrom=%s&key=%s&type=data&doctype=%s&version=1.1&q=" % (YOUDAO_KEY_FROM,YOUDAO_KEY,YOUDAO_DOC_TYPE)
- youdaoURL = "%s%s" % (raw_youdaoURL,urllib2.quote(queryStr))
- req = urllib2.Request(url=youdaoURL)
- result = urllib2.urlopen(req).read()
- replyContent = paraseYouDaoXml(ET.fromstring(result))
- return getReplyXml(msg,replyContent)
- def paraseMsgXml(rootElem):
- msg = {}
- if rootElem.tag == 'xml':
- for child in rootElem:
- msg[child.tag] = smart_str(child.text)
- return msg
- def paraseYouDaoXml(rootElem):
- replyContent = ''
- if rootElem.tag == 'youdao-fanyi':
- for child in rootElem:
- # 错误码
- if child.tag == 'errorCode':
- if child.text == '20':
- return 'too long to translate\n'
- elif child.text == '30':
- return 'can not be able to translate with effect\n'
- elif child.text == '40':
- return 'can not be able to support this language\n'
- elif child.text == '50':
- return 'invalid key\n'
- # 查询字符串
- elif child.tag == 'query':
- replyContent = "%s%s\n" % (replyContent, child.text)
- # 有道翻译
- elif child.tag == 'translation':
- replyContent = '%s%s\n%s\n' % (replyContent, '-' * 3 + u'有道翻译' + '-' * 3, child[0].text)
- # 有道词典-基本词典
- elif child.tag == 'basic':
- replyContent = "%s%s\n" % (replyContent, '-' * 3 + u'基本词典' + '-' * 3)
- for c in child:
- if c.tag == 'phonetic':
- replyContent = '%s%s\n' % (replyContent, c.text)
- elif c.tag == 'explains':
- for ex in c.findall('ex'):
- replyContent = '%s%s\n' % (replyContent, ex.text)
- # 有道词典-网络释义
- elif child.tag == 'web':
- replyContent = "%s%s\n" % (replyContent, '-' * 3 + u'网络释义' + '-' * 3)
- for explain in child.findall('explain'):
- for key in explain.findall('key'):
- replyContent = '%s%s\n' % (replyContent, key.text)
- for value in explain.findall('value'):
- for ex in value.findall('ex'):
- replyContent = '%s%s\n' % (replyContent, ex.text)
- replyContent = '%s%s\n' % (replyContent,'--')
- return replyContent
- def getReplyXml(msg,replyContent):
- extTpl = "<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[%s]]></MsgType><Content><![CDATA[%s]]></Content><FuncFlag>0</FuncFlag></xml>";
- extTpl = extTpl % (msg['FromUserName'],msg['ToUserName'],str(int(time.time())),'text',replyContent)
- return extTpl
# -*- coding: utf-8 -*-
from django.http import HttpResponse
from django.template import RequestContext, Template
from django.views.decorators.csrf import csrf_exempt
from django.utils.encoding import smart_str, smart_unicode
import xml.etree.ElementTree as ET
import urllib,urllib2,time,hashlib
TOKEN = "你设置的Token"
YOUDAO_KEY = 你申请到的有道的Key
YOUDAO_KEY_FROM = "有道的key-from"
YOUDAO_DOC_TYPE = "xml"
@csrf_exempt
def handleRequest(request):
if request.method == 'GET':
#response = HttpResponse(request.GET['echostr'],content_type="text/plain")
response = HttpResponse(checkSignature(request),content_type="text/plain")
return response
elif request.method == 'POST':
#c = RequestContext(request,{'result':responseMsg(request)})
#t = Template('{{result}}')
#response = HttpResponse(t.render(c),content_type="application/xml")
response = HttpResponse(responseMsg(request),content_type="application/xml")
return response
else:
return None
def checkSignature(request):
global TOKEN
signature = request.GET.get("signature", None)
timestamp = request.GET.get("timestamp", None)
nonce = request.GET.get("nonce", None)
echoStr = request.GET.get("echostr",None)
token = TOKEN
tmpList = [token,timestamp,nonce]
tmpList.sort()
tmpstr = "%s%s%s" % tuple(tmpList)
tmpstr = hashlib.sha1(tmpstr).hexdigest()
if tmpstr == signature:
return echoStr
else:
return None
def responseMsg(request):
rawStr = smart_str(request.raw_post_data)
#rawStr = smart_str(request.POST['XML'])
msg = paraseMsgXml(ET.fromstring(rawStr))
queryStr = msg.get('Content','You have input nothing~')
raw_youdaoURL = "http://fanyi.youdao.com/openapi.do?keyfrom=%s&key=%s&type=data&doctype=%s&version=1.1&q=" % (YOUDAO_KEY_FROM,YOUDAO_KEY,YOUDAO_DOC_TYPE)
youdaoURL = "%s%s" % (raw_youdaoURL,urllib2.quote(queryStr))
req = urllib2.Request(url=youdaoURL)
result = urllib2.urlopen(req).read()
replyContent = paraseYouDaoXml(ET.fromstring(result))
return getReplyXml(msg,replyContent)
def paraseMsgXml(rootElem):
msg = {}
if rootElem.tag == 'xml':
for child in rootElem:
msg[child.tag] = smart_str(child.text)
return msg
def paraseYouDaoXml(rootElem):
replyContent = ''
if rootElem.tag == 'youdao-fanyi':
for child in rootElem:
# 错误码
if child.tag == 'errorCode':
if child.text == '20':
return 'too long to translate\n'
elif child.text == '30':
return 'can not be able to translate with effect\n'
elif child.text == '40':
return 'can not be able to support this language\n'
elif child.text == '50':
return 'invalid key\n'
# 查询字符串
elif child.tag == 'query':
replyContent = "%s%s\n" % (replyContent, child.text)
# 有道翻译
elif child.tag == 'translation':
replyContent = '%s%s\n%s\n' % (replyContent, '-' * 3 + u'有道翻译' + '-' * 3, child[0].text)
# 有道词典-基本词典
elif child.tag == 'basic':
replyContent = "%s%s\n" % (replyContent, '-' * 3 + u'基本词典' + '-' * 3)
for c in child:
if c.tag == 'phonetic':
replyContent = '%s%s\n' % (replyContent, c.text)
elif c.tag == 'explains':
for ex in c.findall('ex'):
replyContent = '%s%s\n' % (replyContent, ex.text)
# 有道词典-网络释义
elif child.tag == 'web':
replyContent = "%s%s\n" % (replyContent, '-' * 3 + u'网络释义' + '-' * 3)
for explain in child.findall('explain'):
for key in explain.findall('key'):
replyContent = '%s%s\n' % (replyContent, key.text)
for value in explain.findall('value'):
for ex in value.findall('ex'):
replyContent = '%s%s\n' % (replyContent, ex.text)
replyContent = '%s%s\n' % (replyContent,'--')
return replyContent
def getReplyXml(msg,replyContent):
extTpl = "<xml><ToUserName><![CDATA[%s]]></ToUserName><FromUserName><![CDATA[%s]]></FromUserName><CreateTime>%s</CreateTime><MsgType><![CDATA[%s]]></MsgType><Content><![CDATA[%s]]></Content><FuncFlag>0</FuncFlag></xml>";
extTpl = extTpl % (msg['FromUserName'],msg['ToUserName'],str(int(time.time())),'text',replyContent)
return extTpl
之后通过SVN把项目部署到SAE上,就OK啦~
遇到的问题
现在网上这种参考的代码还很少,在SAE上部署调试也非常困难,无奈下我自己写了个脚本,模仿微信平台给自己部署在SAE上的服务端POST消息,看返回的结果。如果出现错误,Django都会产生一个优美的错误页面,获取这个错误页面把它写到本地的一个html里面,用浏览器打开就可以知道是什么错误了。
写的过程中还是遇到不少问题的:
1.Django的CSRF错误:
我用的Django 1.4,我尝试了大家说的很多解决办法都会出现403错误,无奈下只能暂时通过修饰符把Django的CSRF暂时禁掉,这个还要以后学Django的深入调研一下;
2.Django的编码错误:
我也尝试了很多方法,但是都不行,主要是中文处理上,遇到了很多麻烦,最终在这里找到了完美的解决方案,用可爱的Django自带的可爱的方法:smart_str、smart_unicode,就能完美处理中文了。
成果截图
下面上图:
欢迎试用
怎么样,还不错呃,欢迎大家试用(如果没反应,很可能是SAE没有云豆了):
如果这篇文章对你有用,请顶一下,欢迎讨论。
注:转载请注明出处(本文地址:http://blog.csdn.net/liushuaikobe/article/details/8453716)。