本博客已停止维护,仅供浏览存档内容。了解详情 »

微信机器人开发记

前段时间给我的摄影网站 Camarts 注册了一个微信公众订阅号,方便给大家推送最新发布的作品。注册之后顺便看了看微信的开发者文档,才知道原来微信可以通过 API 将发送给公众号的信息以 POST 到自己指定的服务器上处理,再将服务器上返回的数据回复给发送者。这么一来,似乎可以实现发送关键词来查找相关照片的功能。虽然需要这个功能的人也许并不多,不过在好奇心的驱使下,我决定还是做一个来玩玩。

wechat-robot-header-image

配置 WordPress 环境

因为 Camarts 用的是 WordPress,所以要想方便地读取网站上的内容,就得先把它载入进来。方法很简单,引入根目录下的 wp-load.php 即可:


// 载入 WordPress
define('WP_USE_THEMES', false);
require_once('root/of/wordpress/wp-load.php'); // 随机输出几篇文章的标题测试一下
$query = array(
'showposts' => 9,
'orderby' => 'rand'
);
$the_query = new WP_Query( $query );
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
the_title();
}
}
wp_reset_postdata();

在浏览器上访问,可以看到已经顺利输出了九个标题,说明已经完成所有的环境配置,可以调用 WordPress 的所有功能了。接下来开始连接微信的 API。

配置微信接口

在绑定接入时,微信需要验证服务器配置。方法是将 TOKEN、当前时间戳(timestamp)和随机数(nonce)这三个参数做一个字典序排序,再拼接起来做 sha1 加密,生成一个 signature,最后与微信服务器上 GET 过来的 signature 进行比对。说起来麻烦,实现起来却很简单:


define("TOKEN", "这里可以随便填");
private function checkSignature(){
$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"]; $token = TOKEN;
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode( $tmpArr );
$tmpStr = sha1( $tmpStr ); if( $tmpStr == $signature ){
return true;
} else {
return false;
}
}

如果不确定这个代码的具体用法,可以参考微信公众平台开发者文档,这里不再展开。总之,第一行的 TOKEN 可以自己随便填,之后到微信后台的开发者中心里填入相同的 TOKEN 和这个 php 文件的 url 即可绑定:

201504281229

测试接收和返回微信消息

接口绑定成功之后,就可以试着处理一下来自微信的消息了:


public function responseMsg() {
$postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; if (!empty($postStr)){
libxml_disable_entity_loader(true);
$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 = $keyword;
$resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr);
echo $resultStr;
} else {
echo "Input something...";
} } else {
echo "";
exit;
}
}

其中 $keyword 变量就是微信公众号收到的内容,$contentStr 是要回复给发送者的内容,通过 PHP 的 sprintf() 函数写入到微信的 xml 格式模板 $textTpl 里输出返回。为了测试,我这里直接返回接收到的内容,试试看:

wechat-robot-1

接下来只需结合上面的两步,将从数据库中读取出来的数据按照微信的 xml 格式返回。将上面代码的第 21 行改成这个即可:


$query = array(
'showposts' => 3,
'orderby' => 'rand'
);
$the_query = new WP_Query( $query );
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
$contentStr .= get_the_title()."\n";
$contentStr .= "http://camarts.cn/".get_the_ID()."\n\n";
}
}
wp_reset_postdata();

试试看,可以顺利接收到三篇随机作品的标题和链接了:

wechat-robot-2

为了节约消息字数,我把网址尽可能的缩短——只用了“域名 + 页面 ID”,可这与我的伪静态设置不符,访问就直接 404 了。通常来说可以自己再写一条 URL 转发规则来处理,不过我灵机一动,用了一个简单粗暴的方式——直接在 404 页面里判断,然后用 Header 跳转:

$uri = substr("$_SERVER[REQUEST_URI]", 1);
if(is_numeric($uri)){
if(get_post_status($uri) == "publish" && get_post_type($uri) == "post"){
$permalink = get_permalink($uri);
Header("HTTP/1.1 303 See Other");
Header("Location: $permalink");
exit;
}
}

按照标签归档检索

Camarts 上发布的所有照片都按照拍摄地点加上了相应的标签,只要通过标签名来检索就可以很方便的找到相关作品:

$max_items = 6;
$tag_slug = get_term_by('name', $keyword, 'post_tag');
$query_tag = $tag_slug->slug; // 看看收到的信息是不是一个存在的标签
if($query_tag){
$query_by_tags = array(
'post_type' => 'post',
'posts_per_page' => $max_items,
'tag' => $query_tag
); $the_query = new WP_Query( $query_by_tags ); // 根据 tag 查找
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
$contentStr .= get_the_title()."\n";
$contentStr .= "http://camarts.cn/".get_the_ID()."\n\n";
}
$item_left = $the_query->found_posts - $max_items;
if($item_left > 0){
$contentStr .= "--------------------\n\n";
$contentStr .= "还有 $item_left 辑相关作品:\n";
$contentStr .= "http://camarts.cn/archives/tag/$query_tag";
}
} else {
// tag 存在,但没有相关的 post
$contentStr = "那个地方暂时还没有照片呢";
}
wp_reset_postdata();
}

现在发送一些地名,比如“西藏”或者“呼伦贝尔”,马上就收到了相关的链接,非常方便,心目中微信机器人的功能已经实现。
那么,还能不能做得更好?

自然语言的处理

只发个地名来没问题,可如果说的是“有没有在云南拍的照片?”或者“发几张四川九寨沟的照片来看看”之类的句子,机器人马上就哑巴了。自然语言的处理是个大学问,作为一个数学渣和算法盲,只好再用简单粗暴的方法来解决问题:


public function matchSentence($sentence, $max_items){
foreach (get_tags(array('number' => 0, 'orderby' => 'name', 'order' => 'DESC', 'hide_empty' => false)) as $tag){
if(strpos($sentence, $tag->name) !== false){
$contentStr = ""; $query_by_tags_in_sentence = array(
'post_type' => 'post',
'posts_per_page' => $max_items,
'tag_id' => $tag->term_id
); $the_query = new WP_Query($query_by_tags_in_sentence);
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
$contentStr .= get_the_title()."\n";
$contentStr .= "http://camarts.cn/".get_the_ID()."\n\n";
}
$item_left = $the_query->found_posts - $max_items;
if($item_left > 0){
$contentStr .= "--------------------\n\n";
$contentStr .= "还有 $item_left 辑相关作品:\n";
$contentStr .= "http://camarts.cn/archives/tag/".$tag->slug;
}
} else {
// tag 存在,但没有相关的 post
$contentStr = "你说的那个地方暂时还没有照片呢";
}
break;
wp_reset_postdata();
}
}
return $contentStr;
}

思路是遍历数据库中的所有标签,用 PHP 的 strpos() 函数来查找匹配。虽然效率可能不高,但还算勉强实现了功能,若有更好的方法,欢迎指教。

wechat-robot-3

按照标题检索

虽然通过拍摄地名标签来检索已经可以找到绝大多数作品,那如果想找某些特定主题的照片呢?比如日落、夜景或者瀑布等等,这些都是写在标题中的,可 WordPress 本身并不支持通过标题来检索,只能自己在主题的 functions.php 里写一个 filter 了:


add_filter('posts_where', 'title_like_posts_where', 10, 2);
function title_like_posts_where($where, &$wp_query) {
global $wpdb;
if ( $post_title_like = $wp_query->get('post_title_like')) {
$where .= ' AND ' . $wpdb->posts . '.post_title LIKE \'%' . esc_sql($wpdb->esc_like($post_title_like)) . '%\'';
}
return $where;
}

接下来就大同小异了:


public function matchByTitle($keyword, $max_items){
$query_by_title = array(
'post_type' => 'post',
'posts_per_page' => $max_items,
'post_title_like' => $keyword
); $the_query = new WP_Query( $query_by_title ); // The Loop
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
$contentStr .= get_the_title()."\n";
$contentStr .= "http://camarts.cn/".get_the_ID()."\n\n";
}
} else {
$contentStr = "好像找不到你想看的照片,换个关键词试试呗。";
}
wp_reset_postdata();
return $contentStr;
}

配置被关注时的欢迎信息

在微信的开发者中心启用了服务器配置后,原本在后台就可以设置的“被关注时自动回复”就被覆盖掉了,只能在自己的服务器上通过关注事件来回应:


public function responseMsg() {
$postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; if (!empty($postStr)){ $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$RX_TYPE = trim($postObj->MsgType); switch($RX_TYPE){
case "text":
// 处理普通文字信息
$resultStr = $this->handleText($postObj);
break;
case "event":
// 处理事件
$resultStr = $this->handleEvent($postObj);
break;
default:
$resultStr = "信息类型不支持:".$RX_TYPE;
break;
}
echo $resultStr;
} else {
echo "";
exit;
}
}
public function handleEvent($object){
$contentStr = "";
switch ($object->Event){
case "subscribe":
$contentStr = "欢迎关注 Camarts!\n\n";
$contentStr = "你可以在这里回复关键字找到想看的照片,比如中国省份或城市名“四川”、“甘肃”、“呼伦贝尔”,或者一些景点名称,例如“九寨沟”或“泸沽湖”等等。\n\n";
$contentStr .= "不知道想看什么?试试回复“随便”或者“最新”吧。";
break;
default :
$contentStr = "事件类型不支持:".$object->Event;
break;
}
$resultStr = $this->responseText($object, $contentStr);
return $resultStr;
}

现在的机器人功能已经基本完善了,于是开启了小范围的内测,根据小伙伴们的反馈,机器人比预想的还要好用。不过,还能不能再好一点呢?

回复图文消息

对于一个以图片为主的摄影网站来说,这短短的题目和冷冰冰的网址链接确实很难激起人们点开的欲望,若能以图文消息的方式回复,那就再好不过了。之前我一直以为图文消息只能回复预置在微信公众号后台的素材库里的内容,后来仔细看了遍官方文档之后,才发现图文信息的标题、简介、图片和跳转链接其实都是可以自由定义的,赶紧试试:


$newsTpl = "<xml>
<ToUserName><![CDATA[%s]]></ToUserName>
<FromUserName><![CDATA[%s]]></FromUserName>
<CreateTime>%s</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>1</ArticleCount>
<Articles>
<item>
<Title><![CDATA[%s]]></Title>
<Description><![CDATA[%s]]></Description>
<PicUrl><![CDATA[%s]]></PicUrl>
<Url><![CDATA[%s]]></Url>
</item>
</Articles>
</xml>"; $query_rand = array(
'showposts' => 1,
'orderby' => 'rand'
);
$the_query = new WP_Query( $query_rand );
if ($the_query->have_posts()) {
while ($the_query->have_posts()) {
$the_query->the_post();
$title = get_the_title();
$description = strip_tags(get_the_content(""));
$description = preg_replace("/\r\n|\r|\n/", '', $description);
$pic = wp_get_attachment_url(get_post_thumbnail_id());
$url = get_permalink();
}
}
wp_reset_postdata(); $resultStr = sprintf($newsTpl, $fromUsername, $toUsername, time(), $title, $description, $pic, $url);
echo $resultStr;

测试一下,效果非常好,不但可以显示图片,连照片的配文都能显示出来:

wechat-robot-4

接下来在上面代码的基础上稍作修改,只需调整 ArticleCount 的值,然后循环输出多个 item 即可实现多图文,比如最新作品列表:

wechat-robot-5

还能不能更好?
呃,当然可以。比如做一个菜单来做导航(可惜目前只有认证帐号才能获取自定义菜单接口,而奇葩的腾讯根本不给个人开发者认证),或者把多图文列表中的第一个条目配上个特别的题图,按照标签归档做成一个个小专题……等等,太多太多,脑洞一开就根本收不起来,慢慢实现吧。

跑个题:对话功能

虽然现在从实用角度上来说,这个机器人已经可以满足需求了,可是……还不够好玩啊!在这个娱乐至上的时代,不会卖萌怎么行?而且我在内测过程中也发现,可能是受到了 Siri 的影响,现在很多人都喜欢跟机器人聊(tiao)天(xi),却只能收到一条冷冰冰的“找不到相关照片”提示,确实让人有点郁闷。只好通过判断一些常见的词汇并作出相应的回答,不过这依然无法解决问题。

于是我把目光投向了一个聊天机器人——小黄鸡,它是提供 API 的,用起来倒是很方便,唯一的问题就是……它的嘴实在太贱了。不过经过一番研究,发现它的原理其实很简单:就是一个大大的数据库,把各种问题和回答都存起来了而已。既然如此,那还不如自己建一个数据库:


CREATE TABLE `weixin_answers` (
`id` int(11) NOT NULL,
`question` varchar(1024) COLLATE utf8_unicode_ci NOT NULL,
`answer` varchar(1024) COLLATE utf8_unicode_ci NOT NULL,
`createtime` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

用起来也非常简单,我这里用了 WordPress 的 $wpdb 来读取数据库,省去了连接的步骤。一个问题可以有多个回答,使用时随机抽取一个即可:


global $wpdb;
$answer = $wpdb->get_row("SELECT answer FROM `weixin_answers` WHERE `question` = '$keyword' ORDER BY RAND() LIMIT 1");
if($answer != null)
return $answer->answer;
else
return "好像找不到你想看的照片,换个关键词试试呗。";

当然在这之前需要用正则过滤掉表情和标点符号,非常简单,这里就不细说了。最终效果如下:

wechat-robot-6

为了方便增加和维护数据,顺手再做一个后台吧。考虑到通常都是在手机上操作,就直接做成了 iOS 的 Web App,用 skel 框架做一个极简的响应式 UI,用 HTML 5 离线缓存,AJAX 交互数据。半小时搞定:

wechat-robot-7

实际用起来非常好用,存在 iPhone 的主屏幕上之后,一点开瞬间就加载完成了,感觉甚至比原生 app 的启动速度更快。不过这虽然方便,但仅凭我一己之力慢慢维护这个数据库的效率实在太低,于是便进一步优化:当机器人遇到不会回答的问题时,就将这个问题存进一个临时数据库;再做一个网页版的多人协作平台来读取临时库中的问题,分配给几个即会卖萌又可毒舌的段子手小伙伴们,她们填上答案后,就会被存进正式的数据库中。目前,这个数据库保持每天几十条的速度增加(瓶颈是接收到的信息量),相信数据量达到一万条时,这个机器人就真正可以和大家愉快地谈笑风生了。大家可以用微信扫描下面这个二维码或者在微信“添加好友”处搜索 Camarts 关注来体验,若有任何建议或 bug,可在本文的评论处反馈。

camarts-wechat-qrcode

那…还能不能再好一点啊?

当然是可以的,比如借助第三方的云分词服务 API 来分析问题中的关键词及其词性,实现“以词找句”,再用更先进的匹配算法来生成合适的回答。不过还是等粉丝量达到五位数时再说吧,我先去恶补算法知识了。

扫描二维码可分享到微信
或点击此处分享到新浪微博